feat(workbench): 新增收集箱页面 — 素材列表/详情分栏布局 + 新增/选中/删除 mock 交互

This commit is contained in:
2026-06-03 08:36:38 +08:00
parent 83349bf01b
commit 2cdbe474ce
15 changed files with 607 additions and 1 deletions

View File

@@ -0,0 +1,67 @@
import { App as AntApp, DatePicker, Form, Input, Modal } from "antd";
import dayjs from "dayjs";
import { useEffect } from "react";
import type { Material } from "../types";
interface AddMaterialModalProps {
onAdd: (material: Material) => void;
onOpenChange: (open: boolean) => void;
open: boolean;
}
interface FormValues {
associatedDate: dayjs.Dayjs;
description: string;
}
export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModalProps) {
const { message } = AntApp.useApp();
const [form] = Form.useForm<FormValues>();
useEffect(() => {
if (!open) return;
form.resetFields();
}, [form, open]);
const handleFinish = (values: FormValues) => {
const material: Material = {
associatedDate: values.associatedDate.format("YYYY-MM-DD"),
createdAt: new Date().toISOString(),
description: values.description,
id: crypto.randomUUID(),
};
onAdd(material);
message.success("素材已添加");
onOpenChange(false);
};
return (
<Modal
destroyOnHidden
okText="确定"
onCancel={() => onOpenChange(false)}
onOk={() => void form.submit()}
open={open}
title="新增素材"
>
<Form
form={form}
initialValues={{ associatedDate: dayjs() }}
layout="vertical"
onFinish={(values) => void handleFinish(values)}
>
<Form.Item
label="描述"
name="description"
rules={[{ message: "请输入描述", required: true, whitespace: true }]}
>
<Input.TextArea maxLength={500} placeholder="请输入素材描述" />
</Form.Item>
<Form.Item label="关联时间" name="associatedDate" rules={[{ message: "请选择关联时间", required: true }]}>
<DatePicker className="app-inbox-datepicker" />
</Form.Item>
</Form>
</Modal>
);
}

View File

@@ -0,0 +1,37 @@
import { DeleteOutlined } from "@ant-design/icons";
import { Button, Card, Flex, Typography } from "antd";
import type { Material } from "../types";
import { formatRelativeTime } from "../../../shared/utils/time";
interface MaterialCardProps {
material: Material;
onDelete: () => void;
onSelect: () => void;
selected: boolean;
}
export function MaterialCard({ material, onDelete, onSelect, selected }: MaterialCardProps) {
return (
<Card className={selected ? "app-inbox-card-selected" : undefined} hoverable onClick={onSelect} size="small">
<Typography.Paragraph ellipsis={{ rows: 3 }}>{material.description}</Typography.Paragraph>
<Flex align="center" justify="space-between">
<Typography.Text type="secondary">
{material.associatedDate} · {formatRelativeTime(material.createdAt)}
</Typography.Text>
<Button
aria-label="删除"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
size="small"
type="text"
/>
</Flex>
</Card>
);
}

View File

@@ -0,0 +1,32 @@
import { Card, Descriptions, Empty, Typography } from "antd";
import type { Material } from "../types";
import { formatRelativeTime } from "../../../shared/utils/time";
interface MaterialContentProps {
material: Material | null;
}
export function MaterialContent({ material }: MaterialContentProps) {
if (!material) {
return (
<div className="app-inbox-content">
<Empty description="请在左侧选择素材" />
</div>
);
}
return (
<div className="app-inbox-content">
<Typography.Title level={4}></Typography.Title>
<Card>
<Typography.Paragraph>{material.description}</Typography.Paragraph>
<Descriptions column={1} size="small">
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
</Descriptions>
</Card>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { PlusOutlined } from "@ant-design/icons";
import { Button, Empty } from "antd";
import type { Material } from "../types";
import { MaterialCard } from "./MaterialCard";
interface MaterialListProps {
materials: readonly Material[];
onAddClick: () => void;
onDelete: (id: string) => void;
onSelect: (id: string) => void;
selectedId: null | string;
}
export function MaterialList({ materials, onAddClick, onDelete, onSelect, selectedId }: MaterialListProps) {
return (
<div className="app-inbox-sidebar">
<Button block icon={<PlusOutlined />} onClick={onAddClick} type="primary">
</Button>
<div className="app-inbox-list">
{materials.length === 0 ? (
<Empty description="暂无素材" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
materials.map((material) => (
<MaterialCard
key={material.id}
material={material}
onDelete={() => onDelete(material.id)}
onSelect={() => onSelect(material.id)}
selected={material.id === selectedId}
/>
))
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { useState } from "react";
import type { Material } from "./types";
import { AddMaterialModal } from "./components/AddMaterialModal";
import { MaterialContent } from "./components/MaterialContent";
import { MaterialList } from "./components/MaterialList";
export function InboxPage() {
const [materials, setMaterials] = useState<Material[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [selectedId, setSelectedId] = useState<null | string>(null);
const selectedMaterial = materials.find((m) => m.id === selectedId) ?? null;
const handleAdd = (material: Material) => {
setMaterials((prev) => [...prev, material].sort((a, b) => b.associatedDate.localeCompare(a.associatedDate)));
setSelectedId(material.id);
};
const handleDelete = (id: string) => {
setMaterials((prev) => prev.filter((m) => m.id !== id));
if (selectedId === id) setSelectedId(null);
};
return (
<div className="app-inbox-page">
<MaterialList
materials={materials}
onAddClick={() => setModalOpen(true)}
onDelete={handleDelete}
onSelect={setSelectedId}
selectedId={selectedId}
/>
<MaterialContent material={selectedMaterial} />
<AddMaterialModal onAdd={handleAdd} onOpenChange={setModalOpen} open={modalOpen} />
</div>
);
}

View File

@@ -0,0 +1,6 @@
export interface Material {
associatedDate: string;
createdAt: string;
description: string;
id: string;
}