添加深色/浅色主题本地储存,为a标签添加深色样式

This commit is contained in:
LeonspaceX
2025-11-29 13:20:48 +08:00
parent 91334d4712
commit 0d5f2c4131
7 changed files with 91 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "sycamore_whisper_front",
"private": true,
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,4 +1,4 @@
// 你好感谢你愿意看源代码但是悄悄告诉你代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
// 你好感谢你愿意看源代码但是悄悄告诉你代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { FluentProvider, webLightTheme, webDarkTheme, tokens } from '@fluentui/react-components';
@@ -36,6 +36,8 @@ function App() {
const lastRefreshAtRef = useRef<number>(0);
const REFRESH_COOLDOWN_MS = 5000; // 刷新冷却时间
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
const THEME_PREF_KEY = 'ThemePref';
const userPrefRef = useRef<boolean>(false);
const openImageViewer = (src?: string, alt?: string) => {
if (!src) return;
@@ -67,9 +69,16 @@ function App() {
if (containerRef.current) containerRef.current.scrollTop = 0;
};
// 移除触摸下拉刷新逻辑
// 撤销 Pointer 事件回退,恢复为纯 Touch 逻辑
const handleToggleTheme = () => {
setIsDarkMode(prev => {
const next = !prev;
try {
localStorage.setItem(THEME_PREF_KEY, next ? 'dark' : 'light');
userPrefRef.current = true;
} catch {}
return next;
});
};
const onWheel: React.WheelEventHandler<HTMLDivElement> = (e) => {
const atTop = (containerRef.current?.scrollTop ?? 0) <= 0;
@@ -78,6 +87,43 @@ function App() {
}
};
useEffect(() => {
const mql = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
let handler: ((e: MediaQueryListEvent) => void) | null = null;
try {
const saved = localStorage.getItem(THEME_PREF_KEY);
if (saved === 'dark' || saved === 'light') {
userPrefRef.current = true;
setIsDarkMode(saved === 'dark');
} else {
if (mql) {
setIsDarkMode(mql.matches);
handler = (e: MediaQueryListEvent) => {
if (!userPrefRef.current) {
setIsDarkMode(e.matches);
}
};
if ('addEventListener' in mql) {
mql.addEventListener('change', handler);
} else {
// @ts-ignore
mql.addListener(handler);
}
}
}
} catch {}
return () => {
if (mql && handler) {
if ('removeEventListener' in mql) {
mql.removeEventListener('change', handler);
} else {
// @ts-ignore
mql.removeListener(handler);
}
}
};
}, []);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
@@ -115,7 +161,7 @@ function App() {
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout isDarkMode={isDarkMode} onToggleTheme={() => setIsDarkMode(!isDarkMode)} />}>
<Route path="/" element={<MainLayout isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />}>
<Route
index
element={
@@ -129,9 +175,7 @@ function App() {
flexDirection: 'column',
alignItems: 'center',
minHeight: '100%',
// 移除下拉位移动画
}}>
{/* 刷新提示改为 toast不显示顶部灰字 */}
{articles.map((article, index) => {
if (articles.length === index + 1 && hasMore) {
return (
@@ -178,7 +222,7 @@ function App() {
<Route path="about" element={<AboutPage />} />
</Route>
<Route path="/init" element={<InitPage />} />
<Route path="/admin" element={<AdminPage isDarkMode={isDarkMode} onToggleTheme={() => setIsDarkMode(!isDarkMode)} />} />
<Route path="/admin" element={<AdminPage isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>

View File

@@ -47,7 +47,7 @@ export const voteArticle = async (
throw new Error(`Vote ${type} failed`);
}
} catch (error) {
toast.error(`${type === 'up' ? '赞' : '踩'}失败`);
toast.error(`${type === 'up' ? '赞' : '踩'}失败`);
throw error;
}
};

View File

@@ -67,6 +67,21 @@ const useStyles = makeStyles({
textDecoration: 'underline',
backgroundColor: 'transparent',
},
'& a': {
color: tokens.colorBrandForegroundLink,
textDecoration: 'underline',
wordBreak: 'break-word',
},
'& a:hover': {
textDecoration: 'underline',
},
'& a:visited': {
color: tokens.colorBrandForegroundLink,
},
'& a:focus': {
outline: `2px solid ${tokens.colorNeutralStroke1}`,
outlineOffset: '2px',
},
},
});

View File

@@ -92,13 +92,29 @@ const useStyles = makeStyles({
textDecoration: 'underline',
backgroundColor: 'transparent',
},
// 链接样式与交互状态
'& a': {
color: tokens.colorBrandForegroundLink,
textDecoration: 'underline',
wordBreak: 'break-word',
},
'& a:hover': {
textDecoration: 'underline',
},
'& a:visited': {
color: tokens.colorBrandForegroundLink,
},
'& a:focus': {
outline: `2px solid ${tokens.colorNeutralStroke1}`,
outlineOffset: '2px',
},
// 约束 Markdown 图片不溢出卡片
'& img': {
maxWidth: '100%',
height: 'auto',
display: 'block',
borderRadius: tokens.borderRadiusSmall,
},
}
},
actions: {
display: 'grid',

View File

@@ -40,6 +40,7 @@ const useStyles = makeStyles({
content: {
flex: '1 1 auto',
padding: '20px',
paddingBottom: '64px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',

View File

@@ -19,7 +19,10 @@ const useStyles = makeStyles({
textAlign: 'center',
// 约束可能的图片或表格
'& img': { maxWidth: '100%', height: 'auto', display: 'inline-block', verticalAlign: 'middle' },
'& a': { color: tokens.colorBrandForegroundLink },
'& a': { color: tokens.colorBrandForegroundLink, textDecoration: 'underline', wordBreak: 'break-word' },
'& a:hover': { textDecoration: 'underline' },
'& a:visited': { color: tokens.colorBrandForegroundLink },
'& a:focus': { outline: `2px solid ${tokens.colorNeutralStroke1}`, outlineOffset: '2px' },
},
});