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:
+18
-7
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -499,7 +499,7 @@ export default {
|
||||
// 检测是否为暗色模式
|
||||
isDark() {
|
||||
console.log('isDark', this.customizerStore.uiTheme);
|
||||
return this.customizerStore.uiTheme === 'PurpleThemeDark';
|
||||
return this.customizerStore.isDarkTheme;
|
||||
},
|
||||
|
||||
// 将对话历史转换为 MessageList 组件期望的格式
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user