style: 收集箱布局深化——卡片流、OS滚动、操作区fill按钮
This commit is contained in:
@@ -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` |
|
||||||
|
|
||||||
### 聊天页面
|
### 聊天页面
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user