feat(dashboard): add auto switch theme (default off) (#6405)

* feat(dashboard): add auto switch theme (default off)
feat(dashboard): move all get theme and set theme by check current theme into stores/customizer

* feat(dashboard): fix duplicate for auto switch theme
根据Gemini的意见更改了一些地方。
将原本的状态更新挪到了App.vue里,可以去除很多地方更新theme所需要的theme依赖。
将翻译修改了
将监听器改为了watch
This commit is contained in:
Kangyang Ji
2026-03-17 11:06:01 +00:00
committed by GitHub
parent df1e59e01c
commit 7aae048405
14 changed files with 118 additions and 56 deletions
+18 -7
View File
@@ -15,20 +15,31 @@
<script setup>
import { RouterView } from 'vue-router';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useToastStore } from '@/stores/toast'
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useTheme } from "vuetify";
import { useToastStore } from '@/stores/toast';
import { useCustomizerStore } from '@/stores/customizer';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
const toastStore = useToastStore()
const globalWaitingRef = ref(null)
let disposeTrayRestartListener = null
const toastStore = useToastStore();
const theme = useTheme();
const customizer = useCustomizerStore();
const globalWaitingRef = ref(null);
let disposeTrayRestartListener = null;
const snackbarShow = computed({
get: () => !!toastStore.current,
set: (val) => {
if (!val) toastStore.shift()
}
})
});
// 统一监听 uiTheme 变化并同步到 Vuetify
watch(() => customizer.uiTheme, (newTheme) => {
if (newTheme) {
theme.global.name.value = newTheme;
}
}, { immediate: true });
onMounted(() => {
const desktopBridge = window.astrbotDesktop
+3 -6
View File
@@ -210,7 +210,6 @@ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router';
import { useCustomizerStore } from '@/stores/customizer';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
import MessageList from '@/components/chat/MessageList.vue';
import ConversationSidebar from '@/components/chat/ConversationSidebar.vue';
import ChatInput from '@/components/chat/ChatInput.vue';
@@ -243,7 +242,6 @@ const route = useRoute();
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
const { warning: toastWarning } = useToast();
const theme = useTheme();
const customizer = useCustomizerStore();
// UI 状态
@@ -340,7 +338,7 @@ interface ReplyInfo {
}
const replyTo = ref<ReplyInfo | null>(null);
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
const isDark = computed(() => customizer.isDarkTheme);
const sendShortcut = ref<SendShortcut>('shift_enter');
function setSendShortcut(mode: SendShortcut) {
@@ -380,10 +378,9 @@ watch(() => customizer.chatSidebarOpen, (val) => {
}
});
// 使用新的逻辑切换主题
function toggleTheme() {
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
customizer.TOGGLE_DARK_MODE();
}
function toggleFullscreen() {
+2 -1
View File
@@ -202,7 +202,8 @@ const emit = defineEmits<{
}>();
const { tm } = useModuleI18n('features/chat');
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
// 从新的预设getter获取
const isDark = computed(() => useCustomizerStore().isDarkTheme);
const inputField = ref<HTMLTextAreaElement | null>(null);
const imageInputRef = ref<HTMLInputElement | null>(null);
+2 -3
View File
@@ -99,16 +99,15 @@
<script setup lang="ts">
import axios from "axios";
import { ref, computed, onBeforeUnmount, watch } from "vue";
import { useTheme } from "vuetify";
import { useVADRecording } from "@/composables/useVADRecording";
import SiriOrb from "./LiveOrb.vue";
import { useCustomizerStore } from "@/stores/customizer";
const emit = defineEmits<{
close: [];
}>();
const theme = useTheme();
const isDark = computed(() => theme.global.current.value.dark);
const isDark = computed(() => !useCustomizerStore().isDarkTheme);
// 使用 VAD Recording composable
const vadRecording = useVADRecording();
@@ -74,7 +74,6 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue";
import axios from "axios";
import { useCustomizerStore } from "@/stores/customizer";
import { useI18n, useModuleI18n } from "@/i18n/composables";
import { useTheme } from "vuetify";
import MessageList from "@/components/chat/MessageList.vue";
import ChatInput from "@/components/chat/ChatInput.vue";
import { useMessages } from "@/composables/useMessages";
@@ -181,9 +180,7 @@ const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
// 输入状态
const prompt = ref("");
const isDark = computed(
() => useCustomizerStore().uiTheme === "PurpleThemeDark",
);
const isDark = computed(() => useCustomizerStore().isDarkTheme);
function openImagePreview(imageUrl: string) {
previewImageUrl.value = imageUrl;
@@ -136,13 +136,13 @@ const viewChangelog = () => {
:style="{
position: 'relative',
backgroundColor:
useCustomizerStore().uiTheme === 'PurpleTheme'
!useCustomizerStore().isDarkTheme
? marketMode
? '#f8f0dd'
: '#ffffff'
: '#282833',
color:
useCustomizerStore().uiTheme === 'PurpleTheme'
!useCustomizerStore().isDarkTheme
? '#000000dd'
: '#ffffff',
}"
@@ -51,6 +51,11 @@
"subtitle": "Customize theme primary and secondary colors. Changes apply immediately and are stored locally in your browser.",
"primary": "Primary Color",
"secondary": "Secondary Color"
},
"autoSync": {
"title": "Auto-Switch Light/Dark Theme",
"subtitle": "Automatically switch between light and dark themes based on your system's appearance setting.",
"label": "Enable Auto-Switch"
}
},
"reset": {
@@ -69,8 +69,8 @@
"confirmDelete": "确定要删除“{name}”吗?此操作无法撤销。"
},
"modes": {
"darkMode": "切换到夜间模式",
"lightMode": "切换到日间模式"
"darkMode": "切换到深色模式",
"lightMode": "切换到浅色模式"
},
"shortcuts": {
"help": "获取帮助",
@@ -51,6 +51,11 @@
"subtitle": "自定义主题主色与辅助色。修改后立即生效,并保存在浏览器本地。",
"primary": "主色",
"secondary": "辅助色"
},
"autoSync": {
"title": "自动切换深色主题",
"subtitle": "根据您的浏览器外观设置自动切换主题",
"label": "启用自动切换"
}
},
"reset": {
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import { ref, computed, watch } from "vue";
import { useCustomizerStore } from "@/stores/customizer";
import axios from "axios";
import Logo from "@/components/shared/Logo.vue";
@@ -13,7 +13,6 @@ 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";
@@ -25,7 +24,6 @@ enableMermaid();
const customizer = useCustomizerStore();
const authStore = useAuthStore();
const theme = useTheme();
const { t } = useI18n();
const { languageOptions, currentLanguage, switchLanguage, locale } =
@@ -429,15 +427,36 @@ function updateDashboard() {
});
}
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;
@@ -510,6 +529,7 @@ const isChristmas = computed(() => {
const day = today.getDate();
return month === 12 && day === 25;
});
</script>
<template>
@@ -713,14 +733,14 @@ const isChristmas = computed(() => {
<!-- 主题切换 -->
<v-list-item
@click="toggleDarkMode()"
@click="toggleTheme()"
class="styled-menu-item"
rounded="md"
>
<template v-slot:prepend>
<v-icon>
{{
useCustomizerStore().uiTheme === "PurpleThemeDark"
useCustomizerStore().isDarkTheme
? "mdi-weather-night"
: "mdi-white-balance-sunny"
}}
@@ -728,7 +748,7 @@ const isChristmas = computed(() => {
</template>
<v-list-item-title>
{{
useCustomizerStore().uiTheme === "PurpleThemeDark"
useCustomizerStore().isDarkTheme
? t("core.header.buttons.theme.light")
: t("core.header.buttons.theme.dark")
}}
+28 -7
View File
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import config from '@/config';
export const useCustomizerStore = defineStore({
id: 'customizer',
id: "customizer",
state: () => ({
Sidebar_drawer: config.Sidebar_drawer,
Customizer_drawer: config.Customizer_drawer,
@@ -10,11 +10,14 @@ export const useCustomizerStore = defineStore({
fontTheme: "Poppins",
uiTheme: config.uiTheme,
inputBg: config.inputBg,
viewMode: (localStorage.getItem('viewMode') as 'bot' | 'chat') || 'bot', // 'bot' 或 'chat'
chatSidebarOpen: false // chat mode mobile sidebar state
viewMode: (localStorage.getItem("viewMode") as "bot" | "chat") || "bot", // 'bot' 或 'chat'
chatSidebarOpen: false, // chat mode mobile sidebar state
autoSwitchTheme: localStorage.getItem("autoSwitchTheme") === "true", // 自动同步主题
}),
getters: {},
getters: {
isDarkTheme: (state) => state.uiTheme === "PurpleThemeDark",
},
actions: {
SET_SIDEBAR_DRAWER() {
this.Sidebar_drawer = !this.Sidebar_drawer;
@@ -29,9 +32,27 @@ export const useCustomizerStore = defineStore({
this.uiTheme = payload;
localStorage.setItem("uiTheme", payload);
},
SET_VIEW_MODE(payload: 'bot' | 'chat') {
SET_VIEW_MODE(payload: "bot" | "chat") {
this.viewMode = payload;
localStorage.setItem('viewMode', payload);
localStorage.setItem("viewMode", payload);
},
SET_AUTO_SYNC(payload: boolean) {
this.autoSwitchTheme = payload;
localStorage.setItem("autoSwitchTheme", String(payload));
},
// 新增:手动切换主题(同时关闭自动同步)
TOGGLE_DARK_MODE() {
// 手动切换时禁用自动同步
this.SET_AUTO_SYNC(false);
const newTheme = this.isDarkTheme ? "PurpleTheme" : "PurpleThemeDark";
this.SET_UI_THEME(newTheme);
},
// 新增:应用系统主题(用于自动同步)
APPLY_SYSTEM_THEME() {
if (typeof window === "undefined") return;
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const themeToApply = isDark ? "PurpleThemeDark" : "PurpleTheme";
this.SET_UI_THEME(themeToApply);
},
TOGGLE_CHAT_SIDEBAR() {
this.chatSidebarOpen = !this.chatSidebarOpen;
@@ -39,5 +60,5 @@ export const useCustomizerStore = defineStore({
SET_CHAT_SIDEBAR(payload: boolean) {
this.chatSidebarOpen = payload;
},
}
},
});
+1 -1
View File
@@ -499,7 +499,7 @@ export default {
// 检测是否为暗色模式
isDark() {
console.log('isDark', this.customizerStore.uiTheme);
return this.customizerStore.uiTheme === 'PurpleThemeDark';
return this.customizerStore.isDarkTheme;
},
// 将对话历史转换为 MessageList 组件期望的格式
+13
View File
@@ -380,6 +380,11 @@
</v-row>
</v-list-item>
<v-list-item :subtitle="tm('style.autoSync.subtitle')" :title="tm('style.autoSync.title')">
<v-switch v-model="autoThemeSwitcher" :label="tm('style.autoSync.label')" color="primary" hide-details
class="ml-3" />
</v-list-item>
<v-list-subheader>{{ tm("backup.title") }}</v-list-subheader>
<v-list-item
@@ -568,6 +573,14 @@ const applyThemeColors = () => {
toastStore.success(tm("common.saved"));
};
const autoThemeSwitcher = computed({
get: () => customizer.autoSwitchTheme,
set: (value) => {
customizer.SET_AUTO_SYNC(value);
if (value) { customizer.APPLY_SYSTEM_THEME() };
}
});
const wfr = ref(null);
const migrationDialog = ref(null);
const backupDialog = ref(null);
@@ -7,7 +7,6 @@ import { useApiStore } from "@/stores/api";
import { useRouter } from "vue-router";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from "@/i18n/composables";
import { useTheme } from "vuetify";
const cardVisible = ref(false);
const router = useRouter();
@@ -15,7 +14,6 @@ const authStore = useAuthStore();
const apiStore = useApiStore();
const customizer = useCustomizerStore();
const { tm: t } = useModuleI18n("features/auth");
const theme = useTheme();
const serverConfigDialog = ref(false);
const apiUrl = ref(apiStore.apiBaseUrl);
@@ -47,12 +45,7 @@ function isCustomPreset(name: string) {
// 主题切换函数
function toggleTheme() {
const newTheme =
customizer.uiTheme === "PurpleThemeDark"
? "PurpleTheme"
: "PurpleThemeDark";
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
customizer.TOGGLE_DARK_MODE();
}
onMounted(() => {
@@ -114,10 +107,10 @@ onMounted(() => {
size="small"
>
<v-icon size="18" :color="'rgb(var(--v-theme-primary))'">
mdi-white-balance-sunny
{{ customizer.isDarkTheme ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t("theme.switchToLight") }}
{{ customizer.isDarkTheme ? t('theme.switchToLight') : t('theme.switchToDark') }}
</v-tooltip>
</v-btn>
</div>