InviteView.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <script setup lang="ts">
  2. // P22 邀请好友
  3. import { ref, computed, watch, onMounted } from 'vue'
  4. import { storeToRefs } from 'pinia'
  5. import { useI18n } from 'vue-i18n'
  6. import { useAccountStore } from '@/stores/account'
  7. import { qrCodeImageUrl } from '@/utils/qr'
  8. const { t } = useI18n()
  9. const store = useAccountStore()
  10. const { inviteCode, loading } = storeToRefs(store)
  11. onMounted(() => {
  12. store.fetchInfo()
  13. store.fetchInviteInfo()
  14. })
  15. const inviteLink = computed(() =>
  16. inviteCode.value ? `${window.location.origin}/register?code=${inviteCode.value}` : ''
  17. )
  18. const qrSrc = ref('')
  19. watch(inviteLink, async (link) => { qrSrc.value = link ? await qrCodeImageUrl(link, 200) : '' }, { immediate: true })
  20. const copied = ref<'code' | 'link' | null>(null)
  21. function copy(type: 'code' | 'link') {
  22. const text = type === 'code' ? inviteCode.value : inviteLink.value
  23. navigator.clipboard.writeText(text).catch(() => {})
  24. copied.value = type
  25. setTimeout(() => { copied.value = null }, 1500)
  26. }
  27. const steps = computed(() => [
  28. { num: '01', title: t('invite.step1Title'), desc: t('invite.step1Desc') },
  29. { num: '02', title: t('invite.step2Title'), desc: t('invite.step2Desc') },
  30. { num: '03', title: t('invite.step3Title'), desc: t('invite.step3Desc') },
  31. ])
  32. </script>
  33. <template>
  34. <main class="page-main">
  35. <!-- Hero -->
  36. <section class="hero-section">
  37. <div class="page-container hero-inner">
  38. <div class="hero-content">
  39. <h1 class="hero-title">{{ t('invite.heroLine1') }}<br>{{ t('invite.heroLine2') }}</h1>
  40. <p class="hero-desc">{{ t('invite.heroDesc') }}</p>
  41. </div>
  42. </div>
  43. </section>
  44. <div class="page-container main-content">
  45. <!-- Invite Info -->
  46. <div class="invite-card dark-card">
  47. <h2 class="card-title">{{ t('invite.myInviteInfo') }}</h2>
  48. <div v-if="loading && !inviteCode" class="loading-row">
  49. <div class="spinner" />
  50. </div>
  51. <template v-else>
  52. <!-- Code row -->
  53. <div class="info-row">
  54. <span class="info-label">{{ t('invite.inviteCodeLabel') }}</span>
  55. <div class="info-value-wrap">
  56. <span class="info-code">{{ inviteCode }}</span>
  57. <button class="copy-btn" type="button" @click="copy('code')">
  58. {{ copied === 'code' ? t('invite.copiedDone') : t('invite.copyShort') }}
  59. </button>
  60. </div>
  61. </div>
  62. <!-- Link row -->
  63. <div class="info-row">
  64. <span class="info-label">{{ t('invite.inviteLinkShort') }}</span>
  65. <div class="info-value-wrap">
  66. <span class="info-link">{{ inviteLink }}</span>
  67. <button class="copy-btn" type="button" @click="copy('link')">
  68. {{ copied === 'link' ? t('invite.copiedDone') : t('invite.copyShort') }}
  69. </button>
  70. </div>
  71. </div>
  72. <!-- QR -->
  73. <div class="qr-row">
  74. <span class="info-label">{{ t('invite.qrCodeLabel') }}</span>
  75. <div class="qr-placeholder">
  76. <img v-if="qrSrc" :src="qrSrc" :alt="t('invite.qrInviteAlt')" class="qr-img" width="120" height="120" />
  77. <div v-else class="qr-inner"><div class="spinner" /></div>
  78. <p class="qr-hint">{{ t('invite.scanToRegister') }}</p>
  79. </div>
  80. </div>
  81. </template>
  82. </div>
  83. <!-- Rebate Rules -->
  84. <div class="steps-section">
  85. <h2 class="section-title">{{ t('invite.rebateRulesTitle') }}</h2>
  86. <div class="steps-grid">
  87. <div v-for="step in steps" :key="step.num" class="step-card dark-card">
  88. <div class="step-num">{{ step.num }}</div>
  89. <h3 class="step-title">{{ step.title }}</h3>
  90. <p class="step-desc">{{ step.desc }}</p>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. </main>
  96. </template>
  97. <style scoped>
  98. /* Hero */
  99. .hero-section {
  100. background: var(--color-bg-secondary);
  101. padding: var(--space-12) 0;
  102. border-bottom: 1px solid var(--color-border);
  103. }
  104. .hero-inner {
  105. display: block;
  106. }
  107. .hero-title {
  108. font-size: 44px;
  109. font-weight: var(--fw-bold);
  110. color: var(--color-text-primary);
  111. line-height: 1.3;
  112. margin-bottom: var(--space-4);
  113. }
  114. .hero-desc {
  115. font-size: var(--text-body);
  116. color: var(--color-text-secondary);
  117. line-height: 1.7;
  118. }
  119. /* Main content */
  120. .main-content { padding-top: var(--space-8); padding-bottom: var(--space-16); }
  121. /* Invite card */
  122. .invite-card {
  123. padding: var(--space-6);
  124. margin-bottom: var(--space-8);
  125. }
  126. .card-title {
  127. font-size: var(--text-body);
  128. font-weight: var(--fw-semibold);
  129. color: var(--color-text-primary);
  130. margin-bottom: var(--space-6);
  131. }
  132. .loading-row { display: flex; justify-content: center; padding: var(--space-8); }
  133. .spinner {
  134. width: 28px; height: 28px;
  135. border: 2px solid var(--color-border);
  136. border-top-color: var(--color-primary);
  137. border-radius: 50%;
  138. animation: spin 0.8s linear infinite;
  139. }
  140. @keyframes spin { to { transform: rotate(360deg); } }
  141. .info-row {
  142. display: flex;
  143. align-items: center;
  144. gap: var(--space-6);
  145. padding: var(--space-4) 0;
  146. border-bottom: 1px solid var(--color-border);
  147. }
  148. .info-label {
  149. font-size: var(--text-small);
  150. color: var(--color-text-secondary);
  151. flex-shrink: 0;
  152. width: 60px;
  153. }
  154. .info-value-wrap {
  155. display: flex;
  156. align-items: center;
  157. gap: var(--space-4);
  158. flex: 1;
  159. min-width: 0;
  160. }
  161. .info-code {
  162. font-size: var(--text-h3);
  163. font-weight: var(--fw-bold);
  164. color: var(--color-primary);
  165. font-family: var(--font-family-num);
  166. letter-spacing: 2px;
  167. }
  168. .info-link {
  169. font-size: var(--text-small);
  170. color: var(--color-text-secondary);
  171. font-family: var(--font-family-num);
  172. overflow: hidden;
  173. text-overflow: ellipsis;
  174. white-space: nowrap;
  175. flex: 1;
  176. }
  177. .copy-btn {
  178. font-size: var(--text-caption);
  179. color: var(--color-primary);
  180. border: 1px solid var(--color-primary);
  181. border-radius: var(--radius-sm);
  182. padding: 2px var(--space-3);
  183. cursor: pointer;
  184. background: transparent;
  185. white-space: nowrap;
  186. transition: background var(--transition);
  187. flex-shrink: 0;
  188. }
  189. .copy-btn:hover { background: rgba(240,185,11,0.1); }
  190. .qr-row {
  191. display: flex;
  192. align-items: flex-start;
  193. gap: var(--space-6);
  194. padding-top: var(--space-4);
  195. }
  196. .qr-placeholder {
  197. display: flex;
  198. flex-direction: column;
  199. align-items: center;
  200. gap: var(--space-2);
  201. }
  202. .qr-img {
  203. border: 1px solid var(--color-border);
  204. border-radius: var(--radius-sm);
  205. background: #fff;
  206. padding: 4px;
  207. }
  208. .qr-hint { font-size: var(--text-caption); color: var(--color-text-muted); }
  209. /* Steps */
  210. .steps-section { margin-bottom: var(--space-8); }
  211. .section-title {
  212. font-size: var(--text-h2);
  213. font-weight: var(--fw-bold);
  214. color: var(--color-text-primary);
  215. margin-bottom: var(--space-6);
  216. }
  217. .steps-grid {
  218. display: grid;
  219. grid-template-columns: repeat(3, 1fr);
  220. gap: var(--space-4);
  221. }
  222. .step-card { padding: var(--space-6); }
  223. .step-num {
  224. font-size: 32px;
  225. font-weight: var(--fw-bold);
  226. color: var(--color-primary);
  227. font-family: var(--font-family-num);
  228. opacity: 0.8;
  229. margin-bottom: var(--space-3);
  230. }
  231. .step-title {
  232. font-size: var(--text-body);
  233. font-weight: var(--fw-semibold);
  234. color: var(--color-text-primary);
  235. margin-bottom: var(--space-2);
  236. }
  237. .step-desc {
  238. font-size: var(--text-small);
  239. color: var(--color-text-secondary);
  240. line-height: 1.6;
  241. }
  242. /* ── 移动端 ── */
  243. @media (max-width: 768px) {
  244. .steps-grid { grid-template-columns: 1fr; }
  245. }
  246. </style>