| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- <script setup lang="ts">
- // P22 邀请好友
- import { ref, computed, watch, onMounted } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useI18n } from 'vue-i18n'
- import { useAccountStore } from '@/stores/account'
- import { qrCodeImageUrl } from '@/utils/qr'
- const { t } = useI18n()
- const store = useAccountStore()
- const { inviteCode, loading } = storeToRefs(store)
- onMounted(() => {
- store.fetchInfo()
- store.fetchInviteInfo()
- })
- const inviteLink = computed(() =>
- inviteCode.value ? `${window.location.origin}/register?code=${inviteCode.value}` : ''
- )
- const qrSrc = ref('')
- watch(inviteLink, async (link) => { qrSrc.value = link ? await qrCodeImageUrl(link, 200) : '' }, { immediate: true })
- const copied = ref<'code' | 'link' | null>(null)
- function copy(type: 'code' | 'link') {
- const text = type === 'code' ? inviteCode.value : inviteLink.value
- navigator.clipboard.writeText(text).catch(() => {})
- copied.value = type
- setTimeout(() => { copied.value = null }, 1500)
- }
- const steps = computed(() => [
- { num: '01', title: t('invite.step1Title'), desc: t('invite.step1Desc') },
- { num: '02', title: t('invite.step2Title'), desc: t('invite.step2Desc') },
- { num: '03', title: t('invite.step3Title'), desc: t('invite.step3Desc') },
- ])
- </script>
- <template>
- <main class="page-main">
- <!-- Hero -->
- <section class="hero-section">
- <div class="page-container hero-inner">
- <div class="hero-content">
- <h1 class="hero-title">{{ t('invite.heroLine1') }}<br>{{ t('invite.heroLine2') }}</h1>
- <p class="hero-desc">{{ t('invite.heroDesc') }}</p>
- </div>
- </div>
- </section>
- <div class="page-container main-content">
- <!-- Invite Info -->
- <div class="invite-card dark-card">
- <h2 class="card-title">{{ t('invite.myInviteInfo') }}</h2>
- <div v-if="loading && !inviteCode" class="loading-row">
- <div class="spinner" />
- </div>
- <template v-else>
- <!-- Code row -->
- <div class="info-row">
- <span class="info-label">{{ t('invite.inviteCodeLabel') }}</span>
- <div class="info-value-wrap">
- <span class="info-code">{{ inviteCode }}</span>
- <button class="copy-btn" type="button" @click="copy('code')">
- {{ copied === 'code' ? t('invite.copiedDone') : t('invite.copyShort') }}
- </button>
- </div>
- </div>
- <!-- Link row -->
- <div class="info-row">
- <span class="info-label">{{ t('invite.inviteLinkShort') }}</span>
- <div class="info-value-wrap">
- <span class="info-link">{{ inviteLink }}</span>
- <button class="copy-btn" type="button" @click="copy('link')">
- {{ copied === 'link' ? t('invite.copiedDone') : t('invite.copyShort') }}
- </button>
- </div>
- </div>
- <!-- QR -->
- <div class="qr-row">
- <span class="info-label">{{ t('invite.qrCodeLabel') }}</span>
- <div class="qr-placeholder">
- <img v-if="qrSrc" :src="qrSrc" :alt="t('invite.qrInviteAlt')" class="qr-img" width="120" height="120" />
- <div v-else class="qr-inner"><div class="spinner" /></div>
- <p class="qr-hint">{{ t('invite.scanToRegister') }}</p>
- </div>
- </div>
- </template>
- </div>
- <!-- Rebate Rules -->
- <div class="steps-section">
- <h2 class="section-title">{{ t('invite.rebateRulesTitle') }}</h2>
- <div class="steps-grid">
- <div v-for="step in steps" :key="step.num" class="step-card dark-card">
- <div class="step-num">{{ step.num }}</div>
- <h3 class="step-title">{{ step.title }}</h3>
- <p class="step-desc">{{ step.desc }}</p>
- </div>
- </div>
- </div>
- </div>
- </main>
- </template>
- <style scoped>
- /* Hero */
- .hero-section {
- background: var(--color-bg-secondary);
- padding: var(--space-12) 0;
- border-bottom: 1px solid var(--color-border);
- }
- .hero-inner {
- display: block;
- }
- .hero-title {
- font-size: 44px;
- font-weight: var(--fw-bold);
- color: var(--color-text-primary);
- line-height: 1.3;
- margin-bottom: var(--space-4);
- }
- .hero-desc {
- font-size: var(--text-body);
- color: var(--color-text-secondary);
- line-height: 1.7;
- }
- /* Main content */
- .main-content { padding-top: var(--space-8); padding-bottom: var(--space-16); }
- /* Invite card */
- .invite-card {
- padding: var(--space-6);
- margin-bottom: var(--space-8);
- }
- .card-title {
- font-size: var(--text-body);
- font-weight: var(--fw-semibold);
- color: var(--color-text-primary);
- margin-bottom: var(--space-6);
- }
- .loading-row { display: flex; justify-content: center; padding: var(--space-8); }
- .spinner {
- width: 28px; height: 28px;
- border: 2px solid var(--color-border);
- border-top-color: var(--color-primary);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
- .info-row {
- display: flex;
- align-items: center;
- gap: var(--space-6);
- padding: var(--space-4) 0;
- border-bottom: 1px solid var(--color-border);
- }
- .info-label {
- font-size: var(--text-small);
- color: var(--color-text-secondary);
- flex-shrink: 0;
- width: 60px;
- }
- .info-value-wrap {
- display: flex;
- align-items: center;
- gap: var(--space-4);
- flex: 1;
- min-width: 0;
- }
- .info-code {
- font-size: var(--text-h3);
- font-weight: var(--fw-bold);
- color: var(--color-primary);
- font-family: var(--font-family-num);
- letter-spacing: 2px;
- }
- .info-link {
- font-size: var(--text-small);
- color: var(--color-text-secondary);
- font-family: var(--font-family-num);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- }
- .copy-btn {
- font-size: var(--text-caption);
- color: var(--color-primary);
- border: 1px solid var(--color-primary);
- border-radius: var(--radius-sm);
- padding: 2px var(--space-3);
- cursor: pointer;
- background: transparent;
- white-space: nowrap;
- transition: background var(--transition);
- flex-shrink: 0;
- }
- .copy-btn:hover { background: rgba(240,185,11,0.1); }
- .qr-row {
- display: flex;
- align-items: flex-start;
- gap: var(--space-6);
- padding-top: var(--space-4);
- }
- .qr-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--space-2);
- }
- .qr-img {
- border: 1px solid var(--color-border);
- border-radius: var(--radius-sm);
- background: #fff;
- padding: 4px;
- }
- .qr-hint { font-size: var(--text-caption); color: var(--color-text-muted); }
- /* Steps */
- .steps-section { margin-bottom: var(--space-8); }
- .section-title {
- font-size: var(--text-h2);
- font-weight: var(--fw-bold);
- color: var(--color-text-primary);
- margin-bottom: var(--space-6);
- }
- .steps-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: var(--space-4);
- }
- .step-card { padding: var(--space-6); }
- .step-num {
- font-size: 32px;
- font-weight: var(--fw-bold);
- color: var(--color-primary);
- font-family: var(--font-family-num);
- opacity: 0.8;
- margin-bottom: var(--space-3);
- }
- .step-title {
- font-size: var(--text-body);
- font-weight: var(--fw-semibold);
- color: var(--color-text-primary);
- margin-bottom: var(--space-2);
- }
- .step-desc {
- font-size: var(--text-small);
- color: var(--color-text-secondary);
- line-height: 1.6;
- }
- /* ── 移动端 ── */
- @media (max-width: 768px) {
- .steps-grid { grid-template-columns: 1fr; }
- }
- </style>
|