feat: 素材处理管线——自动处理、审核流程、6状态机
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { App as AntApp, DatePicker, Form, Input, Modal } from "antd";
|
||||
import { App as AntApp, DatePicker, Form, Input, Modal, Select } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { CreateMaterialRequest, Material } from "../types";
|
||||
import type { CreateMaterialRequest, Material, MaterialType } from "../types";
|
||||
|
||||
interface AddMaterialModalProps {
|
||||
onAdd: (body: CreateMaterialRequest) => Promise<Material>;
|
||||
@@ -13,8 +13,14 @@ interface AddMaterialModalProps {
|
||||
interface FormValues {
|
||||
associatedDate: dayjs.Dayjs;
|
||||
description: string;
|
||||
materialType: MaterialType;
|
||||
}
|
||||
|
||||
const MATERIAL_TYPE_OPTIONS = [
|
||||
{ label: "通用", value: "general" },
|
||||
{ label: "会议", value: "meeting" },
|
||||
];
|
||||
|
||||
export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModalProps) {
|
||||
const { message } = AntApp.useApp();
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
@@ -29,6 +35,7 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal
|
||||
const body: CreateMaterialRequest = {
|
||||
associatedDate: values.associatedDate.format("YYYY-MM-DD"),
|
||||
description: values.description,
|
||||
materialType: values.materialType,
|
||||
};
|
||||
|
||||
setSubmitting(true);
|
||||
@@ -55,7 +62,7 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{ associatedDate: dayjs() }}
|
||||
initialValues={{ associatedDate: dayjs(), materialType: "general" as MaterialType }}
|
||||
layout="vertical"
|
||||
onFinish={(values) => void handleFinish(values)}
|
||||
>
|
||||
@@ -69,6 +76,9 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal
|
||||
<Form.Item label="关联时间" name="associatedDate" rules={[{ message: "请选择关联时间", required: true }]}>
|
||||
<DatePicker className="app-inbox-datepicker" />
|
||||
</Form.Item>
|
||||
<Form.Item label="素材类型" name="materialType" rules={[{ message: "请选择素材类型", required: true }]}>
|
||||
<Select options={MATERIAL_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -13,12 +13,16 @@ interface MaterialCardProps {
|
||||
const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
|
||||
approved: { color: "green", label: "已通过" },
|
||||
discarded: { color: "red", label: "已放弃" },
|
||||
pending: { color: "gold", label: "待审核" },
|
||||
failed: { color: "magenta", label: "失败" },
|
||||
pending: { color: "gold", label: "待处理" },
|
||||
processing: { color: "blue", label: "处理中" },
|
||||
review: { color: "orange", label: "待审核" },
|
||||
};
|
||||
|
||||
export function MaterialCard({ material, onDelete, onSelect, selected }: MaterialCardProps) {
|
||||
const statusInfo = STATUS_MAP[material.status];
|
||||
const className = selected ? "app-sidebar-list-item app-sidebar-list-item--selected" : "app-sidebar-list-item";
|
||||
const canDelete = material.status !== "processing";
|
||||
|
||||
return (
|
||||
<Flex align="center" className={className} gap="small" justify="space-between" onClick={onSelect}>
|
||||
@@ -34,26 +38,28 @@ export function MaterialCard({ material, onDelete, onSelect, selected }: Materia
|
||||
{statusInfo && <Tag color={statusInfo.color}>{statusInfo.label}</Tag>}
|
||||
</span>
|
||||
<span className="material-item-actions">
|
||||
<Popconfirm
|
||||
description="删除后不可恢复"
|
||||
okButtonProps={{ danger: true }}
|
||||
okText="删除"
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
title="确认删除该素材?"
|
||||
>
|
||||
<Button
|
||||
aria-label="删除"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</Popconfirm>
|
||||
{canDelete && (
|
||||
<Popconfirm
|
||||
description="删除后不可恢复"
|
||||
okButtonProps={{ danger: true }}
|
||||
okText="删除"
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
title="确认删除该素材?"
|
||||
>
|
||||
<Button
|
||||
aria-label="删除"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, Descriptions, Tag, Typography } from "antd";
|
||||
|
||||
import type { Material } from "../types";
|
||||
import type { Material, MaterialStatus, MaterialType } from "../types";
|
||||
|
||||
import { formatRelativeTime } from "../../../shared/utils/time";
|
||||
|
||||
@@ -8,24 +8,46 @@ interface MaterialContentProps {
|
||||
material: Material;
|
||||
}
|
||||
|
||||
const STATUS_MAP: Record<string, { color: string; label: string }> = {
|
||||
const STATUS_MAP: Record<MaterialStatus, { color: string; label: string }> = {
|
||||
approved: { color: "green", label: "已通过" },
|
||||
discarded: { color: "red", label: "已放弃" },
|
||||
pending: { color: "gold", label: "待审核" },
|
||||
failed: { color: "magenta", label: "失败" },
|
||||
pending: { color: "gold", label: "待处理" },
|
||||
processing: { color: "blue", label: "处理中" },
|
||||
review: { color: "orange", label: "待审核" },
|
||||
};
|
||||
|
||||
const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
|
||||
general: "通用",
|
||||
meeting: "会议",
|
||||
};
|
||||
|
||||
export function MaterialContent({ material }: MaterialContentProps) {
|
||||
const statusInfo = STATUS_MAP[material.status] ?? { color: "default", label: material.status };
|
||||
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
|
||||
|
||||
return (
|
||||
<div className="app-inbox-content">
|
||||
<Typography.Title level={4}>素材详情</Typography.Title>
|
||||
<Card>
|
||||
<Typography.Paragraph>{material.description}</Typography.Paragraph>
|
||||
{material.processedContent && (
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
background: "var(--ant-color-fill-quaternary)",
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{material.processedContent}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { Empty, Result, Spin } from "antd";
|
||||
import { CheckOutlined, CloseOutlined, RedoOutlined } from "@ant-design/icons";
|
||||
import { App as AntApp, Button, Empty, Result, Space, Spin } from "antd";
|
||||
|
||||
import type { Material } from "../types";
|
||||
|
||||
import { useMaterial } from "../../../shared/hooks/use-materials";
|
||||
import { MaterialContent } from "./MaterialContent";
|
||||
|
||||
interface MaterialDetailPanelProps {
|
||||
materialId: null | string;
|
||||
onApprove: (materialId: string) => Promise<void>;
|
||||
onDiscard: (materialId: string) => Promise<void>;
|
||||
onRetry: (materialId: string) => Promise<void>;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function MaterialDetailPanel({ materialId, projectId }: MaterialDetailPanelProps) {
|
||||
export function MaterialDetailPanel({
|
||||
materialId,
|
||||
onApprove,
|
||||
onDiscard,
|
||||
onRetry,
|
||||
projectId,
|
||||
}: MaterialDetailPanelProps) {
|
||||
if (!materialId) {
|
||||
return (
|
||||
<div className="app-inbox-content">
|
||||
@@ -17,11 +29,20 @@ export function MaterialDetailPanel({ materialId, projectId }: MaterialDetailPan
|
||||
);
|
||||
}
|
||||
|
||||
return <MaterialDetailPanelInner materialId={materialId} projectId={projectId} />;
|
||||
return (
|
||||
<MaterialDetailPanelInner
|
||||
materialId={materialId}
|
||||
onApprove={onApprove}
|
||||
onDiscard={onDiscard}
|
||||
onRetry={onRetry}
|
||||
projectId={projectId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MaterialDetailPanelInner({ materialId, projectId }: MaterialDetailPanelProps) {
|
||||
function MaterialDetailPanelInner({ materialId, onApprove, onDiscard, onRetry, projectId }: MaterialDetailPanelProps) {
|
||||
const { data, error, isLoading } = useMaterial({ materialId, projectId });
|
||||
const { message } = AntApp.useApp();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -39,7 +60,7 @@ function MaterialDetailPanelInner({ materialId, projectId }: MaterialDetailPanel
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!data || !materialId) {
|
||||
return (
|
||||
<div className="app-inbox-content">
|
||||
<Empty description="请在左侧选择素材" />
|
||||
@@ -47,5 +68,73 @@ function MaterialDetailPanelInner({ materialId, projectId }: MaterialDetailPanel
|
||||
);
|
||||
}
|
||||
|
||||
return <MaterialContent material={data} />;
|
||||
const id = materialId;
|
||||
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
await onApprove(id);
|
||||
message.success("已通过");
|
||||
} catch (e: unknown) {
|
||||
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = async () => {
|
||||
try {
|
||||
await onDiscard(id);
|
||||
message.success("已放弃");
|
||||
} catch (e: unknown) {
|
||||
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = async () => {
|
||||
try {
|
||||
await onRetry(id);
|
||||
message.success("已重新提交处理");
|
||||
} catch (e: unknown) {
|
||||
message.error(`操作失败:${e instanceof Error ? e.message : "未知错误"}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-inbox-content">
|
||||
<MaterialContent material={data} />
|
||||
<ActionButtons material={data} onApprove={handleApprove} onDiscard={handleDiscard} onRetry={handleRetry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionButtonsProps {
|
||||
material: Material;
|
||||
onApprove: () => Promise<void>;
|
||||
onDiscard: () => Promise<void>;
|
||||
onRetry: () => Promise<void>;
|
||||
}
|
||||
|
||||
function ActionButtons({ material, onApprove, onDiscard, onRetry }: ActionButtonsProps) {
|
||||
if (material.status === "review") {
|
||||
return (
|
||||
<Space style={{ marginTop: 16 }}>
|
||||
<Button icon={<CheckOutlined />} onClick={() => void onApprove()} type="primary">
|
||||
通过
|
||||
</Button>
|
||||
<Button danger icon={<CloseOutlined />} onClick={() => void onDiscard()}>
|
||||
放弃
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
if (material.status === "failed") {
|
||||
return (
|
||||
<Space style={{ marginTop: 16 }}>
|
||||
<Button icon={<RedoOutlined />} onClick={() => void onRetry()}>
|
||||
重试
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
PlusOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Empty, Segmented, Skeleton } from "antd";
|
||||
import "overlayscrollbars/styles/overlayscrollbars.css";
|
||||
@@ -27,9 +27,9 @@ interface MaterialListProps {
|
||||
|
||||
const STATUS_FILTER_OPTIONS = [
|
||||
{ icon: <AppstoreOutlined />, label: "全部", value: "all" },
|
||||
{ color: "#faad14", icon: <ClockCircleOutlined />, label: "待审核", value: "pending" },
|
||||
{ color: "#52c41a", icon: <CheckCircleOutlined />, label: "已通过", value: "approved" },
|
||||
{ color: "#ff4d4f", icon: <CloseCircleOutlined />, label: "已放弃", value: "discarded" },
|
||||
{ color: "#1677ff", icon: <SyncOutlined />, label: "运行中", value: "processing" },
|
||||
{ color: "#fa8c16", icon: <ClockCircleOutlined />, label: "待审核", value: "review" },
|
||||
{ color: "#ff4d4f", icon: <ExclamationCircleOutlined />, label: "失败", value: "failed" },
|
||||
] as const;
|
||||
|
||||
type FilterValue = (typeof STATUS_FILTER_OPTIONS)[number]["value"];
|
||||
|
||||
@@ -3,7 +3,12 @@ import { useState } from "react";
|
||||
import type { CreateMaterialRequest, Material } from "./types";
|
||||
|
||||
import { useCurrentProject } from "../../shared/hooks/use-current-project";
|
||||
import { useCreateMaterial } from "../../shared/hooks/use-materials";
|
||||
import {
|
||||
useApproveMaterial,
|
||||
useCreateMaterial,
|
||||
useDiscardMaterial,
|
||||
useRetryMaterial,
|
||||
} from "../../shared/hooks/use-materials";
|
||||
import { AddMaterialModal } from "./components/AddMaterialModal";
|
||||
import { MaterialDetailPanel } from "./components/MaterialDetailPanel";
|
||||
import { MaterialSidebar } from "./components/MaterialSidebar";
|
||||
@@ -14,6 +19,9 @@ export function InboxPage() {
|
||||
const [selectedId, setSelectedId] = useState<null | string>(null);
|
||||
|
||||
const createMutation = useCreateMaterial(project.id);
|
||||
const approveMutation = useApproveMaterial(project.id);
|
||||
const discardMutation = useDiscardMaterial(project.id);
|
||||
const retryMutation = useRetryMaterial(project.id);
|
||||
|
||||
const handleCreate = async (body: CreateMaterialRequest): Promise<Material> => {
|
||||
const material = await createMutation.mutateAsync({ body, projectId: project.id });
|
||||
@@ -27,6 +35,18 @@ export function InboxPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (materialId: string) => {
|
||||
await approveMutation.mutateAsync({ materialId, projectId: project.id });
|
||||
};
|
||||
|
||||
const handleDiscard = async (materialId: string) => {
|
||||
await discardMutation.mutateAsync({ materialId, projectId: project.id });
|
||||
};
|
||||
|
||||
const handleRetry = async (materialId: string) => {
|
||||
await retryMutation.mutateAsync({ materialId, projectId: project.id });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-inbox-page">
|
||||
<MaterialSidebar
|
||||
@@ -36,7 +56,13 @@ export function InboxPage() {
|
||||
projectId={project.id}
|
||||
selectedId={selectedId}
|
||||
/>
|
||||
<MaterialDetailPanel materialId={selectedId} projectId={project.id} />
|
||||
<MaterialDetailPanel
|
||||
materialId={selectedId}
|
||||
onApprove={handleApprove}
|
||||
onDiscard={handleDiscard}
|
||||
onRetry={handleRetry}
|
||||
projectId={project.id}
|
||||
/>
|
||||
<AddMaterialModal onAdd={handleCreate} onOpenChange={setModalOpen} open={modalOpen} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type { CreateMaterialRequest, Material, MaterialStatus } from "../../../shared/api";
|
||||
export type { CreateMaterialRequest, Material, MaterialStatus, MaterialType } from "../../../shared/api";
|
||||
|
||||
Reference in New Issue
Block a user