From 607735e55b5cb4ff024355c4add703b063074f6f Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sat, 4 Apr 2026 18:37:05 +0800 Subject: [PATCH] fix(dashboard): resolve missing translations and autocomplete warnings --- .gitignore | 1 + .../src/i18n/locales/en-US/features/auth.json | 7 + .../src/i18n/locales/ru-RU/features/auth.json | 9 +- .../src/i18n/locales/zh-CN/features/auth.json | 7 + dashboard/src/layouts/full/FullLayout.vue | 17 +- .../full/vertical-header/VerticalHeader.vue | 1643 ++++++++++------- dashboard/src/main.ts | 14 +- dashboard/src/stores/auth.ts | 9 +- dashboard/src/utils/authLoginProof.ts | 2 +- .../authentication/authForms/AuthLogin.vue | 2 + 10 files changed, 983 insertions(+), 728 deletions(-) diff --git a/.gitignore b/.gitignore index 8b8532bbb..02c0f4188 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,4 @@ dashboard/src/assets/mdi-subset/*.woff dashboard/src/assets/mdi-subset/*.woff2 .planning *cache +node_modules diff --git a/dashboard/src/i18n/locales/en-US/features/auth.json b/dashboard/src/i18n/locales/en-US/features/auth.json index d07f952c6..895cab2e9 100644 --- a/dashboard/src/i18n/locales/en-US/features/auth.json +++ b/dashboard/src/i18n/locales/en-US/features/auth.json @@ -3,6 +3,13 @@ "username": "Username", "password": "Password", "defaultHint": "Default credentials: astrbot / astrbot", + "autoTheme": "Auto switch theme", + "addPreset": "Add preset", + "shareLink": "Share config link", + "presetName": "Preset Name", + "presetUrl": "Preset URL", + "linkCopied": "Link copied to clipboard", + "linkCopyFailed": "Failed to copy link", "logo": { "title": "AstrBot Dashboard", "subtitle": "Welcome" diff --git a/dashboard/src/i18n/locales/ru-RU/features/auth.json b/dashboard/src/i18n/locales/ru-RU/features/auth.json index e7dc9cca7..eed0ae322 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/auth.json +++ b/dashboard/src/i18n/locales/ru-RU/features/auth.json @@ -3,6 +3,13 @@ "username": "Имя пользователя", "password": "Пароль", "defaultHint": "Учетные данные по умолчанию: astrbot / astrbot", + "autoTheme": "Автоматическая смена темы", + "addPreset": "Добавить пресет", + "shareLink": "Поделиться ссылкой", + "presetName": "Имя пресета", + "presetUrl": "URL пресета", + "linkCopied": "Ссылка скопирована в буфер обмена", + "linkCopyFailed": "Не удалось скопировать ссылку", "logo": { "title": "Панель управления AstrBot", "subtitle": "Добро пожаловать" @@ -22,4 +29,4 @@ "cancel": "Отмена", "tooltip": "Настройки сервера" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/auth.json b/dashboard/src/i18n/locales/zh-CN/features/auth.json index 84df8ca9b..7e263be13 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/auth.json +++ b/dashboard/src/i18n/locales/zh-CN/features/auth.json @@ -3,6 +3,13 @@ "username": "用户名", "password": "密码", "defaultHint": "默认用户名和密码为 astrbot,请在登录后立即修改以确保安全。", + "autoTheme": "自动切换主题", + "addPreset": "添加预设", + "shareLink": "分享配置链接", + "presetName": "预设名称", + "presetUrl": "预设 URL", + "linkCopied": "链接已复制到剪贴板", + "linkCopyFailed": "复制链接失败", "logo": { "title": "AstrBot WebUI", "subtitle": "欢迎使用" diff --git a/dashboard/src/layouts/full/FullLayout.vue b/dashboard/src/layouts/full/FullLayout.vue index 9824b8024..c1374cc3c 100644 --- a/dashboard/src/layouts/full/FullLayout.vue +++ b/dashboard/src/layouts/full/FullLayout.vue @@ -7,7 +7,6 @@ import VerticalHeaderVue from "./vertical-header/VerticalHeader.vue"; import MigrationDialog from "@/components/shared/MigrationDialog.vue"; import ReadmeDialog from "@/components/shared/ReadmeDialog.vue"; import Chat from "@/components/chat/Chat.vue"; -import ReactorBg from "@/components/shared/ReactorBg.vue"; import { useCustomizerStore } from "@/stores/customizer"; import { useRouterLoadingStore } from "@/stores/routerLoading"; import { useI18n } from "@/i18n/composables"; @@ -18,9 +17,6 @@ const customizer = useCustomizerStore(); const { locale } = useI18n(); const route = useRoute(); const routerLoadingStore = useRouterLoadingStore(); -const isCurrentChatRoute = computed( - () => route.path === "/chat" || route.path.startsWith("/chat/"), -); const isChatPage = computed(() => { return route.path.startsWith("/chat"); @@ -54,10 +50,7 @@ const checkMigration = async (): Promise => { return true; } } catch (error) { - console.warn( - "Failed to check migration status (backend may not be running):", - error, - ); + console.error("Failed to check migration status:", error); } return false; }; @@ -83,10 +76,7 @@ const maybeShowFirstNotice = async () => { localStorage.setItem(FIRST_NOTICE_SEEN_KEY, "1"); } catch (error) { - console.warn( - "Failed to load first notice (backend may not be running):", - error, - ); + console.error("Failed to load first notice:", error); } }; @@ -117,7 +107,6 @@ onMounted(() => { customizer.inputBg ? 'inputWithbg' : '', ]" > - { -import { ref, computed, watch, onMounted } from 'vue'; -import { useCustomizerStore } from '@/stores/customizer'; -import axios from '@/utils/request'; -import Logo from '@/components/shared/Logo.vue'; -import { useAuthStore } from '@/stores/auth'; -import { useCommonStore } from '@/stores/common'; -import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue'; -import 'markstream-vue/index.css'; -import 'katex/dist/katex.min.css'; -import 'highlight.js/styles/github.css'; -import { useI18n } from '@/i18n/composables'; -import { router } from '@/router'; -import { useRoute } from 'vue-router'; -import { useTheme } from 'vuetify'; -import StyledMenu from '@/components/shared/StyledMenu.vue'; -import { useLanguageSwitcher } from '@/i18n/composables'; -import type { Locale } from '@/i18n/types'; -import AboutPage from '@/views/AboutPage.vue'; -import { getDesktopRuntimeInfo } from '@/utils/desktopRuntime'; -import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue'; +import { ref, computed, watch, onBeforeUnmount } from "vue"; +import { useCustomizerStore } from "@/stores/customizer"; +import axios from "@/utils/request"; +import Logo from "@/components/shared/Logo.vue"; +import { useAuthStore } from "@/stores/auth"; +import { useCommonStore } from "@/stores/common"; +import { MarkdownRender, enableKatex, enableMermaid } from "markstream-vue"; +import "markstream-vue/index.css"; +import "katex/dist/katex.min.css"; +import "highlight.js/styles/github.css"; +import { useI18n } from "@/i18n/composables"; +import { router } from "@/router"; +import { useRoute } from "vue-router"; +import StyledMenu from "@/components/shared/StyledMenu.vue"; +import { useLanguageSwitcher } from "@/i18n/composables"; +import type { Locale } from "@/i18n/types"; +import AboutPage from "@/views/AboutPage.vue"; +import { getDesktopRuntimeInfo } from "@/utils/desktopRuntime"; enableKatex(); enableMermaid(); const customizer = useCustomizerStore(); -const theme = useTheme(); +const authStore = useAuthStore(); const { t } = useI18n(); + +const { languageOptions, currentLanguage, switchLanguage, locale } = + useLanguageSwitcher(); + +const languages = computed(() => + languageOptions.value.map((lang) => ({ + code: lang.value, + name: lang.label, + flag: lang.flag, + })), +); + +const currentLocale = computed(() => locale.value); + +const changeLanguage = async (langCode: string) => { + await switchLanguage(langCode as Locale); +}; + const route = useRoute(); -const LAST_BOT_ROUTE_KEY = 'astrbot:last_bot_route'; -const LAST_CHAT_ROUTE_KEY = 'astrbot:last_chat_route'; -let dialog = ref(false); -let accountWarning = ref(false) -let accountWarningLegacy = ref(false); -let updateStatusDialog = ref(false); -let aboutDialog = ref(false); -const username = localStorage.getItem('user'); -let password = ref(''); -let newPassword = ref(''); -let confirmPassword = ref(''); -let newUsername = ref(''); -let status = ref(''); -let updateStatus = ref('') -let releaseMessage = ref(''); -let hasNewVersion = ref(false); -let botCurrVersion = ref(''); -let dashboardHasNewVersion = ref(false); -let dashboardCurrentVersion = ref(''); -let version = ref(''); -let releases = ref([]); -let updatingDashboardLoading = ref(false); -let installLoading = ref(false); +const LAST_BOT_ROUTE_KEY = "astrbot:last_bot_route"; +const dialog = ref(false); +const accountWarning = ref(false); +const updateStatusDialog = ref(false); +const aboutDialog = ref(false); +const username = localStorage.getItem("user"); +const password = ref(""); +const newPassword = ref(""); +const confirmPassword = ref(""); +const newUsername = ref(""); +const status = ref(""); +const updateStatus = ref(""); +const releaseMessage = ref(""); +const hasNewVersion = ref(false); +const botCurrVersion = ref(""); +const dashboardHasNewVersion = ref(false); +const dashboardCurrentVersion = ref(""); +const version = ref(""); +const releases = ref([]); +const updatingDashboardLoading = ref(false); +const installLoading = ref(false); const isDesktopReleaseMode = ref( - typeof window !== 'undefined' && !!window.astrbotDesktop?.isDesktop + typeof window !== "undefined" && !!window.astrbotDesktop?.isDesktop, ); const desktopUpdateDialog = ref(false); const desktopUpdateChecking = ref(false); const desktopUpdateInstalling = ref(false); const desktopUpdateHasNewVersion = ref(false); -const desktopUpdateCurrentVersion = ref('-'); -const desktopUpdateLatestVersion = ref('-'); -const desktopUpdateStatus = ref(''); -const isChatPath = computed(() => - route.path === '/chat' || route.path.startsWith('/chat/') -); -const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => { - if (typeof window === 'undefined') { +const desktopUpdateCurrentVersion = ref("-"); +const desktopUpdateLatestVersion = ref("-"); +const desktopUpdateStatus = ref(""); + +const getAppUpdaterBridge = (): NonNullable< + Window["astrbotAppUpdater"] +> | null => { + if (typeof window === "undefined") { return null; } const bridge = window.astrbotAppUpdater; if ( bridge && - typeof bridge.checkForAppUpdate === 'function' && - typeof bridge.installAppUpdate === 'function' + typeof bridge.checkForAppUpdate === "function" && + typeof bridge.installAppUpdate === "function" ) { return bridge; } @@ -86,32 +100,44 @@ const getSelectedGitHubProxy = () => { }; // Release Notes Modal -let releaseNotesDialog = ref(false); -let selectedReleaseNotes = ref(''); -let selectedReleaseTag = ref(''); +const releaseNotesDialog = ref(false); +const selectedReleaseNotes = ref(""); +const selectedReleaseTag = ref(""); const releasesHeader = computed(() => [ - { title: t('core.header.updateDialog.table.tag'), key: 'tag_name' }, - { title: t('core.header.updateDialog.table.publishDate'), key: 'published_at' }, - { title: t('core.header.updateDialog.table.content'), key: 'body' }, - { title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url' }, - { title: t('core.header.updateDialog.table.actions'), key: 'switch' } + { title: t("core.header.updateDialog.table.tag"), key: "tag_name" }, + { + title: t("core.header.updateDialog.table.publishDate"), + key: "published_at", + }, + { title: t("core.header.updateDialog.table.content"), key: "body" }, + { title: t("core.header.updateDialog.table.sourceUrl"), key: "zipball_url" }, + { title: t("core.header.updateDialog.table.actions"), key: "switch" }, ]); // Form validation const formValid = ref(true); const passwordRules = computed(() => [ - (v: string) => !!v || t('core.header.accountDialog.validation.passwordRequired'), - (v: string) => v.length >= 12 || t('core.header.accountDialog.validation.passwordMinLength'), - (v: string) => /[A-Z]/.test(v) || t('core.header.accountDialog.validation.passwordUppercase'), - (v: string) => /[a-z]/.test(v) || t('core.header.accountDialog.validation.passwordLowercase'), - (v: string) => /\d/.test(v) || t('core.header.accountDialog.validation.passwordDigit') + (v: string) => !!v || t("core.header.accountDialog.validation.passwordRequired"), + (v: string) => v.length >= 12 || t("core.header.accountDialog.validation.passwordMinLength"), + (v: string) => /[A-Z]/.test(v) || t("core.header.accountDialog.validation.passwordUppercase"), + (v: string) => /[a-z]/.test(v) || t("core.header.accountDialog.validation.passwordLowercase"), + (v: string) => /\d/.test(v) || t("core.header.accountDialog.validation.passwordDigit") ]); const confirmPasswordRules = computed(() => [ - (v: string) => !newPassword.value || !!v || t('core.header.accountDialog.validation.passwordRequired'), - (v: string) => !newPassword.value || v === newPassword.value || t('core.header.accountDialog.validation.passwordMatch') + (v: string) => + !newPassword.value || + !!v || + t("core.header.accountDialog.validation.passwordRequired"), + (v: string) => + !newPassword.value || + v === newPassword.value || + t("core.header.accountDialog.validation.passwordMatch"), ]); const usernameRules = computed(() => [ - (v: string) => !v || v.length >= 3 || t('core.header.accountDialog.validation.usernameMinLength') + (v: string) => + !v || + v.length >= 3 || + t("core.header.accountDialog.validation.usernameMinLength"), ]); // 显示密码相关 @@ -124,7 +150,7 @@ const accountEditStatus = ref({ loading: false, success: false, error: false, - message: '' + message: "", }); function cancelDesktopUpdate() { @@ -139,38 +165,42 @@ async function openDesktopUpdateDialog() { desktopUpdateChecking.value = true; desktopUpdateInstalling.value = false; desktopUpdateHasNewVersion.value = false; - desktopUpdateCurrentVersion.value = '-'; - desktopUpdateLatestVersion.value = '-'; - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checking'); + desktopUpdateCurrentVersion.value = "-"; + desktopUpdateLatestVersion.value = "-"; + desktopUpdateStatus.value = t("core.header.updateDialog.desktopApp.checking"); const bridge = getAppUpdaterBridge(); if (!bridge) { desktopUpdateChecking.value = false; - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); + desktopUpdateStatus.value = t( + "core.header.updateDialog.desktopApp.checkFailed", + ); return; } try { const result = await bridge.checkForAppUpdate(); if (!result?.ok) { - desktopUpdateCurrentVersion.value = result?.currentVersion || '-'; + desktopUpdateCurrentVersion.value = result?.currentVersion || "-"; desktopUpdateLatestVersion.value = - result?.latestVersion || result?.currentVersion || '-'; + result?.latestVersion || result?.currentVersion || "-"; desktopUpdateStatus.value = - result?.reason || t('core.header.updateDialog.desktopApp.checkFailed'); + result?.reason || t("core.header.updateDialog.desktopApp.checkFailed"); return; } - desktopUpdateCurrentVersion.value = result.currentVersion || '-'; + desktopUpdateCurrentVersion.value = result.currentVersion || "-"; desktopUpdateLatestVersion.value = - result.latestVersion || result.currentVersion || '-'; + result.latestVersion || result.currentVersion || "-"; desktopUpdateHasNewVersion.value = !!result.hasUpdate; desktopUpdateStatus.value = result.hasUpdate - ? t('core.header.updateDialog.desktopApp.hasNewVersion') - : t('core.header.updateDialog.desktopApp.isLatest'); + ? t("core.header.updateDialog.desktopApp.hasNewVersion") + : t("core.header.updateDialog.desktopApp.isLatest"); } catch (error) { console.error(error); - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.checkFailed'); + desktopUpdateStatus.value = t( + "core.header.updateDialog.desktopApp.checkFailed", + ); } finally { desktopUpdateChecking.value = false; } @@ -183,12 +213,16 @@ async function confirmDesktopUpdate() { const bridge = getAppUpdaterBridge(); if (!bridge) { - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); + desktopUpdateStatus.value = t( + "core.header.updateDialog.desktopApp.installFailed", + ); return; } desktopUpdateInstalling.value = true; - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installing'); + desktopUpdateStatus.value = t( + "core.header.updateDialog.desktopApp.installing", + ); try { const result = await bridge.installAppUpdate(); @@ -197,10 +231,12 @@ async function confirmDesktopUpdate() { return; } desktopUpdateStatus.value = - result?.reason || t('core.header.updateDialog.desktopApp.installFailed'); + result?.reason || t("core.header.updateDialog.desktopApp.installFailed"); } catch (error) { console.error(error); - desktopUpdateStatus.value = t('core.header.updateDialog.desktopApp.installFailed'); + desktopUpdateStatus.value = t( + "core.header.updateDialog.desktopApp.installFailed", + ); } finally { desktopUpdateInstalling.value = false; } @@ -218,34 +254,38 @@ function handleUpdateClick() { // 检测是否为预发布版本 const isPreRelease = (version: string) => { - const preReleaseKeywords = ['alpha', 'beta', 'rc', 'pre', 'preview', 'dev']; + const preReleaseKeywords = ["alpha", "beta", "rc", "pre", "preview", "dev"]; const lowerVersion = version.toLowerCase(); - return preReleaseKeywords.some(keyword => lowerVersion.includes(keyword)); + return preReleaseKeywords.some((keyword) => lowerVersion.includes(keyword)); }; +// 退出登录 +function handleLogout() { + if (confirm(t("core.common.dialog.confirmMessage"))) { + authStore.logout(); + } +} + // 账户修改 -function accountEdit() { +async function accountEdit() { accountEditStatus.value.loading = true; accountEditStatus.value.error = false; accountEditStatus.value.success = false; - const passwordHash = password.value ? password.value : ''; - const newPasswordHash = newPassword.value ? newPassword.value : ''; - const confirmPasswordHash = confirmPassword.value ? confirmPassword.value : ''; - - axios.post('/api/auth/account/edit', { - password: passwordHash, - new_password: newPasswordHash, - confirm_password: confirmPasswordHash, - new_username: newUsername.value ? newUsername.value : username - }) + axios + .post("/api/auth/account/edit", { + password: password.value, + new_password: newPassword.value, + confirm_password: confirmPassword.value, + new_username: newUsername.value ? newUsername.value : username, + }) .then((res) => { - if (res.data.status == 'error') { + if (res.data.status == "error") { accountEditStatus.value.error = true; accountEditStatus.value.message = res.data.message; - password.value = ''; - newPassword.value = ''; - confirmPassword.value = ''; + password.value = ""; + newPassword.value = ""; + confirmPassword.value = ""; return; } accountEditStatus.value.success = true; @@ -259,10 +299,13 @@ function accountEdit() { .catch((err) => { console.log(err); accountEditStatus.value.error = true; - accountEditStatus.value.message = typeof err === 'string' ? err : t('core.header.accountDialog.messages.updateFailed'); - password.value = ''; - newPassword.value = ''; - confirmPassword.value = ''; + accountEditStatus.value.message = + typeof err === "string" + ? err + : t("core.header.accountDialog.messages.updateFailed"); + password.value = ""; + newPassword.value = ""; + confirmPassword.value = ""; }) .finally(() => { accountEditStatus.value.loading = false; @@ -270,26 +313,18 @@ function accountEdit() { } function getVersion() { - axios.get('/api/stat/version') + axios + .get("/api/stat/version") .then((res) => { botCurrVersion.value = "v" + res.data.data.version; dashboardCurrentVersion.value = res.data.data?.dashboard_version; - let change_pwd_hint = res.data.data?.change_pwd_hint; - const legacy_pwd_hint = res.data.data?.legacy_pwd_hint; - if (change_pwd_hint || legacy_pwd_hint) { + const change_pwd_hint = res.data.data?.change_pwd_hint; + if (change_pwd_hint) { dialog.value = true; accountWarning.value = true; - accountWarningLegacy.value = !!legacy_pwd_hint; - localStorage.setItem('change_pwd_hint', 'true'); - if (legacy_pwd_hint) { - localStorage.setItem('legacy_pwd_hint', 'true'); - } else { - localStorage.removeItem('legacy_pwd_hint'); - } + localStorage.setItem("change_pwd_hint", "true"); } else { - accountWarningLegacy.value = false; - localStorage.removeItem('change_pwd_hint'); - localStorage.removeItem('legacy_pwd_hint'); + localStorage.removeItem("change_pwd_hint"); } }) .catch((err) => { @@ -297,25 +332,16 @@ function getVersion() { }); } -function initPasswordWarningFromStorage() { - const hasChangePwdHint = localStorage.getItem('change_pwd_hint') === 'true'; - const hasLegacyPwdHint = localStorage.getItem('legacy_pwd_hint') === 'true'; - if (hasChangePwdHint || hasLegacyPwdHint) { - dialog.value = true; - accountWarning.value = true; - accountWarningLegacy.value = hasLegacyPwdHint; - } -} - function checkUpdate() { - updateStatus.value = t('core.header.updateDialog.status.checking'); - axios.get('/api/update/check') + updateStatus.value = t("core.header.updateDialog.status.checking"); + axios + .get("/api/update/check") .then((res) => { hasNewVersion.value = res.data.data.has_new_version; if (res.data.data.has_new_version) { releaseMessage.value = res.data.message; - updateStatus.value = t('core.header.version.hasNewVersion'); + updateStatus.value = t("core.header.version.hasNewVersion"); } else { updateStatus.value = res.data.message; } @@ -331,35 +357,35 @@ function checkUpdate() { return; } console.log(err); - updateStatus.value = err + updateStatus.value = err; }); } function getReleases() { - return axios.get('/api/update/releases') + return axios + .get("/api/update/releases") .then((res) => { releases.value = res.data.data.map((item: any) => { item.published_at = new Date(item.published_at).toLocaleString(); return item; - }) + }); }) .catch((err) => { console.log(err); }); } - - function switchVersion(version: string) { - updateStatus.value = t('core.header.updateDialog.status.switching'); + updateStatus.value = t("core.header.updateDialog.status.switching"); installLoading.value = true; - axios.post('/api/update/do', { - version: version, - proxy: getSelectedGitHubProxy() - }) + axios + .post("/api/update/do", { + version: version, + proxy: getSelectedGitHubProxy(), + }) .then((res) => { updateStatus.value = res.data.message; - if (res.data.status == 'ok') { + if (res.data.status == "ok") { setTimeout(() => { window.location.reload(); }, 1000); @@ -367,19 +393,21 @@ function switchVersion(version: string) { }) .catch((err) => { console.log(err); - updateStatus.value = err - }).finally(() => { + updateStatus.value = err; + }) + .finally(() => { installLoading.value = false; }); } function updateDashboard() { updatingDashboardLoading.value = true; - updateStatus.value = t('core.header.updateDialog.status.updating'); - axios.post('/api/update/dashboard') + updateStatus.value = t("core.header.updateDialog.status.updating"); + axios + .post("/api/update/dashboard") .then((res) => { updateStatus.value = res.data.message; - if (res.data.status == 'ok') { + if (res.data.status == "ok") { setTimeout(() => { window.location.reload(); }, 1000); @@ -387,18 +415,47 @@ function updateDashboard() { }) .catch((err) => { console.log(err); - updateStatus.value = err - }).finally(() => { + updateStatus.value = err; + }) + .finally(() => { updatingDashboardLoading.value = false; }); } -function toggleDarkMode() { - const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark'; - customizer.SET_UI_THEME(newTheme); - theme.global.name.value = newTheme; +// 修改:使用状态管理切换主题 +function toggleTheme() { + customizer.TOGGLE_DARK_MODE(); } +function autoSwitchTheme() { + // 根据浏览器主题同步页面主题 + customizer.APPLY_SYSTEM_THEME(); +} + +function autoSwitchThemeListener(e: MediaQueryListEvent) { + if (customizer.autoSwitchTheme) { + autoSwitchTheme(); + } +} + +// 通过 watch 变量来添加和移除监听器 +watch( + () => customizer.autoSwitchTheme, + (isAuto) => { + if (typeof window === "undefined") return; + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + if (isAuto) { + autoSwitchTheme(); + mediaQuery.addEventListener("change", autoSwitchThemeListener); + } else { + mediaQuery.removeEventListener("change", autoSwitchThemeListener); + } + }, + { immediate: true }, +); + function openReleaseNotesDialog(body: string, tag: string) { selectedReleaseNotes.value = body; selectedReleaseTag.value = tag; @@ -406,102 +463,67 @@ function openReleaseNotesDialog(body: string, tag: string) { } function handleLogoClick() { - if (isChatPath.value) { + if (customizer.viewMode === "chat") { aboutDialog.value = true; } else { - router.push('/about'); + router.push("/about"); } } getVersion(); checkUpdate(); -initPasswordWarningFromStorage(); const commonStore = useCommonStore(); commonStore.createEventSource(); // log commonStore.getStartTime(); -// 视图模式切换 -onMounted(() => { - // 初次加載時保存當前路由 - if (typeof window !== 'undefined') { - if (isChatPath.value) { - // 保存 chat ID - const parts = route.fullPath.split('/'); - const sessionId = parts[2]; - if (sessionId) { - sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId); - console.log('Initial save chat ID:', sessionId); - } - } else { - // 保存 bot 路由(非 chat 頁面) - sessionStorage.setItem(LAST_BOT_ROUTE_KEY, route.fullPath); - console.log('Initial save bot route:', route.fullPath); - } - } +onBeforeUnmount(() => { + commonStore.closeEventSourcet(); +}); + +// 视图模式切换 +const viewMode = computed({ + get: () => customizer.viewMode, + set: (value: "bot" | "chat") => { + customizer.SET_VIEW_MODE(value); + }, }); -// 监听 viewMode 变化,切换到 bot 模式时跳转到首页 // 保存 bot 模式的最後路由 // 監聽 route 變化,保存最後一次 bot 路由 -watch(() => route.fullPath, (newPath) => { - if (typeof window === 'undefined') return; - console.log('Route changed:', { - newPath, - isChat: isChatPath.value, - currentChatId: route.params.id - }); - try { - // 使用現有的 isChatPath 計算屬性來避免名稱衝突 - const isChat = isChatPath.value; // 這裡使用已經計算好的 isChatPath - - // ✅ bot:只存「非 chat 頁」 - if (!isChat) { - sessionStorage.setItem(LAST_BOT_ROUTE_KEY, newPath); - } - - // ✅ chat:只存 sessionId - if (isChat) { - const parts = newPath.split('/'); - const sessionId = parts[2]; - - if (sessionId) { - sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId); +watch( + () => route.fullPath, + (newPath) => { + if (customizer.viewMode === "bot" && typeof window !== "undefined") { + try { + localStorage.setItem(LAST_BOT_ROUTE_KEY, newPath); + } catch (e) { + console.error("Failed to save last bot route to localStorage:", e); } } + }, +); - } catch (e) { - console.error('Failed to save route:', e); - } -}); - -const currentMode = computed({ - get: () => (isChatPath.value ? 'chat' : 'bot'), - set: (val: 'chat' | 'bot') => { - try { - // 檢查 window 和 sessionStorage 是否存在 - if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') { - // 如果在非瀏覽器環境中,不做任何 sessionStorage 操作 - console.warn('sessionStorage is not available in this environment'); - return; +// 監聽 viewMode 切換 +watch( + () => customizer.viewMode, + (newMode, oldMode) => { + if ( + newMode === "bot" && + oldMode === "chat" && + typeof window !== "undefined" + ) { + // 從 chat 切換回 bot,跳轉到最後一次的 bot 路由 + let lastBotRoute = "/"; + try { + lastBotRoute = localStorage.getItem(LAST_BOT_ROUTE_KEY) || "/"; + } catch (e) { + console.error("Failed to read last bot route from localStorage:", e); } - - if (val === 'chat') { - const lastSessionId = sessionStorage.getItem(LAST_CHAT_ROUTE_KEY); - router.push(lastSessionId ? `/chat/${lastSessionId}` : '/chat'); - } else { - let lastBotRoute = sessionStorage.getItem(LAST_BOT_ROUTE_KEY) || '/'; - if (lastBotRoute.startsWith('/chat')) { - lastBotRoute = '/'; - } - router.push(lastBotRoute); - } - } catch (e) { - // 在受限隱私模式等環境中,sessionStorage 操作可能會拋出 SecurityError - console.warn('Failed to access sessionStorage in currentMode setter:', e); + router.push(lastBotRoute); } - } -}); + }, +); // Merry Christmas! 🎄 const isChristmas = computed(() => { @@ -510,484 +532,715 @@ const isChristmas = computed(() => { const day = today.getDate(); return month === 12 && day === 25; }); - -// 语言切换相关 -const { languageOptions, currentLanguage, switchLanguage, locale } = useLanguageSwitcher(); -const languages = computed(() => - languageOptions.value.map(lang => ({ - code: lang.value, - name: lang.label, - flag: lang.flag - })) -); -const currentLocale = computed(() => locale.value); -const changeLanguage = async (langCode: string) => { - await switchLanguage(langCode as Locale); -}; - -onMounted(async () => { - const runtimeInfo = await getDesktopRuntimeInfo(); - isDesktopReleaseMode.value = runtimeInfo.isDesktopRuntime; - if (isDesktopReleaseMode.value) { - dashboardHasNewVersion.value = false; - } -}); -