|
- <template>
- <div>
- <AppHero
- kicker="INVITE RECORDS"
- title="邀请记录"
- desc="查看好友注册状态与邀请奖励发放情况。"
- >
- <template #aside>
- <div class="w-full rounded-3xl border border-[var(--c-e8eef7)] bg-[var(--c-ffffff)]/85 p-5 shadow-[0_18px_34px_var(--sh-0-92-190-100)] backdrop-blur lg:w-[380px]">
- <div class="grid grid-cols-3 gap-3 text-center">
- <div v-for="item in summaryCards" :key="item.label">
- <div class="text-2xl font-extrabold tabular-nums" :class="item.tone">{{ loaded ? item.value : '—' }}</div>
- <div class="mt-1.5 text-[11px] font-bold text-[var(--c-8a97a8)]">{{ item.label }}</div>
- </div>
- </div>
- </div>
- </template>
- </AppHero>
-
- <main class="mx-auto max-w-6xl px-4 py-7 sm:px-6 lg:px-8 lg:py-10">
- <section
- v-if="!isLogin"
- class="rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] px-6 py-14 text-center shadow-[0_16px_40px_var(--sh-34-68-112-60)]"
- >
- <UIcon name="i-lucide-lock-keyhole" class="mx-auto size-10 text-[var(--c-a0aabb)]" />
- <h2 class="mt-4 text-lg font-extrabold text-[var(--c-172033)]">登录后查看邀请记录</h2>
- <button
- type="button"
- class="mt-6 cursor-pointer rounded-full bg-[var(--c-0b8cff)] px-7 py-2.5 text-sm font-extrabold text-white"
- @click="showLogin = true"
- >
- 立即登录
- </button>
- </section>
-
- <section
- v-else
- class="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="record-title"
- :aria-busy="loading"
- >
- <header class="flex flex-wrap items-end justify-between gap-4 border-b border-[var(--c-edf2f7)] px-5 py-5 sm:px-6">
- <div>
- <h2 id="record-title" class="text-lg font-extrabold text-[var(--c-172033)]">全部记录</h2>
- <p class="mt-1 text-xs text-[var(--c-8a97a8)]">共 {{ total }} 条邀请,最近的记录排在最前。</p>
- </div>
- <NuxtLink
- to="/invite"
- class="inline-flex h-9 items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-4 text-xs font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e0f0ff)]"
- >
- <UIcon name="i-lucide-user-plus" class="size-3.5" />
- 继续邀请
- </NuxtLink>
- </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-5 py-5 sm:px-6">
- <div class="size-11 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- <div class="min-w-0 flex-1">
- <div class="h-3.5 w-28 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- <div class="mt-3 h-3 w-48 max-w-full animate-pulse rounded-full bg-[var(--c-f1f5fa)] motion-reduce:animate-none" />
- </div>
- <div class="h-5 w-16 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
- </div>
- </div>
-
- <div v-else-if="loadError && !list.length" class="px-5 py-16 text-center" role="alert">
- <UIcon name="i-lucide-circle-alert" class="mx-auto size-9 text-[var(--c-e85f1c)]" />
- <h3 class="mt-4 text-base font-extrabold text-[var(--c-172033)]">记录加载失败</h3>
- <p class="mt-2 text-sm text-[var(--c-8a97a8)]">{{ loadError }}</p>
- <button
- type="button"
- class="mt-6 cursor-pointer rounded-full bg-[var(--c-eef7ff)] px-6 py-2.5 text-sm font-bold text-[var(--c-0b8cff)]"
- @click="fetchList(true)"
- >
- 重新加载
- </button>
- </div>
-
- <div v-else-if="list.length">
- <div class="hidden grid-cols-[minmax(0,1fr)_130px_150px] bg-[var(--c-fbfcfe)] px-6 py-2.5 text-[11px] font-bold text-[var(--c-9aa5b5)] sm:grid">
- <span>受邀好友</span>
- <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-5 py-5 transition-colors hover:bg-[var(--c-fbfdff)] sm:grid-cols-[minmax(0,1fr)_130px_150px] sm:items-center sm:px-6"
- >
- <div class="flex min-w-0 items-center gap-3.5">
- <div class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[var(--c-eef7ff)] text-sm font-extrabold text-[var(--c-0b8cff)]">
- <img v-if="item.avatar" :src="item.avatar" :alt="item.user_name || '好友'" class="size-full object-cover">
- <span v-else>{{ getInitial(item.user_name) }}</span>
- </div>
- <div class="min-w-0">
- <h3 class="truncate text-sm font-extrabold text-[var(--c-172033)]">{{ item.user_name || '好友' }}</h3>
- <p class="mt-1 text-xs text-[var(--c-8a97a8)]">{{ recordDescription(item) }}</p>
- <p class="mt-1.5 inline-flex items-center gap-1 text-[11px] text-[var(--c-9aa5b5)]">
- <UIcon name="i-lucide-clock-3" class="size-3" />
- {{ formatTime(item.reward_time || item.add_time) }}
- </p>
- </div>
- </div>
-
- <div>
- <span class="inline-flex rounded-full px-2.5 py-1 text-[11px] font-extrabold" :class="statusTone(item.status)">
- {{ item.status_text || statusText(item.status) }}
- </span>
- </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] text-[var(--c-9aa5b5)] sm:hidden">邀请奖励</span>
- <div>
- <div class="text-lg font-extrabold tabular-nums" :class="Number(item.status) === 1 ? 'text-[var(--c-159a54)]' : 'text-[var(--c-8a97a8)]'">
- {{ Number(item.status) === 1 ? `+${item.inviter_points || 0}` : '—' }}
- </div>
- <div v-if="Number(item.status) === 1" class="mt-1 text-[11px] text-[var(--c-9aa5b5)]">点数已到账</div>
- </div>
- </div>
- </article>
- </div>
- </div>
-
- <div v-else class="px-5 py-16 text-center">
- <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="i-lucide-user-round-plus" class="size-6" />
- </div>
- <h3 class="mt-6 text-base font-extrabold text-[var(--c-172033)]">还没有邀请记录</h3>
- <p class="mt-2 text-sm text-[var(--c-8a97a8)]">分享邀请链接,好友注册成功后会展示在这里。</p>
- <NuxtLink to="/invite" class="mt-6 inline-flex h-10 items-center rounded-full bg-[var(--c-eef7ff)] px-5 text-sm font-bold text-[var(--c-0b8cff)]">
- 去邀请好友
- </NuxtLink>
- </div>
-
- <footer v-if="list.length" class="border-t border-[var(--c-edf2f7)] px-5 py-4 sm:px-6">
- <div v-if="loading" class="flex items-center justify-center gap-2 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-e0f0ff)]"
- @click="loadMore"
- >
- 加载更多
- <UIcon name="i-lucide-chevron-down" class="size-3.5" />
- </button>
- <div v-else class="text-center text-[11px] text-[var(--c-a0aabb)]">没有更多记录了</div>
- </footer>
- </section>
- </main>
- </div>
- </template>
-
- <script setup>
- import dayjs from 'dayjs'
-
- useSeoMeta({
- title: '邀请记录 - AI在线',
- description: '查看好友邀请和点数奖励记录。',
- robots: 'noindex, nofollow',
- })
-
- const { isLogin, showLogin } = useUser()
- const { post } = useApi()
- const list = ref([])
- const stats = ref({})
- const page = ref(1)
- const pageSize = 10
- const total = ref(0)
- const loading = ref(false)
- const loaded = ref(false)
- const loadError = ref('')
-
- const initialLoading = computed(() => loading.value && !list.value.length)
- const hasMore = computed(() => list.value.length < total.value)
- const summaryCards = computed(() => [
- { label: '全部邀请', value: stats.value.total || total.value || 0, tone: 'text-[var(--c-172033)]' },
- { label: '成功邀请', value: stats.value.success || 0, tone: 'text-[var(--c-0b8cff)]' },
- { label: '累计奖励', value: stats.value.inviter_points || 0, tone: '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('/invite/list', {
- 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)
- stats.value = data.stats || {}
- loaded.value = true
- return true
- } catch (error) {
- loadError.value = error.message || '网络开小差了,请稍后重试。'
- return false
- } finally {
- loading.value = false
- }
- }
-
- async function loadMore() {
- if (loading.value || !hasMore.value) return
- page.value += 1
- const success = await fetchList()
- if (!success) page.value -= 1
- }
-
- function getInitial(name = '') {
- return String(name || '友').slice(0, 1)
- }
-
- function formatTime(value) {
- if (!value) return '时间未知'
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- function statusText(status) {
- const value = Number(status)
- if (value === 1) return '已奖励'
- if (value === 2) return '已记录'
- if (value < 0) return '发放失败'
- return '待发放'
- }
-
- function statusTone(status) {
- const value = Number(status)
- if (value === 1) return 'bg-[var(--c-edfdf4)] text-[var(--c-159a54)]'
- if (value === 0) return 'bg-[var(--c-fff4ec)] text-[var(--c-d95414)]'
- if (value < 0) return 'bg-[var(--c-fff1f0)] text-[var(--c-d92d20)]'
- return 'bg-[var(--c-f1f5fa)] text-[var(--c-66758a)]'
- }
-
- function recordDescription(item) {
- const value = Number(item.status)
- if (value === 1) return '好友注册成功,奖励已发放'
- if (value === 2) return '邀请已记录,本次未触发奖励'
- if (value < 0) return item.remark || '奖励发放失败'
- return '等待系统发放奖励'
- }
-
- onMounted(() => fetchList(true))
- watch(isLogin, (value) => {
- if (value) fetchList(true)
- })
- </script>
|