From 85abc2a515f8a8132f56c63544b245987f0a8fd0 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Fri, 5 Jun 2026 16:01:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20antd=20=E4=B8=BB=E9=A2=98=E6=94=B9?= =?UTF-8?q?=E9=80=A0=20=E2=80=94=20=E5=90=AF=E7=94=A8=20cssVar=E3=80=81?= =?UTF-8?q?=E7=BA=AF=E9=BB=91=E7=99=BD=20colorPrimary=E3=80=81=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=20sidebar/=E6=BB=9A=E5=8A=A8=E6=9D=A1/=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development/frontend.md | 12 ++- src/web/features/chat/ChatScrollArea.tsx | 4 +- .../chat/components/ConversationList.tsx | 4 +- src/web/features/chat/parts/CodeBlock.tsx | 7 +- .../inbox/components/MaterialCard.tsx | 2 +- .../inbox/components/MaterialList.tsx | 4 +- .../projects/components/ProjectTable.tsx | 7 +- .../components/ConsoleShell/ConsoleShell.tsx | 8 +- src/web/shared/theme/theme-config.ts | 33 ++++++++ src/web/styles.css | 77 ++++++++----------- 10 files changed, 88 insertions(+), 70 deletions(-) create mode 100644 src/web/shared/theme/theme-config.ts diff --git a/docs/development/frontend.md b/docs/development/frontend.md index a02ffb4..50f134b 100644 --- a/docs/development/frontend.md +++ b/docs/development/frontend.md @@ -9,7 +9,7 @@ - **AdminLayout**(`src/web/layouts/admin-layout/`):路由 `/`(总览)、`/projects`、`/models`、`/models/providers`。 - **WorkbenchLayout**(`src/web/layouts/workbench-layout/`):路由 `/workbench/:projectId`、`/workbench/:projectId/chat`。`WorkbenchProjectGate` 从 URL 读 projectId,通过 `ProjectContext` 提供项目上下文,仅 active 项目渲染。 -ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Sider/Content) + 主题切换(明亮/黑暗/系统)+ 侧边栏折叠。Header 显示品牌名、版本号和布局标题。 +ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Sider/Content) + 主题切换(明亮/黑暗/系统),主题配置由 `shared/theme/theme-config.ts` 的 `buildThemeConfig(effectiveTheme)` 集中构建(含 `cssVar`、`borderRadius`、`controlHeight`、`components.Layout` 配色);侧边栏折叠。Header 显示品牌名、版本号和布局标题。 `Sidebar`(`src/web/shared/components/Sidebar/`)纯展示组件,通过 `menuItems` props 接收配置。 @@ -69,8 +69,14 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si | `use-theme-preference` | `shared/hooks/use-theme-preference.ts` | 主题偏好 localStorage 持久化 | | `use-sidebar-collapsed` | `shared/hooks/use-sidebar-collapsed.ts` | 侧边栏折叠 localStorage 持久化 | | `use-is-dark` | `shared/hooks/use-is-dark.ts` | 当前是否暗色主题 | -| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) | -| `use-materials.ts` | `shared/hooks/use-materials.ts` | 素材 CRUD(create/delete/fetch/list + Query hooks) | + +### 共享主题配置 + +| 文件 | 导出 | +| ----------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| `theme/theme-config.ts` | `buildThemeConfig(effectiveTheme)` — 构建 antd ThemeConfig(algorithm、cssVar、token、components.Layout) | +| `use-current-project` | `shared/hooks/use-current-project.ts` | 当前工作台项目 + ProjectContext(需在 ProjectProvider 内) | +| `use-materials.ts` | `shared/hooks/use-materials.ts` | 素材 CRUD(create/delete/fetch/list + Query hooks) | ### 共享工具函数 diff --git a/src/web/features/chat/ChatScrollArea.tsx b/src/web/features/chat/ChatScrollArea.tsx index 1147375..03c5bfe 100644 --- a/src/web/features/chat/ChatScrollArea.tsx +++ b/src/web/features/chat/ChatScrollArea.tsx @@ -6,7 +6,6 @@ import "overlayscrollbars/styles/overlayscrollbars.css"; import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { useCallback, useRef, useState } from "react"; -import { useIsDark } from "../../shared/hooks/use-is-dark"; import { useChatScroll } from "./use-chat-scroll"; interface ChatScrollAreaProps { @@ -19,7 +18,6 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro const scrollRef = useRef(null); const osRef = useRef(null); const [viewportElement, setViewportElement] = useState(null); - const isDark = useIsDark(); const handleOsInitialized = useCallback(() => { const os = osRef.current; @@ -42,7 +40,7 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro overflow: { x: "hidden", y: "scroll" }, scrollbars: { autoHide: "move", - theme: isDark ? "os-theme-custom-dark" : "os-theme-custom", + theme: "os-theme-custom", }, }} ref={osRef} diff --git a/src/web/features/chat/components/ConversationList.tsx b/src/web/features/chat/components/ConversationList.tsx index f2c34ff..1945c80 100644 --- a/src/web/features/chat/components/ConversationList.tsx +++ b/src/web/features/chat/components/ConversationList.tsx @@ -7,7 +7,6 @@ import { useMemo, useState } from "react"; import type { Conversation } from "../../../../shared/api"; import { SidebarGroup } from "../../../shared/components/SidebarGroup"; -import { useIsDark } from "../../../shared/hooks/use-is-dark"; import { GROUP_LABELS, groupByDate } from "../../../shared/utils/date-group"; import { ConversationCard } from "./ConversationCard"; @@ -30,7 +29,6 @@ export function ConversationList({ }: ConversationListProps) { const [inputText, setInputText] = useState(""); const [appliedSearch, setAppliedSearch] = useState(""); - const isDark = useIsDark(); const filteredConversations = useMemo(() => { if (!appliedSearch) return conversations; @@ -60,7 +58,7 @@ export function ConversationList({ overflow: { x: "hidden", y: "scroll" }, scrollbars: { autoHide: "move", - theme: isDark ? "os-theme-custom-dark" : "os-theme-custom", + theme: "os-theme-custom", }, }} > diff --git a/src/web/features/chat/parts/CodeBlock.tsx b/src/web/features/chat/parts/CodeBlock.tsx index 016a783..6895b8c 100644 --- a/src/web/features/chat/parts/CodeBlock.tsx +++ b/src/web/features/chat/parts/CodeBlock.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { CopyOutlined } from "@ant-design/icons"; -import { Button, message } from "antd"; +import { App, Button } from "antd"; import { useCallback, useEffect, useState } from "react"; import { codeToHtml } from "shiki"; @@ -14,6 +14,7 @@ interface CodeBlockProps { } export function CodeBlock({ children, className: _className, isStreaming }: CodeBlockProps) { + const { message } = App.useApp(); const isDark = useIsDark(); const [highlighted, setHighlighted] = useState(null); const { codeText, lang } = extractCode(children); @@ -23,7 +24,7 @@ export function CodeBlock({ children, className: _className, isStreaming }: Code () => message.success("已复制"), () => message.error("复制失败"), ); - }, [codeText]); + }, [codeText, message]); useEffect(() => { if (isStreaming || !codeText) return; @@ -57,7 +58,7 @@ export function CodeBlock({ children, className: _className, isStreaming }: Code
{lang} -
{highlighted ? (
diff --git a/src/web/features/inbox/components/MaterialCard.tsx b/src/web/features/inbox/components/MaterialCard.tsx index 8d3492a..617782c 100644 --- a/src/web/features/inbox/components/MaterialCard.tsx +++ b/src/web/features/inbox/components/MaterialCard.tsx @@ -18,7 +18,7 @@ const STATUS_MAP: Record = { export function MaterialCard({ material, onDelete, onSelect, selected }: MaterialCardProps) { const statusInfo = STATUS_MAP[material.status]; - const className = selected ? "material-list-item material-list-item--selected" : "material-list-item"; + const className = selected ? "app-sidebar-list-item app-sidebar-list-item--selected" : "app-sidebar-list-item"; return ( diff --git a/src/web/features/inbox/components/MaterialList.tsx b/src/web/features/inbox/components/MaterialList.tsx index 5546cd7..5918b1f 100644 --- a/src/web/features/inbox/components/MaterialList.tsx +++ b/src/web/features/inbox/components/MaterialList.tsx @@ -13,7 +13,6 @@ import { useMemo, useState } from "react"; import type { Material } from "../types"; import { SidebarGroup } from "../../../shared/components/SidebarGroup"; -import { useIsDark } from "../../../shared/hooks/use-is-dark"; import { GROUP_LABELS, groupByDate } from "../../../shared/utils/date-group"; import { MaterialCard } from "./MaterialCard"; @@ -37,7 +36,6 @@ type FilterValue = (typeof STATUS_FILTER_OPTIONS)[number]["value"]; export function MaterialList({ loading, materials, onAddClick, onDelete, onSelect, selectedId }: MaterialListProps) { const [filterStatus, setFilterStatus] = useState("all"); - const isDark = useIsDark(); const filteredMaterials = useMemo(() => { if (filterStatus === "all") return materials; @@ -74,7 +72,7 @@ export function MaterialList({ loading, materials, onAddClick, onDelete, onSelec overflow: { x: "hidden", y: "scroll" }, scrollbars: { autoHide: "move", - theme: isDark ? "os-theme-custom-dark" : "os-theme-custom", + theme: "os-theme-custom", }, }} > diff --git a/src/web/features/projects/components/ProjectTable.tsx b/src/web/features/projects/components/ProjectTable.tsx index 4260368..a75d608 100644 --- a/src/web/features/projects/components/ProjectTable.tsx +++ b/src/web/features/projects/components/ProjectTable.tsx @@ -1,7 +1,7 @@ import type { TableColumnsType, TableProps } from "antd"; import { DeleteOutlined, EditOutlined, InboxOutlined, LoginOutlined, RedoOutlined } from "@ant-design/icons"; -import { Button, Popconfirm, Space, Table, Tag } from "antd"; +import { Button, Popconfirm, Space, Table, Tag, theme } from "antd"; import { useMemo } from "react"; import { useNavigate } from "react-router"; @@ -40,6 +40,7 @@ export function ProjectTable({ sortOrder, }: ProjectTableProps) { const navigate = useNavigate(); + const { token: themeToken } = theme.useToken(); const columns = useMemo>( () => [ @@ -60,7 +61,7 @@ export function ProjectTable({ if (record.status === "archived") { return 已归档; } - return 进行中; + return 进行中; }, title: "状态", width: 90, @@ -137,7 +138,7 @@ export function ProjectTable({ width: 260, }, ], - [navigate, onEdit, onArchive, onRestore, onDelete, sortBy, sortOrder], + [navigate, onEdit, onArchive, onRestore, onDelete, sortBy, sortOrder, themeToken.colorPrimary], ); const handleTableChange: TableProps["onChange"] = (pagination, _filters, sorter) => { diff --git a/src/web/shared/components/ConsoleShell/ConsoleShell.tsx b/src/web/shared/components/ConsoleShell/ConsoleShell.tsx index ab20aaa..c10200d 100644 --- a/src/web/shared/components/ConsoleShell/ConsoleShell.tsx +++ b/src/web/shared/components/ConsoleShell/ConsoleShell.tsx @@ -1,7 +1,7 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons"; import { XProvider } from "@ant-design/x"; import zhCN_X from "@ant-design/x/locale/zh_CN"; -import { App as AntApp, Layout, Segmented, theme } from "antd"; +import { App as AntApp, Layout, Segmented } from "antd"; import zhCN from "antd/locale/zh_CN"; import { useMemo } from "react"; @@ -11,6 +11,7 @@ import { APP } from "../../../../shared/app"; import { useMeta } from "../../hooks/use-meta"; import { useSidebarCollapsed } from "../../hooks/use-sidebar-collapsed"; import { useThemePreference } from "../../hooks/use-theme-preference"; +import { buildThemeConfig } from "../../theme/theme-config"; import { Sidebar } from "../Sidebar"; import { ConsoleOutlet } from "./ConsoleOutlet"; @@ -28,11 +29,10 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp const { data: meta } = useMeta(); const versionDisplay = meta?.version ? `v${meta.version}` : null; - const themeAlgorithm = effectiveTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm; const locale = useMemo(() => ({ ...zhCN, ...zhCN_X }), []); return ( - +
@@ -58,7 +58,7 @@ export function ConsoleShell({ headerExtra, menuItems, title }: ConsoleShellProp collapsedWidth={64} collapsible onCollapse={(c) => setCollapsed(c)} - theme="light" + theme={effectiveTheme === "dark" ? "dark" : "light"} trigger={collapsed ? : } width={232} > diff --git a/src/web/shared/theme/theme-config.ts b/src/web/shared/theme/theme-config.ts new file mode 100644 index 0000000..99b428c --- /dev/null +++ b/src/web/shared/theme/theme-config.ts @@ -0,0 +1,33 @@ +import { theme } from "antd"; + +import type { EffectiveTheme } from "../hooks/use-theme-preference"; + +export function buildThemeConfig(effectiveTheme: EffectiveTheme) { + const isDark = effectiveTheme === "dark"; + const algorithm = isDark ? theme.darkAlgorithm : theme.defaultAlgorithm; + + return { + algorithm, + components: { + Layout: { + bodyBg: isDark ? "#0a0a0a" : "transparent", + headerBg: "transparent", + siderBg: isDark ? "#0a0a0a" : "#ffffff", + triggerBg: isDark ? "#141414" : "#ffffff", + }, + Menu: { + itemActiveBg: isDark ? "#1f1f1f" : "#e5e5e5", + itemHoverBg: isDark ? "#1a1a1a" : "#f0f0f0", + itemSelectedBg: isDark ? "#2a2a2a" : "#0a0a0a", + itemSelectedColor: "#ffffff", + }, + }, + cssVar: {}, + token: { + borderRadius: 10, + colorLink: isDark ? "#a3a3a3" : "#0a0a0a", + colorPrimary: isDark ? "#525252" : "#0a0a0a", + controlHeight: 36, + }, + }; +} diff --git a/src/web/styles.css b/src/web/styles.css index e6617aa..50c3d97 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -81,10 +81,22 @@ body { min-height: 60vh; } +.ant-layout-sider { + border-right: none; +} + +.ant-layout-sider-trigger { + border-right: 1px solid var(--ant-color-border-secondary); +} + +.ant-menu { + height: 100%; +} + .app-sidebar-list { display: flex; flex-direction: column; - border-right: 1px solid var(--ant-color-border-secondary); + border: 1px solid var(--ant-color-border-secondary); border-radius: var(--ant-border-radius-lg); background: var(--ant-color-bg-container); } @@ -118,11 +130,16 @@ body { } .app-sidebar-list-item--selected { - background: var(--ant-color-primary-bg); + background: var(--ant-color-primary); + color: var(--ant-color-text-light-solid); +} + +.app-sidebar-list-item--selected .ant-typography { + color: var(--ant-color-text-light-solid); } .app-sidebar-list-item--selected:hover { - background: var(--ant-color-primary-bg); + background: var(--ant-color-primary); } .app-sidebar-item-actions { @@ -267,17 +284,19 @@ body { background: var(--ant-color-bg-container); } -.card-extra-actions .btn-dimmed { +.card-extra-actions .btn-dimmed, +.code-block-header .ant-btn.btn-dimmed { color: var(--ant-color-text-quaternary); } -.card-extra-actions .btn-dimmed:hover { +.card-extra-actions .btn-dimmed:hover, +.code-block-header .ant-btn.btn-dimmed:hover { color: var(--ant-color-text-secondary); } .chat-scroll-bottom-btn { position: absolute; - bottom: 115px; + bottom: 140px; left: 50%; transform: translateX(-50%); display: flex; @@ -297,23 +316,9 @@ body { --os-padding-axis: 2px; --os-track-border-radius: 10px; --os-handle-border-radius: 10px; - --os-handle-bg: rgba(0, 0, 0, 0.15); - --os-handle-bg-hover: rgba(0, 0, 0, 0.25); - --os-handle-bg-active: rgba(0, 0, 0, 0.35); - --os-handle-min-size: 33px; - --os-handle-max-size: none; - --os-handle-interactive-area-offset: 4px; -} - -.os-theme-custom-dark { - --os-size: 8px; - --os-padding-perpendicular: 2px; - --os-padding-axis: 2px; - --os-track-border-radius: 10px; - --os-handle-border-radius: 10px; - --os-handle-bg: rgba(255, 255, 255, 0.15); - --os-handle-bg-hover: rgba(255, 255, 255, 0.25); - --os-handle-bg-active: rgba(255, 255, 255, 0.35); + --os-handle-bg: var(--ant-color-border-secondary); + --os-handle-bg-hover: var(--ant-color-text-quaternary); + --os-handle-bg-active: var(--ant-color-text-tertiary); --os-handle-min-size: 33px; --os-handle-max-size: none; --os-handle-interactive-area-offset: 4px; @@ -339,28 +344,6 @@ body { width: 100%; } -/* Inbox material list items */ -.material-list-item { - border: none; - margin: var(--ant-margin-xxs) var(--ant-margin-xxs); - padding: var(--ant-padding-xs) var(--ant-padding-sm); - border-radius: var(--ant-border-radius-lg); - cursor: pointer; - transition: background 0.15s ease; -} - -.material-list-item:hover { - background: var(--ant-color-bg-text-hover); -} - -.material-list-item--selected { - background: var(--ant-color-primary-bg); -} - -.material-list-item--selected:hover { - background: var(--ant-color-primary-bg); -} - .material-item-right { position: relative; display: inline-flex; @@ -385,11 +368,11 @@ body { opacity: 0; } -.material-list-item:hover .material-item-tag { +.app-sidebar-list-item:hover .material-item-tag { opacity: 0; } -.material-list-item:hover .material-item-actions { +.app-sidebar-list-item:hover .material-item-actions { opacity: 1; } From 98712cf04745c4f81bf3f53a295b1374c42770ac Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Fri, 5 Jun 2026 16:44:13 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=B6=88=E9=99=A4=20code-block-body?= =?UTF-8?q?=20=E8=83=8C=E6=99=AF=E8=89=B2=E4=B8=8E=20shiki=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E8=83=8C=E6=99=AF=E8=89=B2=E8=A7=86=E8=A7=89=E5=89=B2?= =?UTF-8?q?=E8=A3=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web/features/chat/parts/CodeBlock.tsx | 8 +++++--- src/web/styles.css | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/web/features/chat/parts/CodeBlock.tsx b/src/web/features/chat/parts/CodeBlock.tsx index 6895b8c..2558955 100644 --- a/src/web/features/chat/parts/CodeBlock.tsx +++ b/src/web/features/chat/parts/CodeBlock.tsx @@ -63,9 +63,11 @@ export function CodeBlock({ children, className: _className, isStreaming }: Code {highlighted ? (
) : ( -
-          {codeText}
-        
+
+
+            {codeText}
+          
+
)}
); diff --git a/src/web/styles.css b/src/web/styles.css index 50c3d97..6ed9c9c 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -408,9 +408,12 @@ body { .code-block-body { margin: 0; +} + +.code-block-body > pre { padding: var(--ant-padding-sm); + margin: 0; overflow-x: auto; - background: var(--ant-color-bg-container); } .code-block-body code {