|
| 1 | +<script setup lang="ts"> |
| 2 | +// 萌萌点记录 — the user's OWN moemoepoint ledger. OAuth is the source of truth; |
| 3 | +// moyu proxies the reduced s2s view via GET /user/moemoepoint/log (id from the |
| 4 | +// session, never a path param). Cursor paginated by the last row's id. |
| 5 | +const open = defineModel<boolean>({ required: true }) |
| 6 | +
|
| 7 | +const api = useApi() |
| 8 | +const userStore = useUserStore() |
| 9 | +
|
| 10 | +interface MoemoepointLogEntry { |
| 11 | + id: number |
| 12 | + delta: number |
| 13 | + reason: string |
| 14 | + source_app: string |
| 15 | + ref: string |
| 16 | + created_at: string |
| 17 | +} |
| 18 | +
|
| 19 | +const LIMIT = 20 |
| 20 | +
|
| 21 | +const items = ref<MoemoepointLogEntry[]>([]) |
| 22 | +const hasMore = ref(false) |
| 23 | +const loading = ref(false) |
| 24 | +const loadingMore = ref(false) |
| 25 | +const loaded = ref(false) |
| 26 | +const failed = ref(false) |
| 27 | +
|
| 28 | +// reason → human label + icon + color. Mirrors OAuth's reason enum |
| 29 | +// (06-moemoepoint.md); admin_*/migration only appear for cross-channel rows. |
| 30 | +const REASONS: Record<string, { label: string; icon: string; class: string }> = { |
| 31 | + content_approved: { |
| 32 | + label: '内容被采纳', |
| 33 | + icon: 'lucide:badge-check', |
| 34 | + class: 'text-success-500' |
| 35 | + }, |
| 36 | + content_removed: { |
| 37 | + label: '内容被移除', |
| 38 | + icon: 'lucide:badge-x', |
| 39 | + class: 'text-danger-500' |
| 40 | + }, |
| 41 | + daily_checkin: { |
| 42 | + label: '每日签到', |
| 43 | + icon: 'lucide:calendar-check', |
| 44 | + class: 'text-primary-500' |
| 45 | + }, |
| 46 | + liked: { label: '获得喜欢', icon: 'lucide:heart', class: 'text-danger-500' }, |
| 47 | + admin_grant: { label: '管理员发放', icon: 'lucide:gift', class: 'text-success-500' }, |
| 48 | + admin_deduct: { label: '管理员扣除', icon: 'lucide:gavel', class: 'text-warning-500' }, |
| 49 | + migration: { label: '初始迁移', icon: 'lucide:database', class: 'text-default-400' } |
| 50 | +} |
| 51 | +
|
| 52 | +const reasonMeta = (reason: string) => |
| 53 | + REASONS[reason] ?? { |
| 54 | + label: reason, |
| 55 | + icon: 'lucide:circle-dot', |
| 56 | + class: 'text-default-400' |
| 57 | + } |
| 58 | +
|
| 59 | +// ref is "type:id" (e.g. "resource:42", "galgame:1207"). Map the ones that have |
| 60 | +// an in-site page to a link; everything else (comment, admin:*, …) has no link. |
| 61 | +const refLink = (refStr: string): string | null => { |
| 62 | + const [type, id] = (refStr || '').split(':') |
| 63 | + if (!id) return null |
| 64 | + if (type === 'resource') return `/resource/${id}` |
| 65 | + if (type === 'galgame' || type === 'patch') return `/patch/${id}` |
| 66 | + return null |
| 67 | +} |
| 68 | +
|
| 69 | +const fetchPage = async (beforeID?: number) => { |
| 70 | + const params = new URLSearchParams({ limit: String(LIMIT) }) |
| 71 | + if (beforeID) params.set('before_id', String(beforeID)) |
| 72 | + const res = await api.get<{ items: MoemoepointLogEntry[]; has_more: boolean }>( |
| 73 | + `/user/moemoepoint/log?${params.toString()}` |
| 74 | + ) |
| 75 | + if (res.code !== 0) throw new Error(res.message) |
| 76 | + return res.data |
| 77 | +} |
| 78 | +
|
| 79 | +const load = async () => { |
| 80 | + loading.value = true |
| 81 | + failed.value = false |
| 82 | + try { |
| 83 | + const data = await fetchPage() |
| 84 | + items.value = data.items ?? [] |
| 85 | + hasMore.value = data.has_more |
| 86 | + loaded.value = true |
| 87 | + } catch { |
| 88 | + failed.value = true |
| 89 | + } finally { |
| 90 | + loading.value = false |
| 91 | + } |
| 92 | +} |
| 93 | +
|
| 94 | +const loadMore = async () => { |
| 95 | + const last = items.value[items.value.length - 1] |
| 96 | + if (loadingMore.value || !hasMore.value || !last) return |
| 97 | + loadingMore.value = true |
| 98 | + try { |
| 99 | + const data = await fetchPage(last.id) |
| 100 | + items.value.push(...(data.items ?? [])) |
| 101 | + hasMore.value = data.has_more |
| 102 | + } catch { |
| 103 | + useKunMessage('加载更多失败', 'error') |
| 104 | + } finally { |
| 105 | + loadingMore.value = false |
| 106 | + } |
| 107 | +} |
| 108 | +
|
| 109 | +// Refetch on each open so a record earned this session (e.g. a fresh check-in) |
| 110 | +// shows without a page reload. |
| 111 | +watch(open, (v) => { |
| 112 | + if (v) load() |
| 113 | +}) |
| 114 | +</script> |
| 115 | + |
| 116 | +<template> |
| 117 | + <KunModal v-model="open" inner-class-name="max-w-lg w-full"> |
| 118 | + <div class="space-y-4"> |
| 119 | + <div class="flex items-center justify-between"> |
| 120 | + <h3 class="flex items-center gap-2 text-lg font-semibold"> |
| 121 | + <KunIcon name="lucide:lollipop" class="size-5" /> |
| 122 | + 萌萌点记录 |
| 123 | + </h3> |
| 124 | + <span class="text-foreground/60 text-sm"> |
| 125 | + 当前 {{ userStore.user.moemoepoint }} |
| 126 | + </span> |
| 127 | + </div> |
| 128 | + |
| 129 | + <KunLoading v-if="loading" description="加载记录中..." /> |
| 130 | + |
| 131 | + <KunNull v-else-if="failed" description="加载失败, 请稍后再试" /> |
| 132 | + |
| 133 | + <KunNull |
| 134 | + v-else-if="loaded && !items.length" |
| 135 | + description="还没有萌萌点记录哦" |
| 136 | + /> |
| 137 | + |
| 138 | + <ul v-else class="max-h-[60vh] space-y-1 overflow-y-auto"> |
| 139 | + <li |
| 140 | + v-for="item in items" |
| 141 | + :key="item.id" |
| 142 | + class="hover:bg-default-100 flex items-center gap-3 rounded-lg px-2 py-2" |
| 143 | + > |
| 144 | + <span |
| 145 | + class="bg-default-100 flex size-9 shrink-0 items-center justify-center rounded-full" |
| 146 | + :class="reasonMeta(item.reason).class" |
| 147 | + > |
| 148 | + <KunIcon :name="reasonMeta(item.reason).icon" class="size-4" /> |
| 149 | + </span> |
| 150 | + <div class="min-w-0 flex-1"> |
| 151 | + <p class="flex items-center gap-2 text-sm font-medium"> |
| 152 | + <span class="truncate">{{ reasonMeta(item.reason).label }}</span> |
| 153 | + <NuxtLink |
| 154 | + v-if="refLink(item.ref)" |
| 155 | + :to="refLink(item.ref)!" |
| 156 | + class="text-primary-500 shrink-0 text-xs hover:underline" |
| 157 | + @click="open = false" |
| 158 | + > |
| 159 | + 查看 |
| 160 | + </NuxtLink> |
| 161 | + </p> |
| 162 | + <p class="text-foreground/50 text-xs"> |
| 163 | + {{ formatTimeDifference(item.created_at) }} |
| 164 | + </p> |
| 165 | + </div> |
| 166 | + <span |
| 167 | + class="shrink-0 text-sm font-semibold tabular-nums" |
| 168 | + :class="item.delta >= 0 ? 'text-success-500' : 'text-danger-500'" |
| 169 | + > |
| 170 | + {{ item.delta >= 0 ? '+' : '' }}{{ item.delta }} |
| 171 | + </span> |
| 172 | + </li> |
| 173 | + |
| 174 | + <li v-if="hasMore" class="pt-1"> |
| 175 | + <KunButton |
| 176 | + variant="light" |
| 177 | + full-width |
| 178 | + :loading="loadingMore" |
| 179 | + :disabled="loadingMore" |
| 180 | + @click="loadMore" |
| 181 | + > |
| 182 | + 加载更多 |
| 183 | + </KunButton> |
| 184 | + </li> |
| 185 | + </ul> |
| 186 | + </div> |
| 187 | + </KunModal> |
| 188 | +</template> |
0 commit comments