style: 收集箱布局深化——卡片流、OS滚动、操作区fill按钮

This commit is contained in:
2026-06-08 11:36:02 +08:00
parent b4e05a4a16
commit 74266dc5cc
4 changed files with 92 additions and 70 deletions

View File

@@ -26,15 +26,15 @@ ConsoleShell 包含:`XProvider(zhCN + zhCN_X)` + `AntApp` + `Layout`(Header/Si
## 页面 ## 页面
| 页面 | 路径 | 入口 | | 页面 | 路径 | 入口 |
| -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 总览 | `/` | `features/dashboard/index.tsx` | | 总览 | `/` | `features/dashboard/index.tsx` |
| 项目管理 | `/projects` | `features/projects/index.tsx` — FilterToolbar状态 Select + 搜索 + 新建/归档恢复删除) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除、列表排序、URL 同步筛选参数。 | | 项目管理 | `/projects` | `features/projects/index.tsx` — FilterToolbar状态 Select + 搜索 + 新建/归档恢复删除) + ProjectTable + ProjectFormModal。支持创建/编辑/归档/恢复/删除、列表排序、URL 同步筛选参数。 |
| 模型管理 | `/models``/models/providers` | 独立路由页面:`ModelListPage.tsx`FilterToolbar + ModelTable + `ProviderListPage.tsx`FilterToolbar + ProviderTable。模型支持供应商/能力筛选和列表排序,供应商支持类型筛选和列表排序。模型表单使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`)。 | | 模型管理 | `/models``/models/providers` | 独立路由页面:`ModelListPage.tsx`FilterToolbar + ModelTable + `ProviderListPage.tsx`FilterToolbar + ProviderTable。模型支持供应商/能力筛选和列表排序,供应商支持类型筛选和列表排序。模型表单使用 `GET /api/providers/options`。供应商表单支持预保存连通性测试(`POST /api/providers/test`)。 |
| 设置 | `/settings` | `features/settings/index.tsx` — 卡片式布局分区管理平台业务设置。"主题"卡片使用 antd Form 水平布局包含主题模式Radio.Group 按钮风:系统/明亮/黑暗和紧凑模式Switch 开关),使用 `useSettings` hook 通过 `GET/PUT /api/settings` 实时保存,`message` toast 反馈。 | | 设置 | `/settings` | `features/settings/index.tsx` — 卡片式布局分区管理平台业务设置。"主题"卡片使用 antd Form 水平布局包含主题模式Radio.Group 按钮风:系统/明亮/黑暗和紧凑模式Switch 开关),使用 `useSettings` hook 通过 `GET/PUT /api/settings` 实时保存,`message` toast 反馈。 |
| 聊天室 | `/workbench/:id` | `features/chat/index.tsx` | | 聊天室 | `/workbench/:id` | `features/chat/index.tsx` |
| 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层selectedId + modalOpen+ MaterialSidebar列表容器+ MaterialDetailPanel详情容器)+ AddMaterialModal。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 | | 收集箱 | `/workbench/:id/inbox` | `features/inbox/index.tsx` — 协调层selectedId + modalOpen+ MaterialSidebar列表容器+ MaterialDetailPanel透明内容区+OS滚动+操作区卡片)+ MaterialContent纵向卡片流基本信息Card+ AddMaterialModal。操作区始终渲染按钮 fill 样式 + disabled 控制。素材 CRUD 通过 TanStack Query hooks 接入后端 API。 |
| 404 | `*` | `features/not-found/index.tsx` | | 404 | `*` | `features/not-found/index.tsx` |
### 聊天页面 ### 聊天页面

View File

@@ -1,4 +1,4 @@
import { Card, Descriptions, Tag, Typography } from "antd"; import { Card, Descriptions, Flex, Tag, Typography } from "antd";
import type { Material, MaterialType } from "../types"; import type { Material, MaterialType } from "../types";
@@ -19,31 +19,32 @@ export function MaterialContent({ material }: MaterialContentProps) {
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType; const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
return ( return (
<> <Flex gap={12} vertical>
<Typography.Title level={4}></Typography.Title> <Card size="small" title="基本信息">
<Card> <Flex gap={12} vertical>
<Typography.Paragraph>{material.description}</Typography.Paragraph> <Typography.Paragraph>{material.description}</Typography.Paragraph>
{material.processedContent && ( {material.processedContent && (
<Typography.Paragraph <Typography.Paragraph
style={{ style={{
background: "var(--ant-color-fill-quaternary)", background: "var(--ant-color-fill-quaternary)",
padding: 12, padding: 12,
borderRadius: 6, borderRadius: 6,
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
}} }}
> >
{material.processedContent} {material.processedContent}
</Typography.Paragraph> </Typography.Paragraph>
)} )}
<Descriptions column={1} size="small"> <Descriptions column={1} size="small">
<Descriptions.Item label="状态"> <Descriptions.Item label="状态">
<Tag color={statusInfo.color}>{statusInfo.label}</Tag> <Tag color={statusInfo.color}>{statusInfo.label}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item> <Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item>
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item> <Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item> <Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
</Descriptions> </Descriptions>
</Flex>
</Card> </Card>
</> </Flex>
); );
} }

View File

@@ -1,10 +1,16 @@
import { CheckOutlined, CloseOutlined, RedoOutlined } from "@ant-design/icons"; import { CheckOutlined, CloseOutlined, RedoOutlined } from "@ant-design/icons";
import { App as AntApp, Button, Empty, Result, Space, Spin, Tag } from "antd"; import "overlayscrollbars/styles/overlayscrollbars.css";
import { App as AntApp, Button, Empty, Result, Space, Spin, Typography } from "antd";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useMaterial } from "../../../shared/hooks/use-materials"; import { useMaterial } from "../../../shared/hooks/use-materials";
import { STATUS_MAP } from "./constants";
import { MaterialContent } from "./MaterialContent"; import { MaterialContent } from "./MaterialContent";
const OS_OPTIONS = {
overflow: { x: "hidden", y: "scroll" },
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
} as const;
interface MaterialDetailPanelProps { interface MaterialDetailPanelProps {
materialId: null | string; materialId: null | string;
onApprove: (materialId: string) => Promise<void>; onApprove: (materialId: string) => Promise<void>;
@@ -23,8 +29,13 @@ export function MaterialDetailPanel({
if (!materialId) { if (!materialId) {
return ( return (
<div className="app-inbox-panel"> <div className="app-inbox-panel">
<div className="app-inbox-content"> <OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
<Empty description="请在左侧选择素材" /> <Empty description="请在左侧选择素材" />
</OverlayScrollbarsComponent>
<div className="app-inbox-action-bar">
<Typography.Text style={{ flex: 1, textAlign: "center", color: "var(--ant-color-text-tertiary)" }}>
</Typography.Text>
</div> </div>
</div> </div>
); );
@@ -48,9 +59,9 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
if (isLoading) { if (isLoading) {
return ( return (
<div className="app-inbox-panel"> <div className="app-inbox-panel">
<div className="app-inbox-content"> <OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
<Spin /> <Spin />
</div> </OverlayScrollbarsComponent>
</div> </div>
); );
} }
@@ -58,9 +69,9 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
if (error) { if (error) {
return ( return (
<div className="app-inbox-panel"> <div className="app-inbox-panel">
<div className="app-inbox-content"> <OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
<Result subTitle="加载素材详情失败" /> <Result subTitle="加载素材详情失败" />
</div> </OverlayScrollbarsComponent>
</div> </div>
); );
} }
@@ -68,9 +79,9 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
if (!data || !materialId) { if (!data || !materialId) {
return ( return (
<div className="app-inbox-panel"> <div className="app-inbox-panel">
<div className="app-inbox-content"> <OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
<Empty description="请在左侧选择素材" /> <Empty description="请在左侧选择素材" />
</div> </OverlayScrollbarsComponent>
</div> </div>
); );
} }
@@ -104,31 +115,39 @@ function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, p
} }
}; };
const statusInfo = STATUS_MAP[data.status] ?? { color: "default", label: data.status };
return ( return (
<div className="app-inbox-panel"> <div className="app-inbox-panel">
<div className="app-inbox-content"> <OverlayScrollbarsComponent className="app-inbox-content" options={OS_OPTIONS}>
<MaterialContent material={data} /> <MaterialContent material={data} />
</div> </OverlayScrollbarsComponent>
<div className="app-inbox-action-bar"> <div className="app-inbox-action-bar">
<Tag color={statusInfo.color}>{statusInfo.label}</Tag> <Space>
{data.status === "review" ? ( <Button
<Space> type="primary"
<Button icon={<CheckOutlined />} onClick={() => void handleApprove()} type="primary"> disabled={data.status !== "review"}
icon={<CheckOutlined />}
</Button> onClick={() => void handleApprove()}
<Button danger icon={<CloseOutlined />} onClick={() => void handleDiscard()}> >
</Button> </Button>
</Space> <Button
) : data.status === "failed" ? ( type="primary"
<Space> danger
<Button icon={<RedoOutlined />} onClick={() => void handleRetry()}> disabled={data.status !== "review"}
icon={<CloseOutlined />}
</Button> onClick={() => void handleDiscard()}
</Space> >
) : null}
</Button>
<Button
type="primary"
disabled={data.status !== "failed"}
icon={<RedoOutlined />}
onClick={() => void handleRetry()}
>
</Button>
</Space>
</div> </div>
</div> </div>
); );

View File

@@ -334,6 +334,7 @@ body {
.app-inbox-page { .app-inbox-page {
display: flex; display: flex;
gap: var(--ant-margin-sm);
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
@@ -342,6 +343,7 @@ body {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
gap: var(--ant-margin-sm);
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
} }
@@ -349,17 +351,17 @@ body {
.app-inbox-content { .app-inbox-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: var(--ant-padding-xl); overflow: hidden;
overflow-y: auto;
} }
.app-inbox-action-bar { .app-inbox-action-bar {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-end;
padding: var(--ant-padding-sm) var(--ant-padding-xl); padding: var(--ant-padding-sm);
border-top: 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); background: var(--ant-color-bg-container);
} }