feat: 优化登录功能 添加UID登录功能

This commit is contained in:
alger
2025-08-07 22:57:02 +08:00
parent aeb7f0361d
commit daa8e7514d
18 changed files with 1030 additions and 195 deletions
+32 -20
View File
@@ -2,27 +2,33 @@
import { useMessage } from 'naive-ui';
import { onBeforeUnmount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getUserDetail } from '@/api/login';
import { useUserStore } from '@/store/modules/user';
import { isElectron, setAnimationClass } from '@/utils';
defineOptions({
name: 'CookieLogin'
});
// Emits
interface Emits {
(e: 'loginSuccess', userProfile: any, loginType: string): void;
(e: 'loginError', error: string): void;
}
const emit = defineEmits<Emits>();
const { t } = useI18n();
const message = useMessage();
const router = useRouter();
const userStore = useUserStore();
const token = ref('');
// Token登录
const loginByToken = async () => {
if (!token.value.trim()) {
message.error(t('login.message.tokenRequired'));
const errorMsg = t('login.message.tokenRequired');
message.error(errorMsg);
emit('loginError', errorMsg);
return;
}
@@ -33,19 +39,22 @@ const loginByToken = async () => {
// 获取用户信息验证token有效性
const user = await getUserDetail();
if (user.data && user.data.profile) {
userStore.user = user.data.profile;
localStorage.setItem('user', JSON.stringify(user.data.profile));
message.success(t('login.message.tokenLoginSuccess'));
router.push('/user');
const successMsg = t('login.message.tokenLoginSuccess');
message.success(successMsg);
emit('loginSuccess', user.data.profile, 'token');
} else {
// token无效,清除localStorage
localStorage.removeItem('token');
message.error(t('login.message.tokenInvalid'));
const errorMsg = t('login.message.tokenInvalid');
message.error(errorMsg);
emit('loginError', errorMsg);
}
} catch (error) {
// token无效,清除localStorage
localStorage.removeItem('token');
message.error(t('login.message.tokenInvalid'));
const errorMsg = t('login.message.tokenInvalid');
message.error(errorMsg);
emit('loginError', errorMsg);
console.error('Token登录失败:', error);
}
};
@@ -70,17 +79,20 @@ const handleCookieReceived = async (_event: any, cookieValue: string) => {
// 验证Cookie有效性
const user = await getUserDetail();
if (user.data && user.data.profile) {
userStore.user = user.data.profile;
localStorage.setItem('user', JSON.stringify(user.data.profile));
message.success(t('login.message.autoGetCookieSuccess'));
router.push('/user');
const successMsg = t('login.message.autoGetCookieSuccess');
message.success(successMsg);
emit('loginSuccess', user.data.profile, 'cookie');
} else {
localStorage.removeItem('token');
message.error(t('login.message.autoGetCookieFailed'));
const errorMsg = t('login.message.autoGetCookieFailed');
message.error(errorMsg);
emit('loginError', errorMsg);
}
} catch (error) {
localStorage.removeItem('token');
message.error(t('login.message.autoGetCookieFailed'));
const errorMsg = t('login.message.autoGetCookieFailed');
message.error(errorMsg);
emit('loginError', errorMsg);
console.error('自动获取Cookie失败:', error);
}
};
@@ -102,18 +114,18 @@ onBeforeUnmount(() => {
<template>
<div class="cookie-login" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">{{ t('login.title.token') }}</div>
<div class="login-title">{{ t('login.title.cookie') }}</div>
<div class="phone-page">
<textarea
v-model="token"
class="token-input"
:placeholder="t('login.placeholder.token')"
:placeholder="t('login.placeholder.cookie')"
rows="4"
/>
</div>
<div class="text">{{ t('login.tokenTip') }}</div>
<n-button class="btn-login" @click="loginByToken()">{{
t('login.button.tokenLogin')
t('login.button.cookieLogin')
}}</n-button>
<n-button v-if="isElectron" class="btn-auto-cookie" @click="autoGetCookie()" type="info">
{{ t('login.button.autoGetCookie') }}
+30 -23
View File
@@ -1,21 +1,25 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { onBeforeUnmount, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { checkQr, createQr, getQrKey, getUserDetail } from '@/api/login';
import { useUserStore } from '@/store/modules/user';
import { setAnimationClass } from '@/utils';
defineOptions({
name: 'QrLogin'
});
// Emits
interface Emits {
(e: 'loginSuccess', userProfile: any, loginType: string): void;
(e: 'loginError', error: string): void;
}
const emit = defineEmits<Emits>();
const { t } = useI18n();
const message = useMessage();
const router = useRouter();
const userStore = useUserStore();
const qrUrl = ref<string>();
const timerRef = ref(null);
@@ -44,7 +48,9 @@ const loadLogin = async () => {
} catch (error) {
console.error(t('login.message.loadError'), error);
qrStatus.value = 'expired';
message.error(t('login.message.loadError'));
const errorMsg = t('login.message.loadError');
message.error(errorMsg);
emit('loginError', errorMsg);
} finally {
isRefreshing.value = false;
}
@@ -60,7 +66,7 @@ const timerIsQr = (key: string) => {
qrStatus.value = 'expired';
clearInterval(timer);
timerRef.value = null;
message.warning('二维码已过期,请点击刷新获取新的二维码');
message.warning(t('login.message.qrExpiredWarning'));
return;
}
@@ -73,7 +79,7 @@ const timerIsQr = (key: string) => {
// 已扫码,等待确认
if (data.code === 802) {
qrStatus.value = 'scanned';
message.info('已扫码,请在手机上确认登录');
message.info(t('login.message.qrScannedInfo'));
return;
}
@@ -82,20 +88,21 @@ const timerIsQr = (key: string) => {
qrStatus.value = 'confirmed';
localStorage.setItem('token', data.cookie);
const user = await getUserDetail();
userStore.user = user.data.profile;
localStorage.setItem('user', JSON.stringify(user.data.profile));
message.success(t('login.message.loginSuccess'));
const successMsg = t('login.message.loginSuccess');
message.success(successMsg);
emit('loginSuccess', user.data.profile, 'qr');
clearInterval(timer);
timerRef.value = null;
router.push('/user');
}
} catch (error) {
console.error(t('login.message.qrCheckError'), error);
qrStatus.value = 'expired';
clearInterval(timer);
timerRef.value = null;
message.error('检查二维码状态失败,请刷新重试');
const errorMsg = t('login.message.qrCheckFailed');
message.error(errorMsg);
emit('loginError', errorMsg);
}
}, 3000);
@@ -111,15 +118,15 @@ const refreshQr = () => {
const getStatusText = () => {
switch (qrStatus.value) {
case 'loading':
return '正在加载二维码...';
return t('login.message.qrLoading');
case 'active':
return t('login.qrTip');
case 'expired':
return '二维码已过期,请点击刷新';
return t('login.message.qrExpired');
case 'scanned':
return '已扫码,请在手机上确认登录';
return t('login.message.qrScanned');
case 'confirmed':
return '登录成功,正在跳转...';
return t('login.message.qrConfirmed');
default:
return t('login.qrTip');
}
@@ -129,7 +136,7 @@ onMounted(() => {
loadLogin();
});
onBeforeUnmount(() => {
onUnmounted(() => {
if (timerRef.value) {
clearInterval(timerRef.value);
timerRef.value = null;
@@ -146,7 +153,7 @@ onBeforeUnmount(() => {
<!-- 加载状态 -->
<div v-if="qrStatus === 'loading'" class="qr-loading">
<n-spin size="large" />
<div class="loading-text">正在生成二维码...</div>
<div class="loading-text">{{ t('login.message.qrGenerating') }}</div>
</div>
<!-- 二维码图片 -->
@@ -155,16 +162,16 @@ onBeforeUnmount(() => {
<!-- 过期遮罩 -->
<div v-if="qrStatus === 'expired'" class="expired-overlay">
<div class="expired-text">二维码已过期</div>
<div class="expired-text">{{ t('login.message.qrExpiredShort') }}</div>
<n-button class="refresh-btn" type="primary" @click="refreshQr" :loading="isRefreshing">
{{ isRefreshing ? '刷新中...' : '点击刷新' }}
{{ isRefreshing ? t('login.button.refreshing') : t('login.button.refresh') }}
</n-button>
</div>
<!-- 已扫码遮罩 -->
<div v-if="qrStatus === 'scanned'" class="scanned-overlay">
<div class="scanned-icon"></div>
<div class="scanned-text">已扫码</div>
<div class="scanned-text">{{ t('login.message.qrScannedShort') }}</div>
</div>
</div>
</div>
@@ -177,7 +184,7 @@ onBeforeUnmount(() => {
<!-- 手动刷新按钮 -->
<div v-if="qrStatus === 'active'" class="refresh-area">
<n-button text class="manual-refresh" @click="refreshQr" :loading="isRefreshing">
刷新二维码
{{ t('login.button.refreshQr') }}
</n-button>
</div>
</div>
+150
View File
@@ -0,0 +1,150 @@
<template>
<div class="uid-login">
<div class="login-title">{{ t('login.title.uid') }}</div>
<div class="uid-page">
<input
v-model="uid"
class="uid-input"
type="text"
:placeholder="t('login.placeholder.uid')"
@keyup.enter="handleLogin"
/>
</div>
<div class="text">{{ t('login.uidTip') }}</div>
<div class="warning-text">
{{ t('login.uidWarning') }}
</div>
<n-button class="btn-login" :loading="loading" @click="handleLogin">
{{ t('login.button.login') }}
</n-button>
</div>
</template>
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { useI18n } from 'vue-i18n';
import { loginByUid } from '@/api/login';
defineOptions({
name: 'UidLogin'
});
// Props
interface Props {
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false
});
// Emits
interface Emits {
(e: 'loginSuccess', userProfile: any, loginType: string): void;
(e: 'loginError', error: string): void;
}
const emit = defineEmits<Emits>();
const { t } = useI18n();
const message = useMessage();
// 状态
const uid = ref('');
const loading = ref(false);
// UID登录处理
const handleLogin = async () => {
if (props.disabled || loading.value) return;
if (!uid.value.trim()) {
const errorMsg = t('login.message.uidRequired');
message.error(errorMsg);
emit('loginError', errorMsg);
return;
}
try {
loading.value = true;
const { data } = await loginByUid(uid.value);
if (data && data.profile) {
const successMsg = t('login.message.uidLoginSuccess');
message.success(successMsg);
emit('loginSuccess', data.profile, 'uid');
} else {
const errorMsg = t('login.message.uidInvalid');
message.error(errorMsg);
emit('loginError', errorMsg);
}
} catch (error: any) {
console.error('UID登录失败:', error);
let errorMsg = t('login.message.uidLoginFailed');
if (error.response?.status === 404 || error.response?.data?.code === 404) {
errorMsg = t('login.message.uidInvalid');
}
message.error(errorMsg);
emit('loginError', errorMsg);
} finally {
loading.value = false;
}
};
// 重置表单
const reset = () => {
uid.value = '';
loading.value = false;
};
// 暴露方法给父组件
defineExpose({
reset
});
</script>
<style lang="scss" scoped>
.uid-login {
animation-duration: 0.5s;
width: 250px;
.login-title {
@apply text-2xl font-bold mb-6 text-white;
}
.text {
@apply mt-4 text-white text-xs;
}
.warning-text {
@apply mt-2 text-orange-400 text-xs text-center max-w-xs;
line-height: 1.4;
}
.uid-page {
@apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;
@apply rounded-2xl overflow-hidden;
}
.uid-input {
height: 40px;
@apply w-full px-4 outline-none;
@apply text-gray-900 dark:text-white bg-transparent;
@apply border-b border-gray-200 dark:border-gray-700;
@apply placeholder-gray-500 dark:placeholder-gray-400;
&:focus {
@apply border-green-500;
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl;
@apply bg-green-600 hover:bg-green-700 transition-colors;
}
}
</style>