|
- <template>
- <div>
- <AppHero
- kicker="INVITE FRIENDS"
- title="邀请好友,一起解锁 AI 创作"
- desc="把好用的 AI 工具分享给朋友,好友完成新用户注册后,双方都能获得点数奖励。"
- >
- <template #aside>
- <div class="w-full overflow-hidden rounded-3xl bg-gradient-to-br from-[var(--c-0758c9)] via-[var(--c-0b8cff)] to-[var(--c-54c8ff)] p-5 text-white shadow-[0_20px_42px_var(--sh-11-126-225-240)] lg:w-[350px] lg:p-6">
- <div class="flex items-center gap-2 text-xs font-bold text-white/75">
- <UIcon name="i-lucide-gift" class="size-4" />
- 双方奖励
- </div>
- <div class="mt-5 grid grid-cols-[1fr_auto_1fr] items-center gap-4 text-center">
- <div>
- <div class="text-4xl font-extrabold tabular-nums">{{ rewardDisplay(config.inviter_points) }}</div>
- <div class="mt-2 text-xs text-white/70">你获得点数</div>
- </div>
- <div class="h-12 w-px bg-white/20" />
- <div>
- <div class="text-4xl font-extrabold tabular-nums">{{ rewardDisplay(config.invitee_points) }}</div>
- <div class="mt-2 text-xs text-white/70">好友获得点数</div>
- </div>
- </div>
- <div class="mt-5 flex items-center justify-center gap-1.5 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>
- </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="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)]"
- aria-labelledby="invite-login-title"
- >
- <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-user-plus" class="size-7" />
- </div>
- <h2 id="invite-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>
- <button
- type="button"
- class="mt-7 inline-flex h-11 cursor-pointer items-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"
- @click="showLogin = true"
- >
- <UIcon name="i-lucide-log-in" class="size-4" />
- 立即登录
- </button>
- </section>
-
- <template v-else>
- <div v-if="loading && !loaded" class="grid gap-5 lg:grid-cols-12" role="status" aria-label="正在加载邀请信息">
- <div class="h-[330px] animate-pulse rounded-3xl bg-[var(--c-ffffff)] motion-reduce:animate-none lg:col-span-8" />
- <div class="h-[330px] animate-pulse rounded-3xl bg-[var(--c-ffffff)] motion-reduce:animate-none lg:col-span-4" />
- </div>
-
- <section
- v-else-if="loadError"
- class="rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] px-6 py-16 text-center shadow-[0_16px_40px_var(--sh-34-68-112-60)]"
- role="alert"
- >
- <UIcon name="i-lucide-circle-alert" class="mx-auto size-10 text-[var(--c-e85f1c)]" />
- <h2 class="mt-4 text-lg font-extrabold text-[var(--c-172033)]">邀请信息加载失败</h2>
- <p class="mt-2 text-sm text-[var(--c-7b8797)]">{{ 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="loadData"
- >
- 重新加载
- </button>
- </section>
-
- <div v-else class="grid gap-5 lg:grid-cols-12 lg:items-start">
- <div class="space-y-5 lg:col-span-8">
- <section 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="share-title">
- <div class="border-b border-[var(--c-edf2f7)] px-5 py-5 sm:px-6">
- <div class="flex flex-wrap items-center justify-between gap-3">
- <div>
- <h2 id="share-title" class="text-lg font-extrabold text-[var(--c-172033)]">分享专属邀请</h2>
- <p class="mt-1 text-xs text-[var(--c-8a97a8)]">每位好友仅能绑定一位邀请人,奖励以注册时的活动配置为准。</p>
- </div>
- <span
- class="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-bold"
- :class="activityEnabled ? 'bg-[var(--c-edfdf4)] text-[var(--c-159a54)]' : 'bg-[var(--c-f1f5fa)] text-[var(--c-7b8797)]'"
- >
- <span class="size-1.5 rounded-full" :class="activityEnabled ? 'bg-[var(--c-17a65a)]' : 'bg-[var(--c-9aa5b5)]'" />
- {{ activityEnabled ? '活动进行中' : '活动暂未开放' }}
- </span>
- </div>
- </div>
-
- <div class="p-5 sm:p-6">
- <div class="rounded-2xl border border-[var(--c-e6edf5)] bg-[var(--c-f8fbff)] p-3.5">
- <div class="text-[11px] font-bold text-[var(--c-8a97a8)]">你的邀请链接</div>
- <div class="mt-2 flex min-w-0 items-center gap-3">
- <div class="min-w-0 flex-1 truncate text-sm font-medium text-[var(--c-4b5b70)]">{{ inviteUrl }}</div>
- <button
- type="button"
- :disabled="!activityEnabled"
- class="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-xl bg-[var(--c-ffffff)] text-[var(--c-0b8cff)] shadow-sm transition-colors hover:bg-[var(--c-eef7ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] disabled:cursor-not-allowed disabled:text-[var(--c-a0aabb)]"
- aria-label="复制邀请链接"
- @click="copyLink"
- >
- <UIcon name="i-lucide-copy" class="size-4" />
- </button>
- </div>
- </div>
-
- <div class="mt-4 grid gap-3 sm:grid-cols-2">
- <button
- type="button"
- :disabled="!activityEnabled"
- class="flex h-12 cursor-pointer items-center justify-center gap-2 rounded-full bg-gradient-to-r from-[var(--c-0b8cff)] to-[var(--c-3aa9ff)] text-sm font-extrabold text-white shadow-[0_10px_22px_var(--sh-11-140-255-220)] transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-45"
- @click="shareLink"
- >
- <UIcon name="i-lucide-share-2" class="size-4" />
- 分享邀请链接
- </button>
- <button
- type="button"
- :disabled="!activityEnabled"
- class="flex h-12 cursor-pointer items-center justify-center gap-2 rounded-full border border-[var(--c-dbe9f7)] bg-[var(--c-f5faff)] text-sm font-extrabold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e8f5ff)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-0b8cff)] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-45"
- @click="openQr"
- >
- <UIcon name="i-lucide-qr-code" class="size-4" />
- 微信小程序码
- </button>
- </div>
- </div>
- </section>
-
- <section class="grid gap-3 sm:grid-cols-2" aria-label="邀请统计">
- <article class="rounded-2xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] p-5 shadow-[0_8px_24px_var(--sh-34-68-112-45)]">
- <div class="flex items-start justify-between">
- <div>
- <p class="text-xs font-bold text-[var(--c-7b8797)]">成功邀请</p>
- <p class="mt-2 text-3xl font-extrabold text-[var(--c-172033)] tabular-nums">{{ stats.success || 0 }}</p>
- <p class="mt-1 text-[11px] text-[var(--c-9aa5b5)]">已完成注册并发放奖励</p>
- </div>
- <span class="flex size-10 items-center justify-center rounded-xl bg-[var(--c-eef7ff)] text-[var(--c-0b8cff)]">
- <UIcon name="i-lucide-users" class="size-4.5" />
- </span>
- </div>
- </article>
- <article class="rounded-2xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] p-5 shadow-[0_8px_24px_var(--sh-34-68-112-45)]">
- <div class="flex items-start justify-between">
- <div>
- <p class="text-xs font-bold text-[var(--c-7b8797)]">累计奖励</p>
- <p class="mt-2 text-3xl font-extrabold text-[var(--c-159a54)] tabular-nums">+{{ stats.inviter_points || 0 }}</p>
- <p class="mt-1 text-[11px] text-[var(--c-9aa5b5)]">已自动计入可用点数</p>
- </div>
- <span class="flex size-10 items-center justify-center rounded-xl bg-[var(--c-edfdf4)] text-[var(--c-159a54)]">
- <UIcon name="i-lucide-coins" class="size-4.5" />
- </span>
- </div>
- </article>
- </section>
-
- <section class="rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] p-5 shadow-[0_12px_30px_var(--sh-34-68-112-50)] sm:p-6" aria-labelledby="steps-title">
- <h2 id="steps-title" class="text-lg font-extrabold text-[var(--c-172033)]">三步获得奖励</h2>
- <div class="mt-5 grid gap-3 sm:grid-cols-3">
- <div v-for="step in steps" :key="step.no" class="rounded-2xl bg-[var(--c-f6f9fd)] p-4">
- <div class="flex items-center justify-between">
- <span class="text-[11px] font-extrabold tracking-[0.12em] text-[var(--c-0b8cff)]">STEP {{ step.no }}</span>
- <UIcon :name="step.icon" class="size-4 text-[var(--c-8a97a8)]" />
- </div>
- <h3 class="mt-4 text-sm font-extrabold text-[var(--c-172033)]">{{ step.title }}</h3>
- <p class="mt-1.5 text-xs leading-5 text-[var(--c-7b8797)]">{{ step.desc }}</p>
- </div>
- </div>
- </section>
- </div>
-
- <aside class="space-y-5 lg:col-span-4">
- <section class="rounded-3xl border border-[var(--c-e7eef7)] bg-[var(--c-ffffff)] p-5 shadow-[0_12px_30px_var(--sh-34-68-112-50)] sm:p-6">
- <div class="flex items-center justify-between gap-3">
- <h2 class="text-base font-extrabold text-[var(--c-172033)]">最近邀请</h2>
- <NuxtLink to="/invite/records" class="inline-flex items-center gap-1 text-xs font-bold text-[var(--c-0b8cff)] hover:underline">
- 全部记录
- <UIcon name="i-lucide-chevron-right" class="size-3.5" />
- </NuxtLink>
- </div>
-
- <div v-if="recentList.length" class="mt-4 divide-y divide-[var(--c-edf2f7)]">
- <div v-for="item in recentList" :key="item.id" class="flex items-center gap-3 py-3 first:pt-0 last:pb-0">
- <div class="flex size-10 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 flex-1">
- <div class="truncate text-sm font-bold text-[var(--c-172033)]">{{ item.user_name || '好友' }}</div>
- <div class="mt-0.5 text-[11px] text-[var(--c-9aa5b5)]">{{ formatTime(item.reward_time || item.add_time) }}</div>
- </div>
- <span class="shrink-0 text-xs font-extrabold" :class="Number(item.status) === 1 ? 'text-[var(--c-159a54)]' : 'text-[var(--c-8a97a8)]'">
- {{ Number(item.status) === 1 ? `+${item.inviter_points || 0}` : item.status_text }}
- </span>
- </div>
- </div>
-
- <div v-else class="py-9 text-center">
- <UIcon name="i-lucide-user-round-plus" class="mx-auto size-8 text-[var(--c-b1bbc8)]" />
- <p class="mt-3 text-sm font-bold text-[var(--c-66758a)]">还没有邀请记录</p>
- <p class="mt-1 text-xs text-[var(--c-9aa5b5)]">分享给第一位好友吧</p>
- </div>
- </section>
-
- <section class="rounded-3xl bg-gradient-to-br from-[var(--c-f0f8ff)] to-[var(--c-f7fbff)] p-5 ring-1 ring-inset ring-[var(--c-dcecfb)] sm:p-6">
- <div class="flex items-center gap-2 text-sm font-extrabold text-[var(--c-172033)]">
- <UIcon name="i-lucide-scroll-text" class="size-4 text-[var(--c-0b8cff)]" />
- 活动规则
- </div>
- <p class="mt-3 text-xs leading-6 text-[var(--c-66758a)]">{{ config.rule_text || '好友通过你的邀请完成新用户注册后,你和好友都可获得点数奖励。' }}</p>
- <div class="mt-4 border-t border-[var(--c-dcecfb)] pt-4 text-[11px] leading-5 text-[var(--c-8a97a8)]">
- 每位新用户仅可触发一次邀请奖励;异常注册或超过每日奖励上限时,本次邀请可能只记录不发放。
- </div>
- </section>
- </aside>
- </div>
- </template>
- </main>
-
- <InviteShareModal
- :open="qrOpen"
- :qrcode-url="qrcodeUrl"
- :invite-url="inviteUrl"
- :loading="qrLoading"
- @close="qrOpen = false"
- @copy="copyLink"
- />
- </div>
- </template>
-
- <script setup>
- import dayjs from 'dayjs'
-
- useSeoMeta({
- title: '邀请好友 - AI在线',
- description: '邀请好友体验 AI 在线,双方领取 AI 创作点数。',
- robots: 'noindex, nofollow',
- })
-
- const { userInfo, isLogin, showLogin } = useUser()
- const { post } = useApi()
- const { buildInviteUrl } = useInvite()
- const toast = useToast()
-
- const config = ref({})
- const stats = ref({})
- const recentList = ref([])
- const inviteCode = ref('')
- const loading = ref(false)
- const loaded = ref(false)
- const loadError = ref('')
- const qrOpen = ref(false)
- const qrLoading = ref(false)
- const qrcodeUrl = ref('')
-
- const steps = [
- { no: '01', title: '分享专属邀请', desc: '复制网页链接,或让好友扫描微信小程序码。', icon: 'i-lucide-share-2' },
- { no: '02', title: '好友完成注册', desc: '好友通过邀请落地页登录并完成新用户注册。', icon: 'i-lucide-user-check' },
- { no: '03', title: '点数自动到账', desc: '系统核验成功后,双方奖励直接计入点数余额。', icon: 'i-lucide-coins' },
- ]
-
- const activityEnabled = computed(() => Number(config.value.status ?? 1) === 1)
- const inviteUrl = computed(() => buildInviteUrl(inviteCode.value || userInfo.value.open_id))
-
- function rewardDisplay(value) {
- if (!loaded.value) return '—'
- return Number(value || 0)
- }
-
- async function loadData() {
- if (!isLogin.value || loading.value) return
- loading.value = true
- loadError.value = ''
- try {
- const [myRes, listRes] = await Promise.all([
- post('/invite/my'),
- post('/invite/list', { page_no: 1, page_size: 4 }),
- ])
- config.value = myRes.data?.config || {}
- stats.value = myRes.data?.stats || {}
- inviteCode.value = myRes.data?.invite_code || ''
- recentList.value = listRes.list || []
- loaded.value = true
- } catch (error) {
- loadError.value = error.message || '网络开小差了,请稍后重试。'
- } finally {
- loading.value = false
- }
- }
-
- async function copyText(text) {
- if (navigator.clipboard && window.isSecureContext) {
- await navigator.clipboard.writeText(text)
- return
- }
- const input = document.createElement('textarea')
- input.value = text
- input.style.position = 'fixed'
- input.style.opacity = '0'
- document.body.appendChild(input)
- input.select()
- document.execCommand('copy')
- input.remove()
- }
-
- async function copyLink() {
- if (!activityEnabled.value) return
- try {
- await copyText(inviteUrl.value)
- toast.add({ title: '邀请链接已复制', color: 'success' })
- } catch {
- toast.add({ title: '复制失败,请手动复制链接', color: 'error' })
- }
- }
-
- async function shareLink() {
- if (!activityEnabled.value) return
- if (navigator.share) {
- try {
- await navigator.share({
- title: config.value.share_title || '邀请你体验 AI ONLINE',
- text: config.value.share_desc || '登录注册即可领取 AI 创作点数',
- url: inviteUrl.value,
- })
- return
- } catch (error) {
- if (error?.name === 'AbortError') return
- }
- }
- await copyLink()
- }
-
- async function openQr() {
- if (!activityEnabled.value) return
- qrOpen.value = true
- if (qrcodeUrl.value || qrLoading.value) return
- qrLoading.value = true
- try {
- const res = await post('/invite/qrcode')
- qrcodeUrl.value = res.data?.qrcode_url || ''
- } catch (error) {
- toast.add({ title: error.message || '小程序码生成失败', color: 'error' })
- } finally {
- qrLoading.value = false
- }
- }
-
- 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')
- }
-
- onMounted(loadData)
- watch(isLogin, (value) => {
- if (value) loadData()
- })
- </script>
|