|
- <template>
- <div class="bill-page">
- <AppHero
- kicker="POINTS LEDGER"
- title="点数账单"
- desc="每一次点数变化都清晰可查,充值、消费与奖励记录一目了然。"
- >
- <template #aside>
- <div
- class="relative w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[var(--c-0758c9)] via-[var(--c-0b8cff)] to-[var(--c-55c8ff)] p-5 text-white shadow-[0_22px_48px_var(--sh-11-112-220-240)] lg:w-[360px] lg:p-6"
- aria-label="点数余额"
- >
- <div class="pointer-events-none absolute -right-10 -top-14 size-40 rounded-full border border-white/15 bg-white/10" />
- <div class="pointer-events-none absolute -bottom-20 -left-10 size-44 rounded-full bg-[var(--c-004da8)]/20 blur-xl" />
-
- <div class="relative">
- <div class="flex items-center justify-between gap-4">
- <div class="flex items-center gap-2.5">
- <span class="flex size-9 items-center justify-center rounded-xl bg-white/15">
- <UIcon name="i-lucide-wallet-cards" class="size-4.5" />
- </span>
- <div>
- <div class="text-[11px] font-bold tracking-[0.12em] text-white/65">AVAILABLE POINTS</div>
- <div class="mt-0.5 text-xs font-bold text-white/90">当前可用点数</div>
- </div>
- </div>
- <span class="flex items-center gap-1.5 rounded-full bg-white/12 px-2.5 py-1 text-[11px] font-bold text-white/80">
- <span class="size-1.5 rounded-full bg-[var(--c-8affd8)]" />
- 多端同步
- </span>
- </div>
-
- <div class="mt-6 flex items-end justify-between gap-5">
- <div class="flex min-w-0 items-end">
- <span class="truncate text-[40px] font-extrabold leading-none tracking-tight tabular-nums lg:text-[44px]">
- {{ balanceText }}
- </span>
- <span v-if="balanceUnit" class="mb-1 ml-2 text-sm font-bold text-white/70">{{ balanceUnit }}</span>
- </div>
-
- <NuxtLink
- v-if="isLogin"
- to="/recharge"
- class="inline-flex h-10 shrink-0 items-center gap-1.5 rounded-full bg-[var(--c-ffffff)] px-4 text-sm font-extrabold text-[var(--c-086fd5)] shadow-[0_8px_18px_var(--sh-0-65-140-160)] transition-colors hover:bg-[var(--c-f5fbff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-e8eef7)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--c-0b8cff)] motion-reduce:transition-none"
- >
- 去充值
- <UIcon name="i-lucide-arrow-up-right" class="size-4" />
- </NuxtLink>
- <button
- v-else
- type="button"
- class="inline-flex h-10 shrink-0 cursor-pointer items-center rounded-full bg-[var(--c-ffffff)] px-4 text-sm font-extrabold text-[var(--c-086fd5)] shadow-[0_8px_18px_var(--sh-0-65-140-160)] transition-colors hover:bg-[var(--c-f5fbff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-e8eef7)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--c-0b8cff)] motion-reduce:transition-none"
- @click="showLogin = true"
- >
- 登录查看
- </button>
- </div>
-
- <div class="mt-6 flex items-center gap-2 border-t border-white/15 pt-4 text-[11px] font-medium text-white/70">
- <UIcon name="i-lucide-shield-check" class="size-3.5" />
- 账目实时同步,点数长期有效
- </div>
- </div>
- </div>
- </template>
- </AppHero>
-
- <main class="mx-auto max-w-7xl px-4 py-7 sm:px-6 lg:px-8 lg:py-10">
- <section
- v-if="!isLogin"
- class="relative overflow-hidden rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] px-6 py-14 text-center shadow-[0_16px_40px_var(--sh-33-68-112-60)] sm:py-18"
- aria-labelledby="bill-login-title"
- >
- <div class="pointer-events-none absolute left-1/2 top-0 h-36 w-72 -translate-x-1/2 rounded-full bg-[var(--c-eaf6ff)] blur-3xl" />
- <div class="relative">
- <div class="mx-auto flex size-16 items-center justify-center rounded-2xl bg-[var(--c-eef7ff)] text-[var(--c-0b8cff)] ring-8 ring-[var(--c-f7fbff)]">
- <UIcon name="i-lucide-receipt-text" class="size-7" />
- </div>
- <h2 id="bill-login-title" class="mt-6 text-xl font-extrabold text-[var(--c-172033)]">登录后查看你的点数账单</h2>
- <p class="mx-auto mt-2 max-w-md text-sm leading-6 text-[var(--c-66758a)]">
- 登录后即可同步各端余额,随时核对每一笔充值、创作消费与奖励记录。
- </p>
- <div class="mt-5 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-xs font-bold text-[var(--c-7b8797)]">
- <span class="inline-flex items-center gap-1.5">
- <UIcon name="i-lucide-check" class="size-3.5 text-[var(--c-17a65a)]" />
- 多端记录同步
- </span>
- <span class="inline-flex items-center gap-1.5">
- <UIcon name="i-lucide-check" class="size-3.5 text-[var(--c-17a65a)]" />
- 点数去向清晰
- </span>
- <span class="inline-flex items-center gap-1.5">
- <UIcon name="i-lucide-check" class="size-3.5 text-[var(--c-17a65a)]" />
- 变动余额可追溯
- </span>
- </div>
- <button
- type="button"
- class="mt-7 inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-full bg-[var(--c-0b8cff)] px-7 text-sm font-extrabold text-white shadow-[0_10px_22px_var(--sh-11-140-255-240)] transition-colors hover:bg-[var(--c-087dde)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 motion-reduce:transition-none"
- @click="showLogin = true"
- >
- <UIcon name="i-lucide-log-in" class="size-4" />
- 立即登录
- </button>
- </div>
- </section>
-
- <template v-else>
- <section class="grid gap-3 sm:grid-cols-3" aria-label="点数概览">
- <article
- v-for="item in summaryStats"
- :key="item.key"
- class="relative overflow-hidden rounded-2xl border border-[var(--c-e8eef7)] bg-[var(--c-ffffff)] p-4 shadow-[0_8px_24px_var(--sh-34-68-112-45)] sm:p-5"
- >
- <div class="flex items-start justify-between gap-4">
- <div>
- <p class="text-xs font-bold text-[var(--c-7b8797)]">{{ item.label }}</p>
- <p class="mt-2 text-2xl font-extrabold tracking-tight tabular-nums" :class="item.valueTone">
- {{ hasLoaded ? item.value : '—' }}
- </p>
- <p class="mt-1 text-[11px] text-[var(--c-9aa5b5)]">{{ item.helper }}</p>
- </div>
- <span class="flex size-10 shrink-0 items-center justify-center rounded-xl" :class="item.iconTone">
- <UIcon :name="item.icon" class="size-4.5" />
- </span>
- </div>
- </article>
- </section>
-
- <section
- class="mt-6 overflow-hidden rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] shadow-[0_16px_40px_var(--sh-34-68-112-60)]"
- aria-labelledby="bill-list-title"
- :aria-busy="loading"
- >
- <header class="border-b border-[var(--c-edf2f7)] px-4 pb-4 pt-5 sm:px-6 sm:pb-5 sm:pt-6">
- <div class="flex flex-wrap items-end justify-between gap-3">
- <div>
- <div class="flex items-center gap-2">
- <h2 id="bill-list-title" class="text-lg font-extrabold text-[var(--c-172033)] sm:text-xl">流水明细</h2>
- <span class="rounded-full bg-[var(--c-f1f5fa)] px-2.5 py-1 text-[11px] font-bold text-[var(--c-7b8797)]">
- {{ total }} 条
- </span>
- </div>
- <p class="mt-1.5 text-xs text-[var(--c-8a97a8)]">按时间倒序展示,最近的点数变动排在最前</p>
- </div>
- <div v-if="list.length" class="text-xs font-medium text-[var(--c-9aa5b5)]">
- 已显示 {{ list.length }} 条
- </div>
- </div>
-
- <div class="bill-tabs -mx-1 mt-5 overflow-x-auto px-1 pb-1" role="tablist" aria-label="账单类型">
- <div class="grid auto-cols-[minmax(104px,1fr)] grid-flow-col gap-2 sm:grid-flow-row sm:grid-cols-5">
- <button
- v-for="item in tabs"
- :key="item.key"
- type="button"
- role="tab"
- class="flex h-11 cursor-pointer items-center justify-center gap-2 rounded-xl px-3 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 disabled:cursor-wait motion-reduce:transition-none"
- :class="activeTab === item.key
- ? 'bg-[var(--c-0b8cff)] text-white shadow-[0_8px_18px_var(--sh-11-140-255-200)]'
- : 'bg-[var(--c-f6f9fd)] text-[var(--c-66758a)] hover:bg-[var(--c-eef7ff)] hover:text-[var(--c-0b8cff)]'"
- :aria-selected="activeTab === item.key"
- :disabled="loading"
- @click="changeTab(item.key)"
- >
- <UIcon :name="item.icon" class="size-4 shrink-0" />
- <span>{{ item.name }}</span>
- <span
- class="min-w-5 rounded-full px-1.5 py-0.5 text-[10px] font-extrabold tabular-nums"
- :class="activeTab === item.key ? 'bg-white/18 text-white' : 'bg-[var(--c-ffffff)] text-[var(--c-8a97a8)]'"
- >
- {{ tabCounts[item.key] || 0 }}
- </span>
- </button>
- </div>
- </div>
- </header>
-
- <div
- v-if="initialLoading"
- class="divide-y divide-[var(--c-edf2f7)]"
- role="status"
- aria-label="正在加载账单"
- >
- <div v-for="i in 5" :key="i" class="flex items-center gap-4 px-4 py-5 sm:px-6">
- <div class="size-11 shrink-0 animate-pulse rounded-2xl bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- <div class="min-w-0 flex-1">
- <div class="h-3.5 w-36 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- <div class="mt-3 h-3 w-56 max-w-full animate-pulse rounded-full bg-[var(--c-f1f5fa)] motion-reduce:animate-none" />
- </div>
- <div class="hidden w-20 sm:block">
- <div class="ml-auto h-4 w-14 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- <div class="ml-auto mt-3 h-3 w-20 animate-pulse rounded-full bg-[var(--c-f1f5fa)] motion-reduce:animate-none" />
- </div>
- <span class="sr-only">加载中</span>
- </div>
- </div>
-
- <div v-else-if="loadError && !list.length" class="px-5 py-16 text-center sm:py-20" role="alert">
- <div class="mx-auto flex size-14 items-center justify-center rounded-2xl bg-[var(--c-fff3f0)] text-[var(--c-e85f1c)] ring-8 ring-[var(--c-fff9f7)]">
- <UIcon name="i-lucide-circle-alert" class="size-6" />
- </div>
- <h3 class="mt-6 text-base font-extrabold text-[var(--c-172033)]">账单加载失败</h3>
- <p class="mx-auto mt-2 max-w-sm text-sm leading-6 text-[var(--c-8a97a8)]">{{ loadError }}</p>
- <button
- type="button"
- class="mt-6 inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-5 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e1f1ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 motion-reduce:transition-none"
- @click="fetchList(true)"
- >
- <UIcon name="i-lucide-refresh-cw" class="size-3.5" />
- 重新加载
- </button>
- </div>
-
- <div v-else-if="list.length">
- <div class="hidden grid-cols-[minmax(0,1fr)_150px] border-b border-[var(--c-edf2f7)] bg-[var(--c-fbfcfe)] px-6 py-2.5 text-[11px] font-bold text-[var(--c-9aa5b5)] sm:grid">
- <span>记录</span>
- <span class="text-right">点数变动</span>
- </div>
-
- <div class="divide-y divide-[var(--c-edf2f7)]">
- <article
- v-for="item in list"
- :key="item.id"
- class="grid gap-4 px-4 py-5 transition-colors hover:bg-[var(--c-fbfdff)] sm:grid-cols-[minmax(0,1fr)_150px] sm:items-center sm:px-6 motion-reduce:transition-none"
- >
- <div class="flex min-w-0 items-start gap-3.5">
- <span
- class="flex size-11 shrink-0 items-center justify-center rounded-2xl ring-1 ring-inset"
- :class="typeTone(item.type)"
- aria-hidden="true"
- >
- <UIcon :name="getTypeIcon(item.type)" class="size-4.5" />
- </span>
-
- <div class="min-w-0 flex-1">
- <div class="flex min-w-0 flex-wrap items-center gap-2">
- <h3 class="min-w-0 truncate text-sm font-extrabold text-[var(--c-172033)] sm:text-[15px]">
- {{ item.title || '点数变动' }}
- </h3>
- <span
- class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-extrabold"
- :class="typeBadgeTone(item.type)"
- >
- {{ item.type_text || '其他' }}
- </span>
- </div>
- <p v-if="item.desc" class="mt-1.5 line-clamp-2 text-xs leading-5 text-[var(--c-66758a)] sm:truncate">
- {{ item.desc }}
- </p>
- <div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[var(--c-9aa5b5)]">
- <span class="inline-flex items-center gap-1">
- <UIcon name="i-lucide-clock-3" class="size-3" />
- {{ item.time || '时间未知' }}
- </span>
- <!-- 充值流水带上订单号,方便对账 / 客服核单,点击可复制 -->
- <button
- v-if="orderNo(item)"
- type="button"
- class="inline-flex max-w-full cursor-pointer items-center gap-1 transition-colors hover:text-[var(--c-0b8cff)]"
- :title="`点击复制订单号 ${orderNo(item)}`"
- @click="copyOrderNo(orderNo(item))"
- >
- <UIcon name="i-lucide-receipt" class="size-3 shrink-0" />
- <span class="truncate tabular-nums">订单号 {{ orderNo(item) }}</span>
- <UIcon name="i-lucide-copy" class="size-3 shrink-0" />
- </button>
- <span class="sm:hidden">变动后 {{ formatNumber(item.balance_after) }} 点</span>
- </div>
- </div>
- </div>
-
- <div class="flex items-center justify-between border-t border-dashed border-[var(--c-e8eef7)] pt-3 sm:block sm:border-0 sm:pt-0 sm:text-right">
- <span class="text-[11px] font-medium text-[var(--c-9aa5b5)] sm:hidden">点数变动</span>
- <div>
- <div class="text-lg font-extrabold tabular-nums" :class="amountTone(item.amount)">
- {{ formatAmount(item.amount) }}
- </div>
- <div class="mt-1 hidden text-[11px] text-[var(--c-9aa5b5)] sm:block">
- 变动后 {{ formatNumber(item.balance_after) }} 点
- </div>
- </div>
- </div>
- </article>
- </div>
- </div>
-
- <div v-else class="px-5 py-16 text-center sm:py-20">
- <div class="mx-auto flex size-14 items-center justify-center rounded-2xl bg-[var(--c-eef7ff)] text-[var(--c-0b8cff)] ring-8 ring-[var(--c-f8fbff)]">
- <UIcon :name="activeTab === 'all' ? 'i-lucide-receipt-text' : 'i-lucide-list-filter'" class="size-6" />
- </div>
- <h3 class="mt-6 text-base font-extrabold text-[var(--c-172033)]">
- {{ activeTab === 'all' ? '还没有点数记录' : `暂无${activeTabName}` }}
- </h3>
- <p class="mx-auto mt-2 max-w-sm text-sm leading-6 text-[var(--c-8a97a8)]">
- {{ activeTab === 'all' ? '充值或使用 AI 工具后,点数变化会自动记录在这里。' : '当前分类暂时没有记录,可以看看其他类型的流水。' }}
- </p>
- <NuxtLink
- v-if="activeTab === 'all'"
- to="/recharge"
- class="mt-6 inline-flex h-10 items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-5 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e1f1ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 motion-reduce:transition-none"
- >
- 去充值
- <UIcon name="i-lucide-arrow-right" class="size-3.5" />
- </NuxtLink>
- <button
- v-else
- type="button"
- class="mt-6 inline-flex h-10 cursor-pointer items-center rounded-full bg-[var(--c-eef7ff)] px-5 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e1f1ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 motion-reduce:transition-none"
- @click="changeTab('all')"
- >
- 查看全部记录
- </button>
- </div>
-
- <footer v-if="list.length" class="border-t border-[var(--c-edf2f7)] px-4 py-4 sm:px-6">
- <div v-if="loading" class="flex items-center justify-center gap-2 py-1 text-xs font-bold text-[var(--c-8a97a8)]" role="status">
- <span class="size-4 animate-spin rounded-full border-2 border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)] motion-reduce:animate-none" />
- 正在加载更多
- </div>
- <button
- v-else-if="hasMore"
- type="button"
- class="mx-auto flex h-10 cursor-pointer items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-6 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e1f1ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 motion-reduce:transition-none"
- @click="loadMore"
- >
- 加载更多
- <UIcon name="i-lucide-chevron-down" class="size-3.5" />
- </button>
- <div v-else class="flex items-center justify-center gap-3 py-1 text-[11px] text-[var(--c-a0aabb)]">
- <span class="h-px w-8 bg-[var(--c-e6edf5)]" />
- 已经到底了
- <span class="h-px w-8 bg-[var(--c-e6edf5)]" />
- </div>
- </footer>
- </section>
-
- <div class="mt-4 flex items-start gap-2 rounded-2xl bg-[var(--c-eef7ff)]/70 px-4 py-3 text-xs leading-5 text-[var(--c-66758a)]">
- <UIcon name="i-lucide-info" class="mt-0.5 size-3.5 shrink-0 text-[var(--c-0b8cff)]" />
- <p>点数账单由系统自动生成。如对某条记录有疑问,请保留记录时间和变动后余额,以便快速核对。</p>
- </div>
- </template>
- </main>
- </div>
- </template>
-
- <script setup>
- useSeoMeta({
- title: '点数账单 - AI在线',
- description: '查看点数充值、消费、奖励与余额变动明细',
- })
-
- const { userInfo, isLogin, showLogin } = useUser()
- const { post } = useApi()
- const toast = useToast()
-
- const tabs = [
- { key: 'all', name: '全部', icon: 'i-lucide-list' },
- { key: 'recharge', name: '充值', icon: 'i-lucide-wallet-cards' },
- { key: 'consume', name: '消费', icon: 'i-lucide-sparkles' },
- { key: 'reward', name: '奖励', icon: 'i-lucide-gift' },
- { key: 'other', name: '其他', icon: 'i-lucide-ellipsis' },
- ]
-
- const activeTab = ref('all')
- const list = ref([])
- const page = ref(1)
- const pageSize = 10
- const total = ref(0)
- const loading = ref(false)
- const loadError = ref('')
- const hasLoaded = ref(false)
- const balance = ref({})
- const summary = ref({
- recharge: 0,
- consume: 0,
- reward: 0,
- refund: 0,
- adjust: 0,
- counts: { all: 0, recharge: 0, consume: 0, reward: 0, other: 0 },
- })
-
- const numberFormatter = new Intl.NumberFormat('zh-CN')
-
- const balanceText = computed(() => {
- if (!isLogin.value) return '—'
- const points = balance.value.points ?? balance.value.point_balance ?? userInfo.value.balance
- return Number(points) < 0 ? '不限' : formatNumber(points)
- })
- const balanceUnit = computed(() => (balanceText.value === '不限' || balanceText.value === '—' ? '' : '点'))
- const tabCounts = computed(() => summary.value.counts || {})
- const hasMore = computed(() => list.value.length < total.value)
- const initialLoading = computed(() => loading.value && !list.value.length)
- const activeTabName = computed(() => {
- const item = tabs.find(tab => tab.key === activeTab.value)
- return item ? `${item.name}记录` : '全部记录'
- })
- const summaryStats = computed(() => [
- {
- key: 'recharge',
- label: '累计充值',
- helper: '历史充值获得',
- value: formatSigned(summary.value.recharge, true),
- icon: 'i-lucide-circle-plus',
- iconTone: 'bg-[var(--c-eef7ff)] text-[var(--c-0b8cff)]',
- valueTone: 'text-[var(--c-0879dc)]',
- },
- {
- key: 'consume',
- label: '累计消费',
- helper: '创作累计使用',
- value: formatSigned(summary.value.consume ? -Math.abs(summary.value.consume) : 0),
- icon: 'i-lucide-sparkles',
- iconTone: 'bg-[var(--c-fff4ec)] text-[var(--c-e85f1c)]',
- valueTone: 'text-[var(--c-e85f1c)]',
- },
- {
- key: 'reward',
- label: '累计奖励',
- helper: '活动与任务获得',
- value: formatSigned(summary.value.reward, true),
- icon: 'i-lucide-gift',
- iconTone: 'bg-[var(--c-edfdf4)] text-[var(--c-159a54)]',
- valueTone: 'text-[var(--c-159a54)]',
- },
- ])
-
- async function fetchList(reset = false) {
- if (!isLogin.value || loading.value) return false
- if (reset) {
- page.value = 1
- list.value = []
- total.value = 0
- }
- loading.value = true
- loadError.value = ''
-
- try {
- const res = await post('/point/loglist', {
- type: activeTab.value,
- page_no: page.value,
- page_size: pageSize,
- })
- const rows = res.list || []
- const data = res.data || {}
- list.value = reset ? rows : list.value.concat(rows)
- total.value = Number(data.total ?? rows.length)
- setBalance(data.balance || {})
- summary.value = normalizeSummary(data.summary || {})
- hasLoaded.value = true
- return true
- } catch (e) {
- loadError.value = e.message || '网络开小差了,请稍后重试。'
- toast.add({ title: loadError.value, color: 'error' })
- return false
- } finally {
- loading.value = false
- }
- }
-
- function normalizeSummary(data = {}) {
- const counts = Object.assign(
- { all: 0, recharge: 0, consume: 0, reward: 0, other: 0 },
- data.counts || {},
- )
- Object.keys(counts).forEach((key) => {
- counts[key] = Number(counts[key] || 0)
- })
- return {
- recharge: Number(data.recharge || 0),
- consume: Number(data.consume || 0),
- reward: Number(data.reward || 0),
- refund: Number(data.refund || 0),
- adjust: Number(data.adjust || 0),
- counts,
- }
- }
-
- function setBalance(data = {}) {
- balance.value = data
- const points = data.points ?? data.point_balance
- if (points !== undefined) userInfo.value.balance = Number(points) || 0
- }
-
- function changeTab(key) {
- if (activeTab.value === key || loading.value) return
- activeTab.value = key
- fetchList(true)
- }
-
- async function loadMore() {
- if (loading.value || !hasMore.value) return
- page.value += 1
- const success = await fetchList()
- if (!success) page.value -= 1
- }
-
- function formatNumber(value) {
- const num = Number(value || 0)
- return numberFormatter.format(Number.isFinite(num) ? num : 0)
- }
-
- function formatSigned(value, plus = false) {
- const num = Number(value || 0)
- return num > 0 && plus ? `+${formatNumber(num)}` : formatNumber(num)
- }
-
- function formatAmount(value) {
- const num = Number(value || 0)
- return num > 0 ? `+${formatNumber(num)}` : formatNumber(num)
- }
-
- /**
- * 充值流水的订单号。
- * 后端 point_log.biz_no 在充值时存的就是 point_order.order_no(消费存的是计费流水号,不展示)。
- */
- function orderNo(item) {
- if (!item || item.type !== 'recharge') return ''
- return String(item.biz_no || '').trim()
- }
-
- async function copyOrderNo(value) {
- if (!value) return
- try {
- await navigator.clipboard.writeText(value)
- toast.add({ title: '订单号已复制', color: 'success' })
- } catch {
- // 非 HTTPS / 旧浏览器下 clipboard 不可用,退回到 execCommand
- const input = document.createElement('input')
- input.value = value
- document.body.appendChild(input)
- input.select()
- const ok = document.execCommand?.('copy')
- document.body.removeChild(input)
- toast.add({ title: ok ? '订单号已复制' : '复制失败,请手动选择', color: ok ? 'success' : 'error' })
- }
- }
-
- function getTypeIcon(type) {
- const map = {
- recharge: 'i-lucide-wallet-cards',
- consume: 'i-lucide-sparkles',
- reward: 'i-lucide-gift',
- refund: 'i-lucide-rotate-ccw',
- adjust: 'i-lucide-sliders-horizontal',
- }
- return map[type] || 'i-lucide-receipt-text'
- }
-
- function typeTone(type) {
- const map = {
- recharge: 'bg-[var(--c-eef7ff)] text-[var(--c-0b8cff)] ring-[var(--c-d9ecff)]',
- consume: 'bg-[var(--c-fff4ec)] text-[var(--c-e85f1c)] ring-[var(--c-ffe0cc)]',
- reward: 'bg-[var(--c-edfdf4)] text-[var(--c-159a54)] ring-[var(--c-d2f5e2)]',
- refund: 'bg-[var(--c-f3f0ff)] text-[var(--c-7253e5)] ring-[var(--c-e2dafd)]',
- adjust: 'bg-[var(--c-f3f0ff)] text-[var(--c-7253e5)] ring-[var(--c-e2dafd)]',
- }
- return map[type] || 'bg-[var(--c-f1f5fa)] text-[var(--c-66758a)] ring-[var(--c-e3eaf2)]'
- }
-
- function typeBadgeTone(type) {
- const map = {
- recharge: 'bg-[var(--c-eef7ff)] text-[var(--c-0879dc)]',
- consume: 'bg-[var(--c-fff4ec)] text-[var(--c-d95414)]',
- reward: 'bg-[var(--c-edfdf4)] text-[var(--c-14894d)]',
- refund: 'bg-[var(--c-f3f0ff)] text-[var(--c-6848d8)]',
- adjust: 'bg-[var(--c-f3f0ff)] text-[var(--c-6848d8)]',
- }
- return map[type] || 'bg-[var(--c-f1f5fa)] text-[var(--c-66758a)]'
- }
-
- function amountTone(value) {
- const amount = Number(value || 0)
- if (amount > 0) return 'text-[var(--c-159a54)]'
- if (amount < 0) return 'text-[var(--c-e85f1c)]'
- return 'text-[var(--c-66758a)]'
- }
-
- onMounted(() => fetchList(true))
- watch(isLogin, (value) => {
- if (value) {
- fetchList(true)
- } else {
- list.value = []
- total.value = 0
- loadError.value = ''
- hasLoaded.value = false
- balance.value = {}
- }
- })
- </script>
-
- <style scoped>
- .bill-tabs {
- scrollbar-width: none;
- }
-
- .bill-tabs::-webkit-scrollbar {
- display: none;
- }
- </style>
|