chore: merge dev-theme into master
This commit is contained in:
@@ -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) |
|
||||
|
||||
### 共享工具函数
|
||||
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(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}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 | string>(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,14 +58,16 @@ export function CodeBlock({ children, className: _className, isStreaming }: Code
|
||||
<div className="code-block">
|
||||
<div className="code-block-header">
|
||||
<span className="code-block-lang">{lang}</span>
|
||||
<Button icon={<CopyOutlined />} onClick={handleCopy} size="small" type="text" />
|
||||
<Button className="btn-dimmed" icon={<CopyOutlined />} onClick={handleCopy} size="small" type="text" />
|
||||
</div>
|
||||
{highlighted ? (
|
||||
<div className="code-block-body" dangerouslySetInnerHTML={{ __html: highlighted }} />
|
||||
) : (
|
||||
<pre className="code-block-body">
|
||||
<code>{codeText}</code>
|
||||
</pre>
|
||||
<div className="code-block-body">
|
||||
<pre>
|
||||
<code>{codeText}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
|
||||
|
||||
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 (
|
||||
<Flex align="center" className={className} gap="small" justify="space-between" onClick={onSelect}>
|
||||
|
||||
@@ -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<FilterValue>("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",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<TableColumnsType<Project>>(
|
||||
() => [
|
||||
@@ -60,7 +61,7 @@ export function ProjectTable({
|
||||
if (record.status === "archived") {
|
||||
return <Tag>已归档</Tag>;
|
||||
}
|
||||
return <Tag color="blue">进行中</Tag>;
|
||||
return <Tag color={themeToken.colorPrimary}>进行中</Tag>;
|
||||
},
|
||||
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<Project>["onChange"] = (pagination, _filters, sorter) => {
|
||||
|
||||
@@ -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 (
|
||||
<XProvider locale={locale} theme={{ algorithm: themeAlgorithm }}>
|
||||
<XProvider locale={locale} theme={buildThemeConfig(effectiveTheme)}>
|
||||
<AntApp>
|
||||
<Layout className="app-layout">
|
||||
<Header className="app-header">
|
||||
@@ -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 ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
width={232}
|
||||
>
|
||||
|
||||
33
src/web/shared/theme/theme-config.ts
Normal file
33
src/web/shared/theme/theme-config.ts
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -425,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 {
|
||||
|
||||
Reference in New Issue
Block a user