feat(theme): 增加暗黑模式与主题切换
This commit is contained in:
@@ -10,6 +10,14 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-slate-600">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
:title="`当前主题:${themeModeText}`"
|
||||
@click="cycleThemeMode"
|
||||
>
|
||||
{{ themeModeText }}
|
||||
</button>
|
||||
<StatusDot :status="statusDot">
|
||||
{{ statusLabel }}
|
||||
</StatusDot>
|
||||
@@ -26,8 +34,30 @@
|
||||
import { computed } from 'vue';
|
||||
import StatusDot from '@/components/ui/StatusDot.vue';
|
||||
import { useWsStore } from '@/stores/wsStore';
|
||||
import { useTheme, type ThemeMode } from '@/composables/useTheme';
|
||||
|
||||
const wsStore = useWsStore();
|
||||
const { themeMode, setThemeMode } = useTheme();
|
||||
|
||||
const THEME_ORDER: ThemeMode[] = ['system', 'dark', 'light'];
|
||||
|
||||
const themeModeText = computed(() => {
|
||||
switch (themeMode.value) {
|
||||
case 'dark':
|
||||
return '深色';
|
||||
case 'light':
|
||||
return '浅色';
|
||||
case 'system':
|
||||
default:
|
||||
return '跟随系统';
|
||||
}
|
||||
});
|
||||
|
||||
function cycleThemeMode(): void {
|
||||
const currentIndex = THEME_ORDER.indexOf(themeMode.value);
|
||||
const nextIndex = (currentIndex + 1) % THEME_ORDER.length;
|
||||
setThemeMode(THEME_ORDER[nextIndex]);
|
||||
}
|
||||
|
||||
const statusDot = computed(() => {
|
||||
switch (wsStore.status) {
|
||||
@@ -57,4 +87,3 @@ const statusLabel = computed(() => {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,3 +33,75 @@ body {
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
@apply inline-flex items-center px-2.5 h-7 rounded-[var(--dt-radius-btn)] border border-slate-300 bg-white text-slate-700 transition;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
@apply border-slate-400 bg-slate-50;
|
||||
}
|
||||
|
||||
html.dark body {
|
||||
color: rgb(241 245 249);
|
||||
background-color: rgb(2 6 23);
|
||||
}
|
||||
|
||||
html.dark .bg-white {
|
||||
background-color: rgb(15 23 42);
|
||||
}
|
||||
|
||||
html.dark .bg-white\/80 {
|
||||
background-color: rgb(15 23 42 / 0.8);
|
||||
}
|
||||
|
||||
html.dark .bg-slate-50 {
|
||||
background-color: rgb(15 23 42);
|
||||
}
|
||||
|
||||
html.dark .bg-slate-100\/80 {
|
||||
background-color: rgb(15 23 42 / 0.8);
|
||||
}
|
||||
|
||||
html.dark .text-slate-900 {
|
||||
color: rgb(241 245 249);
|
||||
}
|
||||
|
||||
html.dark .text-slate-700 {
|
||||
color: rgb(226 232 240);
|
||||
}
|
||||
|
||||
html.dark .text-slate-600 {
|
||||
color: rgb(203 213 225);
|
||||
}
|
||||
|
||||
html.dark .text-slate-500 {
|
||||
color: rgb(148 163 184);
|
||||
}
|
||||
|
||||
html.dark .border-slate-200 {
|
||||
border-color: rgb(51 65 85);
|
||||
}
|
||||
|
||||
html.dark .border-slate-300 {
|
||||
border-color: rgb(71 85 105);
|
||||
}
|
||||
|
||||
html.dark .border-dashed {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
html.dark .placeholder\:text-slate-400::placeholder {
|
||||
color: rgb(148 163 184);
|
||||
}
|
||||
|
||||
html.dark .theme-toggle {
|
||||
border-color: rgb(71 85 105);
|
||||
background-color: rgb(30 41 59);
|
||||
color: rgb(226 232 240);
|
||||
}
|
||||
|
||||
html.dark .theme-toggle:hover {
|
||||
border-color: rgb(100 116 139);
|
||||
background-color: rgb(51 65 85);
|
||||
}
|
||||
|
||||
87
frontend/src/composables/useTheme.ts
Normal file
87
frontend/src/composables/useTheme.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { computed, readonly, ref } from 'vue';
|
||||
|
||||
type ResolvedTheme = 'light' | 'dark';
|
||||
export type ThemeMode = 'system' | 'light' | 'dark';
|
||||
|
||||
const STORAGE_KEY = 'DataTool-theme-mode';
|
||||
|
||||
const themeMode = ref<ThemeMode>(loadThemeMode());
|
||||
const resolvedTheme = ref<ResolvedTheme>('light');
|
||||
|
||||
let mediaQuery: MediaQueryList | null = null;
|
||||
let initialized = false;
|
||||
|
||||
function loadThemeMode(): ThemeMode {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'system';
|
||||
}
|
||||
const saved = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (saved === 'light' || saved === 'dark' || saved === 'system') {
|
||||
return saved;
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
||||
return 'light';
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme: ResolvedTheme): void {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle('dark', theme === 'dark');
|
||||
root.style.colorScheme = theme;
|
||||
}
|
||||
|
||||
function resolveTheme(mode: ThemeMode): ResolvedTheme {
|
||||
if (mode === 'system') {
|
||||
return getSystemTheme();
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
function syncTheme(): void {
|
||||
const next = resolveTheme(themeMode.value);
|
||||
resolvedTheme.value = next;
|
||||
applyTheme(next);
|
||||
}
|
||||
|
||||
function ensureInit(): void {
|
||||
if (initialized || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', () => {
|
||||
if (themeMode.value === 'system') {
|
||||
syncTheme();
|
||||
}
|
||||
});
|
||||
syncTheme();
|
||||
}
|
||||
|
||||
function setThemeMode(nextMode: ThemeMode): void {
|
||||
themeMode.value = nextMode;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, nextMode);
|
||||
}
|
||||
syncTheme();
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
ensureInit();
|
||||
|
||||
const isDark = computed(() => resolvedTheme.value === 'dark');
|
||||
|
||||
return {
|
||||
themeMode: readonly(themeMode),
|
||||
resolvedTheme: readonly(resolvedTheme),
|
||||
isDark,
|
||||
setThemeMode,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user