修复系统写死颜色并接入主题变量切换

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-01 20:08:30 +08:00
parent cf53787bb2
commit f130d33d42
8 changed files with 120 additions and 34 deletions
+34
View File
@@ -545,3 +545,37 @@
- 风险与影响:
- 影响范围仅前端用户管理页面展示层,不涉及后端接口与权限模型。
## Work Log - 修复系统写死颜色并支持主题变量切换(2026-05-01)
- 背景:
- Issue `FL-146` 要求修正系统中写死颜色,支持主题色切换,统一通过变量驱动。
- 现状存在多处硬编码:`error/global-error` 内联 hex,后台列表选中态 `bg-blue-50`,以及主题菜单徽标固定蓝色。
- 本次改动(最小闭环):
- 文件:`web/src/app/globals.css`
- 新增主题语义变量桥接:`--fquiz-theme-*`(布局背景、容器背景、文本、边框、主色、阴影、选中态等)。
- 新增暗色模式变量覆盖:`:root[data-fquiz-theme="dark"]`
- 新增统一行选中样式:`.fquiz-row-selected > td { background: var(--fquiz-theme-bg-active) !important; }`
- 文件:`web/src/components/ui-antd.tsx`
- 保留主题算法逻辑,新增 `buildThemeVisualTokens(isDark)` 收敛布局/壳层/表头色常量来源,避免散落硬编码。
-`ThemeCssVarsScope` 补充 `--ant-color-primary-bg` / `--ant-color-primary-bg-hover` / `--ant-color-primary-hover` 变量导出。
- 在主题切换时同步写入 `document.documentElement.dataset.fquizTheme``dark|light`),驱动 CSS 变量切换。
- 文件:`web/src/app/error.tsx`
- 将所有硬编码颜色替换为 `var(--fquiz-theme-*)` / `var(--ant-*)` 变量。
- 文件:`web/src/app/global-error.tsx`
- 同步替换硬编码颜色为主题变量。
- 文件:`web/src/app/admin/lightning-currents/page.tsx`
- 表格选中行样式由 `bg-blue-50` 改为 `fquiz-row-selected`
- 文件:`web/src/app/admin/power-lines/atp-viewer/page.tsx`
- 表格选中行样式由 `bg-blue-50` 改为 `fquiz-row-selected`
- 文件:`web/src/app/admin/layout.tsx`
- 主题菜单徽标颜色由固定 `blue` 改为 `var(--fquiz-theme-primary, var(--ant-color-primary))`
- 验证(按任务约束未执行编译/安装依赖):
- `rg` 检查确认目标文件内 `#155eef/#101828/#f5f7fb/bg-blue-50` 已清理。
- `git diff` 检查改动范围仅涉及本次主题变量改造文件。
- 风险与影响:
- 影响范围为前端主题层与错误页展示层,不涉及后端接口行为。
- `fquiz-row-selected` 使用 `!important` 以覆盖 AntD 表格单元背景,后续若引入更高优先级行态样式需注意冲突。
+4 -1
View File
@@ -219,7 +219,10 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
});
}, [activeMenuState.openKeys]);
const themeBadge = useMemo(() => <Badge color="blue" style={{ marginTop: -1 }} />, []);
const themeBadge = useMemo(
() => <Badge color="var(--fquiz-theme-primary, var(--ant-color-primary))" style={{ marginTop: -1 }} />,
[],
);
const themeMenuItems = useMemo<NonNullable<MenuProps["items"]>>(
() => [
{
@@ -1109,7 +1109,7 @@ export default function AdminLightningCurrentsPage() {
loading={eventsQuery.isFetching}
pagination={false}
scroll={{ x: 1700 }}
rowClassName={(row) => (row.id === selectedEventId ? "bg-blue-50" : "")}
rowClassName={(row) => (row.id === selectedEventId ? "fquiz-row-selected" : "")}
onRow={(row) => ({
onClick: () => setSelectedEventId(row.id),
})}
@@ -1031,7 +1031,7 @@ export default function PowerLinesAtpViewerPage() {
loading={modelsQuery.isFetching}
pagination={false}
scroll={{ x: 1500 }}
rowClassName={(row) => (row.id === selectedModelId ? "bg-blue-50" : "")}
rowClassName={(row) => (row.id === selectedModelId ? "fquiz-row-selected" : "")}
onRow={(row) => ({
onClick: () => setSelectedModelId(row.id),
})}
+12 -12
View File
@@ -24,8 +24,8 @@ export default function AppError({ error, reset }: AppErrorProps) {
alignItems: "center",
justifyContent: "center",
padding: 24,
background: "#f5f7fb",
color: "#101828",
background: "var(--fquiz-theme-bg-layout, var(--ant-color-bg-layout))",
color: "var(--fquiz-theme-text-primary, var(--ant-color-text))",
}}
>
<section
@@ -33,14 +33,14 @@ export default function AppError({ error, reset }: AppErrorProps) {
width: "100%",
maxWidth: 520,
borderRadius: 14,
background: "#ffffff",
border: "1px solid #e5e7eb",
boxShadow: "0 10px 24px rgba(15, 23, 42, 0.08)",
background: "var(--fquiz-theme-bg-container, var(--ant-color-bg-container))",
border: "1px solid var(--fquiz-theme-border, var(--ant-color-border-secondary))",
boxShadow: "var(--fquiz-theme-shadow-card, var(--ant-box-shadow-tertiary))",
padding: 24,
}}
>
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}></h1>
<p style={{ margin: "12px 0 0", lineHeight: 1.6, color: "#344054" }}>
<p style={{ margin: "12px 0 0", lineHeight: 1.6, color: "var(--fquiz-theme-text-secondary, var(--ant-color-text-secondary))" }}>
{chunkError
? "检测到前端静态资源已更新,页面将尝试自动刷新恢复。"
: "页面发生运行时错误,请重试或手动刷新。"}
@@ -50,9 +50,9 @@ export default function AppError({ error, reset }: AppErrorProps) {
type="button"
onClick={reset}
style={{
border: "1px solid #d0d5dd",
background: "#ffffff",
color: "#101828",
border: "1px solid var(--fquiz-theme-border, var(--ant-color-border-secondary))",
background: "var(--fquiz-theme-bg-container, var(--ant-color-bg-container))",
color: "var(--fquiz-theme-text-primary, var(--ant-color-text))",
borderRadius: 8,
padding: "8px 14px",
cursor: "pointer",
@@ -65,9 +65,9 @@ export default function AppError({ error, reset }: AppErrorProps) {
type="button"
onClick={() => window.location.reload()}
style={{
border: "1px solid #155eef",
background: "#155eef",
color: "#ffffff",
border: "1px solid var(--fquiz-theme-primary, var(--ant-color-primary))",
background: "var(--fquiz-theme-primary, var(--ant-color-primary))",
color: "var(--fquiz-theme-text-on-primary, var(--accent-contrast))",
borderRadius: 8,
padding: "8px 14px",
cursor: "pointer",
+12 -12
View File
@@ -26,8 +26,8 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
alignItems: "center",
justifyContent: "center",
padding: 24,
background: "#f5f7fb",
color: "#101828",
background: "var(--fquiz-theme-bg-layout, var(--ant-color-bg-layout))",
color: "var(--fquiz-theme-text-primary, var(--ant-color-text))",
fontFamily:
"Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
}}
@@ -37,14 +37,14 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
width: "100%",
maxWidth: 560,
borderRadius: 14,
background: "#ffffff",
border: "1px solid #e5e7eb",
boxShadow: "0 10px 24px rgba(15, 23, 42, 0.08)",
background: "var(--fquiz-theme-bg-container, var(--ant-color-bg-container))",
border: "1px solid var(--fquiz-theme-border, var(--ant-color-border-secondary))",
boxShadow: "var(--fquiz-theme-shadow-card, var(--ant-box-shadow-tertiary))",
padding: 24,
}}
>
<h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}></h1>
<p style={{ margin: "12px 0 0", lineHeight: 1.6, color: "#344054" }}>
<p style={{ margin: "12px 0 0", lineHeight: 1.6, color: "var(--fquiz-theme-text-secondary, var(--ant-color-text-secondary))" }}>
{chunkError
? "检测到静态资源版本不一致,正在尝试自动刷新页面。"
: "应用遇到未处理错误,请重试或刷新后再试。"}
@@ -54,9 +54,9 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
type="button"
onClick={reset}
style={{
border: "1px solid #d0d5dd",
background: "#ffffff",
color: "#101828",
border: "1px solid var(--fquiz-theme-border, var(--ant-color-border-secondary))",
background: "var(--fquiz-theme-bg-container, var(--ant-color-bg-container))",
color: "var(--fquiz-theme-text-primary, var(--ant-color-text))",
borderRadius: 8,
padding: "8px 14px",
cursor: "pointer",
@@ -69,9 +69,9 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
type="button"
onClick={() => window.location.reload()}
style={{
border: "1px solid #155eef",
background: "#155eef",
color: "#ffffff",
border: "1px solid var(--fquiz-theme-primary, var(--ant-color-primary))",
background: "var(--fquiz-theme-primary, var(--ant-color-primary))",
color: "var(--fquiz-theme-text-on-primary, var(--accent-contrast))",
borderRadius: 8,
padding: "8px 14px",
cursor: "pointer",
+35
View File
@@ -63,6 +63,37 @@
--fquiz-scrollbar-thumb: var(--ant-color-fill-secondary, rgba(0, 0, 0, 0.15));
--fquiz-scrollbar-thumb-hover: var(--ant-color-fill, rgba(0, 0, 0, 0.25));
/* Theme semantic bridge vars (for pages/components avoid hard-coded colors) */
--fquiz-theme-bg-layout: var(--ant-color-bg-layout);
--fquiz-theme-bg-container: var(--ant-color-bg-container);
--fquiz-theme-bg-elevated: #ffffff;
--fquiz-theme-bg-soft: var(--gray-2);
--fquiz-theme-bg-active: var(--accent-a2);
--fquiz-theme-text-primary: var(--ant-color-text);
--fquiz-theme-text-secondary: var(--ant-color-text-secondary);
--fquiz-theme-text-muted: var(--gray-10);
--fquiz-theme-text-on-primary: var(--accent-contrast);
--fquiz-theme-border: var(--ant-color-border-secondary);
--fquiz-theme-border-strong: var(--gray-6);
--fquiz-theme-primary: var(--ant-color-primary);
--fquiz-theme-primary-hover: var(--accent-10);
--fquiz-theme-table-header-bg: var(--gray-2);
--fquiz-theme-shell-header-bg: var(--fquiz-theme-bg-elevated);
--fquiz-theme-shell-sider-bg: var(--fquiz-theme-bg-elevated);
--fquiz-theme-shadow-card: 0 10px 24px color-mix(in srgb, var(--gray-12) 12%, transparent);
--fquiz-theme-shadow-soft: var(--ant-box-shadow-tertiary);
}
:root[data-fquiz-theme="dark"] {
--fquiz-theme-bg-elevated: color-mix(in srgb, var(--ant-color-bg-container) 80%, black);
--fquiz-theme-table-header-bg: color-mix(in srgb, var(--ant-color-bg-container) 88%, black);
--fquiz-theme-shell-header-bg: color-mix(in srgb, var(--ant-color-bg-container) 82%, black);
--fquiz-theme-shell-sider-bg: color-mix(in srgb, var(--ant-color-bg-container) 82%, black);
--fquiz-theme-shadow-card: 0 10px 24px color-mix(in srgb, black 45%, transparent);
}
body {
@@ -177,6 +208,10 @@ body {
scrollbar-gutter: stable;
}
.fquiz-row-selected > td {
background: var(--fquiz-theme-bg-active) !important;
}
:root.fquiz-happy-work body {
animation: fquiz-happy-work-glow 12s ease-in-out infinite;
}
+21 -7
View File
@@ -257,6 +257,14 @@ const RADIUS_MAP: Record<string, number> = {
full: 999,
};
function buildThemeVisualTokens(isDark: boolean) {
return {
colorBgLayout: isDark ? "#0f1419" : "#f5f5f5",
shellBg: isDark ? "#111a2c" : "#ffffff",
tableHeaderBg: isDark ? "#1f1f1f" : "#fafafa",
};
}
function ThemeCssVarsScope({ children }: { children: ReactNode }) {
const { token } = antdTheme.useToken();
@@ -269,6 +277,9 @@ function ThemeCssVarsScope({ children }: { children: ReactNode }) {
"--ant-color-text-secondary": token.colorTextSecondary,
"--ant-color-bg-layout": token.colorBgLayout,
"--ant-color-bg-container": token.colorBgContainer,
"--ant-color-primary-bg": token.colorPrimaryBg,
"--ant-color-primary-bg-hover": token.colorPrimaryBgHover,
"--ant-color-primary-hover": token.colorPrimaryHover,
"--ant-color-fill-alter": token.colorFillAlter,
"--ant-color-border-secondary": token.colorBorderSecondary,
"--ant-border-radius": `${token.borderRadius}px`,
@@ -462,6 +473,7 @@ export function Theme({
}, []);
const isDark = themePrimaryMode === "auto" ? systemPrefersDark : themePrimaryMode === "dark";
const visualTokens = useMemo(() => buildThemeVisualTokens(isDark), [isDark]);
const themeMode = useMemo<ThemeMode>(() => toLegacyThemeMode(isDark, compactMode), [compactMode, isDark]);
@@ -496,10 +508,12 @@ export function Theme({
return;
}
document.documentElement.classList.toggle("fquiz-happy-work", happyWorkMode);
document.documentElement.dataset.fquizTheme = isDark ? "dark" : "light";
return () => {
document.documentElement.classList.remove("fquiz-happy-work");
delete document.documentElement.dataset.fquizTheme;
};
}, [happyWorkMode]);
}, [happyWorkMode, isDark]);
const themeConfig = useMemo<ThemeConfig>(
() => ({
@@ -509,13 +523,13 @@ export function Theme({
borderRadius: RADIUS_MAP[radius] ?? RADIUS_MAP.medium,
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
colorBgLayout: isDark ? "#0f1419" : "#f5f5f5",
colorBgLayout: visualTokens.colorBgLayout,
},
components: {
Layout: {
headerBg: isDark ? "#111a2c" : "#ffffff",
siderBg: isDark ? "#111a2c" : "#ffffff",
bodyBg: isDark ? "#0f1419" : "#f5f5f5",
headerBg: visualTokens.shellBg,
siderBg: visualTokens.shellBg,
bodyBg: visualTokens.colorBgLayout,
headerHeight: 64,
headerPadding: "0 24px",
},
@@ -527,11 +541,11 @@ export function Theme({
subMenuItemBorderRadius: RADIUS_MAP.medium,
},
Table: {
headerBg: isDark ? "#1f1f1f" : "#fafafa",
headerBg: visualTokens.tableHeaderBg,
},
},
}),
[isDark, radius, resolvedAccentColor, themeAlgorithm],
[radius, resolvedAccentColor, themeAlgorithm, visualTokens],
);
return (