mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 06:10:49 +08:00
feat: 优化登录功能 添加UID登录功能
This commit is contained in:
@@ -66,14 +66,10 @@ export default [
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^value$|^loading$|^width$|^height$|^showPlaylist$|^id$|^enabledSources$|^progress$|^status$|^success$|^filePath$|^locale$|^channel$|^listener$|^url$|^songId$|^delta$|^item$|^err$|^gradient$|^theme$',
|
||||
varsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^NONE$|^TIME$|^SONGS$|^PLAYLIST_END$|^c$|^l$|^Window$|^key$|^color$',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
'error',
|
||||
// we are only using this rule to check for unused arguments since TS
|
||||
// catches unused variables but not args.
|
||||
{ varsIgnorePattern: '.*', args: 'none' }
|
||||
],
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
@@ -83,14 +79,10 @@ export default [
|
||||
'simple-import-sort/exports': 'error',
|
||||
'no-console': 'off',
|
||||
'no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^value$|^loading$|^width$|^height$|^showPlaylist$|^id$|^enabledSources$|^progress$|^status$|^success$|^filePath$|^locale$|^channel$|^listener$|^url$|^songId$|^delta$|^item$|^err$|^gradient$|^theme$',
|
||||
varsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^NONE$|^TIME$|^SONGS$|^PLAYLIST_END$|^c$|^l$|^Window$|^key$|^color$',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
'error',
|
||||
// we are only using this rule to check for unused arguments since TS
|
||||
// catches unused variables but not args.
|
||||
{ varsIgnorePattern: '.*', args: 'none' }
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'max-classes-per-file': 'off',
|
||||
@@ -235,14 +227,10 @@ export default [
|
||||
'class-methods-use-this': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^value$|^loading$|^width$|^height$|^showPlaylist$|^id$|^enabledSources$|^progress$|^status$|^success$|^filePath$|^locale$|^channel$|^listener$|^url$|^songId$|^delta$|^item$|^err$|^gradient$|^theme$',
|
||||
varsIgnorePattern:
|
||||
'^_|^e$|^event$|^error$|^data$|^callback$|^args$|^NONE$|^TIME$|^SONGS$|^PLAYLIST_END$|^c$|^l$|^Window$|^key$|^color$',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
'error',
|
||||
// we are only using this rule to check for unused arguments since TS
|
||||
// catches unused variables but not args.
|
||||
{ varsIgnorePattern: '.*', args: 'none' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,61 @@ export default {
|
||||
title: {
|
||||
qr: 'QR Code Login',
|
||||
phone: 'Phone Login',
|
||||
token: 'Cookie Login'
|
||||
cookie: 'Cookie Login',
|
||||
uid: 'UID Login'
|
||||
},
|
||||
qrTip: 'Scan with NetEase Cloud Music APP',
|
||||
phoneTip: 'Login with NetEase Cloud account',
|
||||
tokenTip: 'Enter a valid NetEase Cloud Music Cookie to login',
|
||||
uidTip: 'Enter User ID for quick login',
|
||||
placeholder: {
|
||||
phone: 'Phone Number',
|
||||
password: 'Password',
|
||||
token: 'Please enter NetEase Cloud Music Cookie (token)'
|
||||
cookie: 'Please enter NetEase Cloud Music Cookie (token)',
|
||||
uid: 'Please enter User ID (UID)'
|
||||
},
|
||||
button: {
|
||||
login: 'Login',
|
||||
switchToQr: 'QR Code Login',
|
||||
switchToPhone: 'Phone Login',
|
||||
switchToToken: 'Use Cookie Login',
|
||||
switchToUid: 'UID Login',
|
||||
backToQr: 'Back to QR Code Login',
|
||||
tokenLogin: 'Cookie Login',
|
||||
autoGetCookie: 'Auto Get Cookie'
|
||||
cookieLogin: 'Cookie Login',
|
||||
autoGetCookie: 'Auto Get Cookie',
|
||||
refresh: 'Click to Refresh',
|
||||
refreshing: 'Refreshing...',
|
||||
refreshQr: 'Refresh QR Code'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: 'Login successful',
|
||||
loginFailed: 'Login failed',
|
||||
tokenLoginSuccess: 'Cookie login successful',
|
||||
uidLoginSuccess: 'UID login successful',
|
||||
loadError: 'Error loading login information',
|
||||
qrCheckError: 'Error checking QR code status',
|
||||
tokenRequired: 'Please enter Cookie',
|
||||
tokenInvalid: 'Invalid Cookie, please check and try again',
|
||||
uidRequired: 'Please enter User ID',
|
||||
uidInvalid: 'Invalid User ID or user does not exist',
|
||||
uidLoginFailed: 'UID login failed, please check if User ID is correct',
|
||||
phoneRequired: 'Please enter phone number',
|
||||
passwordRequired: 'Please enter password',
|
||||
phoneLoginFailed: 'Phone login failed, please check if phone number and password are correct',
|
||||
autoGetCookieSuccess: 'Auto get Cookie successful',
|
||||
autoGetCookieFailed: 'Auto get Cookie failed',
|
||||
autoGetCookieTip: 'Will open NetEase Cloud Music login page, please complete login and close the window'
|
||||
autoGetCookieTip: 'Will open NetEase Cloud Music login page, please complete login and close the window',
|
||||
qrCheckFailed: 'Failed to check QR code status, please refresh and try again',
|
||||
qrLoading: 'Loading QR code...',
|
||||
qrExpired: 'QR code has expired, please click to refresh',
|
||||
qrExpiredShort: 'QR code expired',
|
||||
qrExpiredWarning: 'QR code has expired, please click to refresh for a new one',
|
||||
qrScanned: 'QR code scanned, please confirm login on your phone',
|
||||
qrScannedShort: 'Scanned',
|
||||
qrScannedInfo: 'QR code scanned, please confirm login on your phone',
|
||||
qrConfirmed: 'Login successful, redirecting...',
|
||||
qrGenerating: 'Generating QR code...'
|
||||
},
|
||||
qrTitle: 'NetEase Cloud Music QR Code Login'
|
||||
qrTitle: 'NetEase Cloud Music QR Code Login',
|
||||
uidWarning: 'Note: UID login is only for viewing user public information and cannot access features that require login permissions.'
|
||||
};
|
||||
|
||||
@@ -2,35 +2,47 @@ export default {
|
||||
title: {
|
||||
qr: 'QRコードログイン',
|
||||
phone: '電話番号ログイン',
|
||||
token: 'Cookieログイン'
|
||||
cookie: 'Cookieログイン',
|
||||
uid: 'UIDログイン'
|
||||
},
|
||||
qrTip: 'NetEase Cloudアプリでログイン',
|
||||
phoneTip: 'NetEase Cloudアカウントでログイン',
|
||||
tokenTip: '有効なNetEase Cloud MusicのCookieを入力してログイン',
|
||||
uidTip: 'ユーザーIDを入力してクイックログイン',
|
||||
placeholder: {
|
||||
phone: '電話番号',
|
||||
password: 'パスワード',
|
||||
token: 'NetEase Cloud MusicのCookie(token)を入力してください'
|
||||
cookie: 'NetEase Cloud MusicのCookie(token)を入力してください',
|
||||
uid: 'ユーザーID(UID)を入力してください'
|
||||
},
|
||||
button: {
|
||||
login: 'ログイン',
|
||||
switchToQr: 'QRコードログイン',
|
||||
switchToPhone: '電話番号ログイン',
|
||||
switchToToken: 'Cookieログインを使用',
|
||||
switchToUid: 'UIDログイン',
|
||||
backToQr: 'QRコードログインに戻る',
|
||||
tokenLogin: 'Cookieログイン',
|
||||
autoGetCookie: 'Cookie自動取得'
|
||||
cookieLogin: 'Cookieログイン',
|
||||
autoGetCookie: 'Cookie自動取得',
|
||||
refresh: 'クリックしてリフレッシュ',
|
||||
refreshing: 'リフレッシュ中...',
|
||||
refreshQr: 'QRコードをリフレッシュ'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: 'ログイン成功',
|
||||
tokenLoginSuccess: 'Cookieログイン成功',
|
||||
uidLoginSuccess: 'UIDログイン成功',
|
||||
loadError: 'ログイン情報の読み込み中にエラーが発生しました',
|
||||
qrCheckError: 'QRコードの状態確認中にエラーが発生しました',
|
||||
tokenRequired: 'Cookieを入力してください',
|
||||
tokenInvalid: 'Cookieが無効です。確認して再試行してください',
|
||||
uidRequired: 'ユーザーIDを入力してください',
|
||||
uidInvalid: 'ユーザーIDが無効またはユーザーが存在しません',
|
||||
uidLoginFailed: 'UIDログインに失敗しました。ユーザーIDが正しいか確認してください',
|
||||
autoGetCookieSuccess: 'Cookie自動取得成功',
|
||||
autoGetCookieFailed: 'Cookie自動取得失敗',
|
||||
autoGetCookieTip: 'NetEase Cloud Musicのログインページを開きます。ログイン完了後、ウィンドウを閉じてください'
|
||||
},
|
||||
qrTitle: 'NetEase Cloud Music QRコードログイン'
|
||||
qrTitle: 'NetEase Cloud Music QRコードログイン',
|
||||
uidWarning: '注意:UIDログインはユーザーの公開情報を表示するためのみ使用でき、ログイン権限が必要な機能にはアクセスできません。'
|
||||
};
|
||||
@@ -2,35 +2,47 @@ export default {
|
||||
title: {
|
||||
qr: 'QR코드 로그인',
|
||||
phone: '휴대폰 번호 로그인',
|
||||
token: 'Cookie 로그인'
|
||||
cookie: 'Cookie 로그인',
|
||||
uid: 'UID 로그인'
|
||||
},
|
||||
qrTip: '넷이즈 클라우드 뮤직 앱으로 QR코드를 스캔하여 로그인',
|
||||
phoneTip: '넷이즈 클라우드 계정으로 로그인',
|
||||
tokenTip: '유효한 넷이즈 클라우드 뮤직 Cookie을 입력하여 로그인',
|
||||
uidTip: '사용자 ID를 입력하여 빠른 로그인',
|
||||
placeholder: {
|
||||
phone: '휴대폰 번호',
|
||||
password: '비밀번호',
|
||||
token: '넷이즈 클라우드 뮤직 Cookie(token)을 입력하세요'
|
||||
cookie: '넷이즈 클라우드 뮤직 Cookie(token)을 입력하세요',
|
||||
uid: '사용자 ID(UID)를 입력하세요'
|
||||
},
|
||||
button: {
|
||||
login: '로그인',
|
||||
switchToQr: 'QR코드 로그인',
|
||||
switchToPhone: '휴대폰 번호 로그인',
|
||||
switchToToken: 'Cookie 로그인 사용',
|
||||
switchToUid: 'UID 로그인',
|
||||
backToQr: 'QR코드 로그인으로 돌아가기',
|
||||
tokenLogin: 'Cookie 로그인',
|
||||
autoGetCookie: 'Cookie 자동 가져오기'
|
||||
cookieLogin: 'Cookie 로그인',
|
||||
autoGetCookie: 'Cookie 자동 가져오기',
|
||||
refresh: '새로고침',
|
||||
refreshing: '새로고침 중...',
|
||||
refreshQr: 'QR코드 새로고침'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '로그인 성공',
|
||||
tokenLoginSuccess: 'Cookie 로그인 성공',
|
||||
uidLoginSuccess: 'UID 로그인 성공',
|
||||
loadError: '로그인 정보 로드 중 오류 발생',
|
||||
qrCheckError: 'QR코드 상태 확인 중 오류 발생',
|
||||
tokenRequired: 'Cookie을 입력하세요',
|
||||
tokenInvalid: 'Cookie이 유효하지 않습니다. 확인 후 다시 시도하세요',
|
||||
uidRequired: '사용자 ID를 입력하세요',
|
||||
uidInvalid: '사용자 ID가 유효하지 않거나 사용자가 존재하지 않습니다',
|
||||
uidLoginFailed: 'UID 로그인에 실패했습니다. 사용자 ID가 올바른지 확인하세요',
|
||||
autoGetCookieSuccess: 'Cookie 자동 가져오기 성공',
|
||||
autoGetCookieFailed: 'Cookie 자동 가져오기 실패',
|
||||
autoGetCookieTip: '넷이즈 클라우드 뮤직 로그인 페이지를 열겠습니다. 로그인 완료 후 창을 닫아주세요'
|
||||
},
|
||||
qrTitle: '넷이즈 클라우드 뮤직 QR코드 로그인'
|
||||
qrTitle: '넷이즈 클라우드 뮤직 QR코드 로그인',
|
||||
uidWarning: '주의: UID 로그인은 사용자 공개 정보를 확인하는 데만 사용할 수 있으며, 로그인 권한이 필요한 기능에 액세스할 수 없습니다.'
|
||||
};
|
||||
@@ -2,35 +2,61 @@ export default {
|
||||
title: {
|
||||
qr: '扫码登录',
|
||||
phone: '手机号登录',
|
||||
token: 'Cookie登录'
|
||||
cookie: 'Cookie登录',
|
||||
uid: 'UID登录'
|
||||
},
|
||||
qrTip: '使用网易云APP扫码登录',
|
||||
phoneTip: '使用网易云账号登录',
|
||||
tokenTip: '输入有效的网易云音乐Cookie即可登录',
|
||||
uidTip: '输入用户ID快速登录',
|
||||
placeholder: {
|
||||
phone: '手机号',
|
||||
password: '密码',
|
||||
token: '请输入网易云音乐Cookie(token)'
|
||||
cookie: '请输入网易云音乐Cookie(token)',
|
||||
uid: '请输入用户ID(UID)'
|
||||
},
|
||||
button: {
|
||||
login: '登录',
|
||||
switchToQr: '扫码登录',
|
||||
switchToPhone: '手机号登录',
|
||||
switchToToken: '使用Cookie登录',
|
||||
switchToUid: 'UID登录',
|
||||
backToQr: '返回二维码登录',
|
||||
tokenLogin: 'Cookie登录',
|
||||
autoGetCookie: '自动获取Cookie'
|
||||
cookieLogin: 'Cookie登录',
|
||||
autoGetCookie: '自动获取Cookie',
|
||||
refresh: '点击刷新',
|
||||
refreshing: '刷新中...',
|
||||
refreshQr: '刷新二维码'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '登录成功',
|
||||
loginFailed: '登录失败',
|
||||
tokenLoginSuccess: 'Cookie登录成功',
|
||||
uidLoginSuccess: 'UID登录成功',
|
||||
loadError: '加载登录信息时出错',
|
||||
qrCheckError: '检查二维码状态时出错',
|
||||
tokenRequired: '请输入Cookie',
|
||||
tokenInvalid: 'Cookie无效,请检查后重试',
|
||||
uidRequired: '请输入用户ID',
|
||||
uidInvalid: '用户ID无效或用户不存在',
|
||||
uidLoginFailed: 'UID登录失败,请检查用户ID是否正确',
|
||||
phoneRequired: '请输入手机号',
|
||||
passwordRequired: '请输入密码',
|
||||
phoneLoginFailed: '手机号登录失败,请检查手机号和密码是否正确',
|
||||
autoGetCookieSuccess: '自动获取Cookie成功',
|
||||
autoGetCookieFailed: '自动获取Cookie失败',
|
||||
autoGetCookieTip: '将打开网易云音乐登录页面,请完成登录后关闭窗口'
|
||||
autoGetCookieTip: '将打开网易云音乐登录页面,请完成登录后关闭窗口',
|
||||
qrCheckFailed: '检查二维码状态失败,请刷新重试',
|
||||
qrLoading: '正在加载二维码...',
|
||||
qrExpired: '二维码已过期,请点击刷新',
|
||||
qrExpiredShort: '二维码已过期',
|
||||
qrExpiredWarning: '二维码已过期,请点击刷新获取新的二维码',
|
||||
qrScanned: '已扫码,请在手机上确认登录',
|
||||
qrScannedShort: '已扫码',
|
||||
qrScannedInfo: '已扫码,请在手机上确认登录',
|
||||
qrConfirmed: '登录成功,正在跳转...',
|
||||
qrGenerating: '正在生成二维码...'
|
||||
},
|
||||
qrTitle: '扫码登录网易云音乐'
|
||||
qrTitle: '扫码登录网易云音乐',
|
||||
uidWarning: '注意:UID登录仅用于查看用户公开信息,无法访问需要登录权限的功能'
|
||||
};
|
||||
|
||||
@@ -2,35 +2,47 @@ export default {
|
||||
title: {
|
||||
qr: '掃碼登入',
|
||||
phone: '手機號登入',
|
||||
token: 'Cookie登入'
|
||||
cookie: 'Cookie登入',
|
||||
uid: 'UID登入'
|
||||
},
|
||||
qrTip: '使用網易雲APP掃碼登入',
|
||||
phoneTip: '使用網易雲帳號登入',
|
||||
tokenTip: '輸入有效的網易雲音樂Cookie即可登入',
|
||||
uidTip: '輸入使用者ID快速登入',
|
||||
placeholder: {
|
||||
phone: '手機號',
|
||||
password: '密碼',
|
||||
token: '請輸入網易雲音樂Cookie(token)'
|
||||
cookie: '請輸入網易雲音樂Cookie(token)',
|
||||
uid: '請輸入使用者ID(UID)'
|
||||
},
|
||||
button: {
|
||||
login: '登入',
|
||||
switchToQr: '掃碼登入',
|
||||
switchToPhone: '手機號登入',
|
||||
switchToToken: '使用Cookie登入',
|
||||
switchToUid: 'UID登入',
|
||||
backToQr: '返回二維碼登入',
|
||||
tokenLogin: 'Cookie登入',
|
||||
autoGetCookie: '自動取得Cookie'
|
||||
cookieLogin: 'Cookie登入',
|
||||
autoGetCookie: '自動取得Cookie',
|
||||
refresh: '點擊刷新',
|
||||
refreshing: '刷新中...',
|
||||
refreshQr: '刷新二維碼'
|
||||
},
|
||||
message: {
|
||||
loginSuccess: '登入成功',
|
||||
tokenLoginSuccess: 'Cookie登入成功',
|
||||
uidLoginSuccess: 'UID登入成功',
|
||||
loadError: '載入登入資訊時出錯',
|
||||
qrCheckError: '檢查二維碼狀態時出錯',
|
||||
tokenRequired: '請輸入Cookie',
|
||||
tokenInvalid: 'Cookie無效,請檢查後重試',
|
||||
uidRequired: '請輸入使用者ID',
|
||||
uidInvalid: '使用者ID無效或使用者不存在',
|
||||
uidLoginFailed: 'UID登入失敗,請檢查使用者ID是否正確',
|
||||
autoGetCookieSuccess: '自動取得Cookie成功',
|
||||
autoGetCookieFailed: '自動取得Cookie失敗',
|
||||
autoGetCookieTip: '將開啟網易雲音樂登入頁面,請完成登入後關閉視窗'
|
||||
},
|
||||
qrTitle: '掃碼登入網易雲音樂'
|
||||
qrTitle: '掃碼登入網易雲音樂',
|
||||
uidWarning: '注意:UID登入僅用於查看使用者公開資訊,無法訪問需要登入權限的功能'
|
||||
};
|
||||
|
||||
@@ -44,3 +44,11 @@ export function loginByCellphone(phone: string, password: string) {
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
// UID登录 - 通过用户ID获取用户信息
|
||||
// /user/detail
|
||||
export function loginByUid(uid: string | number) {
|
||||
return request.get('/user/detail', {
|
||||
params: { uid }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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
src/renderer/components/login/UidLogin.vue
Normal file
150
src/renderer/components/login/UidLogin.vue
Normal 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>
|
||||
@@ -46,7 +46,10 @@
|
||||
<div class="user-popover">
|
||||
<div v-if="userStore.user" class="user-header" @click="selectItem('user')">
|
||||
<n-avatar circle size="small" :src="getImgUrl(userStore.user?.avatarUrl)" />
|
||||
<span class="username">{{ userStore.user?.nickname || 'Theodore' }}</span>
|
||||
<div>
|
||||
<p class="username">{{ userStore.user?.nickname || 'Theodore' }}</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-items">
|
||||
<div v-if="!userStore.user" class="menu-item" @click="toLogin">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { logout } from '@/api/login';
|
||||
import { getLikedList } from '@/api/music';
|
||||
import { clearLoginStatus } from '@/utils/auth';
|
||||
|
||||
interface UserData {
|
||||
userId: number;
|
||||
@@ -21,6 +22,7 @@ function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const user = ref<UserData | null>(getLocalStorageItem('user', null));
|
||||
const loginType = ref<'token' | 'cookie' | 'qr' | 'uid' | null>(getLocalStorageItem('loginType', null));
|
||||
const searchValue = ref('');
|
||||
const searchType = ref(1);
|
||||
|
||||
@@ -30,16 +32,30 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
};
|
||||
|
||||
const setLoginType = (type: typeof loginType.value) => {
|
||||
loginType.value = type;
|
||||
if (type) {
|
||||
localStorage.setItem('loginType', type);
|
||||
} else {
|
||||
localStorage.removeItem('loginType');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
user.value = null;
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
loginType.value = null;
|
||||
clearLoginStatus();
|
||||
// 刷新
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error);
|
||||
// 即使API调用失败,也要清除本地状态
|
||||
user.value = null;
|
||||
loginType.value = null;
|
||||
clearLoginStatus();
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,11 +89,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
loginType,
|
||||
searchValue,
|
||||
searchType,
|
||||
|
||||
// 方法
|
||||
setUser,
|
||||
setLoginType,
|
||||
handleLogout,
|
||||
setSearchValue,
|
||||
setSearchType,
|
||||
|
||||
@@ -3,3 +3,264 @@ export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'bilibili'
|
||||
|
||||
// 默认平台列表
|
||||
export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||
|
||||
export interface IRecommendMusic {
|
||||
code: number;
|
||||
category: number;
|
||||
result: SongResult[];
|
||||
}
|
||||
export interface ILyricText {
|
||||
text: string;
|
||||
trText: string;
|
||||
}
|
||||
export interface ILyric {
|
||||
lrcTimeArray: number[];
|
||||
lrcArray: ILyricText[];
|
||||
}
|
||||
|
||||
export interface SongResult {
|
||||
id: string | number;
|
||||
name: string;
|
||||
picUrl: string;
|
||||
playCount?: number;
|
||||
song?: any;
|
||||
copywriter?: string;
|
||||
type?: number;
|
||||
canDislike?: boolean;
|
||||
program?: any;
|
||||
alg?: string;
|
||||
ar: Artist[];
|
||||
artists?: Artist[];
|
||||
al: Album;
|
||||
album?: Album;
|
||||
count: number;
|
||||
playMusicUrl?: string;
|
||||
playLoading?: boolean;
|
||||
lyric?: ILyric;
|
||||
backgroundColor?: string;
|
||||
primaryColor?: string;
|
||||
bilibiliData?: {
|
||||
bvid: string;
|
||||
cid: number;
|
||||
};
|
||||
source?: 'netease' | 'bilibili';
|
||||
// 过期时间
|
||||
expiredAt?: number;
|
||||
// 获取时间
|
||||
createdAt?: number;
|
||||
// 时长
|
||||
duration?: number;
|
||||
dt?: number;
|
||||
isFirstPlay?: boolean;
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
name: string;
|
||||
id: number;
|
||||
position: number;
|
||||
alias: string[];
|
||||
status: number;
|
||||
fee: number;
|
||||
copyrightId: number;
|
||||
disc: string;
|
||||
no: number;
|
||||
artists: Artist[];
|
||||
album: Album;
|
||||
starred: boolean;
|
||||
popularity: number;
|
||||
score: number;
|
||||
starredNum: number;
|
||||
duration: number;
|
||||
playedNum: number;
|
||||
dayPlays: number;
|
||||
hearTime: number;
|
||||
ringtone: string;
|
||||
crbt?: any;
|
||||
audition?: any;
|
||||
copyFrom: string;
|
||||
commentThreadId: string;
|
||||
rtUrl?: any;
|
||||
ftype: number;
|
||||
rtUrls: any[];
|
||||
copyright: number;
|
||||
transName?: any;
|
||||
sign?: any;
|
||||
mark: number;
|
||||
originCoverType: number;
|
||||
originSongSimpleData?: any;
|
||||
single: number;
|
||||
noCopyrightRcmd?: any;
|
||||
rtype: number;
|
||||
rurl?: any;
|
||||
mvid: number;
|
||||
bMusic: BMusic;
|
||||
mp3Url?: any;
|
||||
hMusic: BMusic;
|
||||
mMusic: BMusic;
|
||||
lMusic: BMusic;
|
||||
exclusive: boolean;
|
||||
privilege: Privilege;
|
||||
count?: number;
|
||||
playLoading?: boolean;
|
||||
picUrl?: string;
|
||||
ar: Artist[];
|
||||
}
|
||||
|
||||
interface Privilege {
|
||||
id: number;
|
||||
fee: number;
|
||||
payed: number;
|
||||
st: number;
|
||||
pl: number;
|
||||
dl: number;
|
||||
sp: number;
|
||||
cp: number;
|
||||
subp: number;
|
||||
cs: boolean;
|
||||
maxbr: number;
|
||||
fl: number;
|
||||
toast: boolean;
|
||||
flag: number;
|
||||
preSell: boolean;
|
||||
playMaxbr: number;
|
||||
downloadMaxbr: number;
|
||||
rscl?: any;
|
||||
freeTrialPrivilege: FreeTrialPrivilege;
|
||||
chargeInfoList: ChargeInfoList[];
|
||||
}
|
||||
|
||||
interface ChargeInfoList {
|
||||
rate: number;
|
||||
chargeUrl?: any;
|
||||
chargeMessage?: any;
|
||||
chargeType: number;
|
||||
}
|
||||
|
||||
interface FreeTrialPrivilege {
|
||||
resConsumable: boolean;
|
||||
userConsumable: boolean;
|
||||
}
|
||||
|
||||
interface BMusic {
|
||||
name?: any;
|
||||
id: number;
|
||||
size: number;
|
||||
extension: string;
|
||||
sr: number;
|
||||
dfsId: number;
|
||||
bitrate: number;
|
||||
playTime: number;
|
||||
volumeDelta: number;
|
||||
}
|
||||
|
||||
interface Album {
|
||||
name: string;
|
||||
id: number;
|
||||
type: string;
|
||||
size: number;
|
||||
picId: number;
|
||||
blurPicUrl: string;
|
||||
companyId: number;
|
||||
pic: number;
|
||||
picUrl: string;
|
||||
publishTime: number;
|
||||
description: string;
|
||||
tags: string;
|
||||
company: string;
|
||||
briefDesc: string;
|
||||
artist: Artist;
|
||||
songs: any[];
|
||||
alias: string[];
|
||||
status: number;
|
||||
copyrightId: number;
|
||||
commentThreadId: string;
|
||||
artists: Artist[];
|
||||
subType: string;
|
||||
transName?: any;
|
||||
onSale: boolean;
|
||||
mark: number;
|
||||
picId_str: string;
|
||||
}
|
||||
|
||||
export interface Artist {
|
||||
name: string;
|
||||
id: number;
|
||||
picId: number;
|
||||
img1v1Id: number;
|
||||
briefDesc: string;
|
||||
picUrl: string;
|
||||
img1v1Url: string;
|
||||
albumSize: number;
|
||||
alias: any[];
|
||||
trans: string;
|
||||
musicSize: number;
|
||||
topicPerson: number;
|
||||
}
|
||||
|
||||
export interface IPlayMusicUrl {
|
||||
data: Datum[];
|
||||
code: number;
|
||||
}
|
||||
|
||||
interface Datum {
|
||||
id: number;
|
||||
url: string;
|
||||
br: number;
|
||||
size: number;
|
||||
md5: string;
|
||||
code: number;
|
||||
expi: number;
|
||||
type: string;
|
||||
gain: number;
|
||||
fee: number;
|
||||
uf?: any;
|
||||
payed: number;
|
||||
flag: number;
|
||||
canExtend: boolean;
|
||||
freeTrialInfo?: any;
|
||||
level: string;
|
||||
encodeType: string;
|
||||
freeTrialPrivilege: FreeTrialPrivilege;
|
||||
freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
|
||||
urlSource: number;
|
||||
}
|
||||
|
||||
interface FreeTimeTrialPrivilege {
|
||||
resConsumable: boolean;
|
||||
userConsumable: boolean;
|
||||
type: number;
|
||||
remainTime: number;
|
||||
}
|
||||
|
||||
interface FreeTrialPrivilege {
|
||||
resConsumable: boolean;
|
||||
userConsumable: boolean;
|
||||
}
|
||||
|
||||
export interface IArtists {
|
||||
id: number;
|
||||
name: string;
|
||||
picUrl: string | null;
|
||||
alias: string[];
|
||||
albumSize: number;
|
||||
picId: number;
|
||||
fansGroup: null;
|
||||
img1v1Url: string;
|
||||
img1v1: number;
|
||||
trans: null;
|
||||
}
|
||||
|
||||
// 音乐源类型定义
|
||||
export type MusicSourceType =
|
||||
| 'tencent'
|
||||
| 'kugou'
|
||||
| 'migu'
|
||||
| 'netease'
|
||||
| 'joox'
|
||||
| 'ytmusic'
|
||||
| 'spotify'
|
||||
| 'qobuz'
|
||||
| 'deezer'
|
||||
| 'gdmusic';
|
||||
|
||||
// 更多音乐相关的类型可以在这里定义
|
||||
|
||||
125
src/renderer/utils/auth.ts
Normal file
125
src/renderer/utils/auth.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 登录状态管理工具
|
||||
*
|
||||
* 注意:这个工具主要用于在组件外部或 store 初始化之前检查登录状态。
|
||||
* 在组件内部,建议直接使用 userStore 中的状态。
|
||||
*/
|
||||
|
||||
export interface LoginInfo {
|
||||
isLoggedIn: boolean;
|
||||
loginType: 'token' | 'cookie' | 'qr' | 'uid' | null;
|
||||
hasToken: boolean;
|
||||
hasUser: boolean;
|
||||
user: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
* @returns 登录信息对象
|
||||
*/
|
||||
export function checkLoginStatus(): LoginInfo {
|
||||
const token = localStorage.getItem('token');
|
||||
const userData = localStorage.getItem('user');
|
||||
const loginType = localStorage.getItem('loginType') as LoginInfo['loginType'];
|
||||
const uidLogin = localStorage.getItem('uidLogin');
|
||||
|
||||
const hasToken = !!token;
|
||||
const hasUser = !!userData;
|
||||
let user = null;
|
||||
|
||||
if (hasUser) {
|
||||
try {
|
||||
user = JSON.parse(userData);
|
||||
} catch (error) {
|
||||
console.error('解析用户数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否已登录
|
||||
let isLoggedIn = false;
|
||||
|
||||
if (loginType === 'uid' || uidLogin === 'true') {
|
||||
// UID登录:只需要有用户数据即可
|
||||
isLoggedIn = hasUser && !!user;
|
||||
} else {
|
||||
// 其他登录方式:需要token和用户数据
|
||||
isLoggedIn = hasToken && hasUser && !!user;
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
loginType,
|
||||
hasToken,
|
||||
hasUser,
|
||||
user
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有访问权限
|
||||
* @param requireAuth 是否需要真实登录权限(token)
|
||||
* @returns 是否有权限
|
||||
*/
|
||||
export function hasPermission(requireAuth: boolean = false): boolean {
|
||||
const loginInfo = checkLoginStatus();
|
||||
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果需要真实登录权限,UID登录无法满足
|
||||
if (requireAuth && loginInfo.loginType === 'uid') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录状态
|
||||
*/
|
||||
export function clearLoginStatus(): void {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('loginType');
|
||||
localStorage.removeItem('uidLogin');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置登录状态(不包括用户数据,用户数据应通过 userStore.setUser 设置)
|
||||
* @param loginType 登录类型
|
||||
* @param token 登录token(可选)
|
||||
*/
|
||||
export function setLoginStatus(
|
||||
loginType: LoginInfo['loginType'],
|
||||
token?: string
|
||||
): void {
|
||||
localStorage.setItem('loginType', loginType || '');
|
||||
|
||||
if (token) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
if (loginType === 'uid') {
|
||||
localStorage.setItem('uidLogin', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录错误信息
|
||||
* @param requireAuth 是否需要真实登录权限
|
||||
* @returns 错误信息
|
||||
*/
|
||||
export function getLoginErrorMessage(requireAuth: boolean = false): string {
|
||||
const loginInfo = checkLoginStatus();
|
||||
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
return '请先登录';
|
||||
}
|
||||
|
||||
if (requireAuth && loginInfo.loginType === 'uid') {
|
||||
return 'UID登录无法访问此功能,请使用Cookie或二维码登录';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -1,3 +1,86 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="phone-login" :class="setAnimationClass('animate__fadeInDown')">
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<!-- Tab导航 -->
|
||||
<div class="login-tabs" :class="setAnimationClass('animate__fadeInUp')">
|
||||
<div
|
||||
v-for="tab in loginTabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: activeMode === tab.key }"
|
||||
@click="switchToMode(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录内容区域 -->
|
||||
<div class="login-content">
|
||||
<!-- 过渡动画包装器 -->
|
||||
<transition
|
||||
name="login-content"
|
||||
mode="out-in"
|
||||
enter-active-class="animate__animated animate__fadeIn"
|
||||
leave-active-class="animate__animated animate__fadeOut"
|
||||
>
|
||||
<!-- 二维码登录组件 -->
|
||||
<div v-if="activeMode === LoginMode.QR && !isTransitioning" key="qr" class="phone">
|
||||
<qr-login @login-success="handleLoginSuccess" @login-error="handleLoginError" />
|
||||
</div>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<div
|
||||
v-else-if="activeMode === LoginMode.PHONE && !isTransitioning"
|
||||
key="phone"
|
||||
class="phone"
|
||||
>
|
||||
<div class="login-title">{{ t('login.title.phone') }}</div>
|
||||
<div class="phone-page">
|
||||
<input
|
||||
v-model="phone"
|
||||
class="phone-input"
|
||||
type="text"
|
||||
:placeholder="t('login.placeholder.phone')"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
class="phone-input"
|
||||
type="password"
|
||||
:placeholder="t('login.placeholder.password')"
|
||||
/>
|
||||
</div>
|
||||
<div class="text">{{ t('login.phoneTip') }}</div>
|
||||
<n-button class="btn-login" @click="loginPhone()">{{
|
||||
t('login.button.login')
|
||||
}}</n-button>
|
||||
</div>
|
||||
|
||||
<!-- UID登录组件 -->
|
||||
<div
|
||||
v-else-if="activeMode === LoginMode.UID && !isTransitioning"
|
||||
key="uid"
|
||||
class="phone"
|
||||
>
|
||||
<uid-login @login-success="handleLoginSuccess" @login-error="handleLoginError" />
|
||||
</div>
|
||||
|
||||
<!-- Cookie登录组件 -->
|
||||
<div
|
||||
v-else-if="activeMode === LoginMode.TOKEN && !isTransitioning"
|
||||
key="token"
|
||||
class="phone"
|
||||
>
|
||||
<cookie-login @login-success="handleLoginSuccess" @login-error="handleLoginError" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -6,6 +89,7 @@ import { useRouter } from 'vue-router';
|
||||
import { loginByCellphone } from '@/api/login';
|
||||
import CookieLogin from '@/components/login/CookieLogin.vue';
|
||||
import QrLogin from '@/components/login/QrLogin.vue';
|
||||
import UidLogin from '@/components/login/UidLogin.vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
@@ -13,84 +97,108 @@ defineOptions({
|
||||
name: 'Login'
|
||||
});
|
||||
|
||||
// 登录模式枚举
|
||||
enum LoginMode {
|
||||
QR = 'qr',
|
||||
PHONE = 'phone',
|
||||
UID = 'uid',
|
||||
TOKEN = 'token'
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const isQr = ref(true);
|
||||
const isTokenLogin = ref(false);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 当前激活的登录模式
|
||||
const activeMode = ref<LoginMode>(LoginMode.TOKEN);
|
||||
// 用于控制内容切换动画
|
||||
const isTransitioning = ref(false);
|
||||
|
||||
// 登录选项配置
|
||||
const loginTabs = computed(() => [
|
||||
{ key: LoginMode.TOKEN, label: t('login.title.cookie') },
|
||||
{ key: LoginMode.UID, label: t('login.title.uid') },
|
||||
{ key: LoginMode.QR, label: t('login.title.qr') }
|
||||
]);
|
||||
|
||||
// 手机号登录
|
||||
const phone = ref('');
|
||||
const password = ref('');
|
||||
const loginPhone = async () => {
|
||||
const { data } = await loginByCellphone(phone.value, password.value);
|
||||
if (data.code === 200) {
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
userStore.user = data.profile;
|
||||
localStorage.setItem('token', data.cookie);
|
||||
setTimeout(() => {
|
||||
router.push('/user');
|
||||
}, 1000);
|
||||
try {
|
||||
if (!phone.value.trim()) {
|
||||
message.error(t('login.message.phoneRequired'));
|
||||
return;
|
||||
}
|
||||
if (!password.value.trim()) {
|
||||
message.error(t('login.message.passwordRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await loginByCellphone(phone.value, password.value);
|
||||
if (data.code === 200) {
|
||||
message.success(t('login.message.loginSuccess'));
|
||||
userStore.setUser(data.profile);
|
||||
localStorage.setItem('token', data.cookie);
|
||||
setTimeout(() => {
|
||||
router.push('/user');
|
||||
}, 1000);
|
||||
} else {
|
||||
message.error(t('login.message.phoneLoginFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(t('login.message.phoneLoginFailed'));
|
||||
console.error(t('login.message.loginFailed') + ':', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换登录模式(带动画效果)
|
||||
const switchToMode = (mode: LoginMode) => {
|
||||
if (mode === activeMode.value) return;
|
||||
|
||||
isTransitioning.value = true;
|
||||
setTimeout(() => {
|
||||
activeMode.value = mode;
|
||||
setTimeout(() => {
|
||||
isTransitioning.value = false;
|
||||
}, 50);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// 通用登录成功处理
|
||||
const handleLoginSuccess = (userProfile: any, loginType: string) => {
|
||||
// 更新 userStore(这会同时更新 store 状态和 localStorage 中的用户数据)
|
||||
userStore.setUser(userProfile);
|
||||
|
||||
// 设置登录类型到 userStore 和 localStorage
|
||||
userStore.setLoginType(loginType as any);
|
||||
|
||||
// 设置其他相关状态
|
||||
const token = loginType !== 'uid' ? localStorage.getItem('token') : undefined;
|
||||
|
||||
if (token) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
if (loginType === 'uid') {
|
||||
localStorage.setItem('uidLogin', 'true');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
router.push('/user');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 通用登录错误处理
|
||||
const handleLoginError = (error: string) => {
|
||||
console.error(t('login.message.loginFailed') + ':', error);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="phone-login">
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<!-- 二维码登录组件 -->
|
||||
<div v-if="isQr && !isTokenLogin" class="phone">
|
||||
<qr-login />
|
||||
</div>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<div
|
||||
v-else-if="!isQr && !isTokenLogin"
|
||||
class="phone"
|
||||
:class="setAnimationClass('animate__fadeInUp')"
|
||||
>
|
||||
<div class="login-title">{{ t('login.title.phone') }}</div>
|
||||
<div class="phone-page">
|
||||
<input
|
||||
v-model="phone"
|
||||
class="phone-input"
|
||||
type="text"
|
||||
:placeholder="t('login.placeholder.phone')"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
class="phone-input"
|
||||
type="password"
|
||||
:placeholder="t('login.placeholder.password')"
|
||||
/>
|
||||
</div>
|
||||
<div class="text">{{ t('login.phoneTip') }}</div>
|
||||
<n-button class="btn-login" @click="loginPhone()">{{ t('login.button.login') }}</n-button>
|
||||
</div>
|
||||
|
||||
<!-- Cookie登录组件 -->
|
||||
<div v-else-if="isTokenLogin" class="phone">
|
||||
<cookie-login />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="login-switch">
|
||||
<div class="title" @click="isTokenLogin = !isTokenLogin">
|
||||
{{ isTokenLogin ? t('login.button.backToQr') : t('login.button.switchToToken') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
@apply flex flex-col items-center justify-center pt-20;
|
||||
@apply flex flex-col items-center justify-center;
|
||||
@apply bg-light dark:bg-black;
|
||||
}
|
||||
|
||||
@@ -104,37 +212,57 @@ const loginPhone = async () => {
|
||||
|
||||
.phone-login {
|
||||
width: 350px;
|
||||
height: 550px;
|
||||
height: 550px; /* 恢复原来的高度 */
|
||||
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.dev/svgjs' width='400' height='560' preserveAspectRatio='none' viewBox='0 0 400 560'%3e%3cg mask='url(%26quot%3b%23SvgjsMask1066%26quot%3b)' fill='none'%3e%3crect width='400' height='560' x='0' y='0' fill='rgba(24%2c 106%2c 59%2c 1)'%3e%3c/rect%3e%3cpath d='M0%2c234.738C43.535%2c236.921%2c80.103%2c205.252%2c116.272%2c180.923C151.738%2c157.067%2c188.295%2c132.929%2c207.855%2c94.924C227.898%2c55.979%2c233.386%2c10.682%2c226.119%2c-32.511C218.952%2c-75.107%2c199.189%2c-115.793%2c167.469%2c-145.113C137.399%2c-172.909%2c92.499%2c-171.842%2c55.779%2c-189.967C8.719%2c-213.196%2c-28.344%2c-282.721%2c-78.217%2c-266.382C-128.725%2c-249.834%2c-111.35%2c-166.696%2c-143.781%2c-124.587C-173.232%2c-86.348%2c-244.72%2c-83.812%2c-255.129%2c-36.682C-265.368%2c9.678%2c-217.952%2c48.26%2c-190.512%2c87.004C-167.691%2c119.226%2c-140.216%2c145.431%2c-109.013%2c169.627C-74.874%2c196.1%2c-43.147%2c232.575%2c0%2c234.738' fill='%23114b2a'%3e%3c/path%3e%3cpath d='M400 800.9010000000001C443.973 795.023 480.102 765.6 513.011 735.848 541.923 709.71 561.585 676.6320000000001 577.037 640.85 592.211 605.712 606.958 568.912 601.458 531.035 595.962 493.182 568.394 464.36400000000003 546.825 432.775 522.317 396.88300000000004 507.656 347.475 466.528 333.426 425.366 319.366 384.338 352.414 342.111 362.847 297.497 373.869 242.385 362.645 211.294 396.486 180.212 430.318 192.333 483.83299999999997 188.872 529.644 185.656 572.218 178.696 614.453 191.757 655.101 205.885 699.068 227.92 742.4110000000001 265.75 768.898 304.214 795.829 353.459 807.1220000000001 400 800.9010000000001' fill='%231f894c'%3e%3c/path%3e%3c/g%3e%3cdefs%3e%3cmask id='SvgjsMask1066'%3e%3crect width='400' height='560' fill='white'%3e%3c/rect%3e%3c/mask%3e%3c/defs%3e%3c/svg%3e");
|
||||
box-shadow: inset 0px 0px 20px 5px rgba(0, 0, 0, 0.37);
|
||||
animation-duration: 0.8s;
|
||||
|
||||
.bg {
|
||||
@apply absolute w-full h-full bg-light-100 dark:bg-dark-100 opacity-20;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 200%;
|
||||
height: 250px;
|
||||
bottom: -180px;
|
||||
border-radius: 50%;
|
||||
left: 50%;
|
||||
padding: 10px;
|
||||
transform: translateX(-50%);
|
||||
@apply absolute bg-light dark:bg-dark flex justify-center text-lg font-bold cursor-pointer;
|
||||
@apply text-gray-400 hover:text-gray-800 hover:dark:text-white transition-colors;
|
||||
box-shadow: 10px 0px 20px rgba(0, 0, 0, 0.66);
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
|
||||
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center text-center;
|
||||
|
||||
.login-tabs {
|
||||
@apply flex mb-6 bg-black bg-opacity-20 rounded-xl p-1;
|
||||
width: 320px;
|
||||
animation-duration: 0.6s;
|
||||
animation-delay: 0.2s;
|
||||
|
||||
.tab-item {
|
||||
@apply flex-1 py-2 px-3 text-sm text-white text-center cursor-pointer rounded-lg transition-all duration-300;
|
||||
@apply hover:bg-white hover:bg-opacity-10;
|
||||
transform: translateY(0);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-green-600 text-white font-medium;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
@apply flex-1 flex items-center justify-center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.phone {
|
||||
animation-duration: 0.5s;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
|
||||
&-page {
|
||||
@apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;
|
||||
width: 250px;
|
||||
@apply rounded-2xl overflow-hidden;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&-input {
|
||||
@@ -143,9 +271,11 @@ const loginPhone = async () => {
|
||||
@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;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
@apply border-green-500;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,14 +284,42 @@ const loginPhone = async () => {
|
||||
width: 250px;
|
||||
height: 40px;
|
||||
@apply mt-10 text-white rounded-xl;
|
||||
@apply bg-green-600 hover:bg-green-700 transition-colors;
|
||||
@apply bg-green-600 hover:bg-green-700 transition-all duration-300;
|
||||
transform: translateY(0);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录内容切换动画 */
|
||||
.login-content-enter-active,
|
||||
.login-content-leave-active {
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.login-content-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.login-content-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.login-page {
|
||||
@apply pt-0;
|
||||
}
|
||||
|
||||
.phone-login {
|
||||
width: 90vw;
|
||||
max-width: 350px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,6 +75,7 @@ import { getUserFollowers } from '@/api/user';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import type { IUserFollow } from '@/type/user';
|
||||
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
import { checkLoginStatus as checkAuthStatus } from '@/utils/auth';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserFollowers'
|
||||
@@ -114,17 +115,16 @@ const checkTargetUser = () => {
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userData = localStorage.getItem('user');
|
||||
const loginInfo = checkAuthStatus();
|
||||
|
||||
if (!token || !userData) {
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
router.push('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果store中没有用户数据,但localStorage中有,则恢复用户数据
|
||||
if (!userStore.user && userData) {
|
||||
userStore.setUser(JSON.parse(userData));
|
||||
if (!userStore.user && loginInfo.user) {
|
||||
userStore.setUser(loginInfo.user);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -75,6 +75,7 @@ import { getUserFollows } from '@/api/user';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import type { IUserFollow } from '@/type/user';
|
||||
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
import { checkLoginStatus as checkAuthStatus } from '@/utils/auth';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserFollows'
|
||||
@@ -114,17 +115,16 @@ const checkTargetUser = () => {
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userData = localStorage.getItem('user');
|
||||
const loginInfo = checkAuthStatus();
|
||||
|
||||
if (!token || !userData) {
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
router.push('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果store中没有用户数据,但localStorage中有,则恢复用户数据
|
||||
if (!userStore.user && userData) {
|
||||
userStore.setUser(JSON.parse(userData));
|
||||
if (!userStore.user && loginInfo.user) {
|
||||
userStore.setUser(loginInfo.user);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
|
||||
>
|
||||
<div class="page">
|
||||
<div class="user-name">{{ user.nickname }}</div>
|
||||
<div class="user-name">
|
||||
<span>{{ user.nickname }}</span>
|
||||
<span class="login-type">{{ t('login.title.' + currentLoginType) }}</span>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
|
||||
<div class="user-info-list">
|
||||
@@ -26,7 +29,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
|
||||
|
||||
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
<div class="title">
|
||||
<div>{{ t('user.playlist.created') }}</div>
|
||||
@@ -119,6 +121,7 @@ import { useUserStore } from '@/store/modules/user';
|
||||
import type { Playlist } from '@/type/listDetail';
|
||||
import type { IUserDetail } from '@/type/user';
|
||||
import { getImgUrl, isElectron, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
import { checkLoginStatus as checkAuthStatus } from '@/utils/auth';
|
||||
import LoginComponent from '@/views/login/index.vue';
|
||||
|
||||
defineOptions({
|
||||
@@ -150,17 +153,26 @@ onBeforeUnmount(() => {
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userData = localStorage.getItem('user');
|
||||
// 优先使用 userStore 中的状态
|
||||
if (userStore.user && userStore.loginType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!token || !userData) {
|
||||
// 如果 store 中没有数据,尝试从 localStorage 恢复
|
||||
const loginInfo = checkAuthStatus();
|
||||
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
!isMobile.value && router.push('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果store中没有用户数据,但localStorage中有,则恢复用户数据
|
||||
if (!userStore.user && userData) {
|
||||
userStore.setUser(JSON.parse(userData));
|
||||
// 恢复用户数据和登录类型到 store
|
||||
if (!userStore.user && loginInfo.user) {
|
||||
userStore.setUser(loginInfo.user);
|
||||
}
|
||||
|
||||
if (!userStore.loginType && loginInfo.loginType) {
|
||||
userStore.setLoginType(loginInfo.loginType);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -289,6 +301,7 @@ const handleLoginSuccess = () => {
|
||||
};
|
||||
|
||||
const isLoggedIn = computed(() => userStore.user);
|
||||
const currentLoginType = computed(() => userStore.loginType);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -314,7 +327,7 @@ const isLoggedIn = computed(() => userStore.user);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
@apply text-xl font-bold mb-4;
|
||||
@apply text-xl font-bold mb-4 flex justify-between;
|
||||
@apply text-white text-opacity-70;
|
||||
}
|
||||
|
||||
@@ -414,6 +427,10 @@ const isLoggedIn = computed(() => userStore.user);
|
||||
}
|
||||
}
|
||||
|
||||
.login-type {
|
||||
@apply text-sm text-green-500 dark:text-green-400;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.user-page {
|
||||
@apply px-4;
|
||||
|
||||
Reference in New Issue
Block a user