|
- <template>
- <div class="fixed inset-0 z-[999] flex items-center justify-center">
- <!-- 遮罩 -->
- <div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="$emit('close')" />
-
- <!-- 弹窗 -->
- <div class="relative w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden">
- <!-- 关闭按钮 -->
- <button
- class="absolute top-4 right-4 z-10 w-8 h-8 flex items-center justify-center rounded-full text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors cursor-pointer"
- @click="$emit('close')"
- >
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
- </svg>
- </button>
-
- <!-- 标题 -->
- <div class="px-8 pt-8 pb-4 text-center">
- <h2 class="text-xl font-bold text-slate-900">登录奇想宇宙</h2>
- <p class="mt-1 text-sm text-slate-500">登录后享受更多 AI 创作能力</p>
- </div>
-
- <!-- Tab 切换 -->
- <div class="px-8">
- <div class="flex border-b border-slate-200">
- <button
- :class="[
- 'flex-1 pb-3 text-sm font-medium transition-colors border-b-2 cursor-pointer',
- activeTab === 'qr'
- ? 'text-indigo-600 border-indigo-500'
- : 'text-slate-400 border-transparent hover:text-slate-600',
- ]"
- @click="activeTab = 'qr'"
- >
- 微信扫码登录
- </button>
- <button
- :class="[
- 'flex-1 pb-3 text-sm font-medium transition-colors border-b-2 cursor-pointer',
- activeTab === 'phone'
- ? 'text-indigo-600 border-indigo-500'
- : 'text-slate-400 border-transparent hover:text-slate-600',
- ]"
- @click="activeTab = 'phone'"
- >
- 手机号登录
- </button>
- </div>
- </div>
-
- <!-- 微信扫码登录 -->
- <div v-if="activeTab === 'qr'" class="px-8 py-6">
- <div class="flex flex-col items-center">
- <!-- 二维码区域 -->
- <div
- class="relative w-48 h-48 rounded-xl border border-slate-200 flex items-center justify-center bg-white"
- >
- <!-- 加载中 -->
- <div v-if="qrLoading" class="flex flex-col items-center gap-2">
- <svg class="w-8 h-8 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
- </svg>
- <span class="text-xs text-slate-400">获取二维码...</span>
- </div>
-
- <!-- 二维码过期 -->
- <div
- v-else-if="qrExpired"
- class="flex flex-col items-center gap-3 cursor-pointer"
- @click="refreshQR"
- >
- <svg class="w-10 h-10 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v1m6 11h2m-6 0h-2m4-8h2m-8 0H6m5 5h2m-4 0h2m2 4h-2m2-4h2" />
- </svg>
- <span class="text-sm text-slate-400">二维码已过期</span>
- <span class="text-xs text-indigo-500">点击刷新</span>
- </div>
-
- <!-- 二维码 -->
- <vue-qr
- v-else-if="qrData.url"
- :text="qrData.url"
- :size="180"
- :margin="8"
- :logo-image="qrLogo"
- :logo-size="36"
- />
- </div>
-
- <p class="mt-4 text-sm text-slate-500 text-center">
- 请使用微信扫描二维码完成登录
- </p>
- </div>
- </div>
-
- <!-- 手机号登录 -->
- <div v-if="activeTab === 'phone'" class="px-8 py-6">
- <div class="space-y-4">
- <!-- 手机号 -->
- <div>
- <label class="block text-xs font-medium text-slate-600 mb-1.5">手机号</label>
- <input
- v-model="phoneForm.phone"
- type="tel"
- maxlength="11"
- placeholder="请输入手机号"
- class="w-full px-3 py-2.5 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400 transition-colors"
- @input="phoneForm.phone = phoneForm.phone.replace(/\D/g, '')"
- />
- </div>
-
- <!-- 验证码 -->
- <div>
- <label class="block text-xs font-medium text-slate-600 mb-1.5">验证码</label>
- <div class="flex gap-3">
- <input
- v-model="phoneForm.code"
- type="text"
- maxlength="6"
- placeholder="请输入验证码"
- class="flex-1 px-3 py-2.5 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400 transition-colors"
- />
- <button
- :disabled="phoneForm.sendingCode || countdown > 0"
- :class="[
- 'shrink-0 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors cursor-pointer',
- countdown > 0
- ? 'text-slate-400 bg-slate-100 cursor-not-allowed'
- : 'text-indigo-600 bg-indigo-50 hover:bg-indigo-100',
- ]"
- @click="sendCode"
- >
- {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
- </button>
- </div>
- </div>
-
- <!-- 错误提示 -->
- <p v-if="phoneForm.error" class="text-xs text-red-500">{{ phoneForm.error }}</p>
-
- <!-- 登录按钮 -->
- <button
- :disabled="phoneForm.loading || !phoneForm.phone || !phoneForm.code"
- class="w-full py-2.5 text-sm font-semibold text-white bg-indigo-500 hover:bg-indigo-600 disabled:bg-slate-300 disabled:cursor-not-allowed rounded-lg transition-colors cursor-pointer"
- @click="loginByPhone"
- >
- <span v-if="phoneForm.loading" class="inline-flex items-center gap-2">
- <svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
- </svg>
- 登录中...
- </span>
- <span v-else>登录</span>
- </button>
- </div>
- </div>
-
- <!-- 底部协议 -->
- <div class="px-8 pb-6 text-center">
- <p class="text-xs text-slate-400">
- 登录即表示同意
- <a href="#" class="text-indigo-500 hover:underline">《服务条款》</a>
- 和
- <a href="#" class="text-indigo-500 hover:underline">《隐私政策》</a>
- </p>
- </div>
- </div>
- </div>
- </template>
-
- <script setup>
- import VueQr from 'vue-qr'
-
- const emit = defineEmits(['close', 'login-success'])
-
- const { post } = useApi()
- const { setLogin } = useUser()
-
- // Tab
- const activeTab = ref('qr')
-
- // ===== 微信扫码 =====
- const qrLoading = ref(true)
- const qrExpired = ref(false)
- const qrData = ref({ ticket: '', url: '' })
- const qrLogo = '' // 可替换为 logo URL
- let qrTimer = null
-
- // 获取二维码
- async function refreshQR() {
- qrLoading.value = true
- qrExpired.value = false
-
- try {
- const res = await post('/user/getloginqr')
- if (res.code === 0 && res.data) {
- qrData.value = res.data
- qrLoading.value = false
- startPolling()
- } else {
- qrLoadError()
- }
- } catch {
- qrLoadError()
- }
- }
-
- function qrLoadError() {
- qrLoading.value = false
- qrExpired.value = true
- }
-
- // 轮询扫码结果
- function startPolling() {
- clearInterval(qrTimer)
- qrTimer = setInterval(async () => {
- try {
- const res = await post('/user/scanquery', { ticket: qrData.value.ticket })
- if (res.code === 0 && res.data?.token) {
- clearInterval(qrTimer)
- handleLoginSuccess(res.data)
- } else if (res.code === 1010) {
- // 二维码过期
- clearInterval(qrTimer)
- qrExpired.value = true
- }
- } catch {
- // 轮询失败不中断
- }
- }, 1000)
- }
-
- // ===== 手机号登录 =====
- const phoneForm = reactive({
- phone: '',
- code: '',
- sendingCode: false,
- loading: false,
- error: '',
- })
-
- const countdown = ref(0)
- let countdownTimer = null
-
- // 发送验证码
- async function sendCode() {
- if (!phoneForm.phone || phoneForm.phone.length !== 11) {
- phoneForm.error = '请输入正确的11位手机号'
- return
- }
- phoneForm.error = ''
- phoneForm.sendingCode = true
-
- try {
- const res = await post('/common/sendcode', {
- phone: phoneForm.phone,
- type: 3,
- })
- if (res.code === 0) {
- countdown.value = 60
- countdownTimer = setInterval(() => {
- countdown.value--
- if (countdown.value <= 0) {
- clearInterval(countdownTimer)
- }
- }, 1000)
- } else {
- phoneForm.error = res.msg || '发送验证码失败'
- }
- } catch (e) {
- phoneForm.error = e.message || '发送验证码失败'
- } finally {
- phoneForm.sendingCode = false
- }
- }
-
- // 手机号登录
- async function loginByPhone() {
- if (!phoneForm.phone || !phoneForm.code) return
- phoneForm.error = ''
- phoneForm.loading = true
-
- try {
- const res = await post('/user/login_phone', {
- phone: phoneForm.phone,
- code: phoneForm.code,
- })
- if (res.code === 0 && res.data?.token) {
- handleLoginSuccess(res.data)
- } else {
- phoneForm.error = res.msg || '登录失败'
- }
- } catch (e) {
- phoneForm.error = e.message || '登录失败'
- } finally {
- phoneForm.loading = false
- }
- }
-
- // ===== 公共 =====
- function handleLoginSuccess(data) {
- setLogin(data)
- emit('login-success', data)
- }
-
- // 初始化
- onMounted(() => {
- refreshQR()
- })
-
- onUnmounted(() => {
- clearInterval(qrTimer)
- clearInterval(countdownTimer)
- })
- </script>
|