From dce6ab2282cc8a397454ff27477c4cf5e93aa300 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Thu, 21 May 2026 11:04:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor(theme):=20=E9=87=8D=E6=9E=84=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E9=85=8D=E7=BD=AE=E5=92=8C=E7=AE=A1=E7=90=86=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Ant Design 主题配置提取到独立的 getAntThemeConfig 函数 - 移除 AGENTS.md 中关于状态管理和样式的冗余说明 - 删除 globals.css 中的 admin 相关样式和颜色变量 - 更新管理后台布局使用新的主题配置和样式常量 - 调整代码编辑器样式使用 Ant Design 的 CSS 变量 - 优化管理后台整体视觉风格为中性色调 --- AGENTS.md | 4 + docs/pending-test.md | 1 + web/src/app/(admin)/admin/layout.tsx | 78 ++++----- web/src/app/(admin)/admin/settings/page.tsx | 36 ++-- web/src/app/globals.css | 68 -------- web/src/components/ant-theme-provider.tsx | 174 +------------------- web/src/lib/app-theme.ts | 59 +++++++ 7 files changed, 118 insertions(+), 302 deletions(-) create mode 100644 web/src/lib/app-theme.ts diff --git a/AGENTS.md b/AGENTS.md index 39adff6..3e1a3c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ - 前端使用 Next.js App Router、React、TypeScript、Ant Design、Tailwind、Zustand。 - API 请求统一放在 `web/src/services/api/`。 - 全局或跨页面状态优先放在 `web/src/stores/`。 +- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。 - 画布相关状态和组件放在 `web/src/app/(user)/canvas/` 内部。 - 页面里只有一个主业务组件时直接写在 `page.tsx`,不要单独拆 `Manager` 组件再传一堆 props。 - 管理后台页面私有组件放到各自页面目录的 `components/` 下,例如 `admin/assets/components/`、`admin/prompts/components/`;不要为了单页面使用放到 `admin/components/` 共享目录。 @@ -42,6 +43,9 @@ - UI 图标优先使用 `lucide-react` 或项目已经使用的 Ant Design 图标。 - 页面文案保持中文。 - 不要在组件里堆太多无关逻辑;复杂逻辑优先抽成同目录工具函数或小组件。 +- 样式优先由组件自己管理;组件私有样式优先使用 Tailwind className 或少量内联 style,不要为单个组件新增大量全局 CSS。 +- 全局 CSS 只放基础变量、全局重置、跨页面通用样式和少量第三方组件必要覆盖;不要在 `globals.css` 堆页面私有样式。 +- 代码尽量短小直接,少拆不必要组件,少做多层 props 传递,避免为了抽象堆出更多代码。 - 前端业务数据需要浏览器本地持久化时,默认使用 `localforage`;`localStorage` 只用于极小的简单配置,不要用来保存业务列表、生成记录、图片、base64 或大 JSON。 ## 画布 UI 规范 diff --git a/docs/pending-test.md b/docs/pending-test.md index a0d40ee..37735b7 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -6,3 +6,4 @@ - 后端应自动维护 `settings` 表,并支持公开读取 public 配置、管理员读取和保存 public/private 两行 JSON 配置。 - 管理后台系统设置页面应按公开/私有两个 Tab 区分配置,并支持可视化编辑和手动编辑 JSON,且可保存 public/private 配置。 - 管理后台私有配置中的模型渠道应以列表展示、通过抽屉新增编辑,并支持填写权重、远程获取模型列表、模型清单测试和批量测试。 +- 管理后台亮色和暗色主题应使用接近 shadcn 的黑白中性色,侧栏、顶部栏、卡片、表格、弹窗和 JSON 编辑器不应再出现偏棕色或默认蓝色主色。 diff --git a/web/src/app/(admin)/admin/layout.tsx b/web/src/app/(admin)/admin/layout.tsx index 23f751f..32121ac 100644 --- a/web/src/app/(admin)/admin/layout.tsx +++ b/web/src/app/(admin)/admin/layout.tsx @@ -1,7 +1,7 @@ "use client"; import { FileTextOutlined, HomeOutlined, LogoutOutlined, PictureOutlined, SettingOutlined } from "@ant-design/icons"; -import { Button, Flex, Layout, Menu, Typography } from "antd"; +import { Button, Flex, Layout, Menu, Typography, theme } from "antd"; import { LogOut } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; @@ -9,10 +9,12 @@ import type { ReactNode } from "react"; import { useEffect } from "react"; import { UserStatusActions } from "@/components/user-status-actions"; +import { adminLayoutStyle } from "@/lib/app-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import { useUserStore } from "@/stores/use-user-store"; export default function AdminLayout({ children }: { children: ReactNode }) { + const { token: antToken } = theme.useToken(); const router = useRouter(); const pathname = usePathname(); const token = useUserStore((state) => state.token); @@ -38,50 +40,50 @@ export default function AdminLayout({ children }: { children: ReactNode }) { if (!isReady || !token || user?.role !== "admin") { return ( -
+
); } return ( - - - - - 无限画布 + + + + + 无限画布 + + , label: 提示词管理, style: adminLayoutStyle.menuItem }, + { key: "/admin/assets", icon: , label: 素材库, style: adminLayoutStyle.menuItem }, + { key: "/admin/settings", icon: , label: 系统设置, style: adminLayoutStyle.menuItem }, + ]} + /> + + + + + + + + {pageTitle} + + , label: "退出登录", onClick: logout }]} + /> - , label: 提示词管理 }, - { key: "/admin/assets", icon: , label: 素材库 }, - { key: "/admin/settings", icon: , label: 系统设置 }, - ]} - /> - - - - - - - - {pageTitle} - - , label: "退出登录", onClick: logout }]} - /> - - - {children} - + + {children} + ); } diff --git a/web/src/app/(admin)/admin/settings/page.tsx b/web/src/app/(admin)/admin/settings/page.tsx index 025c40e..3c31382 100644 --- a/web/src/app/(admin)/admin/settings/page.tsx +++ b/web/src/app/(admin)/admin/settings/page.tsx @@ -1,9 +1,7 @@ "use client"; import { CheckCircleOutlined, DeleteOutlined, FormatPainterOutlined, PlusOutlined, ReloadOutlined, SaveOutlined } from "@ant-design/icons"; -import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { json } from "@codemirror/lang-json"; -import { tags } from "@lezer/highlight"; import { App, Button, Card, Col, Drawer, Flex, Form, Input, InputNumber, Modal, Row, Segmented, Select, Space, Switch, Table, Tabs, Tag, Typography } from "antd"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; @@ -14,27 +12,17 @@ import { useUserStore } from "@/stores/use-user-store"; const CodeMirror = dynamic(() => import("@uiw/react-codemirror"), { ssr: false }); const jsonEditorTheme = EditorView.theme({ - "&": { backgroundColor: "var(--card)", color: "var(--foreground)" }, - ".cm-content": { caretColor: "var(--foreground)", padding: "12px 0" }, + "&": { backgroundColor: "var(--ant-color-bg-container)", color: "var(--ant-color-text)" }, + ".cm-content": { caretColor: "var(--ant-color-text)", padding: "12px 0" }, ".cm-line": { padding: "0 18px" }, - ".cm-gutters": { backgroundColor: "color-mix(in srgb, var(--card) 94%, var(--foreground))", borderRight: "1px solid var(--border)", color: "var(--muted-foreground)" }, - ".cm-activeLine": { backgroundColor: "color-mix(in srgb, var(--primary) 9%, transparent)" }, - ".cm-activeLineGutter": { backgroundColor: "color-mix(in srgb, var(--primary) 10%, transparent)", color: "var(--foreground)" }, - ".cm-cursor": { borderLeftColor: "var(--foreground)" }, - ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: "color-mix(in srgb, var(--primary) 25%, transparent)" }, - ".cm-foldPlaceholder": { backgroundColor: "var(--muted)", border: "1px solid var(--border)", color: "var(--muted-foreground)" }, + ".cm-gutters": { backgroundColor: "var(--ant-color-fill-quaternary)", borderRight: "1px solid var(--ant-color-border)", color: "var(--ant-color-text-tertiary)" }, + ".cm-activeLine": { backgroundColor: "var(--ant-color-fill-quaternary)" }, + ".cm-activeLineGutter": { backgroundColor: "var(--ant-color-fill-quaternary)", color: "var(--ant-color-text)" }, + ".cm-cursor": { borderLeftColor: "var(--ant-color-text)" }, + ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: "var(--ant-control-item-bg-active)" }, + ".cm-foldPlaceholder": { backgroundColor: "var(--ant-color-fill-quaternary)", border: "1px solid var(--ant-color-border)", color: "var(--ant-color-text-tertiary)" }, "&.cm-focused": { outline: "none" }, -}, { dark: true }); -const jsonHighlightStyle = HighlightStyle.define([ - { tag: tags.propertyName, color: "#f2a7a7" }, - { tag: tags.string, color: "#d6c6a8" }, - { tag: tags.number, color: "#ddb27d" }, - { tag: tags.bool, color: "#c5b3ff" }, - { tag: tags.null, color: "#c5b3ff" }, - { tag: tags.punctuation, color: "rgba(250, 250, 249, 0.62)" }, - { tag: tags.squareBracket, color: "rgba(250, 250, 249, 0.62)" }, - { tag: tags.brace, color: "rgba(250, 250, 249, 0.62)" }, -]); +}); const emptySettings: AdminSettings = { public: { @@ -291,11 +279,10 @@ export default function AdminSettingsPage() { setJsonText((current) => ({ ...current, public: value }))} - className="admin-json-editor" style={{ fontSize: 13 }} />
@@ -341,11 +328,10 @@ export default function AdminSettingsPage() { setJsonText((current) => ({ ...current, private: value }))} - className="admin-json-editor" style={{ fontSize: 13 }} /> diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 6716c2f..d2b5295 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -80,7 +80,6 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); - --admin-card-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); } .dark { @@ -115,7 +114,6 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); - --admin-card-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06); } @layer base { @@ -370,72 +368,6 @@ top: 21px; } - .admin-layout .ant-layout-sider { - border-right: 1px solid var(--border); - } - - .admin-layout .ant-layout-header { - border-bottom: 1px solid var(--border); - } - - .admin-layout .ant-layout-sider-children > .ant-flex:first-child { - border-bottom: 1px solid var(--border); - } - - .admin-logo { - background: currentColor; - display: inline-block; - height: 32px; - mask: url(/logo.svg) center / contain no-repeat; - width: 32px; - -webkit-mask: url(/logo.svg) center / contain no-repeat; - } - - .admin-layout .ant-layout-sider-children > .ant-flex:last-child { - border-top: 1px solid var(--border); - background: var(--card); - } - - .admin-layout .ant-card, - .admin-layout .ant-pro-card { - box-shadow: var(--admin-card-shadow); - } - - .admin-layout .ant-table-cell .ant-btn-icon-only.ant-btn-sm { - height: 26px; - width: 26px; - } - - .admin-json-editor .cm-editor { - background: var(--card); - color: var(--foreground); - font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - } - - .admin-json-editor .cm-scroller { - font-family: inherit; - } - - .admin-json-editor .cm-gutters { - background: color-mix(in srgb, var(--card) 92%, var(--foreground)); - border-right: 1px solid var(--border); - color: var(--muted-foreground); - } - - .admin-json-editor .cm-activeLine, - .admin-json-editor .cm-activeLineGutter { - background: color-mix(in srgb, var(--primary) 12%, transparent); - } - - .admin-json-editor .cm-cursor { - border-left-color: var(--foreground); - } - - .admin-json-editor .cm-selectionBackground, - .admin-json-editor .cm-focused .cm-selectionBackground { - background: color-mix(in srgb, var(--primary) 28%, transparent); - } - @keyframes canvas-batch-child-in { from { opacity: 0; diff --git a/web/src/components/ant-theme-provider.tsx b/web/src/components/ant-theme-provider.tsx index 2ef6e08..4bd284a 100644 --- a/web/src/components/ant-theme-provider.tsx +++ b/web/src/components/ant-theme-provider.tsx @@ -2,188 +2,20 @@ import type { ReactNode } from "react"; import { ProConfigProvider } from "@ant-design/pro-components"; -import { App, ConfigProvider, theme as antdTheme } from "antd"; +import { App, ConfigProvider } from "antd"; import zhCN from "antd/locale/zh_CN"; +import { getAntThemeConfig } from "@/lib/app-theme"; import { useThemeStore } from "@/stores/use-theme-store"; export function AntThemeProvider({ children }: { children: ReactNode }) { const theme = useThemeStore((state) => state.theme); const dark = theme === "dark"; - const colors = dark - ? { - bg: "#1f1d1a", - layout: "#181715", - panel: "#24211e", - elevated: "#292524", - fill: "#322e29", - fillHover: "#3a3631", - border: "#44403c", - borderSoft: "rgba(214, 211, 209, 0.18)", - text: "#f5f5f4", - textSecondary: "#d6d3d1", - textTertiary: "#a8a29e", - primary: "#f5f5f4", - primaryText: "#1c1917", - menuSelected: "#3a3631", - tableHeader: "#2b2521", - } - : { - bg: "#fbfaf7", - layout: "#f4f2ed", - panel: "#ffffff", - elevated: "#ffffff", - fill: "#f5f5f4", - fillHover: "#e7e5df", - border: "#d6d3ca", - borderSoft: "rgba(87, 83, 78, 0.18)", - text: "#292524", - textSecondary: "#57534e", - textTertiary: "#78716c", - primary: "#111111", - primaryText: "#ffffff", - menuSelected: "#f5f5f4", - tableHeader: "#f8fafc", - }; return ( {children} diff --git a/web/src/lib/app-theme.ts b/web/src/lib/app-theme.ts new file mode 100644 index 0000000..dc93918 --- /dev/null +++ b/web/src/lib/app-theme.ts @@ -0,0 +1,59 @@ +import type { CSSProperties } from "react"; +import type { ThemeConfig } from "antd"; +import { theme as antdTheme } from "antd"; + +const neutral = { + light: { + primary: "#171717", + primaryHover: "#000000", + primaryText: "#ffffff", + menuBg: "#f5f5f5", + menuText: "#171717", + }, + dark: { + primary: "#fafafa", + primaryHover: "#ffffff", + primaryText: "#171717", + menuBg: "#262626", + menuText: "#fafafa", + }, +}; + +export const adminLayoutStyle = { + siderWidth: 232, + headerHeight: 56, + brandHeight: 64, + menu: { borderInlineEnd: 0, padding: "18px 12px", fontSize: 15 } satisfies CSSProperties, + menuItem: { height: 44, lineHeight: "44px", marginBlock: 4, borderRadius: 8 } satisfies CSSProperties, +}; + +export function getAntThemeConfig(dark: boolean): ThemeConfig { + const color = dark ? neutral.dark : neutral.light; + + return { + algorithm: dark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm, + cssVar: true, + token: { + colorPrimary: color.primary, + colorInfo: color.primary, + colorLink: color.primary, + colorLinkHover: color.primaryHover, + colorLinkActive: color.primary, + colorTextLightSolid: color.primaryText, + }, + components: { + Button: { + primaryShadow: "none", + }, + Menu: { + itemActiveBg: color.menuBg, + itemHoverBg: color.menuBg, + itemSelectedBg: color.menuBg, + itemSelectedColor: color.menuText, + darkItemHoverBg: neutral.dark.menuBg, + darkItemSelectedBg: neutral.dark.menuBg, + darkItemSelectedColor: neutral.dark.menuText, + }, + }, + }; +}