feat: 添加Cookie登录功能及自动获取等相关管理设置

feat: #413 #424
This commit is contained in:
alger
2025-08-06 22:36:30 +08:00
parent 09ccd9f2a6
commit 16aeaf2948
18 changed files with 1245 additions and 115 deletions
@@ -0,0 +1,202 @@
<script lang="ts" setup>
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'
});
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'));
return;
}
try {
// 直接设置token到localStorage
localStorage.setItem('token', token.value.trim());
// 获取用户信息验证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');
} else {
// token无效,清除localStorage
localStorage.removeItem('token');
message.error(t('login.message.tokenInvalid'));
}
} catch (error) {
// token无效,清除localStorage
localStorage.removeItem('token');
message.error(t('login.message.tokenInvalid'));
console.error('Token登录失败:', error);
}
};
// 自动获取Cookie
const autoGetCookie = () => {
if (!isElectron) {
message.error('此功能仅在桌面版中可用');
return;
}
message.info(t('login.message.autoGetCookieTip'));
window.electron.ipcRenderer.send('open-login');
};
// 监听Cookie接收
const handleCookieReceived = async (_event: any, cookieValue: string) => {
try {
// 设置Cookie到localStorage
localStorage.setItem('token', cookieValue);
// 验证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');
} else {
localStorage.removeItem('token');
message.error(t('login.message.autoGetCookieFailed'));
}
} catch (error) {
localStorage.removeItem('token');
message.error(t('login.message.autoGetCookieFailed'));
console.error('自动获取Cookie失败:', error);
}
};
// 在组件挂载时添加监听器
onMounted(() => {
if (isElectron) {
window.electron.ipcRenderer.on('send-cookies', handleCookieReceived);
}
});
// 在组件卸载时移除监听器
onBeforeUnmount(() => {
if (isElectron) {
window.electron.ipcRenderer.removeAllListeners('send-cookies');
}
});
</script>
<template>
<div class="cookie-login" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">{{ t('login.title.token') }}</div>
<div class="phone-page">
<textarea
v-model="token"
class="token-input"
:placeholder="t('login.placeholder.token')"
rows="4"
/>
</div>
<div class="text">{{ t('login.tokenTip') }}</div>
<n-button class="btn-login" @click="loginByToken()">{{
t('login.button.tokenLogin')
}}</n-button>
<n-button v-if="isElectron" class="btn-auto-cookie" @click="autoGetCookie()" type="info">
{{ t('login.button.autoGetCookie') }}
</n-button>
</div>
</template>
<style lang="scss" scoped>
.cookie-login {
animation-duration: 0.5s;
@apply flex flex-col items-center;
}
.login-title {
@apply text-2xl font-bold mb-6 text-white;
}
.text {
@apply mt-4 text-white text-xs;
}
.phone-page {
@apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;
width: 250px;
@apply rounded-2xl overflow-hidden;
padding: 0;
border: none;
}
.token-input {
@apply w-full outline-none resize-none;
@apply text-gray-900 dark:text-white bg-transparent;
@apply placeholder-gray-500 dark:placeholder-gray-400;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
min-height: 100px;
padding: 16px;
margin: 0;
border: none;
border-radius: inherit;
box-sizing: border-box;
&:focus {
@apply outline-none;
box-shadow: none;
border: none;
}
&::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
/* 移除浏览器默认样式 */
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(156, 163, 175, 0.3);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.5);
}
}
.btn-login {
width: 250px;
height: 40px;
@apply mt-10 text-white rounded-xl;
@apply bg-green-600 hover:bg-green-700 transition-colors;
}
.btn-auto-cookie {
width: 250px;
height: 40px;
@apply mt-4 text-white rounded-xl;
@apply bg-blue-600 hover:bg-blue-700 transition-colors;
}
</style>
+270
View File
@@ -0,0 +1,270 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { onBeforeUnmount, 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'
});
const { t } = useI18n();
const message = useMessage();
const router = useRouter();
const userStore = useUserStore();
const qrUrl = ref<string>();
const timerRef = ref(null);
const qrStatus = ref<'loading' | 'active' | 'expired' | 'scanned' | 'confirmed'>('loading');
const isRefreshing = ref(false);
const loadLogin = async () => {
try {
isRefreshing.value = true;
qrStatus.value = 'loading';
// 清理之前的定时器
if (timerRef.value) {
clearInterval(timerRef.value);
timerRef.value = null;
}
const qrKey = await getQrKey();
const key = qrKey.data.data.unikey;
const { data } = await createQr(key);
qrUrl.value = data.data.qrimg;
qrStatus.value = 'active';
const timer = timerIsQr(key);
timerRef.value = timer as any;
} catch (error) {
console.error(t('login.message.loadError'), error);
qrStatus.value = 'expired';
message.error(t('login.message.loadError'));
} finally {
isRefreshing.value = false;
}
};
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
try {
const { data } = await checkQr(key);
// 二维码过期或不存在
if (data.code === 800) {
qrStatus.value = 'expired';
clearInterval(timer);
timerRef.value = null;
message.warning('二维码已过期,请点击刷新获取新的二维码');
return;
}
// 等待扫码
if (data.code === 801) {
qrStatus.value = 'active';
return;
}
// 已扫码,等待确认
if (data.code === 802) {
qrStatus.value = 'scanned';
message.info('已扫码,请在手机上确认登录');
return;
}
// 登录成功
if (data.code === 803) {
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'));
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('检查二维码状态失败,请刷新重试');
}
}, 3000);
return timer;
};
// 手动刷新二维码
const refreshQr = () => {
loadLogin();
};
// 获取状态显示文本
const getStatusText = () => {
switch (qrStatus.value) {
case 'loading':
return '正在加载二维码...';
case 'active':
return t('login.qrTip');
case 'expired':
return '二维码已过期,请点击刷新';
case 'scanned':
return '已扫码,请在手机上确认登录';
case 'confirmed':
return '登录成功,正在跳转...';
default:
return t('login.qrTip');
}
};
onMounted(() => {
loadLogin();
});
onBeforeUnmount(() => {
if (timerRef.value) {
clearInterval(timerRef.value);
timerRef.value = null;
}
});
</script>
<template>
<div class="qr-login" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">{{ t('login.title.qr') }}</div>
<!-- 二维码容器 -->
<div class="qr-container">
<!-- 加载状态 -->
<div v-if="qrStatus === 'loading'" class="qr-loading">
<n-spin size="large" />
<div class="loading-text">正在生成二维码...</div>
</div>
<!-- 二维码图片 -->
<div v-else class="qr-image-wrapper" :class="{ expired: qrStatus === 'expired' }">
<img class="qr-img" :src="qrUrl" />
<!-- 过期遮罩 -->
<div v-if="qrStatus === 'expired'" class="expired-overlay">
<div class="expired-text">二维码已过期</div>
<n-button class="refresh-btn" type="primary" @click="refreshQr" :loading="isRefreshing">
{{ isRefreshing ? '刷新中...' : '点击刷新' }}
</n-button>
</div>
<!-- 已扫码遮罩 -->
<div v-if="qrStatus === 'scanned'" class="scanned-overlay">
<div class="scanned-icon"></div>
<div class="scanned-text">已扫码</div>
</div>
</div>
</div>
<!-- 状态文本 -->
<div class="text" :class="{ expired: qrStatus === 'expired', scanned: qrStatus === 'scanned' }">
{{ getStatusText() }}
</div>
<!-- 手动刷新按钮 -->
<div v-if="qrStatus === 'active'" class="refresh-area">
<n-button text class="manual-refresh" @click="refreshQr" :loading="isRefreshing">
刷新二维码
</n-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.qr-login {
animation-duration: 0.5s;
}
.login-title {
@apply text-2xl font-bold mb-6 text-white;
}
.qr-container {
@apply relative;
width: 200px;
height: 200px;
@apply mx-auto;
}
.qr-loading {
@apply flex flex-col items-center justify-center h-full;
.loading-text {
@apply mt-4 text-white text-sm;
}
}
.qr-image-wrapper {
@apply relative w-full h-full;
&.expired {
.qr-img {
@apply opacity-30;
}
}
}
.qr-img {
@apply w-full h-full rounded-2xl transition-all duration-300;
object-fit: cover;
}
.expired-overlay {
@apply absolute inset-0 flex flex-col items-center justify-center;
@apply bg-black bg-opacity-50 rounded-2xl;
.expired-text {
@apply text-white text-sm mb-3;
}
.refresh-btn {
@apply text-sm;
}
}
.scanned-overlay {
@apply absolute inset-0 flex flex-col items-center justify-center;
@apply bg-green-500 bg-opacity-80 rounded-2xl;
.scanned-icon {
@apply text-white text-4xl font-bold mb-2;
}
.scanned-text {
@apply text-white text-sm;
}
}
.text {
@apply mt-4 text-white text-xs transition-colors duration-300;
&.expired {
@apply text-orange-400;
}
&.scanned {
@apply text-green-400;
}
}
.refresh-area {
@apply mt-3;
.manual-refresh {
@apply text-gray-300 hover:text-white text-xs;
}
}
</style>
@@ -36,12 +36,8 @@
<div class="tab-content">
<div class="settings-grid">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hidePlayBar') }}</span>
<n-switch v-model:value="config.hidePlayBar" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideMiniPlayBar') }}</span>
<n-switch v-model:value="config.hideMiniPlayBar" />
<span>{{ t('settings.lyricSettings.showMiniPlayBar') }}</span>
<n-switch v-model:value="showMiniPlayBar" />
</div>
</div>
<div class="theme-section">
@@ -120,7 +116,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
@@ -129,6 +125,22 @@ const { t } = useI18n();
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
const emit = defineEmits(['themeChange']);
// 显示mini播放栏开关
const showMiniPlayBar = computed({
get: () => !config.value.hideMiniPlayBar,
set: (value: boolean) => {
if (value) {
// 显示mini播放栏,隐藏普通播放栏
config.value.hideMiniPlayBar = false;
config.value.hidePlayBar = true;
} else {
// 显示普通播放栏,隐藏mini播放栏
config.value.hideMiniPlayBar = true;
config.value.hidePlayBar = false;
}
}
});
watch(
() => config.value,
(newConfig) => {
@@ -0,0 +1,197 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'CookieSettingsModal'
});
interface Props {
show: boolean;
initialValue?: string;
}
interface Emits {
(e: 'update:show', value: boolean): void;
(e: 'save', value: string): void;
}
const props = withDefaults(defineProps<Props>(), {
initialValue: ''
});
const emit = defineEmits<Emits>();
const { t } = useI18n();
const message = useMessage();
const tokenInput = ref('');
const isLoading = ref(false);
// 监听显示状态变化,重置输入框
watch(
() => props.show,
(newShow) => {
if (newShow) {
tokenInput.value = props.initialValue;
}
}
);
// 监听初始值变化
watch(
() => props.initialValue,
(newValue) => {
if (props.show) {
tokenInput.value = newValue;
}
}
);
// 关闭弹窗
const handleClose = () => {
emit('update:show', false);
};
// 保存Cookie
const handleSave = async () => {
const trimmedToken = tokenInput.value.trim();
if (!trimmedToken) {
message.error(t('settings.cookie.validation.required'));
return;
}
// 简单验证Cookie格式
if (!trimmedToken.includes('MUSIC_U=')) {
message.warning(t('settings.cookie.validation.format'));
}
try {
isLoading.value = true;
emit('save', trimmedToken);
message.success(t('settings.cookie.message.saveSuccess'));
handleClose();
} catch (error) {
console.error('保存Cookie失败:', error);
message.error(t('settings.cookie.message.saveError'));
} finally {
isLoading.value = false;
}
};
// 清空输入框
const handleClear = () => {
tokenInput.value = '';
};
// 从剪贴板粘贴
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
if (text) {
tokenInput.value = text;
message.success(t('settings.cookie.message.pasteSuccess'));
}
} catch (error) {
console.error('粘贴失败:', error);
message.error(t('settings.cookie.message.pasteError'));
}
};
</script>
<template>
<n-modal
:show="show"
preset="dialog"
:title="t('settings.cookie.title')"
@update:show="emit('update:show', $event)"
>
<template #header>
<div class="flex items-center gap-2">
<i class="ri-key-line"></i>
<span>{{ t('settings.cookie.title') }}</span>
</div>
</template>
<div class="space-y-4">
<div>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
{{ t('settings.cookie.description') }}
</div>
<div class="relative">
<n-input
v-model:value="tokenInput"
type="textarea"
:placeholder="t('settings.cookie.placeholder')"
:rows="6"
:autosize="{ minRows: 4, maxRows: 8 }"
style="font-family: monospace; font-size: 12px"
class="cookie-input"
/>
<!-- 工具按钮 -->
<div class="absolute top-2 right-2 flex gap-1">
<n-button
size="tiny"
quaternary
@click="handlePaste"
:title="t('settings.cookie.action.paste')"
>
<i class="ri-clipboard-line"></i>
</n-button>
<n-button
size="tiny"
quaternary
@click="handleClear"
:title="t('settings.cookie.action.clear')"
>
<i class="ri-delete-bin-line"></i>
</n-button>
</div>
</div>
</div>
<!-- 帮助信息 -->
<div class="text-xs text-gray-500 space-y-1">
<p> {{ t('settings.cookie.help.format') }}</p>
<p> {{ t('settings.cookie.help.source') }}</p>
<p> {{ t('settings.cookie.help.storage') }}</p>
</div>
<!-- Cookie长度提示 -->
<div v-if="tokenInput" class="text-xs text-gray-400">
{{ t('settings.cookie.info.length', { length: tokenInput.length }) }}
</div>
</div>
<template #action>
<div class="flex gap-2">
<n-button @click="handleClose">
{{ t('common.cancel') }}
</n-button>
<n-button
type="primary"
@click="handleSave"
:disabled="!tokenInput.trim()"
:loading="isLoading"
>
{{ t('settings.cookie.action.save') }}
</n-button>
</div>
</template>
</n-modal>
</template>
<style lang="scss" scoped>
.cookie-input {
:deep(.n-input__textarea) {
font-family:
'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Consolas',
monospace;
line-height: 1.4;
}
}
</style>
+25 -83
View File
@@ -1,10 +1,11 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
import { loginByCellphone } from '@/api/login';
import CookieLogin from '@/components/login/CookieLogin.vue';
import QrLogin from '@/components/login/QrLogin.vue';
import { useUserStore } from '@/store/modules/user';
import { setAnimationClass } from '@/utils';
@@ -16,79 +17,10 @@ const { t } = useI18n();
const message = useMessage();
const router = useRouter();
const isQr = ref(true);
const isTokenLogin = ref(false);
const qrUrl = ref<string>();
const userStore = useUserStore();
onMounted(() => {
loadLogin();
});
const timerRef = ref(null);
const loadLogin = async () => {
try {
if (timerRef.value) {
clearInterval(timerRef.value);
timerRef.value = null;
}
if (!isQr.value) return;
const qrKey = await getQrKey();
const key = qrKey.data.data.unikey;
const { data } = await createQr(key);
qrUrl.value = data.data.qrimg;
const timer = timerIsQr(key);
timerRef.value = timer as any;
} catch (error) {
console.error(t('login.message.loadError'), error);
}
};
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
try {
const { data } = await checkQr(key);
if (data.code === 800) {
clearInterval(timer);
timerRef.value = null;
}
if (data.code === 803) {
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'));
clearInterval(timer);
timerRef.value = null;
router.push('/user');
}
} catch (error) {
console.error(t('login.message.qrCheckError'), error);
clearInterval(timer);
timerRef.value = null;
}
}, 3000);
return timer;
};
// 离开页面时
onBeforeUnmount(() => {
if (timerRef.value) {
clearInterval(timerRef.value);
timerRef.value = null;
}
});
// 是否扫码登陆
// const chooseQr = () => {
// isQr.value = !isQr.value;
// loadLogin();
// };
// 手机号登录
const phone = ref('');
const password = ref('');
@@ -110,12 +42,17 @@ const loginPhone = async () => {
<div class="phone-login">
<div class="bg"></div>
<div class="content">
<div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">{{ t('login.title.qr') }}</div>
<img class="qr-img" :src="qrUrl" />
<div class="text">{{ t('login.qrTip') }}</div>
<!-- 二维码登录组件 -->
<div v-if="isQr && !isTokenLogin" class="phone">
<qr-login />
</div>
<div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
<!-- 手机号登录 -->
<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
@@ -134,11 +71,18 @@ const loginPhone = async () => {
<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="title" @click="chooseQr()">
{{ isQr ? t('login.button.switchToPhone') : t('login.button.switchToQr') }}
</div> -->
<div class="login-switch">
<div class="title" @click="isTokenLogin = !isTokenLogin">
{{ isTokenLogin ? t('login.button.backToQr') : t('login.button.switchToToken') }}
</div>
</div>
</div>
</div>
</div>
@@ -184,9 +128,6 @@ const loginPhone = async () => {
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
.qr-img {
@apply rounded-2xl cursor-pointer transition-opacity;
}
.phone {
animation-duration: 0.5s;
@@ -208,6 +149,7 @@ const loginPhone = async () => {
}
}
}
.btn-login {
width: 250px;
height: 40px;
+145
View File
@@ -108,6 +108,34 @@
</div>
</div>
<!-- Token管理 -->
<div class="set-item">
<div>
<div class="set-item-title">{{ t('settings.basic.tokenManagement') }}</div>
<div class="set-item-content">
<div class="text-sm text-gray-500 mb-2">
{{ t('settings.basic.tokenStatus') }}:
{{
currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet')
}}
</div>
<div v-if="currentToken" class="text-xs text-gray-400 mb-2 font-mono break-all">
{{ currentToken.substring(0, 50) }}...
</div>
</div>
</div>
<div class="flex gap-2">
<n-button size="small" @click="showTokenModal = true">
{{
currentToken ? t('settings.basic.modifyToken') : t('settings.basic.setToken')
}}
</n-button>
<n-button v-if="currentToken" size="small" type="error" @click="clearToken">
{{ t('settings.basic.clearToken') }}
</n-button>
</div>
</div>
<div class="set-item">
<div>
<div class="set-item-title">{{ t('settings.basic.animation') }}</div>
@@ -524,6 +552,44 @@
<remote-control-setting v-model:visible="showRemoteControlModal" />
</template>
<!-- Token设置弹窗 -->
<n-modal v-model:show="showTokenModal" preset="dialog" title="Cookie设置">
<template #header>
<div class="flex items-center gap-2">
<i class="ri-key-line"></i>
<span>Cookie设置</span>
</div>
</template>
<div class="space-y-4">
<div>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
请输入网易云音乐的Cookie
</div>
<n-input
v-model:value="tokenInput"
type="textarea"
placeholder="请粘贴完整的Cookie..."
:rows="6"
:autosize="{ minRows: 4, maxRows: 8 }"
style="font-family: monospace; font-size: 12px"
/>
</div>
<div class="text-xs text-gray-500">
<p> Cookie通常以 "MUSIC_U=" 开头</p>
<p> 可以从浏览器开发者工具的网络请求中获取</p>
<p> Cookie设置后将自动保存到本地存储</p>
</div>
</div>
<template #action>
<div class="flex gap-2">
<n-button @click="showTokenModal = false">取消</n-button>
<n-button type="primary" @click="saveToken" :disabled="!tokenInput.trim()">
保存Cookie
</n-button>
</div>
</template>
</n-modal>
<!-- 清除缓存弹窗 -->
<clear-cache-settings v-model:show="showClearCacheModal" @confirm="clearCache" />
</div>
@@ -536,6 +602,7 @@ import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import localData from '@/../main/set.json';
import { getUserDetail } from '@/api/login';
import Coffee from '@/components/Coffee.vue';
import DonationList from '@/components/common/DonationList.vue';
import PlayBottom from '@/components/common/PlayBottom.vue';
@@ -991,6 +1058,84 @@ const showMusicSourcesModal = ref(false);
// 远程控制设置弹窗
const showRemoteControlModal = ref(false);
// Token管理相关
const showTokenModal = ref(false);
const tokenInput = ref('');
const currentToken = ref(localStorage.getItem('token') || '');
// 保存Token
const saveToken = async () => {
if (!tokenInput.value.trim()) {
message.error('请输入Token');
return;
}
try {
// 临时保存原有token
const originalToken = localStorage.getItem('token');
// 设置新token
localStorage.setItem('token', tokenInput.value.trim());
// 验证token有效性
const user = await getUserDetail();
if (user.data && user.data.profile) {
// token有效,更新用户信息
userStore.setUser(user.data.profile);
currentToken.value = tokenInput.value.trim();
message.success('Token设置成功');
showTokenModal.value = false;
tokenInput.value = '';
// 刷新当前页面
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// token无效,恢复原有token
if (originalToken) {
localStorage.setItem('token', originalToken);
} else {
localStorage.removeItem('token');
}
message.error('Token无效,请检查后重试');
}
} catch (error) {
// token无效,恢复原有token
const originalToken = localStorage.getItem('token');
if (originalToken) {
localStorage.setItem('token', originalToken);
} else {
localStorage.removeItem('token');
}
message.error('Token无效,请检查后重试');
console.error('Token验证失败:', error);
}
};
// 清除Token
const clearToken = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
currentToken.value = '';
userStore.user = null;
message.success('Token已清除');
// 刷新页面
setTimeout(() => {
window.location.reload();
}, 1000);
};
// 监听localStorage中token的变化
watch(
() => localStorage.getItem('token'),
(newToken) => {
currentToken.value = newToken || '';
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>