添加深色/浅色主题本地储存,为a标签添加深色样式
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "sycamore_whisper_front",
|
"name": "sycamore_whisper_front",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
60
src/App.tsx
60
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
// 你好,感谢你愿意看源代码,但是悄悄告诉你,代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
|
// 你好,感谢你愿意看源代码,但是悄悄告诉你,代码其实是AI写的,所以质量很差喵。抱歉呜呜呜😭。
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { FluentProvider, webLightTheme, webDarkTheme, tokens } from '@fluentui/react-components';
|
import { FluentProvider, webLightTheme, webDarkTheme, tokens } from '@fluentui/react-components';
|
||||||
@@ -36,6 +36,8 @@ function App() {
|
|||||||
const lastRefreshAtRef = useRef<number>(0);
|
const lastRefreshAtRef = useRef<number>(0);
|
||||||
const REFRESH_COOLDOWN_MS = 5000; // 刷新冷却时间
|
const REFRESH_COOLDOWN_MS = 5000; // 刷新冷却时间
|
||||||
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
|
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) => {
|
const openImageViewer = (src?: string, alt?: string) => {
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
@@ -67,9 +69,16 @@ function App() {
|
|||||||
if (containerRef.current) containerRef.current.scrollTop = 0;
|
if (containerRef.current) containerRef.current.scrollTop = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 移除触摸下拉刷新逻辑
|
const handleToggleTheme = () => {
|
||||||
|
setIsDarkMode(prev => {
|
||||||
// 撤销 Pointer 事件回退,恢复为纯 Touch 逻辑
|
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 onWheel: React.WheelEventHandler<HTMLDivElement> = (e) => {
|
||||||
const atTop = (containerRef.current?.scrollTop ?? 0) <= 0;
|
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(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const signal = controller.signal;
|
const signal = controller.signal;
|
||||||
@@ -115,7 +161,7 @@ function App() {
|
|||||||
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
|
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<MainLayout isDarkMode={isDarkMode} onToggleTheme={() => setIsDarkMode(!isDarkMode)} />}>
|
<Route path="/" element={<MainLayout isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />}>
|
||||||
<Route
|
<Route
|
||||||
index
|
index
|
||||||
element={
|
element={
|
||||||
@@ -129,9 +175,7 @@ function App() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
// 移除下拉位移动画
|
|
||||||
}}>
|
}}>
|
||||||
{/* 刷新提示改为 toast,不显示顶部灰字 */}
|
|
||||||
{articles.map((article, index) => {
|
{articles.map((article, index) => {
|
||||||
if (articles.length === index + 1 && hasMore) {
|
if (articles.length === index + 1 && hasMore) {
|
||||||
return (
|
return (
|
||||||
@@ -178,7 +222,7 @@ function App() {
|
|||||||
<Route path="about" element={<AboutPage />} />
|
<Route path="about" element={<AboutPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/init" element={<InitPage />} />
|
<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 />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const voteArticle = async (
|
|||||||
throw new Error(`Vote ${type} failed`);
|
throw new Error(`Vote ${type} failed`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(`点赞${type === 'up' ? '赞' : '踩'}失败`);
|
toast.error(`点${type === 'up' ? '赞' : '踩'}失败`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ const useStyles = makeStyles({
|
|||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
backgroundColor: 'transparent',
|
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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,13 +92,29 @@ const useStyles = makeStyles({
|
|||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
backgroundColor: 'transparent',
|
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 图片不溢出卡片
|
// 约束 Markdown 图片不溢出卡片
|
||||||
'& img': {
|
'& img': {
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
borderRadius: tokens.borderRadiusSmall,
|
borderRadius: tokens.borderRadiusSmall,
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const useStyles = makeStyles({
|
|||||||
content: {
|
content: {
|
||||||
flex: '1 1 auto',
|
flex: '1 1 auto',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
|
paddingBottom: '64px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ const useStyles = makeStyles({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
// 约束可能的图片或表格
|
// 约束可能的图片或表格
|
||||||
'& img': { maxWidth: '100%', height: 'auto', display: 'inline-block', verticalAlign: 'middle' },
|
'& 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' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user