Skip to content

Commit 4d7efb7

Browse files
committed
feat: 实现暗色主题切换功能并优化样式
添加完整的暗色主题支持,包括主题模式切换功能(自动/浅色/深色),重构主题相关代码为模块化结构,更新CSS变量以支持主题切换,并在布局中添加主题切换控件
1 parent e77bbe5 commit 4d7efb7

4 files changed

Lines changed: 206 additions & 41 deletions

File tree

src/components/FileCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const FileCard: React.FC<FileCardProps> = ({
7070
<div className="file-card">
7171
<Card
7272
title={title}
73-
actions={<span style={{color:"black"}}>{info.PushTime}</span>}
73+
actions={<span className="file-card-time">{info.PushTime}</span>}
7474
bordered
7575
hoverShadow
7676
footer={

src/global/index.ts

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,72 @@
1+
export type ThemeMode = 'auto' | 'dark' | 'light';
12

3+
const THEME_MODE_KEY = 'winnew-theme-mode';
4+
const THEME_MODE_ATTR = 'theme-mode';
5+
const THEME_MODE_CHANGE_EVENT = 'theme-mode-change';
6+
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
27

3-
//设置颜色模式
4-
document.documentElement.removeAttribute('theme-mode');
5-
/* function matchMode() {
6-
// detect if on dark mode
7-
var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
8-
const body = document.body;
9-
if (isDarkMode) {
10-
if (!body.hasAttribute('theme-mode')) {
11-
// 设置暗色模式
12-
document.documentElement.setAttribute('theme-mode', 'dark');
13-
}
14-
}
15-
else {
16-
// 重置为浅色模式
17-
document.documentElement.removeAttribute('theme-mode');
18-
}
8+
function setDocumentThemeMode(mode: ThemeMode): void {
9+
if (mode === 'dark') {
10+
document.documentElement.setAttribute(THEME_MODE_ATTR, 'dark');
11+
return;
1912
}
2013

21-
matchMode()
14+
if (mode === 'light') {
15+
document.documentElement.removeAttribute(THEME_MODE_ATTR);
16+
return;
17+
}
18+
19+
if (colorSchemeMedia.matches) {
20+
document.documentElement.setAttribute(THEME_MODE_ATTR, 'dark');
21+
} else {
22+
document.documentElement.removeAttribute(THEME_MODE_ATTR);
23+
}
24+
}
25+
26+
function dispatchThemeModeChange(mode: ThemeMode): void {
27+
window.dispatchEvent(new CustomEvent<ThemeMode>(THEME_MODE_CHANGE_EVENT, { detail: mode }));
28+
}
29+
30+
export function getThemeMode(): ThemeMode {
31+
const storedMode = window.localStorage.getItem(THEME_MODE_KEY);
32+
if (storedMode === 'dark' || storedMode === 'light' || storedMode === 'auto') {
33+
return storedMode;
34+
}
35+
return 'auto';
36+
}
37+
38+
export function setThemeMode(mode: ThemeMode): void {
39+
window.localStorage.setItem(THEME_MODE_KEY, mode);
40+
setDocumentThemeMode(mode);
41+
dispatchThemeModeChange(mode);
42+
}
43+
44+
export function subscribeThemeModeChange(callback: (mode: ThemeMode) => void): () => void {
45+
const handler = (event: Event) => {
46+
const customEvent = event as CustomEvent<ThemeMode>;
47+
callback(customEvent.detail);
48+
};
49+
50+
window.addEventListener(THEME_MODE_CHANGE_EVENT, handler);
51+
return () => window.removeEventListener(THEME_MODE_CHANGE_EVENT, handler);
52+
}
53+
54+
function syncAutoTheme(): void {
55+
const mode = getThemeMode();
56+
if (mode === 'auto') {
57+
setDocumentThemeMode('auto');
58+
}
59+
}
60+
61+
function initThemeMode(): void {
62+
const currentMode = getThemeMode();
63+
setDocumentThemeMode(currentMode);
64+
65+
if (typeof colorSchemeMedia.addEventListener === 'function') {
66+
colorSchemeMedia.addEventListener('change', syncAutoTheme);
67+
} else {
68+
colorSchemeMedia.addListener(syncAutoTheme);
69+
}
70+
}
2271

23-
//监听颜色模式
24-
const mql = window.matchMedia('(prefers-color-scheme: dark)');
25-
mql.addListener(matchMode); */
72+
initThemeMode();

src/global/main.css

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,50 @@
44
--app-text: #1d2638;
55
--app-text-muted: #5f6f87;
66
--app-primary: #0f6adf;
7+
--app-primary-hover: #0d5bc0;
78
--app-primary-soft: #eaf3ff;
89
--app-border: #dbe4f2;
910
--app-shadow: 0 6px 20px rgba(18, 41, 79, 0.05);
11+
--app-bg-grad-start: #e9f1ff;
12+
--app-bg-grad-mid: #f5f7fb;
13+
--app-bg-grad-end: #eef2fa;
14+
--app-header-bg: rgba(255, 255, 255, 0.78);
15+
--app-header-border: rgba(219, 228, 242, 0.7);
16+
--app-header-shadow: 0 6px 22px rgba(26, 48, 87, 0.08);
17+
--app-panel-bg-start: rgba(255, 255, 255, 0.9);
18+
--app-panel-bg-end: rgba(255, 255, 255, 0.83);
19+
--app-panel-border: rgba(219, 228, 242, 0.7);
20+
--app-panel-inset: rgba(255, 255, 255, 0.65);
21+
--app-panel-lite-bg: rgba(255, 255, 255, 0.72);
22+
--app-panel-lite-border: rgba(219, 228, 242, 0.85);
23+
--app-row-bg: #ffffff;
24+
--app-file-card-shadow: 0 8px 24px rgba(17, 36, 66, 0.06);
25+
}
26+
27+
[theme-mode='dark'] {
28+
--app-bg: #121212;
29+
--app-surface: #1b1b1b;
30+
--app-text: #f1f1f1;
31+
--app-text-muted: #acacac;
32+
--app-primary: #f59e0b;
33+
--app-primary-hover: #fbbf24;
34+
--app-primary-soft: #3a2a12;
35+
--app-border: #353535;
36+
--app-shadow: 0 12px 30px rgba(0, 0, 0, 0.38);
37+
--app-bg-grad-start: #1a1a1a;
38+
--app-bg-grad-mid: #121212;
39+
--app-bg-grad-end: #0d0d0d;
40+
--app-header-bg: rgba(24, 24, 24, 0.8);
41+
--app-header-border: rgba(63, 63, 63, 0.78);
42+
--app-header-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
43+
--app-panel-bg-start: rgba(30, 30, 30, 0.92);
44+
--app-panel-bg-end: rgba(24, 24, 24, 0.86);
45+
--app-panel-border: rgba(70, 70, 70, 0.58);
46+
--app-panel-inset: rgba(255, 255, 255, 0.03);
47+
--app-panel-lite-bg: rgba(28, 28, 28, 0.74);
48+
--app-panel-lite-border: rgba(74, 74, 74, 0.68);
49+
--app-row-bg: #202020;
50+
--app-file-card-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
1051
}
1152

1253
*,
@@ -22,11 +63,17 @@ body,
2263
}
2364

2465
html {
25-
background: radial-gradient(circle at top right, #e9f1ff 0%, #f5f7fb 42%, #eef2fa 100%);
66+
background: radial-gradient(
67+
circle at top right,
68+
var(--app-bg-grad-start) 0%,
69+
var(--app-bg-grad-mid) 42%,
70+
var(--app-bg-grad-end) 100%
71+
);
2672
}
2773

2874
body {
2975
margin: 0;
76+
background: var(--app-bg);
3077
color: var(--app-text);
3178
line-height: 1.6;
3279
}
@@ -51,9 +98,9 @@ p {
5198
top: 0;
5299
z-index: 100;
53100
backdrop-filter: blur(9px);
54-
background: rgba(255, 255, 255, 0.78);
55-
border-bottom: 1px solid rgba(219, 228, 242, 0.7);
56-
box-shadow: 0 6px 22px rgba(26, 48, 87, 0.08);
101+
background: var(--app-header-bg);
102+
border-bottom: 1px solid var(--app-header-border);
103+
box-shadow: var(--app-header-shadow);
57104
}
58105

59106
.main-menu {
@@ -65,6 +112,28 @@ p {
65112
flex: 1;
66113
}
67114

115+
.theme-mode-control {
116+
width: 40px;
117+
margin-right: 10px;
118+
}
119+
120+
.theme-mode-link {
121+
display: inline-flex;
122+
width: 28px;
123+
height: 28px;
124+
align-items: center;
125+
justify-content: center;
126+
border-radius: 8px;
127+
font-size: 16px;
128+
color: var(--app-text-muted);
129+
transition: color 0.2s ease, background-color 0.2s ease;
130+
}
131+
132+
.theme-mode-link:hover {
133+
background: var(--app-primary-soft);
134+
color: var(--app-primary);
135+
}
136+
68137
.brand-logo {
69138
color: var(--app-text);
70139
font-size: 24px;
@@ -87,10 +156,10 @@ p {
87156
}
88157

89158
.panel {
90-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.83) 100%);
91-
border: 1px solid rgba(219, 228, 242, 0.7);
159+
background: linear-gradient(180deg, var(--app-panel-bg-start) 0%, var(--app-panel-bg-end) 100%);
160+
border: 1px solid var(--app-panel-border);
92161
border-radius: 20px;
93-
box-shadow: var(--app-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.65);
162+
box-shadow: var(--app-shadow), inset 0 1px 0 var(--app-panel-inset);
94163
}
95164

96165
.home-page {
@@ -131,9 +200,9 @@ p {
131200
}
132201

133202
.panel-lite {
134-
border: 1px solid rgba(219, 228, 242, 0.85);
203+
border: 1px solid var(--app-panel-lite-border);
135204
border-radius: 14px;
136-
background: rgba(255, 255, 255, 0.72);
205+
background: var(--app-panel-lite-bg);
137206
padding: 14px;
138207
}
139208

@@ -187,7 +256,7 @@ p {
187256
border: 1px solid var(--app-border);
188257
border-radius: 12px;
189258
padding: 10px 12px;
190-
background: #fff;
259+
background: var(--app-row-bg);
191260
}
192261

193262
.file-list-main {
@@ -236,7 +305,7 @@ p {
236305
.file-card .t-card {
237306
border-radius: 16px;
238307
border: 1px solid var(--app-border);
239-
box-shadow: 0 8px 24px rgba(17, 36, 66, 0.06);
308+
box-shadow: var(--app-file-card-shadow);
240309
}
241310

242311
.file-card .t-card__body {
@@ -255,13 +324,17 @@ p {
255324
margin-top: 10px;
256325
display: block;
257326
text-align: center;
258-
color: #000;
327+
color: var(--app-text);
259328
white-space: nowrap;
260329
overflow: hidden;
261330
text-overflow: ellipsis;
262331
cursor: pointer;
263332
}
264333

334+
.file-card-time {
335+
color: var(--app-text);
336+
}
337+
265338
.file-card-sha-label {
266339
color: var(--app-text-muted);
267340
font-weight: 400;
@@ -356,7 +429,7 @@ a.file-card-title-edition .t-link__suffix-icon {
356429

357430
.copyright {
358431
margin-top: 10px;
359-
border-top: 1px solid rgba(219, 228, 242, 0.9);
432+
border-top: 1px solid var(--app-panel-lite-border);
360433
padding-top: 12px;
361434
}
362435

@@ -367,7 +440,7 @@ a.file-card-title-edition .t-link__suffix-icon {
367440
}
368441

369442
.class-link:hover {
370-
color: #0d5bc0;
443+
color: var(--app-primary-hover);
371444
opacity: 0.95;
372445
}
373446

src/layout/layout.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
2-
import { Layout, Menu } from 'tdesign-react';
1+
import React, { useEffect, useState } from 'react';
2+
import { Layout, Menu, Link as TLink, Tooltip } from 'tdesign-react';
3+
import { LaptopIcon, ModeDarkIcon, ModeLightIcon } from 'tdesign-icons-react';
34
import 'tdesign-react/es/style/index.css';
45
import {
56
BrowserRouter as Router,
67
Routes,
78
Route,
8-
Link,
9+
Link as RouterLink,
910
useLocation,
1011
} from 'react-router-dom';
1112

@@ -18,10 +19,28 @@ import { FooterContent } from '../layout/footer';
1819
import type { RouterItem } from '../types/layout';
1920
import { useRouterSEO, type SEOConfig } from '../hooks/useSEO';
2021
import { pages } from '../pages.config';
22+
import {
23+
getThemeMode,
24+
setThemeMode,
25+
subscribeThemeModeChange,
26+
type ThemeMode,
27+
} from '../global/index';
2128

2229
const { Header, Content, Footer } = Layout;
2330
const { HeadMenu, MenuItem } = Menu;
2431

32+
const THEME_MODE_ICON: Record<ThemeMode, React.ReactNode> = {
33+
auto: <LaptopIcon />,
34+
dark: <ModeDarkIcon />,
35+
light: <ModeLightIcon />,
36+
};
37+
38+
const THEME_MODE_LABEL: Record<ThemeMode, string> = {
39+
auto: '自动',
40+
dark: '深色',
41+
light: '浅色',
42+
};
43+
2544
const routers: RouterItem[] = [
2645
{
2746
word: '首页',
@@ -75,21 +94,47 @@ export function Layout_() {
7594

7695
function AppLayout() {
7796
const location = useLocation();
97+
const [themeMode, setThemeModeState] = useState<ThemeMode>(getThemeMode());
98+
99+
useEffect(() => subscribeThemeModeChange(setThemeModeState), []);
100+
101+
const handleThemeModeToggle = () => {
102+
const nextMode: ThemeMode =
103+
themeMode === 'auto' ? 'dark' : themeMode === 'dark' ? 'light' : 'auto';
104+
setThemeMode(nextMode);
105+
};
78106

79107
return (
80108
<Layout className="app-layout">
81109
<Header className="app-header">
82110
<div className="container">
83111
<HeadMenu
84112
value={location.pathname}
85-
theme="light"
86-
logo={<Link className="brand-logo" to="/">WinNew</Link>}
113+
theme={themeMode === 'dark' ? 'dark' : 'light'}
114+
logo={<RouterLink className="brand-logo" to="/">WinNew</RouterLink>}
87115
className="main-menu"
88116
>
89117
<div className="menu-spacer" />
118+
<div className="theme-mode-control">
119+
<Tooltip
120+
content={`当前:${THEME_MODE_LABEL[themeMode]}(点击切换)`}
121+
placement="bottom"
122+
showArrow
123+
>
124+
<TLink
125+
className="theme-mode-link"
126+
theme="default"
127+
hover="color"
128+
underline={false}
129+
onClick={handleThemeModeToggle}
130+
>
131+
{THEME_MODE_ICON[themeMode]}
132+
</TLink>
133+
</Tooltip>
134+
</div>
90135
{routers.map((item) => (
91136
<MenuItem key={item.path} value={item.path}>
92-
<Link to={item.path}>{item.word}</Link>
137+
<RouterLink to={item.path}>{item.word}</RouterLink>
93138
</MenuItem>
94139
))}
95140
</HeadMenu>

0 commit comments

Comments
 (0)