Files
DataMate/frontend/src/pages/KnowledgeManagement/components/KnowledgeItemEditor.tsx
Jerry Yan ce98be5778 feat(knowledge): 添加知识条目文件预览和替换功能
- 后端实现知识条目文件预览接口,支持多种文件类型在线预览
- 后端实现知识条目文件替换功能,保留原有文件管理逻辑
- 前端新增文件预览模态框组件,支持文本、图片、音视频预览
- 前端知识条目编辑器添加文件替换上传功能
- 前端优化文件内容截断预览逻辑,统一使用工具函数处理
- 前端修复 PUT 请求中 FormData 处理问题,确保文件上传正常工作
- 新增文件预览相关工具函数和常量配置
2026-01-29 11:38:43 +08:00

404 lines
13 KiB
TypeScript

import { useEffect, useState } from "react";
import { Button, DatePicker, Form, Input, message, Modal, Select, Upload, UploadFile } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import {
downloadKnowledgeItemFileUsingGet,
replaceKnowledgeItemFileUsingPut,
updateKnowledgeItemByIdUsingPut,
uploadKnowledgeItemsUsingPost,
} from "../knowledge-management.api";
import {
knowledgeStatusOptions,
} from "../knowledge-management.const";
import {
KnowledgeContentType,
KnowledgeItem,
KnowledgeSourceType,
KnowledgeStatusType,
} from "../knowledge-management.model";
import { queryDatasetTagsUsingGet } from "@/pages/DataManagement/dataset.api";
const stripFileExtension = (fileName: string) => {
const dotIndex = fileName.lastIndexOf(".");
if (dotIndex <= 0) {
return fileName;
}
return fileName.slice(0, dotIndex);
};
export default function KnowledgeItemEditor({
open,
setId,
data,
onCancel,
onSuccess,
readOnly,
}: {
open: boolean;
setId: string;
data?: Partial<KnowledgeItem> | null;
readOnly?: boolean;
onCancel: () => void;
onSuccess: () => void;
}) {
const [form] = Form.useForm();
const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [replaceFileList, setReplaceFileList] = useState<UploadFile[]>([]);
const [titleBeforeReplace, setTitleBeforeReplace] = useState<string | null>(null);
const isMultiFile = fileList.length > 1;
const isFileItem =
data?.contentType === KnowledgeContentType.FILE ||
data?.sourceType === KnowledgeSourceType.FILE_UPLOAD;
const contentTypeLabel = !data?.id
? "文件"
: data?.contentType === KnowledgeContentType.MARKDOWN
? "Markdown"
: data?.contentType === KnowledgeContentType.TEXT
? "文本"
: "文件";
const fetchTags = async () => {
try {
const { data: tagData } = await queryDatasetTagsUsingGet();
const options = Array.isArray(tagData)
? tagData.map((tag) => ({ label: tag.name, value: tag.name }))
: [];
setTagOptions(options);
} catch (error) {
console.error("获取标签失败", error);
}
};
useEffect(() => {
if (open) {
fetchTags();
}
}, [open]);
useEffect(() => {
if (open) {
if (data?.id) {
form.setFieldsValue({
title: data.title,
status: data.status ?? KnowledgeStatusType.DRAFT,
domain: data.domain,
businessLine: data.businessLine,
owner: data.owner,
sensitivity: data.sensitivity,
validFrom: data.validFrom ? dayjs(data.validFrom) : null,
validTo: data.validTo ? dayjs(data.validTo) : null,
tags: data.tags?.map((tag) => tag.name) || [],
metadata: data.metadata,
});
setTitleBeforeReplace(null);
} else {
form.resetFields();
form.setFieldsValue({
status: KnowledgeStatusType.DRAFT,
tags: [],
});
setTitleBeforeReplace(null);
}
setFileList([]);
setReplaceFileList([]);
} else {
setFileList([]);
setReplaceFileList([]);
setTitleBeforeReplace(null);
}
}, [open, data, form]);
const handleFileBeforeUpload = (file: File) => {
setFileList((prev) => {
const nextList = [
...prev,
{
uid: `${Date.now()}-${file.name}`,
name: file.name,
status: "done",
originFileObj: file,
},
];
if (nextList.length === 1) {
const currentTitle = form.getFieldValue("title");
if (!currentTitle) {
form.setFieldsValue({
title: stripFileExtension(file.name),
});
}
}
return nextList;
});
message.success("文件已就绪,可提交创建条目");
return false;
};
const handleFileRemove = (removedFile: UploadFile) => {
setFileList((prev) => {
const nextList = prev.filter((file) => file.uid !== removedFile.uid);
if (!data?.id && nextList.length === 0) {
form.setFieldsValue({ title: undefined });
}
return nextList;
});
return true;
};
const handleReplaceFileBeforeUpload = (file: File) => {
if (!titleBeforeReplace) {
setTitleBeforeReplace(form.getFieldValue("title") || null);
}
setReplaceFileList([
{
uid: `${Date.now()}-${file.name}`,
name: file.name,
status: "done",
originFileObj: file,
},
]);
form.setFieldsValue({ title: stripFileExtension(file.name) });
message.success("已选择替换文件,提交后生效");
return false;
};
const handleReplaceFileRemove = (removedFile: UploadFile) => {
setReplaceFileList((prev) => prev.filter((file) => file.uid !== removedFile.uid));
if (titleBeforeReplace !== null) {
form.setFieldsValue({ title: titleBeforeReplace || undefined });
setTitleBeforeReplace(null);
}
return true;
};
const handleDownloadFile = async () => {
if (!data?.id) {
return;
}
try {
await downloadKnowledgeItemFileUsingGet(setId, data.id, data.sourceFileId);
} catch (error) {
console.error("下载文件失败", error);
message.error("下载失败,请稍后重试");
}
};
const handleSubmit = async () => {
try {
if (!data?.id && fileList.length === 0) {
message.warning("请先选择文件");
return;
}
const values = await form.validateFields();
const validFrom = values.validFrom ? values.validFrom.format("YYYY-MM-DD") : undefined;
const validTo = values.validTo ? values.validTo.format("YYYY-MM-DD") : undefined;
if (validFrom && validTo && dayjs(validFrom).isAfter(dayjs(validTo))) {
message.warning("有效期开始不能晚于结束时间");
return;
}
if (data?.id) {
const payload: Record<string, unknown> = {
...values,
validFrom,
validTo,
tags: values.tags || [],
};
if (replaceFileList.length > 0) {
delete payload.title;
}
await updateKnowledgeItemByIdUsingPut(setId, data.id, payload);
if (replaceFileList.length > 0) {
const formData = new FormData();
const replaceFile = replaceFileList[0]?.originFileObj as File | undefined;
if (!replaceFile) {
message.warning("请先选择要替换的文件");
return;
}
formData.append("file", replaceFile);
await replaceKnowledgeItemFileUsingPut(setId, data.id, formData);
}
message.success("知识条目更新成功");
} else {
if (fileList.length === 0) {
message.warning("请先选择文件");
return;
}
const formData = new FormData();
fileList.forEach((file) => {
const origin = file.originFileObj as File | undefined;
if (origin) {
formData.append("files", origin);
}
});
if (fileList.length === 1 && values.title) {
formData.append("title", values.title);
}
if (values.status) {
formData.append("status", values.status);
}
if (values.tags && Array.isArray(values.tags)) {
values.tags.forEach((tag) => formData.append("tags", tag));
}
if (values.domain) {
formData.append("domain", values.domain);
}
if (values.businessLine) {
formData.append("businessLine", values.businessLine);
}
if (values.owner) {
formData.append("owner", values.owner);
}
if (values.sensitivity) {
formData.append("sensitivity", values.sensitivity);
}
if (validFrom) {
formData.append("validFrom", validFrom);
}
if (validTo) {
formData.append("validTo", validTo);
}
if (values.metadata) {
formData.append("metadata", values.metadata);
}
await uploadKnowledgeItemsUsingPost(setId, formData);
message.success(`已创建 ${fileList.length} 个知识条目`);
}
form.resetFields();
setFileList([]);
setReplaceFileList([]);
setTitleBeforeReplace(null);
onSuccess();
} catch {
message.error("操作失败,请重试");
}
};
const title = data?.id ? "编辑知识条目" : "新建知识条目";
return (
<Modal
title={title}
open={open}
onCancel={onCancel}
onOk={handleSubmit}
okText="确定"
cancelText="取消"
width={860}
maskClosable={false}
okButtonProps={{ disabled: readOnly }}
>
<Form form={form} layout="vertical" disabled={readOnly}>
<div className="grid grid-cols-2 gap-4">
<Form.Item
label="标题"
name="title"
rules={[{ required: !isMultiFile, message: "请输入标题" }]}
>
<Input
placeholder={isMultiFile ? "多文件将按文件名自动生成" : "请输入标题"}
disabled={readOnly || (!data?.id && isMultiFile)}
/>
</Form.Item>
<Form.Item label="状态" name="status">
<Select options={knowledgeStatusOptions} />
</Form.Item>
</div>
{!data?.id && (
<Form.Item label="上传文件" required>
<Upload
beforeUpload={handleFileBeforeUpload}
fileList={fileList}
multiple
onRemove={handleFileRemove}
showUploadList={{ showPreviewIcon: false }}
disabled={readOnly}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<div className="text-xs text-gray-500 mt-1">
</div>
</Form.Item>
)}
{data?.id && isFileItem && (
<Form.Item label="文件">
<div className="flex items-center gap-2">
<span className="truncate" title={data.sourceFileId || data.title}>
{data.sourceFileId || data.title || "-"}
</span>
<Button size="small" onClick={handleDownloadFile} disabled={readOnly}>
</Button>
</div>
</Form.Item>
)}
{data?.id && isFileItem && !readOnly && (
<Form.Item label="替换文件">
<Upload
beforeUpload={handleReplaceFileBeforeUpload}
fileList={replaceFileList}
onRemove={handleReplaceFileRemove}
showUploadList={{ showPreviewIcon: false }}
maxCount={1}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
<div className="text-xs text-gray-500 mt-1"></div>
</Form.Item>
)}
<div className="grid grid-cols-2 gap-4">
<Form.Item label="内容类型">
<Input value={contentTypeLabel} disabled />
</Form.Item>
<Form.Item label="敏感级别" name="sensitivity">
<Input placeholder="请输入敏感级别" />
</Form.Item>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="领域" name="domain">
<Input placeholder="请输入领域" />
</Form.Item>
<Form.Item label="业务线" name="businessLine">
<Input placeholder="请输入业务线" />
</Form.Item>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="负责人" name="owner">
<Input placeholder="请输入负责人" />
</Form.Item>
<Form.Item label="标签" name="tags">
<Select
mode="tags"
options={tagOptions}
placeholder="请选择或输入标签"
/>
</Form.Item>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="有效期开始" name="validFrom">
<DatePicker className="w-full" />
</Form.Item>
<Form.Item label="有效期结束" name="validTo">
<DatePicker className="w-full" />
</Form.Item>
</div>
<Form.Item label="扩展元数据" name="metadata">
<Input.TextArea placeholder="请输入元数据(JSON)" rows={3} />
</Form.Item>
{data?.sourceType && (
<div className="grid grid-cols-2 gap-4 text-sm text-gray-500">
<div>{data.sourceType}</div>
<div>{data.sourceFileId || "-"}</div>
</div>
)}
</Form>
</Modal>
);
}