You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

261 rivejä
11 KiB

  1. <template>
  2. <div>
  3. <AppHero
  4. kicker="INVITE RECORDS"
  5. title="邀请记录"
  6. desc="查看好友注册状态与邀请奖励发放情况。"
  7. >
  8. <template #aside>
  9. <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]">
  10. <div class="grid grid-cols-3 gap-3 text-center">
  11. <div v-for="item in summaryCards" :key="item.label">
  12. <div class="text-2xl font-extrabold tabular-nums" :class="item.tone">{{ loaded ? item.value : '—' }}</div>
  13. <div class="mt-1.5 text-[11px] font-bold text-[var(--c-8a97a8)]">{{ item.label }}</div>
  14. </div>
  15. </div>
  16. </div>
  17. </template>
  18. </AppHero>
  19. <main class="mx-auto max-w-6xl px-4 py-7 sm:px-6 lg:px-8 lg:py-10">
  20. <section
  21. v-if="!isLogin"
  22. 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)]"
  23. >
  24. <UIcon name="i-lucide-lock-keyhole" class="mx-auto size-10 text-[var(--c-a0aabb)]" />
  25. <h2 class="mt-4 text-lg font-extrabold text-[var(--c-172033)]">登录后查看邀请记录</h2>
  26. <button
  27. type="button"
  28. class="mt-6 cursor-pointer rounded-full bg-[var(--c-0b8cff)] px-7 py-2.5 text-sm font-extrabold text-white"
  29. @click="showLogin = true"
  30. >
  31. 立即登录
  32. </button>
  33. </section>
  34. <section
  35. v-else
  36. class="overflow-hidden rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] shadow-[0_16px_40px_var(--sh-34-68-112-60)]"
  37. aria-labelledby="record-title"
  38. :aria-busy="loading"
  39. >
  40. <header class="flex flex-wrap items-end justify-between gap-4 border-b border-[var(--c-edf2f7)] px-5 py-5 sm:px-6">
  41. <div>
  42. <h2 id="record-title" class="text-lg font-extrabold text-[var(--c-172033)]">全部记录</h2>
  43. <p class="mt-1 text-xs text-[var(--c-8a97a8)]">共 {{ total }} 条邀请,最近的记录排在最前。</p>
  44. </div>
  45. <NuxtLink
  46. to="/invite"
  47. 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)]"
  48. >
  49. <UIcon name="i-lucide-user-plus" class="size-3.5" />
  50. 继续邀请
  51. </NuxtLink>
  52. </header>
  53. <div v-if="initialLoading" class="divide-y divide-[var(--c-edf2f7)]" role="status" aria-label="正在加载邀请记录">
  54. <div v-for="i in 5" :key="i" class="flex items-center gap-4 px-5 py-5 sm:px-6">
  55. <div class="size-11 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
  56. <div class="min-w-0 flex-1">
  57. <div class="h-3.5 w-28 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
  58. <div class="mt-3 h-3 w-48 max-w-full animate-pulse rounded-full bg-[var(--c-f1f5fa)] motion-reduce:animate-none" />
  59. </div>
  60. <div class="h-5 w-16 animate-pulse rounded-full bg-[var(--c-edf2f7)] motion-reduce:animate-none" />
  61. </div>
  62. </div>
  63. <div v-else-if="loadError && !list.length" class="px-5 py-16 text-center" role="alert">
  64. <UIcon name="i-lucide-circle-alert" class="mx-auto size-9 text-[var(--c-e85f1c)]" />
  65. <h3 class="mt-4 text-base font-extrabold text-[var(--c-172033)]">记录加载失败</h3>
  66. <p class="mt-2 text-sm text-[var(--c-8a97a8)]">{{ loadError }}</p>
  67. <button
  68. type="button"
  69. class="mt-6 cursor-pointer rounded-full bg-[var(--c-eef7ff)] px-6 py-2.5 text-sm font-bold text-[var(--c-0b8cff)]"
  70. @click="fetchList(true)"
  71. >
  72. 重新加载
  73. </button>
  74. </div>
  75. <div v-else-if="list.length">
  76. <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">
  77. <span>受邀好友</span>
  78. <span>状态</span>
  79. <span class="text-right">邀请奖励</span>
  80. </div>
  81. <div class="divide-y divide-[var(--c-edf2f7)]">
  82. <article
  83. v-for="item in list"
  84. :key="item.id"
  85. 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"
  86. >
  87. <div class="flex min-w-0 items-center gap-3.5">
  88. <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)]">
  89. <img v-if="item.avatar" :src="item.avatar" :alt="item.user_name || '好友'" class="size-full object-cover">
  90. <span v-else>{{ getInitial(item.user_name) }}</span>
  91. </div>
  92. <div class="min-w-0">
  93. <h3 class="truncate text-sm font-extrabold text-[var(--c-172033)]">{{ item.user_name || '好友' }}</h3>
  94. <p class="mt-1 text-xs text-[var(--c-8a97a8)]">{{ recordDescription(item) }}</p>
  95. <p class="mt-1.5 inline-flex items-center gap-1 text-[11px] text-[var(--c-9aa5b5)]">
  96. <UIcon name="i-lucide-clock-3" class="size-3" />
  97. {{ formatTime(item.reward_time || item.add_time) }}
  98. </p>
  99. </div>
  100. </div>
  101. <div>
  102. <span class="inline-flex rounded-full px-2.5 py-1 text-[11px] font-extrabold" :class="statusTone(item.status)">
  103. {{ item.status_text || statusText(item.status) }}
  104. </span>
  105. </div>
  106. <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">
  107. <span class="text-[11px] text-[var(--c-9aa5b5)] sm:hidden">邀请奖励</span>
  108. <div>
  109. <div class="text-lg font-extrabold tabular-nums" :class="Number(item.status) === 1 ? 'text-[var(--c-159a54)]' : 'text-[var(--c-8a97a8)]'">
  110. {{ Number(item.status) === 1 ? `+${item.inviter_points || 0}` : '—' }}
  111. </div>
  112. <div v-if="Number(item.status) === 1" class="mt-1 text-[11px] text-[var(--c-9aa5b5)]">点数已到账</div>
  113. </div>
  114. </div>
  115. </article>
  116. </div>
  117. </div>
  118. <div v-else class="px-5 py-16 text-center">
  119. <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)]">
  120. <UIcon name="i-lucide-user-round-plus" class="size-6" />
  121. </div>
  122. <h3 class="mt-6 text-base font-extrabold text-[var(--c-172033)]">还没有邀请记录</h3>
  123. <p class="mt-2 text-sm text-[var(--c-8a97a8)]">分享邀请链接,好友注册成功后会展示在这里。</p>
  124. <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)]">
  125. 去邀请好友
  126. </NuxtLink>
  127. </div>
  128. <footer v-if="list.length" class="border-t border-[var(--c-edf2f7)] px-5 py-4 sm:px-6">
  129. <div v-if="loading" class="flex items-center justify-center gap-2 text-xs font-bold text-[var(--c-8a97a8)]" role="status">
  130. <span class="size-4 animate-spin rounded-full border-2 border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)] motion-reduce:animate-none" />
  131. 正在加载更多
  132. </div>
  133. <button
  134. v-else-if="hasMore"
  135. type="button"
  136. 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)]"
  137. @click="loadMore"
  138. >
  139. 加载更多
  140. <UIcon name="i-lucide-chevron-down" class="size-3.5" />
  141. </button>
  142. <div v-else class="text-center text-[11px] text-[var(--c-a0aabb)]">没有更多记录了</div>
  143. </footer>
  144. </section>
  145. </main>
  146. </div>
  147. </template>
  148. <script setup>
  149. import dayjs from 'dayjs'
  150. useSeoMeta({
  151. title: '邀请记录 - AI在线',
  152. description: '查看好友邀请和点数奖励记录。',
  153. robots: 'noindex, nofollow',
  154. })
  155. const { isLogin, showLogin } = useUser()
  156. const { post } = useApi()
  157. const list = ref([])
  158. const stats = ref({})
  159. const page = ref(1)
  160. const pageSize = 10
  161. const total = ref(0)
  162. const loading = ref(false)
  163. const loaded = ref(false)
  164. const loadError = ref('')
  165. const initialLoading = computed(() => loading.value && !list.value.length)
  166. const hasMore = computed(() => list.value.length < total.value)
  167. const summaryCards = computed(() => [
  168. { label: '全部邀请', value: stats.value.total || total.value || 0, tone: 'text-[var(--c-172033)]' },
  169. { label: '成功邀请', value: stats.value.success || 0, tone: 'text-[var(--c-0b8cff)]' },
  170. { label: '累计奖励', value: stats.value.inviter_points || 0, tone: 'text-[var(--c-159a54)]' },
  171. ])
  172. async function fetchList(reset = false) {
  173. if (!isLogin.value || loading.value) return false
  174. if (reset) {
  175. page.value = 1
  176. list.value = []
  177. total.value = 0
  178. }
  179. loading.value = true
  180. loadError.value = ''
  181. try {
  182. const res = await post('/invite/list', {
  183. page_no: page.value,
  184. page_size: pageSize,
  185. })
  186. const rows = res.list || []
  187. const data = res.data || {}
  188. list.value = reset ? rows : list.value.concat(rows)
  189. total.value = Number(data.total ?? rows.length)
  190. stats.value = data.stats || {}
  191. loaded.value = true
  192. return true
  193. } catch (error) {
  194. loadError.value = error.message || '网络开小差了,请稍后重试。'
  195. return false
  196. } finally {
  197. loading.value = false
  198. }
  199. }
  200. async function loadMore() {
  201. if (loading.value || !hasMore.value) return
  202. page.value += 1
  203. const success = await fetchList()
  204. if (!success) page.value -= 1
  205. }
  206. function getInitial(name = '') {
  207. return String(name || '友').slice(0, 1)
  208. }
  209. function formatTime(value) {
  210. if (!value) return '时间未知'
  211. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  212. }
  213. function statusText(status) {
  214. const value = Number(status)
  215. if (value === 1) return '已奖励'
  216. if (value === 2) return '已记录'
  217. if (value < 0) return '发放失败'
  218. return '待发放'
  219. }
  220. function statusTone(status) {
  221. const value = Number(status)
  222. if (value === 1) return 'bg-[var(--c-edfdf4)] text-[var(--c-159a54)]'
  223. if (value === 0) return 'bg-[var(--c-fff4ec)] text-[var(--c-d95414)]'
  224. if (value < 0) return 'bg-[var(--c-fff1f0)] text-[var(--c-d92d20)]'
  225. return 'bg-[var(--c-f1f5fa)] text-[var(--c-66758a)]'
  226. }
  227. function recordDescription(item) {
  228. const value = Number(item.status)
  229. if (value === 1) return '好友注册成功,奖励已发放'
  230. if (value === 2) return '邀请已记录,本次未触发奖励'
  231. if (value < 0) return item.remark || '奖励发放失败'
  232. return '等待系统发放奖励'
  233. }
  234. onMounted(() => fetchList(true))
  235. watch(isLogin, (value) => {
  236. if (value) fetchList(true)
  237. })
  238. </script>