|
| 1 | +<script setup lang="ts"> |
| 2 | +import { computed, onUnmounted, ref } from 'vue' |
| 3 | +import { useI18n } from 'vue-i18n' |
| 4 | +import { toast } from 'vue-sonner' |
| 5 | +import { Button } from '@/components/ui/button' |
| 6 | +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' |
| 7 | +import { Input } from '@/components/ui/input' |
| 8 | +import { useDeepSeekProvider } from '@/lib/providers/deepseek' |
| 9 | +import { useKeysStore, useServiceStore } from '@/stores' |
| 10 | +import ShumeiCaptcha from './ShumeiCaptcha.vue' |
| 11 | +
|
| 12 | +const { t, locale } = useI18n() |
| 13 | +const keysStore = useKeysStore() |
| 14 | +const provider = useDeepSeekProvider() |
| 15 | +const { fetch } = useServiceStore() |
| 16 | +
|
| 17 | +const captchaConfig = { |
| 18 | + organization: 'P9usCUBauxft8eAmUXaZ', |
| 19 | + maskBindClose: !1, |
| 20 | + lang: locale.value === 'zh-CN' ? 'zh-cn' : 'en', |
| 21 | + mode: 'spatial_select', |
| 22 | + domains: ['captcha.fengkongcloud.com'], |
| 23 | +} |
| 24 | +
|
| 25 | +const phoneNumber = ref('') |
| 26 | +const smsCode = ref('') |
| 27 | +const agreed = ref(true) |
| 28 | +const isLoading = ref(false) |
| 29 | +const isSendingCode = ref(false) |
| 30 | +const countdown = ref(0) |
| 31 | +
|
| 32 | +const canSendCode = computed(() => { |
| 33 | + return phoneNumber.value && !isSendingCode.value && countdown.value === 0 |
| 34 | +}) |
| 35 | +
|
| 36 | +const canLogin = computed(() => { |
| 37 | + return phoneNumber.value && smsCode.value && agreed.value && !isLoading.value |
| 38 | +}) |
| 39 | +
|
| 40 | +let countdownInterval: NodeJS.Timeout | null = null |
| 41 | +
|
| 42 | +function startCountdown() { |
| 43 | + countdown.value = 60 |
| 44 | + countdownInterval = setInterval(() => { |
| 45 | + countdown.value-- |
| 46 | + if (countdown.value <= 0) { |
| 47 | + clearInterval(countdownInterval!) |
| 48 | + countdownInterval = null |
| 49 | + } |
| 50 | + }, 1000) |
| 51 | +} |
| 52 | +
|
| 53 | +async function sendSMS(rid: string) { |
| 54 | + if (!canSendCode.value) |
| 55 | + return |
| 56 | +
|
| 57 | + try { |
| 58 | + isSendingCode.value = true |
| 59 | +
|
| 60 | + // Send SMS with captcha verification result |
| 61 | + const res = await fetch('deepseek/sms', { |
| 62 | + body: JSON.stringify({ |
| 63 | + locale: locale.value === 'zh-CN' ? 'zh_CN' : 'en_US', |
| 64 | + mobile_number: phoneNumber.value, |
| 65 | + turnstile_token: '', |
| 66 | + shumei_verification: { region: 'GLOBAL', rid }, |
| 67 | + device_id: 'BpeI75x/8jEyx0Cf8+ceENFycckj5NmfAgbRg/za+xaDDzFfBlTiLwSJAqAg0PpFarvtePSmNZWgonTdCjntvWw==', |
| 68 | + }), |
| 69 | + method: 'POST', |
| 70 | + }) |
| 71 | +
|
| 72 | + if (res.ok) { |
| 73 | + const { data } = await res.json() |
| 74 | + if (data.code === 0) { |
| 75 | + toast.success(t('smsSent')) |
| 76 | + startCountdown() |
| 77 | + } |
| 78 | + else { |
| 79 | + toast.error(t('smsFailure')) |
| 80 | + } |
| 81 | + } |
| 82 | + else { |
| 83 | + toast.error(t('smsFailure')) |
| 84 | + } |
| 85 | + } |
| 86 | + catch (error) { |
| 87 | + console.error('Send SMS error:', error) |
| 88 | + toast.error(t('smsFailure')) |
| 89 | + } |
| 90 | + finally { |
| 91 | + isSendingCode.value = false |
| 92 | + } |
| 93 | +} |
| 94 | +
|
| 95 | +async function login() { |
| 96 | + if (!canLogin.value) |
| 97 | + return |
| 98 | +
|
| 99 | + try { |
| 100 | + isLoading.value = true |
| 101 | +
|
| 102 | + const res = await fetch('deepseek/login', { |
| 103 | + body: JSON.stringify({ |
| 104 | + phone: phoneNumber.value, |
| 105 | + code: smsCode.value, |
| 106 | + area_code: '+86', |
| 107 | + }), |
| 108 | + method: 'POST', |
| 109 | + }) |
| 110 | +
|
| 111 | + if (res.ok) { |
| 112 | + await provider.refreshUser() |
| 113 | + // Clear login form |
| 114 | + phoneNumber.value = '' |
| 115 | + smsCode.value = '' |
| 116 | + toast.success(t('loginSuccess')) |
| 117 | +
|
| 118 | + // Automatically create API key after successful login |
| 119 | + await keysStore.createAndAddKey(provider) |
| 120 | + } |
| 121 | + else { |
| 122 | + const errorData = await res.json() |
| 123 | + toast.error(errorData.message || t('loginFailure')) |
| 124 | + } |
| 125 | + } |
| 126 | + catch (error) { |
| 127 | + console.error('Login error:', error) |
| 128 | + toast.error(t('networkError')) |
| 129 | + } |
| 130 | + finally { |
| 131 | + isLoading.value = false |
| 132 | + } |
| 133 | +} |
| 134 | +
|
| 135 | +function cleanup() { |
| 136 | + if (countdownInterval) { |
| 137 | + clearInterval(countdownInterval) |
| 138 | + countdownInterval = null |
| 139 | + } |
| 140 | +} |
| 141 | +
|
| 142 | +// Cleanup on unmount |
| 143 | +onUnmounted(cleanup) |
| 144 | +</script> |
| 145 | + |
| 146 | +<template> |
| 147 | + <Card> |
| 148 | + <CardHeader class="pb-4"> |
| 149 | + <CardTitle class="text-lg"> |
| 150 | + {{ t('loginTitle') }} |
| 151 | + </CardTitle> |
| 152 | + <CardDescription class="text-sm"> |
| 153 | + {{ t('loginDescription') }} |
| 154 | + </CardDescription> |
| 155 | + </CardHeader> |
| 156 | + <CardContent class="space-y-4"> |
| 157 | + <!-- Phone Number Input --> |
| 158 | + <div class="flex rounded-md border border-input bg-background"> |
| 159 | + <div class="flex items-center px-3 border-r border-input bg-muted/50 rounded-l-md"> |
| 160 | + <span class="text-sm font-medium text-muted-foreground">+86</span> |
| 161 | + </div> |
| 162 | + <Input |
| 163 | + id="phone" |
| 164 | + v-model="phoneNumber" |
| 165 | + :placeholder="t('phoneNumber')" |
| 166 | + type="tel" |
| 167 | + class="border-0 rounded-l-none focus-visible:ring-0 focus-visible:ring-offset-0 h-10" |
| 168 | + :disabled="isLoading" |
| 169 | + /> |
| 170 | + </div> |
| 171 | + |
| 172 | + <!-- SMS Code Input --> |
| 173 | + <div class="flex rounded-md border border-input bg-background"> |
| 174 | + <Input |
| 175 | + id="sms" |
| 176 | + v-model="smsCode" |
| 177 | + :placeholder="t('smsCode')" |
| 178 | + type="text" |
| 179 | + maxlength="6" |
| 180 | + class="w-fit flex-grow border-0 rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 h-10" |
| 181 | + :disabled="isLoading" |
| 182 | + /> |
| 183 | + <div class="border-l border-input"> |
| 184 | + <ShumeiCaptcha |
| 185 | + :enabled="phoneNumber.length > 0 && !isSendingCode && countdown === 0" |
| 186 | + :config="captchaConfig" |
| 187 | + class="h-10 px-4 bg-muted/50 rounded-r-md border-0 text-xs text-primary hover:bg-muted/70 transition-colors disabled:opacity-50" |
| 188 | + @next="sendSMS" |
| 189 | + > |
| 190 | + {{ isSendingCode ? t('sending') : countdown > 0 ? `${countdown}s` : t('getCode') }} |
| 191 | + </ShumeiCaptcha> |
| 192 | + </div> |
| 193 | + </div> |
| 194 | + </CardContent> |
| 195 | + <CardFooter class="flex flex-col space-y-3 pt-3 items-start"> |
| 196 | + <div class="flex items-center space-x-2 text-xs text-muted-foreground"> |
| 197 | + <input |
| 198 | + id="agree" |
| 199 | + v-model="agreed" |
| 200 | + type="checkbox" |
| 201 | + class="h-3 w-3 rounded border border-input" |
| 202 | + :disabled="isLoading" |
| 203 | + > |
| 204 | + <label for="agree" class="flex items-center gap-1 cursor-pointer"> |
| 205 | + <span>{{ t('agreeToTerms') }}</span> |
| 206 | + <a |
| 207 | + href="https://www.deepseek.com/zh/terms" |
| 208 | + target="_blank" |
| 209 | + class="text-primary hover:underline" |
| 210 | + >{{ t('userAgreement') }}</a> |
| 211 | + <span>{{ t('and') }}</span> |
| 212 | + <a |
| 213 | + href="https://www.deepseek.com/zh/privacy" |
| 214 | + target="_blank" |
| 215 | + class="text-primary hover:underline" |
| 216 | + >{{ t('privacyPolicy') }}</a> |
| 217 | + </label> |
| 218 | + </div> |
| 219 | + <Button |
| 220 | + class="w-full h-10" |
| 221 | + :disabled="!canLogin" |
| 222 | + @click="login" |
| 223 | + > |
| 224 | + <span v-if="isLoading">{{ t('loggingIn') }}</span> |
| 225 | + <span v-else>{{ t('registerLogin') }}</span> |
| 226 | + </Button> |
| 227 | + </CardFooter> |
| 228 | + </Card> |
| 229 | +</template> |
| 230 | + |
| 231 | +<i18n lang="yaml"> |
| 232 | +en-US: |
| 233 | + loginTitle: Login to DeepSeek |
| 234 | + loginDescription: Login using phone number and SMS verification code |
| 235 | + phoneNumber: Your phone number |
| 236 | + smsCode: SMS verification code |
| 237 | + agreeToTerms: I agree to DeepSeek's |
| 238 | + userAgreement: User Agreement |
| 239 | + and: and |
| 240 | + privacyPolicy: Privacy Policy |
| 241 | + loggingIn: Logging in... |
| 242 | + registerLogin: Register/Login |
| 243 | + getCode: Get Code |
| 244 | + sending: Sending... |
| 245 | + smsSent: SMS sent successfully |
| 246 | + smsFailure: Failed to send SMS |
| 247 | + loginSuccess: Login successful |
| 248 | + loginFailure: Login failed |
| 249 | + networkError: Network error |
| 250 | +
|
| 251 | +zh-CN: |
| 252 | + loginTitle: 登录 DeepSeek |
| 253 | + loginDescription: 使用手机号码和短信验证码登录 |
| 254 | + phoneNumber: 您的手机号 |
| 255 | + smsCode: 短信验证码 |
| 256 | + agreeToTerms: 我同意 DeepSeek 的 |
| 257 | + userAgreement: 用户协议 |
| 258 | + and: 和 |
| 259 | + privacyPolicy: 隐私政策 |
| 260 | + loggingIn: 登录中... |
| 261 | + registerLogin: 注册/登录 |
| 262 | + getCode: 获取验证码 |
| 263 | + sending: 发送中... |
| 264 | + smsSent: 短信发送成功 |
| 265 | + smsFailure: 短信发送失败 |
| 266 | + loginSuccess: 登录成功 |
| 267 | + loginFailure: 登录失败 |
| 268 | + networkError: 网络错误 |
| 269 | +</i18n> |
0 commit comments