mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-09-20 11:33:32 +08:00
feat(panel): 深色模式(跟随系统/亮色/深色),主页顶栏图标切换
- 主页顶栏最右加主题图标按钮,循环切换 跟随系统 / 亮色 / 深色,存浏览器 localStorage。 - index.html 内联脚本首屏前置 data-theme,避免亮/暗闪烁。 - styles.css 加牛奶布艺·暗色变量(亮蓝黑 base、surface 更高一档、近纯黑阴影、极淡顶面高光), data-theme=dark 强制深色、=auto 跟随 prefers-color-scheme、=light 维持亮色。 界面基于 CSS 变量,覆盖核心变量即整体适配。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,15 @@
|
||||
<meta name="apple-mobile-web-app-title" content="云微" />
|
||||
<link rel="apple-touch-icon" href="/icon-180.png" />
|
||||
<title>云微</title>
|
||||
<script>
|
||||
// 首屏前置好主题,避免亮/暗闪烁。auto/light/dark;缺省 auto(CSS 用 prefers-color-scheme 决定)。
|
||||
try {
|
||||
var t = localStorage.getItem('woc_theme');
|
||||
document.documentElement.dataset.theme = t === 'light' || t === 'dark' ? t : 'auto';
|
||||
} catch (e) {
|
||||
document.documentElement.dataset.theme = 'auto';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from './auth';
|
||||
import { useUI, PasswordInput } from './ui';
|
||||
import { api, appProfile, type InstanceWithStatus } from './api';
|
||||
import { InstanceIcon } from './AppIcon';
|
||||
import { getThemeMode, applyThemeMode, nextThemeMode, type ThemeMode } from './theme';
|
||||
import InstanceView from './pages/Desktop';
|
||||
import Admin from './pages/Admin';
|
||||
|
||||
@@ -240,6 +241,41 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl
|
||||
);
|
||||
}
|
||||
|
||||
// 主题切换图标(跟随系统 / 亮色 / 深色 循环)。
|
||||
const themeIcon: Record<ThemeMode, JSX.Element> = {
|
||||
auto: (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
),
|
||||
light: (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.4 1.4M17.6 17.6L19 19M19 5l-1.4 1.4M6.4 17.6L5 19" />
|
||||
</svg>
|
||||
),
|
||||
dark: (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M20 14.5A8 8 0 0 1 9.5 4a7 7 0 1 0 10.5 10.5z" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
function ThemeToggle() {
|
||||
const [mode, setMode] = useState<ThemeMode>(() => getThemeMode());
|
||||
const cycle = () => {
|
||||
const m = nextThemeMode(mode);
|
||||
applyThemeMode(m);
|
||||
setMode(m);
|
||||
};
|
||||
const label = mode === 'auto' ? '跟随系统' : mode === 'light' ? '亮色' : '深色';
|
||||
return (
|
||||
<button className="theme-toggle" onClick={cycle} title={`主题:${label}(点击切换:跟随系统 / 亮色 / 深色)`} aria-label={`主题:${label}`}>
|
||||
{themeIcon[mode]}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeView({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; onChangePassword: () => void }) {
|
||||
const { user } = useAuth();
|
||||
const { instances, loaded } = useInstances();
|
||||
@@ -253,6 +289,7 @@ function HomeView({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; on
|
||||
{Icon.menu}
|
||||
</button>
|
||||
<span className="ws-title">主页</span>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<div className="content">
|
||||
|
||||
@@ -33,6 +33,36 @@
|
||||
rgba(255, 255, 255, 0.16) 38%,
|
||||
transparent 74%
|
||||
);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---------- 深色模式(牛奶布艺·暗):base=亮蓝黑、surface 更高一档、阴影近纯黑、顶面高光极淡 ---------- */
|
||||
/* data-theme=dark 强制深色;data-theme=auto 跟随系统 prefers-color-scheme;data-theme=light 维持亮色。 */
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
--base: #14161c;
|
||||
--surface: #23262f;
|
||||
--trough: #0e0f14;
|
||||
--shadow: 0 0 0;
|
||||
--text: #e7eaef;
|
||||
--muted: #969ca6;
|
||||
--crease: 0 1px 3px rgba(0 0 0 / 0.55), 0 2px 10px rgba(0 0 0 / 0.45);
|
||||
--crease-press: 0 1px 2px rgba(0 0 0 / 0.4), 0 1px 3px rgba(0 0 0 / 0.25);
|
||||
--sheen: radial-gradient(ellipse at 50% 28%, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.02) 40%, transparent 75%);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme='auto'] {
|
||||
color-scheme: dark;
|
||||
--base: #14161c;
|
||||
--surface: #23262f;
|
||||
--trough: #0e0f14;
|
||||
--shadow: 0 0 0;
|
||||
--text: #e7eaef;
|
||||
--muted: #969ca6;
|
||||
--crease: 0 1px 3px rgba(0 0 0 / 0.55), 0 2px 10px rgba(0 0 0 / 0.45);
|
||||
--crease-press: 0 1px 2px rgba(0 0 0 / 0.4), 0 1px 3px rgba(0 0 0 / 0.25);
|
||||
--sheen: radial-gradient(ellipse at 50% 28%, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.02) 40%, transparent 75%);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -1608,6 +1638,32 @@ button {
|
||||
cursor: pointer;
|
||||
box-shadow: var(--crease);
|
||||
}
|
||||
/* 主题切换:顶栏最右的图标按钮 */
|
||||
.theme-toggle {
|
||||
margin-left: auto;
|
||||
flex: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
border-radius: 10px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
transform 0.15s;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
color: var(--text);
|
||||
background: rgba(var(--shadow) / 0.06);
|
||||
}
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.ws-action:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
28
panel/web/src/theme.ts
Normal file
28
panel/web/src/theme.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 主题:auto(跟随系统)/ light / dark。存浏览器 localStorage,应用到 <html data-theme>。
|
||||
// 首屏无闪烁由 index.html 里的内联脚本提前置好 data-theme;这里负责读取与切换。
|
||||
export type ThemeMode = 'auto' | 'light' | 'dark';
|
||||
const KEY = 'woc_theme';
|
||||
|
||||
export function getThemeMode(): ThemeMode {
|
||||
try {
|
||||
const v = localStorage.getItem(KEY);
|
||||
if (v === 'light' || v === 'dark' || v === 'auto') return v;
|
||||
} catch {
|
||||
/* 隐私模式禁用 localStorage */
|
||||
}
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
export function applyThemeMode(m: ThemeMode): void {
|
||||
try {
|
||||
localStorage.setItem(KEY, m);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
document.documentElement.dataset.theme = m;
|
||||
}
|
||||
|
||||
// 循环切换顺序:跟随系统 → 亮色 → 深色 → 跟随系统
|
||||
export function nextThemeMode(m: ThemeMode): ThemeMode {
|
||||
return m === 'auto' ? 'light' : m === 'light' ? 'dark' : 'auto';
|
||||
}
|
||||
Reference in New Issue
Block a user