index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. <template>
  2. <div class="register-page">
  3. <!-- 顶部导航 -->
  4. <div class="nav-header">
  5. <van-icon name="arrow-left" @click="goBack" />
  6. <span class="title">{{ $t('auth.register') }}</span>
  7. <span></span>
  8. </div>
  9. <!-- 注册表单 -->
  10. <div class="form-section">
  11. <!-- 注册方式切换 -->
  12. <div class="register-tabs">
  13. <div
  14. class="tab-item"
  15. :class="{ active: registerType === 'phone' }"
  16. @click="registerType = 'phone'"
  17. >
  18. {{ $t('auth.phone') }}
  19. </div>
  20. <div
  21. class="tab-item"
  22. :class="{ active: registerType === 'email' }"
  23. @click="registerType = 'email'"
  24. >
  25. {{ $t('auth.email') }}
  26. </div>
  27. </div>
  28. <!-- 手机号注册 -->
  29. <div v-if="registerType === 'phone'" class="form-content">
  30. <div class="input-group">
  31. <van-field
  32. v-model="phoneForm.phone"
  33. type="tel"
  34. :placeholder="$t('auth.pleaseInputPhone')"
  35. clearable
  36. >
  37. <template #left-icon>
  38. <div class="area-code" @click="showAreaCode = true">
  39. +{{ areaCode }}
  40. <van-icon name="arrow-down" />
  41. </div>
  42. </template>
  43. </van-field>
  44. </div>
  45. <div class="input-group">
  46. <van-field
  47. v-model="phoneForm.code"
  48. type="number"
  49. maxlength="6"
  50. :placeholder="$t('auth.pleaseInputVerifyCode')"
  51. >
  52. <template #button>
  53. <van-button
  54. size="small"
  55. type="primary"
  56. :disabled="countdown > 0"
  57. @click="sendPhoneCode"
  58. >
  59. {{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}
  60. </van-button>
  61. </template>
  62. </van-field>
  63. </div>
  64. <div class="input-group">
  65. <van-field
  66. v-model="phoneForm.password"
  67. :type="showPassword ? 'text' : 'password'"
  68. :placeholder="$t('auth.pleaseInputPassword')"
  69. >
  70. <template #right-icon>
  71. <van-icon
  72. :name="showPassword ? 'eye-o' : 'closed-eye'"
  73. @click="showPassword = !showPassword"
  74. />
  75. </template>
  76. </van-field>
  77. </div>
  78. <div class="input-group">
  79. <van-field
  80. v-model="phoneForm.confirmPassword"
  81. :type="showConfirmPassword ? 'text' : 'password'"
  82. :placeholder="$t('auth.pleaseInputConfirmPassword')"
  83. >
  84. <template #right-icon>
  85. <van-icon
  86. :name="showConfirmPassword ? 'eye-o' : 'closed-eye'"
  87. @click="showConfirmPassword = !showConfirmPassword"
  88. />
  89. </template>
  90. </van-field>
  91. </div>
  92. <div class="input-group">
  93. <van-field
  94. v-model="phoneForm.inviteCode"
  95. :placeholder="$t('auth.inviteCodeOptional')"
  96. clearable
  97. >
  98. <template #left-icon>
  99. <van-icon name="friends-o" />
  100. </template>
  101. </van-field>
  102. </div>
  103. </div>
  104. <!-- 邮箱注册 -->
  105. <div v-if="registerType === 'email'" class="form-content">
  106. <div class="input-group">
  107. <van-field
  108. v-model="emailForm.email"
  109. type="email"
  110. :placeholder="$t('auth.pleaseInputEmail')"
  111. clearable
  112. >
  113. <template #left-icon>
  114. <van-icon name="envelop-o" />
  115. </template>
  116. </van-field>
  117. </div>
  118. <div class="input-group">
  119. <van-field
  120. v-model="emailForm.code"
  121. type="number"
  122. maxlength="6"
  123. :placeholder="$t('auth.pleaseInputVerifyCode')"
  124. >
  125. <template #button>
  126. <van-button
  127. size="small"
  128. type="primary"
  129. :disabled="countdown > 0"
  130. @click="sendEmailCode"
  131. >
  132. {{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}
  133. </van-button>
  134. </template>
  135. </van-field>
  136. </div>
  137. <div class="input-group">
  138. <van-field
  139. v-model="emailForm.password"
  140. :type="showPassword ? 'text' : 'password'"
  141. :placeholder="$t('auth.pleaseInputPassword')"
  142. >
  143. <template #right-icon>
  144. <van-icon
  145. :name="showPassword ? 'eye-o' : 'closed-eye'"
  146. @click="showPassword = !showPassword"
  147. />
  148. </template>
  149. </van-field>
  150. </div>
  151. <div class="input-group">
  152. <van-field
  153. v-model="emailForm.confirmPassword"
  154. :type="showConfirmPassword ? 'text' : 'password'"
  155. :placeholder="$t('auth.pleaseInputConfirmPassword')"
  156. >
  157. <template #right-icon>
  158. <van-icon
  159. :name="showConfirmPassword ? 'eye-o' : 'closed-eye'"
  160. @click="showConfirmPassword = !showConfirmPassword"
  161. />
  162. </template>
  163. </van-field>
  164. </div>
  165. <div class="input-group">
  166. <van-field
  167. v-model="emailForm.inviteCode"
  168. :placeholder="$t('auth.inviteCodeOptional')"
  169. clearable
  170. >
  171. <template #left-icon>
  172. <van-icon name="friends-o" />
  173. </template>
  174. </van-field>
  175. </div>
  176. </div>
  177. <!-- 用户协议 -->
  178. <div class="agreement">
  179. <van-checkbox v-model="agreed" icon-size="16px">
  180. <span class="text">
  181. {{ $t('auth.agreeTerms') }}
  182. <span class="link" @click.stop="showTerms">{{ $t('auth.termsOfService') }}</span>
  183. {{ $t('auth.and') }}
  184. <span class="link" @click.stop="showPrivacy">{{ $t('auth.privacyPolicy') }}</span>
  185. </span>
  186. </van-checkbox>
  187. </div>
  188. <!-- 注册按钮 -->
  189. <van-button
  190. class="register-btn"
  191. type="primary"
  192. block
  193. :loading="loading"
  194. :disabled="!agreed"
  195. @click="handleRegister"
  196. >
  197. {{ $t('auth.register') }}
  198. </van-button>
  199. <!-- 第三方登录 -->
  200. <div class="third-party-section">
  201. <div class="divider">
  202. <span>{{ $t('auth.or') || '或' }}</span>
  203. </div>
  204. <!-- Google 登录按钮 -->
  205. <div class="oauth-btn google-btn" :class="{ loading: googleLoading }" @click="handleGoogleLogin">
  206. <svg class="google-icon" viewBox="0 0 24 24" width="20" height="20">
  207. <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
  208. <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
  209. <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
  210. <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
  211. </svg>
  212. <span>{{ $t('auth.googleLogin') || '使用 Google 登录' }}</span>
  213. <van-loading v-if="googleLoading" size="16px" color="#fff" />
  214. </div>
  215. <!-- Telegram 登录按钮 -->
  216. <div class="oauth-btn telegram-btn" @click="handleTelegramLogin" v-if="telegramBotName">
  217. <svg class="telegram-icon" viewBox="0 0 24 24" width="20" height="20">
  218. <path fill="#fff" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5.46 7.12l-1.67 6.14c-.13.46-.47.58-.95.36l-2.62-1.93-1.26 1.22c-.14.14-.26.26-.53.26l.19-2.67 4.84-4.37c.21-.19-.05-.29-.32-.12l-5.99 3.78-2.58-.81c-.56-.17-.57-.56.12-.83l10.1-3.9c.46-.17.87.11.67.87z"/>
  219. </svg>
  220. <span>{{ $t('auth.telegramLogin') || 'Telegram 登录' }}</span>
  221. </div>
  222. <!-- TikTok 登录按钮 -->
  223. <div class="oauth-btn tiktok-btn" :class="{ loading: tiktokLoading }" @click="handleTiktokLogin" v-if="tiktokClientKey">
  224. <svg class="tiktok-icon" viewBox="0 0 24 24" width="20" height="20">
  225. <path fill="#25F4EE" d="M16.6 5.82s.51.5 0 0A4.278 4.278 0 0 1 15.54 3h-3.09v12.4a2.592 2.592 0 0 1-2.59 2.5c-1.42 0-2.6-1.16-2.6-2.6 0-1.72 1.66-3.01 3.37-2.48V9.66c-3.45-.46-6.47 2.22-6.47 5.64 0 3.33 2.76 5.7 5.69 5.7 3.14 0 5.69-2.55 5.69-5.7V9.01a7.35 7.35 0 0 0 4.3 1.38V7.3s-1.88.09-3.24-1.48z"/>
  226. <path fill="#FE2C55" d="M17.6 5.82s.51.5 0 0A4.278 4.278 0 0 1 16.54 3h-3.09v12.4a2.592 2.592 0 0 1-2.59 2.5c-1.42 0-2.6-1.16-2.6-2.6 0-1.72 1.66-3.01 3.37-2.48V9.66c-3.45-.46-6.47 2.22-6.47 5.64 0 3.33 2.76 5.7 5.69 5.7 3.14 0 5.69-2.55 5.69-5.7V9.01a7.35 7.35 0 0 0 4.3 1.38V7.3s-1.88.09-3.24-1.48z"/>
  227. </svg>
  228. <span>{{ $t('auth.tiktokLogin') || 'TikTok 登录' }}</span>
  229. <van-loading v-if="tiktokLoading" size="16px" color="#fff" />
  230. </div>
  231. <!-- Zalo 登录按钮 -->
  232. <div class="oauth-btn zalo-btn" @click="handleZaloLogin">
  233. <svg class="zalo-icon" viewBox="0 0 24 24" width="20" height="20">
  234. <path fill="#0068FF" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5.46 7.12l-1.67 6.14c-.13.46-.47.58-.95.36l-2.62-1.93-1.26 1.22c-.14.14-.26.26-.53.26l.19-2.67 4.84-4.37c.21-.19-.05-.29-.32-.12l-5.99 3.78-2.58-.81c-.56-.17-.57-.56.12-.83l10.1-3.9c.46-.17.87.11.67.87z"/>
  235. </svg>
  236. <span>{{ $t('auth.zaloLogin') || '使用 Zalo 登录' }}</span>
  237. </div>
  238. </div>
  239. <!-- 登录入口 -->
  240. <div class="login-entry">
  241. {{ $t('auth.hasAccount') }}
  242. <span class="link" @click="goLogin">{{ $t('auth.goLogin') }}</span>
  243. </div>
  244. </div>
  245. <!-- 区号选择弹窗 -->
  246. <van-popup v-model:show="showAreaCode" position="bottom" round>
  247. <van-picker
  248. :columns="areaCodes"
  249. @confirm="onAreaCodeConfirm"
  250. @cancel="showAreaCode = false"
  251. show-toolbar
  252. />
  253. </van-popup>
  254. </div>
  255. </template>
  256. <script setup lang="ts">
  257. import { ref, reactive, onMounted } from 'vue';
  258. import { useRouter, useRoute } from 'vue-router';
  259. import { useI18n } from 'vue-i18n';
  260. import { toast } from 'vue-sonner';
  261. import { requestRegister, requestSendCode, requestOAuthLogin, requestOAuthConfig } from '@/api/auth';
  262. import { useUserStore } from '@/store/modules/userStore';
  263. const { t } = useI18n();
  264. const router = useRouter();
  265. const route = useRoute();
  266. const userStore = useUserStore();
  267. const googleLoading = ref(false);
  268. const googleClientId = ref('');
  269. const telegramBotName = ref('');
  270. const telegramBotId = ref('');
  271. const tiktokLoading = ref(false);
  272. const tiktokClientKey = ref('');
  273. const registerType = ref<'phone' | 'email'>('phone');
  274. const showPassword = ref(false);
  275. const showConfirmPassword = ref(false);
  276. const loading = ref(false);
  277. const showAreaCode = ref(false);
  278. const areaCode = ref('84');
  279. const agreed = ref(false);
  280. const countdown = ref(0);
  281. const phoneForm = reactive({
  282. phone: '',
  283. code: '',
  284. password: '',
  285. confirmPassword: '',
  286. inviteCode: (route.query.smid as string) || ''
  287. });
  288. const emailForm = reactive({
  289. email: '',
  290. code: '',
  291. password: '',
  292. confirmPassword: '',
  293. inviteCode: (route.query.smid as string) || ''
  294. });
  295. const areaCodes = [
  296. { text: '+84 Vietnam', value: '84' },
  297. { text: '+62 Indonesia', value: '62' },
  298. { text: '+86 China', value: '86' },
  299. { text: '+1 USA', value: '1' }
  300. ];
  301. const onAreaCodeConfirm = ({ selectedValues }) => {
  302. areaCode.value = selectedValues[0];
  303. showAreaCode.value = false;
  304. };
  305. const startCountdown = () => {
  306. countdown.value = 60;
  307. const timer = setInterval(() => {
  308. countdown.value--;
  309. if (countdown.value <= 0) {
  310. clearInterval(timer);
  311. }
  312. }, 1000);
  313. };
  314. const sendPhoneCode = async () => {
  315. if (!phoneForm.phone) {
  316. toast.error(t('auth.pleaseInputPhone'));
  317. return;
  318. }
  319. try {
  320. const res = await requestSendCode({
  321. type: 'phone',
  322. account: phoneForm.phone,
  323. areaCode: areaCode.value,
  324. scene: 'register'
  325. });
  326. if (res.code === 200) {
  327. toast.success('验证码已发送');
  328. startCountdown();
  329. }
  330. } catch (e) {
  331. // error handled in http interceptor
  332. }
  333. };
  334. const sendEmailCode = async () => {
  335. if (!emailForm.email) {
  336. toast.error(t('auth.pleaseInputEmail'));
  337. return;
  338. }
  339. try {
  340. const res = await requestSendCode({
  341. type: 'email',
  342. account: emailForm.email,
  343. scene: 'register'
  344. });
  345. if (res.code === 200) {
  346. toast.success('验证码已发送');
  347. startCountdown();
  348. }
  349. } catch (e) {
  350. // error handled in http interceptor
  351. }
  352. };
  353. const validateForm = (form: typeof phoneForm | typeof emailForm, type: 'phone' | 'email') => {
  354. if (type === 'phone' && !form.phone) {
  355. toast.error(t('auth.pleaseInputPhone'));
  356. return false;
  357. }
  358. if (type === 'email' && !(form as typeof emailForm).email) {
  359. toast.error(t('auth.pleaseInputEmail'));
  360. return false;
  361. }
  362. if (!form.code) {
  363. toast.error(t('auth.pleaseInputVerifyCode'));
  364. return false;
  365. }
  366. if (!form.password) {
  367. toast.error(t('auth.pleaseInputPassword'));
  368. return false;
  369. }
  370. if (form.password.length < 6) {
  371. toast.error('密码至少6位');
  372. return false;
  373. }
  374. if (form.password !== form.confirmPassword) {
  375. toast.error(t('auth.passwordNotMatch'));
  376. return false;
  377. }
  378. return true;
  379. };
  380. const handleRegister = async () => {
  381. const form = registerType.value === 'phone' ? phoneForm : emailForm;
  382. if (!validateForm(form, registerType.value)) return;
  383. loading.value = true;
  384. try {
  385. const res = await requestRegister({
  386. type: registerType.value,
  387. account: registerType.value === 'phone' ? phoneForm.phone : emailForm.email,
  388. password: form.password,
  389. code: form.code,
  390. inviteCode: form.inviteCode,
  391. areaCode: registerType.value === 'phone' ? areaCode.value : undefined
  392. });
  393. if (res.code === 200) {
  394. localStorage.setItem('token', res.data.token);
  395. userStore.setUserInfo(res.data.user);
  396. toast.success(t('auth.registerSuccess'));
  397. router.replace('/home');
  398. }
  399. } finally {
  400. loading.value = false;
  401. }
  402. };
  403. const goBack = () => {
  404. router.back();
  405. };
  406. const goLogin = () => {
  407. router.push('/login');
  408. };
  409. const showTerms = () => {
  410. router.push('/terms');
  411. };
  412. const showPrivacy = () => {
  413. router.push('/privacy');
  414. };
  415. // 获取OAuth配置并初始化 Google Sign-In
  416. const initGoogleSignIn = async () => {
  417. try {
  418. const configRes = await requestOAuthConfig();
  419. if (configRes.code === 200) {
  420. if (configRes.data.googleClientId) {
  421. googleClientId.value = configRes.data.googleClientId;
  422. const script = document.createElement('script');
  423. script.src = 'https://accounts.google.com/gsi/client';
  424. script.async = true;
  425. script.defer = true;
  426. script.onload = () => {
  427. if (window.google && googleClientId.value) {
  428. window.google.accounts.id.initialize({
  429. client_id: googleClientId.value,
  430. callback: handleGoogleCallback,
  431. auto_select: false,
  432. cancel_on_tap_outside: true
  433. });
  434. }
  435. };
  436. document.head.appendChild(script);
  437. }
  438. if (configRes.data.telegramBotName) {
  439. telegramBotName.value = configRes.data.telegramBotName;
  440. telegramBotId.value = configRes.data.telegramBotId || '';
  441. }
  442. // 初始化 TikTok Login
  443. if (configRes.data.tiktokClientKey) {
  444. tiktokClientKey.value = configRes.data.tiktokClientKey;
  445. }
  446. }
  447. } catch (error) {
  448. console.warn('Failed to load OAuth config:', error);
  449. }
  450. };
  451. // Google 登录回调处理
  452. const handleGoogleCallback = async (response: any) => {
  453. if (!response.credential) {
  454. toast.error('Google 登录失败');
  455. return;
  456. }
  457. googleLoading.value = true;
  458. try {
  459. const payload = JSON.parse(atob(response.credential.split('.')[1]));
  460. const inviteCode = phoneForm.inviteCode || emailForm.inviteCode || (route.query.smid as string) || '';
  461. const res = await requestOAuthLogin({
  462. provider: 'google',
  463. openId: payload.sub,
  464. nickname: payload.name,
  465. avatar: payload.picture,
  466. email: payload.email,
  467. inviteCode
  468. });
  469. if (res.code === 200) {
  470. localStorage.setItem('token', res.data.token);
  471. userStore.setUserInfo(res.data.user);
  472. toast.success(t('auth.loginSuccess'));
  473. router.replace('/home');
  474. }
  475. } catch (error) {
  476. console.error('Google login error:', error);
  477. toast.error('Google 登录失败');
  478. } finally {
  479. googleLoading.value = false;
  480. }
  481. };
  482. const handleGoogleLogin = () => {
  483. if (!googleClientId.value) {
  484. toast.error('Google 登录未配置');
  485. return;
  486. }
  487. if (window.google) {
  488. window.google.accounts.id.prompt();
  489. } else {
  490. toast.error('Google SDK 加载失败');
  491. }
  492. };
  493. const handleZaloLogin = () => {
  494. toast.info('Zalo Login - Coming Soon');
  495. };
  496. // 初始化 Telegram 登录 - 加载 widget 脚本以获取 Telegram.Login.auth API
  497. // 点击 Telegram 登录按钮 - 直接打开 Telegram OAuth 弹窗
  498. const handleTelegramLogin = () => {
  499. if (!telegramBotId.value) {
  500. toast.error(t('auth.telegramNotConfigured') || 'Telegram 登录未配置');
  501. return;
  502. }
  503. // 支持传入完整 bot token (如 123456:ABC) 或纯数字 ID
  504. const botId = telegramBotId.value.split(':')[0];
  505. const origin = encodeURIComponent(window.location.origin);
  506. const authUrl = `https://oauth.telegram.org/auth?bot_id=${botId}&origin=${origin}&request_access=write`;
  507. const width = 550;
  508. const height = 470;
  509. const left = Math.round((screen.width - width) / 2);
  510. const top = Math.round((screen.height - height) / 2);
  511. const popup = window.open(
  512. authUrl,
  513. 'telegram_oauth',
  514. `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,scrollbars=no`
  515. );
  516. const onMessage = (event: MessageEvent) => {
  517. if (event.origin !== 'https://oauth.telegram.org') return;
  518. try {
  519. let data = event.data;
  520. if (typeof data === 'string') {
  521. try { data = JSON.parse(data); } catch { return; }
  522. }
  523. if (data?.event === 'auth_result' && data?.result) {
  524. window.removeEventListener('message', onMessage);
  525. handleTelegramCallback(data.result);
  526. }
  527. } catch (e) {
  528. console.error('Telegram auth message error:', e);
  529. }
  530. };
  531. window.addEventListener('message', onMessage);
  532. const checkClosed = setInterval(() => {
  533. if (popup?.closed) {
  534. clearInterval(checkClosed);
  535. window.removeEventListener('message', onMessage);
  536. }
  537. }, 500);
  538. };
  539. // Telegram 登录回调
  540. const handleTelegramCallback = async (user: any) => {
  541. try {
  542. const inviteCode = phoneForm.inviteCode || emailForm.inviteCode || (route.query.smid as string) || '';
  543. const res = await requestOAuthLogin({
  544. provider: 'telegram',
  545. openId: String(user.id),
  546. nickname: [user.first_name, user.last_name].filter(Boolean).join(' '),
  547. avatar: user.photo_url || '',
  548. extra: JSON.stringify(user),
  549. inviteCode
  550. });
  551. if (res.code === 200) {
  552. localStorage.setItem('token', res.data.token);
  553. userStore.setUserInfo(res.data.user);
  554. toast.success(t('auth.loginSuccess'));
  555. router.replace('/home');
  556. }
  557. } catch (error) {
  558. console.error('Telegram login error:', error);
  559. toast.error(t('auth.telegramLoginFailed') || 'Telegram 登录失败');
  560. }
  561. };
  562. // 生成随机字符串
  563. const generateRandomString = (length: number): string => {
  564. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  565. let result = '';
  566. for (let i = 0; i < length; i++) {
  567. result += chars.charAt(Math.floor(Math.random() * chars.length));
  568. }
  569. return result;
  570. };
  571. // 初始化 TikTok 登录(检查回调参数)
  572. const initTiktokLogin = () => {
  573. const code = sessionStorage.getItem('tiktok_oauth_code');
  574. const redirectUri = sessionStorage.getItem('tiktok_redirect_uri');
  575. if (code && redirectUri) {
  576. sessionStorage.removeItem('tiktok_oauth_code');
  577. sessionStorage.removeItem('tiktok_oauth_state');
  578. sessionStorage.removeItem('tiktok_redirect_uri');
  579. handleTiktokCallback(code, redirectUri);
  580. }
  581. };
  582. // TikTok 登录回调处理
  583. const handleTiktokCallback = async (code: string, redirectUri: string) => {
  584. tiktokLoading.value = true;
  585. try {
  586. const inviteCode = phoneForm.inviteCode || emailForm.inviteCode || (route.query.smid as string) || '';
  587. const res = await requestOAuthLogin({
  588. provider: 'tiktok',
  589. openId: code,
  590. nickname: '',
  591. avatar: '',
  592. extra: redirectUri,
  593. inviteCode
  594. });
  595. if (res.code === 200) {
  596. localStorage.setItem('token', res.data.token);
  597. userStore.setUserInfo(res.data.user);
  598. toast.success(t('auth.loginSuccess'));
  599. router.replace('/home');
  600. } else {
  601. toast.error(res.msg || t('auth.tiktokLoginFailed') || 'TikTok 登录失败');
  602. }
  603. } catch (error) {
  604. console.error('TikTok login error:', error);
  605. toast.error(t('auth.tiktokLoginFailed') || 'TikTok 登录失败');
  606. } finally {
  607. tiktokLoading.value = false;
  608. }
  609. };
  610. // 触发 TikTok 登录
  611. const handleTiktokLogin = () => {
  612. if (!tiktokClientKey.value) {
  613. toast.error(t('auth.tiktokNotConfigured') || 'TikTok 登录未配置');
  614. return;
  615. }
  616. const redirectUri = window.location.origin + '/login';
  617. sessionStorage.setItem('tiktok_redirect_uri', redirectUri);
  618. const state = 'tiktok_' + generateRandomString(16);
  619. const authUrl = `https://www.tiktok.com/v2/auth/authorize/?client_key=${tiktokClientKey.value}&scope=user.info.basic&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
  620. window.location.href = authUrl;
  621. };
  622. onMounted(() => {
  623. initTiktokLogin();
  624. initGoogleSignIn();
  625. });
  626. </script>
  627. <style lang="scss" scoped>
  628. .register-page {
  629. min-height: 100vh;
  630. background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
  631. padding-bottom: 40px;
  632. }
  633. .nav-header {
  634. display: flex;
  635. align-items: center;
  636. justify-content: space-between;
  637. padding: 16px 20px;
  638. color: #fff;
  639. .van-icon {
  640. font-size: 20px;
  641. cursor: pointer;
  642. }
  643. .title {
  644. font-size: 18px;
  645. font-weight: 500;
  646. }
  647. }
  648. .form-section {
  649. padding: 20px 24px;
  650. }
  651. .register-tabs {
  652. display: flex;
  653. margin-bottom: 24px;
  654. background: rgba(255, 255, 255, 0.1);
  655. border-radius: 8px;
  656. padding: 4px;
  657. .tab-item {
  658. flex: 1;
  659. text-align: center;
  660. padding: 10px 0;
  661. color: rgba(255, 255, 255, 0.6);
  662. font-size: 14px;
  663. border-radius: 6px;
  664. cursor: pointer;
  665. transition: all 0.3s;
  666. &.active {
  667. background: #ffc300;
  668. color: #000;
  669. font-weight: 500;
  670. }
  671. }
  672. }
  673. .form-content {
  674. .input-group {
  675. margin-bottom: 16px;
  676. :deep(.van-field) {
  677. background: rgba(255, 255, 255, 0.1);
  678. border-radius: 8px;
  679. padding: 12px 16px;
  680. .van-field__control {
  681. color: #fff;
  682. &::placeholder {
  683. color: rgba(255, 255, 255, 0.4);
  684. }
  685. }
  686. .van-icon {
  687. color: rgba(255, 255, 255, 0.6);
  688. }
  689. .van-button--primary {
  690. background: #ffc300;
  691. border-color: #ffc300;
  692. color: #000;
  693. border-radius: 4px;
  694. }
  695. }
  696. .area-code {
  697. display: flex;
  698. align-items: center;
  699. gap: 4px;
  700. color: #fff;
  701. padding-right: 12px;
  702. border-right: 1px solid rgba(255, 255, 255, 0.2);
  703. margin-right: 12px;
  704. cursor: pointer;
  705. }
  706. }
  707. }
  708. .agreement {
  709. margin: 20px 0;
  710. :deep(.van-checkbox) {
  711. .van-checkbox__icon {
  712. .van-icon {
  713. border-color: rgba(255, 255, 255, 0.4);
  714. background: transparent;
  715. }
  716. }
  717. .van-checkbox__icon--checked .van-icon {
  718. background: #ffc300;
  719. border-color: #ffc300;
  720. color: #000;
  721. }
  722. }
  723. .text {
  724. color: rgba(255, 255, 255, 0.6);
  725. font-size: 12px;
  726. .link {
  727. color: #ffc300;
  728. }
  729. }
  730. }
  731. .register-btn {
  732. height: 48px;
  733. border-radius: 8px;
  734. font-size: 16px;
  735. font-weight: 500;
  736. background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
  737. border: none;
  738. &:disabled {
  739. opacity: 0.5;
  740. }
  741. }
  742. .third-party-section {
  743. margin-top: 24px;
  744. .divider {
  745. display: flex;
  746. align-items: center;
  747. margin-bottom: 16px;
  748. &::before,
  749. &::after {
  750. content: '';
  751. flex: 1;
  752. height: 1px;
  753. background: rgba(255, 255, 255, 0.2);
  754. }
  755. span {
  756. padding: 0 16px;
  757. color: rgba(255, 255, 255, 0.4);
  758. font-size: 12px;
  759. }
  760. }
  761. .oauth-btn {
  762. display: flex;
  763. align-items: center;
  764. justify-content: center;
  765. gap: 10px;
  766. height: 48px;
  767. border-radius: 8px;
  768. font-size: 15px;
  769. font-weight: 500;
  770. cursor: pointer;
  771. transition: all 0.3s;
  772. margin-bottom: 12px;
  773. &.loading {
  774. pointer-events: none;
  775. opacity: 0.7;
  776. }
  777. .google-icon, .zalo-icon, .telegram-icon, .tiktok-icon {
  778. flex-shrink: 0;
  779. }
  780. &.google-btn {
  781. background: #fff;
  782. color: #333;
  783. border: 1px solid rgba(255, 255, 255, 0.2);
  784. &:active {
  785. background: #f5f5f5;
  786. }
  787. }
  788. &.telegram-btn {
  789. background: #0088cc;
  790. color: #fff;
  791. &:active {
  792. background: #0077b5;
  793. }
  794. }
  795. &.tiktok-btn {
  796. background: #000;
  797. color: #fff;
  798. &:active {
  799. background: #333;
  800. }
  801. }
  802. &.zalo-btn {
  803. background: #0068FF;
  804. color: #fff;
  805. &:active {
  806. background: #0055cc;
  807. }
  808. }
  809. }
  810. }
  811. .login-entry {
  812. text-align: center;
  813. margin-top: 24px;
  814. color: rgba(255, 255, 255, 0.6);
  815. font-size: 14px;
  816. .link {
  817. color: #ffc300;
  818. margin-left: 4px;
  819. cursor: pointer;
  820. }
  821. }
  822. </style>