完成文档类型增删改查
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
import { useState } from "react";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
getAllEvaluationPointGroups,
|
||||
type DocumentTypeUI,
|
||||
type DocumentTypeSearchParams,
|
||||
type DocumentTypeGroup
|
||||
} from "~/api/document-types/document-types";
|
||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||
|
||||
// 引入CSS样式
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: documentTypesStyles }
|
||||
];
|
||||
}
|
||||
|
||||
// 页面元数据
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "管理文档类型,包括查看、编辑和删除文档类型" },
|
||||
];
|
||||
};
|
||||
|
||||
// 定义加载器返回的数据类型
|
||||
interface LoaderData {
|
||||
types: DocumentTypeUI[];
|
||||
total: number;
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
error?: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
}
|
||||
|
||||
// 加载函数 - 获取文档类型列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const group_id = url.searchParams.get('group_id') || undefined;
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||||
|
||||
// 构建搜索参数
|
||||
const searchParams: DocumentTypeSearchParams = {
|
||||
name,
|
||||
group_id,
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
|
||||
// 并行获取文档类型数据和所有评查点分组
|
||||
const [typesResult, groupsResult] = await Promise.all([
|
||||
getDocumentTypes(searchParams),
|
||||
getAllEvaluationPointGroups()
|
||||
]);
|
||||
|
||||
// console.log('文档类型数据:', typesResult.data?.types);
|
||||
// console.log('评查点分组数据:', groupsResult.data);
|
||||
|
||||
if (typesResult.error) {
|
||||
return json<LoaderData>(
|
||||
{
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
error: typesResult.error,
|
||||
groups: groupsResult.data || []
|
||||
},
|
||||
{ status: typesResult.status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return json<LoaderData>({
|
||||
types: typesResult.data?.types || [],
|
||||
total: typesResult.data?.total || 0,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
groups: groupsResult.data || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载文档类型列表失败:", error);
|
||||
return json<LoaderData>(
|
||||
{
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
error: "加载文档类型列表失败",
|
||||
groups: []
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 动作函数 - 处理删除请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// 获取表单数据
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const intent = formData.get("intent") as string;
|
||||
|
||||
if (intent === "delete" && id) {
|
||||
try {
|
||||
const result = await deleteDocumentType(id);
|
||||
|
||||
if (result.error) {
|
||||
return Response.json({ success: false, error: result.error }, { status: result.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true });
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "删除文档类型失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ success: false, error: "无效的操作" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 文档类型列表组件
|
||||
export default function DocumentTypesList() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, pageSize: initialPageSize, currentPage: initialPage, error, groups } = useLoaderData<LoaderData>();
|
||||
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
const group_id = searchParams.get('group_id') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || String(initialPage), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(initialPageSize), 10);
|
||||
|
||||
// 处理名称搜索
|
||||
const handleNameSearch = (value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value) {
|
||||
newParams.set('name', value);
|
||||
} else {
|
||||
newParams.delete('name');
|
||||
}
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理分组筛选
|
||||
const handleGroupChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value) {
|
||||
newParams.set('group_id', value);
|
||||
} else {
|
||||
newParams.delete('group_id');
|
||||
}
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理重置筛选
|
||||
const handleReset = () => {
|
||||
const nameInput = document.querySelector('input[name="name"]');
|
||||
if (nameInput) {
|
||||
(nameInput as HTMLInputElement).value = '';
|
||||
}
|
||||
const groupIdInput = document.querySelector('select[name="group_id"]');
|
||||
if (groupIdInput) {
|
||||
(groupIdInput as HTMLSelectElement).value = '';
|
||||
}
|
||||
setSearchParams(new URLSearchParams());
|
||||
};
|
||||
|
||||
// 处理删除文档类型
|
||||
const handleDelete = async (id: number) => {
|
||||
if (confirm('确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。')) {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('id', id.toString());
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
const response = await fetch('/document-types?index', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(`删除失败: ${result.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理编辑文档类型
|
||||
const handleEdit = (id: number) => {
|
||||
navigate(`/document-types/new?id=${id}`);
|
||||
};
|
||||
|
||||
// 处理分页变更
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理每页条数变更
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('pageSize', size.toString());
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: "文档类型名称",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-text-line text-primary mr-2"></i>
|
||||
<span>{record.name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
key: "description",
|
||||
width: "300px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
|
||||
{record.description}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "关联的评查点分组",
|
||||
key: "groups",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="groups-container">
|
||||
{record.groups && record.groups.length > 0 ? (
|
||||
record.groups.map(group => (
|
||||
<span key={group.id} className="type-badge">
|
||||
{group.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-400">暂无关联分组</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "created_at",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => record.created_at
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
key: "updated_at",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => record.updated_at
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEdit(record.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
</button>
|
||||
<button
|
||||
className="operation-btn text-error !hidden"
|
||||
onClick={() => handleDelete(record.id)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="document-types-page">
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">文档类型管理</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => navigate("/document-types/new")}
|
||||
>
|
||||
新增文档类型
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<FilterPanel
|
||||
className="mb-4"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-refresh-line"
|
||||
onClick={handleReset}
|
||||
className="mr-2"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
noActionDivider={true}
|
||||
>
|
||||
<SearchFilter
|
||||
label="类型名称"
|
||||
placeholder="请输入文档类型名称"
|
||||
value={name}
|
||||
onSearch={handleNameSearch}
|
||||
className="flex-1 min-w-[200px]"
|
||||
instantSearch={true}
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="关联分组"
|
||||
name="group_id"
|
||||
value={group_id}
|
||||
options={[
|
||||
...(groups || []).map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleGroupChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Card bodyClassName="px-0 py-0">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={types}
|
||||
rowKey="id"
|
||||
emptyText="暂无文档类型数据"
|
||||
loading={false}
|
||||
/>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="px-4 py-3">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
|
||||
import { getAllRuleGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType } from "~/api/document-types/document-types";
|
||||
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: documentTypesNewStyles }];
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: (data:LoaderData) => {
|
||||
if (data.isEdit) {
|
||||
return "编辑文档类型";
|
||||
} else {
|
||||
return "新增文档类型";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = ({ location }) => {
|
||||
const isEdit = new URLSearchParams(location.search).has("id");
|
||||
return [
|
||||
{ title: `${isEdit ? "编辑" : "新增"}文档类型 - 中国烟草AI合同及卷宗审核系统` },
|
||||
{ name: "description", content: `${isEdit ? "编辑" : "新增"}文档类型,设置文档类型名称、描述和关联的评查点分组` }
|
||||
];
|
||||
};
|
||||
|
||||
// 定义模板类型
|
||||
const TEMPLATE_TYPES = {
|
||||
LLM_EXTRACTION: "LLM_Extraction",
|
||||
VLM_EXTRACTION: "VLM_Extraction",
|
||||
EVALUATION: "Evaluation",
|
||||
SUMMARY: "Summary"
|
||||
};
|
||||
|
||||
|
||||
// 加载函数 - 获取数据
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const isEdit = id ? true : false;
|
||||
|
||||
// 1. 获取评查点分组 - 使用getAllRuleGroups获取所有分组
|
||||
const ruleGroupsResponse = await getAllRuleGroups();
|
||||
if (ruleGroupsResponse.error) {
|
||||
console.error("获取评查点分组失败:", ruleGroupsResponse.error);
|
||||
}
|
||||
|
||||
// ruleGroupsResponse.data已经是树形结构数据,getAllRuleGroups内部已处理好parent-children关系
|
||||
const groupsTree = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
|
||||
|
||||
|
||||
// 2. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
await Promise.all([
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY })
|
||||
]);
|
||||
|
||||
// 3. 如果是编辑模式,获取文档类型详情
|
||||
let documentType = undefined;
|
||||
if (id) {
|
||||
const typeResponse = await getDocumentType(id);
|
||||
if (typeResponse.data) {
|
||||
documentType = typeResponse.data;
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
isEdit,
|
||||
documentType,
|
||||
ruleGroups: groupsTree,
|
||||
llmExtractionTemplates: llmExtractionTemplatesResponse.data?.templates || [],
|
||||
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data?.templates || [],
|
||||
evaluationTemplates: evaluationTemplatesResponse.data?.templates || [],
|
||||
summaryTemplates: summaryTemplatesResponse.data?.templates || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
return Response.json({
|
||||
isEdit: false,
|
||||
documentType: undefined,
|
||||
ruleGroups: [],
|
||||
llmExtractionTemplates: [],
|
||||
vlmExtractionTemplates: [],
|
||||
evaluationTemplates: [],
|
||||
summaryTemplates: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 定义动作返回的数据类型
|
||||
interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
name?: string;
|
||||
groups?: string;
|
||||
general?: string;
|
||||
llmExtractionTemplate?: string;
|
||||
vlmExtractionTemplate?: string;
|
||||
evaluationTemplate?: string;
|
||||
summaryTemplate?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 动作函数 - 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
|
||||
const vlmExtractionTemplateId = formData.get("vlm_extraction_template") as string;
|
||||
const evaluationTemplateId = formData.get("evaluation_template") as string;
|
||||
const summaryTemplateId = formData.get("summary_template") as string;
|
||||
|
||||
// 获取选中的评查点分组ID列表
|
||||
const selectedGroups = formData.getAll("checkpoint_group_ids") as string[];
|
||||
|
||||
// 表单验证
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 收集所有错误
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "文档类型名称不能为空";
|
||||
}
|
||||
|
||||
if (selectedGroups.length === 0) {
|
||||
errors.groups = "请至少选择一个关联的评查点分组";
|
||||
}
|
||||
|
||||
if (!llmExtractionTemplateId) {
|
||||
errors.llmExtractionTemplate = "请选择llm抽取提示词模板";
|
||||
}
|
||||
|
||||
if (!vlmExtractionTemplateId) {
|
||||
errors.vlmExtractionTemplate = "请选择vlm抽取提示词模板";
|
||||
}
|
||||
|
||||
if (!evaluationTemplateId) {
|
||||
errors.evaluationTemplate = "请选择评查提示词模板";
|
||||
}
|
||||
|
||||
if (!summaryTemplateId) {
|
||||
errors.summaryTemplate = "请选择总结提示词模板";
|
||||
}
|
||||
|
||||
// 如果有错误,返回错误信息
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({ errors });
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建文档类型数据
|
||||
const documentTypeData = {
|
||||
name,
|
||||
description,
|
||||
group_ids: selectedGroups,
|
||||
// 确保映射关系与prompt_config字段对应正确
|
||||
llm_extraction_template_id: llmExtractionTemplateId ? parseInt(llmExtractionTemplateId) : null,
|
||||
vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null,
|
||||
evaluation_template_id: evaluationTemplateId ? parseInt(evaluationTemplateId) : null,
|
||||
summary_template_id: summaryTemplateId ? parseInt(summaryTemplateId) : null
|
||||
};
|
||||
|
||||
// 调用API创建或更新文档类型
|
||||
let response;
|
||||
if (id) {
|
||||
// 更新文档类型
|
||||
response = await updateDocumentType(id, {
|
||||
...documentTypeData,
|
||||
id: parseInt(id)
|
||||
});
|
||||
} else {
|
||||
// 创建新文档类型
|
||||
response = await createDocumentType(documentTypeData);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
console.error("保存/更新文档类型失败:", response.error);
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// 操作成功,重定向到列表页
|
||||
return redirect("/document-types");
|
||||
} catch (error) {
|
||||
console.error("保存文档类型失败:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: {
|
||||
general: error instanceof Error ? error.message : "保存文档类型失败"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function DocumentTypeNew() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isEditMode = searchParams.has("id");
|
||||
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
llmExtractionTemplates,
|
||||
vlmExtractionTemplates,
|
||||
evaluationTemplates,
|
||||
summaryTemplates
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
const actionData = useActionData<ActionData>();
|
||||
|
||||
// 状态管理
|
||||
const [formData, setFormData] = useState({
|
||||
id: documentType?.id || "",
|
||||
name: documentType?.name || "",
|
||||
description: documentType?.description || "",
|
||||
llmExtractionTemplateId: documentType?.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType?.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType?.evaluation_template_id || "",
|
||||
summaryTemplateId: documentType?.summary_template_id || "",
|
||||
selectedGroups: documentType?.groups?.map((g: { id: string }) => g.id) || []
|
||||
});
|
||||
|
||||
// 添加本地验证错误状态
|
||||
const [localErrors, setLocalErrors] = useState<ActionData["errors"]>({} as ActionData["errors"]);
|
||||
|
||||
// 从actionData初始化本地错误
|
||||
useEffect(() => {
|
||||
if (actionData?.errors) {
|
||||
setLocalErrors(actionData.errors);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// 分组展开状态
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 当文档类型数据加载完成时更新表单
|
||||
useEffect(() => {
|
||||
if (documentType) {
|
||||
setFormData({
|
||||
id: documentType.id,
|
||||
name: documentType.name,
|
||||
description: documentType.description,
|
||||
llmExtractionTemplateId: documentType.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType.evaluation_template_id || "",
|
||||
summaryTemplateId: documentType.summary_template_id || "",
|
||||
selectedGroups: documentType.groups.map((g: { id: string }) => g.id)
|
||||
});
|
||||
|
||||
// 初始化展开状态 - 对于有选中子分组的父分组,默认展开
|
||||
const newExpandedGroups: Record<string, boolean> = {};
|
||||
|
||||
ruleGroups.forEach((parentGroup: RuleGroup) => {
|
||||
// 如果父分组被选中或者有子分组被选中,则展开
|
||||
const isParentSelected = documentType.groups.some((g: { id: string }) => g.id === parentGroup.id);
|
||||
const hasSelectedChild = parentGroup.children &&
|
||||
parentGroup.children.some(child =>
|
||||
documentType.groups.some((g: { id: string }) => g.id === child.id)
|
||||
);
|
||||
|
||||
if (isParentSelected || hasSelectedChild) {
|
||||
newExpandedGroups[parentGroup.id] = true;
|
||||
}
|
||||
});
|
||||
|
||||
setExpandedGroups(newExpandedGroups);
|
||||
}
|
||||
}, [documentType, ruleGroups]);
|
||||
|
||||
// 处理分组勾选
|
||||
const handleGroupCheckChange = (
|
||||
groupId: string,
|
||||
isChecked: boolean
|
||||
) => {
|
||||
// 单选模式:清空之前所有选择,只保留当前选中的
|
||||
let newSelectedGroups: string[] = [];
|
||||
|
||||
if (isChecked) {
|
||||
// 只添加当前选中的分组
|
||||
newSelectedGroups = [groupId];
|
||||
|
||||
// 如果选择的是父分组,不自动选择子分组
|
||||
// 如果选择的是子分组,不影响父分组状态
|
||||
}
|
||||
// 如果取消选中,则清空选择(在单选模式下可能不需要,但保留逻辑以防万一)
|
||||
|
||||
setFormData(prev => ({ ...prev, selectedGroups: newSelectedGroups }));
|
||||
|
||||
// 清除groups相关的错误
|
||||
if (localErrors?.groups) {
|
||||
setLocalErrors(prev => ({...prev, groups: undefined}));
|
||||
}
|
||||
};
|
||||
|
||||
// 修复展开/折叠功能
|
||||
const handleGroupExpand = (groupId: string, event: React.MouseEvent) => {
|
||||
// 阻止事件冒泡,避免触发checkbox选中
|
||||
event.stopPropagation();
|
||||
|
||||
setExpandedGroups(prev => ({
|
||||
...prev,
|
||||
[groupId]: !prev[groupId]
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理表单输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// 根据name属性映射到对应的formData字段
|
||||
if (name === 'llm_extraction_template') {
|
||||
setFormData(prev => ({ ...prev, llmExtractionTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.llmExtractionTemplate) {
|
||||
setLocalErrors(prev => ({...prev, llmExtractionTemplate: undefined}));
|
||||
}
|
||||
} else if (name === 'vlm_extraction_template') {
|
||||
setFormData(prev => ({ ...prev, vlmExtractionTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.vlmExtractionTemplate) {
|
||||
setLocalErrors(prev => ({...prev, vlmExtractionTemplate: undefined}));
|
||||
}
|
||||
} else if (name === 'evaluation_template') {
|
||||
setFormData(prev => ({ ...prev, evaluationTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.evaluationTemplate) {
|
||||
setLocalErrors(prev => ({...prev, evaluationTemplate: undefined}));
|
||||
}
|
||||
} else if (name === 'summary_template') {
|
||||
setFormData(prev => ({ ...prev, summaryTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.summaryTemplate) {
|
||||
setLocalErrors(prev => ({...prev, summaryTemplate: undefined}));
|
||||
}
|
||||
} else if (name === 'name') {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.name) {
|
||||
setLocalErrors(prev => ({...prev, name: undefined}));
|
||||
}
|
||||
} else {
|
||||
// 其他表单字段(description等)
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="document-type-new-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">{isEditMode ? "编辑文档类型" : "新增文档类型"}</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate("/document-types")}
|
||||
className="mr-2"
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
form="type-form"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<Card>
|
||||
<Form id="type-form" method="post" noValidate>
|
||||
{/* 如果是编辑模式,添加隐藏的ID字段 */}
|
||||
{formData.id && <input type="hidden" name="id" value={formData.id} />}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* 错误提示 */}
|
||||
{localErrors?.general && (
|
||||
<div className="error-message general-error error-show">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
{localErrors.general}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文档类型名称 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-name" className="form-label">
|
||||
文档类型名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="type-name"
|
||||
name="name"
|
||||
className={`form-input ${localErrors?.name ? 'input-error' : ''}`}
|
||||
placeholder="请输入文档类型名称"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
{localErrors?.name && (
|
||||
<div className="error-message error-show">{localErrors.name}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 类型描述 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-description" className="form-label">类型描述</label>
|
||||
<textarea
|
||||
id="type-description"
|
||||
name="description"
|
||||
className="form-textarea"
|
||||
placeholder="请输入类型描述,介绍此类型文档的用途和特点"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
rows={3}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{/* 提示词模板选择区域 */}
|
||||
<div className="form-group flex space-x-4">
|
||||
{/* llm抽取提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="llm-extraction-template" className="form-label">llm抽取提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="llm-extraction-template"
|
||||
name="llm_extraction_template"
|
||||
className={`form-select ${localErrors?.llmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.llmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">请选择llm抽取提示词模板</option>
|
||||
{llmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.llmExtractionTemplate && (
|
||||
<div className="error-message error-show">{localErrors.llmExtractionTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于从此类文档中抽取信息的llm提示词模板</div>
|
||||
</div>
|
||||
|
||||
{/* vlm抽取提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="vlm-extraction-template" className="form-label">vlm抽取提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="vlm-extraction-template"
|
||||
name="vlm_extraction_template"
|
||||
className={`form-select ${localErrors?.vlmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.vlmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">请选择vlm抽取提示词模板</option>
|
||||
{vlmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.vlmExtractionTemplate && (
|
||||
<div className="error-message error-show">{localErrors.vlmExtractionTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于从此类文档中抽取信息的vlm提示词模板</div>
|
||||
</div>
|
||||
|
||||
{/* 评查提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="evaluation-template" className="form-label">评查提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="evaluation-template"
|
||||
name="evaluation_template"
|
||||
className={`form-select ${localErrors?.evaluationTemplate ? 'input-error' : ''}`}
|
||||
value={formData.evaluationTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">请选择评查提示词模板</option>
|
||||
{evaluationTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.evaluationTemplate && (
|
||||
<div className="error-message error-show">{localErrors.evaluationTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于评估此类文档内容的提示词模板</div>
|
||||
</div>
|
||||
|
||||
{/* 总结提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="summary-template" className="form-label">总结提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="summary-template"
|
||||
name="summary_template"
|
||||
className={`form-select ${localErrors?.summaryTemplate ? 'input-error' : ''}`}
|
||||
value={formData.summaryTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">请选择总结提示词模板</option>
|
||||
{summaryTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.summaryTemplate && (
|
||||
<div className="error-message error-show">{localErrors.summaryTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于生成此类文档摘要的提示词模板</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关联评查点分组 */}
|
||||
<div className="form-group">
|
||||
<fieldset>
|
||||
<legend className="form-label">
|
||||
关联评查点分组 <span className="text-red-500">*</span>
|
||||
</legend>
|
||||
<div
|
||||
className={`checkbox-group ${localErrors?.groups ? 'group-error' : ''}`}
|
||||
aria-labelledby="checkpoint-groups-label"
|
||||
role="group"
|
||||
>
|
||||
{ruleGroups.map((group: RuleGroup) => (
|
||||
<React.Fragment key={group.id}>
|
||||
{/* 父分组 */}
|
||||
<div
|
||||
className={`checkbox-item parent-checkbox-item ${formData.selectedGroups.includes(group.id) ? 'checked' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="expand-icon"
|
||||
onClick={(e) => handleGroupExpand(group.id, e)}
|
||||
aria-label={`${expandedGroups[group.id] ? '收起' : '展开'}${group.name}分组`}
|
||||
>
|
||||
<i className={`ri-arrow-${expandedGroups[group.id] ? 'down' : 'right'}-s-line text-primary`}></i>
|
||||
</button>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${group.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={group.id}
|
||||
checked={formData.selectedGroups.includes(group.id)}
|
||||
onChange={(e) => handleGroupCheckChange(group.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${group.id}`}
|
||||
className="checkbox-label"
|
||||
>
|
||||
{group.name}
|
||||
<span className="group-badge parent-badge">一级分组</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 子分组 */}
|
||||
{group.children && group.children.length > 0 && expandedGroups[group.id] && (
|
||||
group.children.map((child: RuleGroup) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className={`checkbox-item child-checkbox-item ${formData.selectedGroups.includes(child.id) ? 'checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${child.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={child.id}
|
||||
checked={formData.selectedGroups.includes(child.id)}
|
||||
onChange={(e) => handleGroupCheckChange(child.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${child.id}`}
|
||||
className="checkbox-label"
|
||||
>
|
||||
{child.name}
|
||||
<span className="group-badge child-badge">二级分组</span>
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-tip">选择与此文档类型关联的评查点分组,文档上传后将应用这些分组中的评查点进行审核</div>
|
||||
{localErrors?.groups && (
|
||||
<div className="error-message error-show">{localErrors.groups}</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {type MetaFunction} from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { MetaFunction } from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{title: "文档类型列表 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "document-types", content: "文档类型列表,新增,修改"}
|
||||
{title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统"},
|
||||
{name: "description", content: "管理文档类型,包括查看、创建、编辑和删除文档类型"}
|
||||
]
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "文档类型列表"
|
||||
breadcrumb: "文档类型管理"
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档类型列表路由布局
|
||||
* 文档类型管理路由布局
|
||||
*/
|
||||
export default function DocumentTypesLayout() {
|
||||
return (
|
||||
<Outlet />
|
||||
<div className="document-types-layout">
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+144
-213
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useSearchParams, Link } from "@remix-run/react";
|
||||
import { useSearchParams, Link, useLoaderData, useFetcher } from "@remix-run/react";
|
||||
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
@@ -10,6 +10,8 @@ import { FileTypeTag } from "~/components/ui/FileTypeTag";
|
||||
import { FileTag } from "~/components/ui/FileTag";
|
||||
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
|
||||
import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
|
||||
import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents";
|
||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||
|
||||
// 导入样式
|
||||
export function links() {
|
||||
@@ -26,20 +28,6 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
interface DocumentItem {
|
||||
id: string;
|
||||
name: string;
|
||||
documentNumber: string;
|
||||
type: string;
|
||||
typeName: string;
|
||||
size: number;
|
||||
status: string;
|
||||
issues: number | null;
|
||||
uploadTime: string;
|
||||
fileType: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// 数据加载器
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
// 获取URL查询参数
|
||||
@@ -51,84 +39,41 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const dateFrom = url.searchParams.get("dateFrom") || "";
|
||||
const dateTo = url.searchParams.get("dateTo") || "";
|
||||
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "20", 10);
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
// 在实际应用中,这里会调用API获取数据
|
||||
// const response = await fetch(`/api/documents?search=${search}&...`);
|
||||
// const data = await response.json();
|
||||
|
||||
// 使用模拟数据
|
||||
const mockData = {
|
||||
documents: [
|
||||
{
|
||||
id: "1",
|
||||
name: "2023年度烟草销售框架合同.pdf",
|
||||
documentNumber: "XS20230001",
|
||||
type: "sales-contract",
|
||||
typeName: "销售合同",
|
||||
size: 2.5 * 1024 * 1024, // 2.5MB
|
||||
status: "pass",
|
||||
issues: 0,
|
||||
uploadTime: "2023-10-15 15:30",
|
||||
fileType: "pdf"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "设备采购合同-打印机.docx",
|
||||
documentNumber: "CG20230052",
|
||||
type: "purchase-contract",
|
||||
typeName: "采购合同",
|
||||
size: 1.2 * 1024 * 1024, // 1.2MB
|
||||
status: "warning",
|
||||
issues: 3,
|
||||
uploadTime: "2023-10-14 09:15",
|
||||
fileType: "docx"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "烟草零售许可证.pdf",
|
||||
documentNumber: "ZM2023100345",
|
||||
type: "license",
|
||||
typeName: "专卖许可证",
|
||||
size: 0.8 * 1024 * 1024, // 0.8MB
|
||||
status: "pending",
|
||||
issues: null,
|
||||
uploadTime: "2023-10-13 14:20",
|
||||
fileType: "pdf"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "非法售烟行政处罚决定书.docx",
|
||||
documentNumber: "CF20230087",
|
||||
type: "punishment",
|
||||
typeName: "行政处罚决定书",
|
||||
size: 1.5 * 1024 * 1024, // 1.5MB
|
||||
status: "processing",
|
||||
issues: null,
|
||||
uploadTime: "2023-10-10 16:45",
|
||||
fileType: "docx"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "烟草种植承包协议-2023.pdf",
|
||||
documentNumber: "CB20230024",
|
||||
type: "agreement",
|
||||
typeName: "承包协议",
|
||||
size: 3.2 * 1024 * 1024, // 3.2MB
|
||||
status: "fail",
|
||||
issues: 8,
|
||||
uploadTime: "2023-10-09 10:30",
|
||||
fileType: "pdf",
|
||||
tags: ["测试"]
|
||||
},
|
||||
],
|
||||
total: 156,
|
||||
// 构建搜索参数
|
||||
const searchParams = {
|
||||
name: search || undefined,
|
||||
documentNumber: documentNumber || undefined,
|
||||
documentType: documentType || undefined,
|
||||
status: status || undefined,
|
||||
dateFrom: dateFrom || undefined,
|
||||
dateTo: dateTo || undefined,
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
|
||||
// 返回数据
|
||||
return Response.json(mockData);
|
||||
// 获取文档列表
|
||||
const documentsResponse = await getDocuments(searchParams);
|
||||
if (documentsResponse.error) {
|
||||
throw new Error(documentsResponse.error);
|
||||
}
|
||||
|
||||
// 获取文档类型列表,用于筛选条件
|
||||
const typesResponse = await getDocumentTypes();
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
const documentTypeOptions = documentTypes.map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
return Response.json({
|
||||
documents: documentsResponse.data?.documents || [],
|
||||
total: documentsResponse.data?.total || 0,
|
||||
page,
|
||||
pageSize,
|
||||
documentTypeOptions
|
||||
});
|
||||
};
|
||||
|
||||
// 处理表单提交和删除等操作
|
||||
@@ -136,22 +81,31 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const action = formData.get("_action");
|
||||
|
||||
// 在实际应用中,这里会根据action类型调用相应的API
|
||||
// 例如删除文档,批量删除,等等
|
||||
|
||||
if (action === "delete") {
|
||||
const id = formData.get("id");
|
||||
// await fetch(`/api/documents/${id}`, { method: "DELETE" });
|
||||
const id = formData.get("id") as string;
|
||||
const response = await deleteDocument(id);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, message: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, message: "文档已成功删除" });
|
||||
}
|
||||
|
||||
if (action === "batchDelete") {
|
||||
const ids = formData.getAll("ids");
|
||||
// await fetch(`/api/documents/batch-delete`, {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify({ ids }),
|
||||
// headers: { "Content-Type": "application/json" }
|
||||
// });
|
||||
const ids = formData.getAll("ids") as string[];
|
||||
|
||||
// 批量删除处理
|
||||
const results = await Promise.all(ids.map(id => deleteDocument(id)));
|
||||
const failures = results.filter(r => r.error);
|
||||
|
||||
if (failures.length > 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
|
||||
}
|
||||
|
||||
@@ -159,18 +113,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
return Response.json({ success: false, message: "未知操作" }, { status: 400 });
|
||||
};
|
||||
|
||||
// 文档类型选项
|
||||
const documentTypeOptions = [
|
||||
{ value: "sales-contract", label: "销售合同" },
|
||||
{ value: "purchase-contract", label: "采购合同" },
|
||||
{ value: "license", label: "专卖许可证" },
|
||||
{ value: "punishment", label: "行政处罚决定书" },
|
||||
{ value: "agreement", label: "承包协议" },
|
||||
];
|
||||
|
||||
// 文档状态选项
|
||||
const documentStatusOptions = [
|
||||
{ value: "pending", label: "待审核" },
|
||||
{ value: "waiting", label: "待审核" },
|
||||
{ value: "processing", label: "审核中" },
|
||||
{ value: "pass", label: "通过" },
|
||||
{ value: "warning", label: "警告" },
|
||||
@@ -203,7 +148,9 @@ const formatFileSize = (bytes: number) => {
|
||||
export default function DocumentsIndex() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// 从URL获取当前筛选条件
|
||||
const search = searchParams.get("search") || "";
|
||||
const documentType = searchParams.get("documentType") || "";
|
||||
@@ -212,77 +159,10 @@ export default function DocumentsIndex() {
|
||||
const dateFrom = searchParams.get("dateFrom") || "";
|
||||
const dateTo = searchParams.get("dateTo") || "";
|
||||
const currentPage = parseInt(searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(searchParams.get("pageSize") || "20", 10);
|
||||
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
// API 返回的模拟数据
|
||||
const mockData = {
|
||||
documents: [
|
||||
{
|
||||
id: "1",
|
||||
name: "2023年度烟草销售框架合同.pdf",
|
||||
documentNumber: "XS20230001",
|
||||
type: "sales-contract",
|
||||
typeName: "销售合同",
|
||||
size: 2.5 * 1024 * 1024, // 2.5MB
|
||||
status: "pass",
|
||||
issues: 0,
|
||||
uploadTime: "2023-10-15 15:30",
|
||||
fileType: "pdf"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "设备采购合同-打印机.docx",
|
||||
documentNumber: "CG20230052",
|
||||
type: "purchase-contract",
|
||||
typeName: "采购合同",
|
||||
size: 1.2 * 1024 * 1024, // 1.2MB
|
||||
status: "warning",
|
||||
issues: 3,
|
||||
uploadTime: "2023-10-14 09:15",
|
||||
fileType: "docx"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "烟草零售许可证.pdf",
|
||||
documentNumber: "ZM2023100345",
|
||||
type: "license",
|
||||
typeName: "专卖许可证",
|
||||
size: 0.8 * 1024 * 1024, // 0.8MB
|
||||
status: "pending",
|
||||
issues: null,
|
||||
uploadTime: "2023-10-13 14:20",
|
||||
fileType: "pdf"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "非法售烟行政处罚决定书.docx",
|
||||
documentNumber: "CF20230087",
|
||||
type: "punishment",
|
||||
typeName: "行政处罚决定书",
|
||||
size: 1.5 * 1024 * 1024, // 1.5MB
|
||||
status: "processing",
|
||||
issues: null,
|
||||
uploadTime: "2023-10-10 16:45",
|
||||
fileType: "docx"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "烟草种植承包协议-2023.pdf",
|
||||
documentNumber: "CB20230024",
|
||||
type: "agreement",
|
||||
typeName: "承包协议",
|
||||
size: 3.2 * 1024 * 1024, // 3.2MB
|
||||
status: "fail",
|
||||
issues: 8,
|
||||
uploadTime: "2023-10-09 10:30",
|
||||
fileType: "pdf",
|
||||
tags: ["测试"]
|
||||
},
|
||||
],
|
||||
total: 156,
|
||||
page: currentPage,
|
||||
pageSize
|
||||
};
|
||||
// 获取API返回的数据
|
||||
const { documents, total, documentTypeOptions } = loaderData;
|
||||
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number) => {
|
||||
@@ -359,6 +239,30 @@ export default function DocumentsIndex() {
|
||||
|
||||
// 重置搜索条件
|
||||
const handleReset = () => {
|
||||
// 直接重置所有筛选条件的DOM值
|
||||
const resetInput = (selector: string, value: string = "") => {
|
||||
const element = document.querySelector<HTMLInputElement | HTMLSelectElement>(selector);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
|
||||
// 对于搜索框,触发其input事件以激活搜索
|
||||
if (element instanceof HTMLInputElement && element.type === "text") {
|
||||
// 创建一个input事件
|
||||
const event = new Event('input', { bubbles: true });
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 重置所有搜索字段
|
||||
resetInput('input[placeholder="请输入文档名称"]');
|
||||
resetInput('input[placeholder="请输入文档编号"]');
|
||||
resetInput('select[name="documentType"]');
|
||||
resetInput('select[name="status"]');
|
||||
resetInput('input[name="dateFrom"]');
|
||||
resetInput('input[name="dateTo"]');
|
||||
|
||||
// 重置URL参数
|
||||
setSearchParams(new URLSearchParams({
|
||||
page: "1",
|
||||
pageSize: pageSize.toString()
|
||||
@@ -377,33 +281,57 @@ export default function DocumentsIndex() {
|
||||
// 全选处理
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedRowKeys(mockData.documents.map(doc => doc.id));
|
||||
setSelectedRowKeys(documents.map((doc: DocumentUI) => doc.id.toString()));
|
||||
} else {
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除确认
|
||||
const confirmDelete = (id: string, name: string) => {
|
||||
// 下载文档
|
||||
const handleDownload = (path: string, fileName: string) => {
|
||||
console.log('handleDownload',path,fileName)
|
||||
// 创建一个隐藏的a标签并点击它
|
||||
const a = document.createElement('a');
|
||||
a.href = path;
|
||||
a.download = fileName; // 设置下载的文件名
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
// 删除文档
|
||||
const handleDelete = (id: string, name: string) => {
|
||||
if (window.confirm(`确认删除文档 "${name}"?`)) {
|
||||
// 在实际应用中这里会提交表单到action处理
|
||||
console.log('删除文档:', id, name);
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
formData.append('id', id);
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
// 更新选中行
|
||||
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除确认
|
||||
const confirmBatchDelete = () => {
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
alert('请至少选择一个文档');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) {
|
||||
// 在实际应用中这里会提交表单到action处理
|
||||
console.log('批量删除文档IDs:', selectedRowKeys);
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'batchDelete');
|
||||
|
||||
// 添加所有选中的ID
|
||||
selectedRowKeys.forEach(id => {
|
||||
formData.append('ids', id);
|
||||
});
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
// 清空选中行
|
||||
setSelectedRowKeys([]);
|
||||
@@ -416,24 +344,24 @@ export default function DocumentsIndex() {
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRowKeys.length === mockData.documents.length}
|
||||
checked={selectedRowKeys.length === documents.length}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
/>
|
||||
),
|
||||
key: "selection",
|
||||
width: "50px",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRowKeys.includes(record.id)}
|
||||
onChange={() => handleRowSelectionChange(record.id)}
|
||||
checked={selectedRowKeys.includes(record.id.toString())}
|
||||
onChange={() => handleRowSelectionChange(record.id.toString())}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "文档名称",
|
||||
key: "name",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<div className="flex items-center m-1">
|
||||
<FileTag
|
||||
extension={record.fileType}
|
||||
@@ -451,10 +379,11 @@ export default function DocumentsIndex() {
|
||||
text={record.typeName}
|
||||
size="sm"
|
||||
showIcon={false}
|
||||
fileType={record.fileType}
|
||||
/>
|
||||
{record.tags && record.tags.map((tag: string) => (
|
||||
<span key={tag} className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">{tag}</span>
|
||||
))}
|
||||
{record.isTest && (
|
||||
<span className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">测试</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,41 +392,43 @@ export default function DocumentsIndex() {
|
||||
{
|
||||
title: "文档编号",
|
||||
key: "documentNumber",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<span className="document-number">{record.documentNumber}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "文件大小",
|
||||
key: "size",
|
||||
render: (_: unknown, record: DocumentItem) => formatFileSize(record.size)
|
||||
width: "100px",
|
||||
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
|
||||
},
|
||||
{
|
||||
title: "审核状态",
|
||||
key: "status",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<StatusBadge status={record.status} showIcon={false} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "问题数量",
|
||||
key: "issues",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
width:"60px",
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
record.issues === null ? "-" : record.issues
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
key: "uploadTime",
|
||||
render: (_: unknown, record: DocumentItem) => record.uploadTime
|
||||
render: (_: unknown, record: DocumentUI) => record.uploadTime
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: "280px",
|
||||
render: (_: unknown, record: DocumentItem) => (
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<div className="operations-cell">
|
||||
{record.status === "pending" ? (
|
||||
{record.status === "waiting" ? (
|
||||
<Link
|
||||
to={`/documents/${record.id}/review`}
|
||||
className="mr-1 hover:underline"
|
||||
@@ -523,7 +454,7 @@ export default function DocumentsIndex() {
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to={`/documents/${record.id}/edit`}
|
||||
to={`/documents/edit?id=${record.id}`}
|
||||
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||
>
|
||||
<i className="ri-edit-line"></i>
|
||||
@@ -532,7 +463,7 @@ export default function DocumentsIndex() {
|
||||
<button
|
||||
type="button"
|
||||
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||
onClick={() => alert(`下载文档: ${record.name}`)}
|
||||
onClick={() => handleDownload(record.path, record.name)}
|
||||
>
|
||||
<i className="ri-download-line"></i>
|
||||
下载
|
||||
@@ -540,7 +471,7 @@ export default function DocumentsIndex() {
|
||||
<button
|
||||
type="button"
|
||||
className="text-error hover:underline hover:text-red-700"
|
||||
onClick={() => confirmDelete(record.id, record.name)}
|
||||
onClick={() => handleDelete(record.id.toString(), record.name)}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
删除
|
||||
@@ -579,7 +510,7 @@ export default function DocumentsIndex() {
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
type="primary"
|
||||
icon="ri-search-line"
|
||||
onClick={() => {
|
||||
@@ -588,7 +519,7 @@ export default function DocumentsIndex() {
|
||||
}}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Button> */}
|
||||
</>
|
||||
}
|
||||
noActionDivider={true}
|
||||
@@ -648,7 +579,7 @@ export default function DocumentsIndex() {
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={confirmBatchDelete}
|
||||
onClick={handleBatchDelete}
|
||||
className="mr-2"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
@@ -662,14 +593,14 @@ export default function DocumentsIndex() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-secondary">
|
||||
共 <span className="font-medium text-primary">{mockData.total}</span> 条记录
|
||||
共 <span className="font-medium text-primary">{total}</span> 条记录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={mockData.documents}
|
||||
dataSource={documents}
|
||||
rowKey="id"
|
||||
emptyText="暂无数据"
|
||||
/>
|
||||
@@ -678,7 +609,7 @@ export default function DocumentsIndex() {
|
||||
{/* 分页 */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
total={mockData.total}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
import { useState } from "react";
|
||||
import { useLoaderData, useActionData, useNavigate, Form } from "@remix-run/react";
|
||||
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import documentEditStyles from "~/styles/pages/documents_edit.css?url";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: documentEditStyles }];
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "修改文档 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "修改文档信息,包括文档类型、编号、状态和备注信息等" }
|
||||
];
|
||||
};
|
||||
|
||||
// 文档状态定义
|
||||
enum DocumentStatus {
|
||||
PENDING = "pending",
|
||||
PROCESSING = "processing",
|
||||
PASS = "pass",
|
||||
WARNING = "warning",
|
||||
FAIL = "fail"
|
||||
}
|
||||
|
||||
// 文档状态对应的中文标签
|
||||
const STATUS_LABELS: Record<DocumentStatus, string> = {
|
||||
[DocumentStatus.PENDING]: "待审核",
|
||||
[DocumentStatus.PROCESSING]: "审核中",
|
||||
[DocumentStatus.PASS]: "通过",
|
||||
[DocumentStatus.WARNING]: "警告",
|
||||
[DocumentStatus.FAIL]: "不通过"
|
||||
};
|
||||
|
||||
// 文档类型接口
|
||||
interface DocumentType {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 历史记录项接口
|
||||
interface HistoryItem {
|
||||
time: string;
|
||||
user: string;
|
||||
action: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
// 文档接口
|
||||
interface Document {
|
||||
id: string;
|
||||
name: string;
|
||||
type_id: string;
|
||||
document_number: string | null;
|
||||
file_size: number;
|
||||
upload_time: string;
|
||||
is_test_document: boolean;
|
||||
status: DocumentStatus;
|
||||
remark: string | null;
|
||||
history: HistoryItem[];
|
||||
file_url?: string;
|
||||
}
|
||||
|
||||
// 模拟API获取文档类型列表
|
||||
async function getDocumentTypes(): Promise<DocumentType[]> {
|
||||
// 这里应该是实际API调用
|
||||
return [
|
||||
{ id: "1", name: "销售合同" },
|
||||
{ id: "2", name: "采购合同" },
|
||||
{ id: "3", name: "专卖许可证" },
|
||||
{ id: "4", name: "行政处罚决定书" },
|
||||
{ id: "5", name: "承包协议" }
|
||||
];
|
||||
}
|
||||
|
||||
// 模拟API获取文档详情
|
||||
async function getDocument(id: string): Promise<Document> {
|
||||
// 这里应该是实际API调用
|
||||
return {
|
||||
id,
|
||||
name: "2023年度烟草销售框架合同.pdf",
|
||||
type_id: "1", // 销售合同
|
||||
document_number: "XS20230001",
|
||||
file_size: 2.5 * 1024 * 1024, // 2.5MB
|
||||
upload_time: "2023-10-15 15:30",
|
||||
is_test_document: false,
|
||||
status: DocumentStatus.PASS,
|
||||
remark: "此合同为2023年度与XX公司的销售框架协议,适用于全年的烟草销售业务。",
|
||||
history: [
|
||||
{
|
||||
time: "2023-10-15 15:30",
|
||||
user: "系统",
|
||||
action: "创建了此文档",
|
||||
details: "首次上传文档,文档类型:销售合同,状态:待审核"
|
||||
},
|
||||
{
|
||||
time: "2023-10-15 16:45",
|
||||
user: "张三",
|
||||
action: "启动了文档审核",
|
||||
details: "状态由'待审核'变更为'审核中'"
|
||||
},
|
||||
{
|
||||
time: "2023-10-15 17:20",
|
||||
user: "系统",
|
||||
action: "完成了文档审核",
|
||||
details: "状态由'审核中'变更为'通过',未发现问题"
|
||||
},
|
||||
{
|
||||
time: "2023-10-16 09:10",
|
||||
user: "李四",
|
||||
action: "修改了文档属性",
|
||||
details: "添加了备注信息,完善了文档编号"
|
||||
}
|
||||
],
|
||||
file_url: "/mock/documents/sample.pdf"
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟API更新文档信息
|
||||
async function updateDocument(id: string, data: Partial<Document>): Promise<Document> {
|
||||
// 这里应该是实际API调用
|
||||
console.log("更新文档:", id, data);
|
||||
|
||||
// 模拟获取原始数据
|
||||
const document = await getDocument(id);
|
||||
|
||||
// 合并更新的数据
|
||||
const updatedDocument = {
|
||||
...document,
|
||||
...data,
|
||||
// 添加新的历史记录
|
||||
history: [
|
||||
{
|
||||
time: new Date().toISOString().replace("T", " ").slice(0, 16),
|
||||
user: "当前用户",
|
||||
action: "修改了文档信息",
|
||||
details: `更新了文档类型、状态和备注信息`
|
||||
},
|
||||
...document.history
|
||||
]
|
||||
};
|
||||
|
||||
return updatedDocument;
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
// Loader函数
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 从URL查询参数获取文档ID
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
throw new Response("缺少文档ID", { status: 400 });
|
||||
}
|
||||
|
||||
// 并行获取文档详情和文档类型列表
|
||||
const [document, documentTypes] = await Promise.all([
|
||||
getDocument(id),
|
||||
getDocumentTypes()
|
||||
]);
|
||||
|
||||
return Response.json({ document, documentTypes });
|
||||
} catch (error) {
|
||||
console.error("加载文档数据失败:", error);
|
||||
throw new Response("加载文档数据失败", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Action函数处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// 从URL查询参数获取文档ID
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
|
||||
if (!id) {
|
||||
return Response.json({ error: "缺少文档ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
|
||||
// 从表单数据中提取字段
|
||||
const type_id = formData.get("type_id") as string;
|
||||
const document_number = formData.get("document_number") as string;
|
||||
const status = formData.get("status") as DocumentStatus;
|
||||
const is_test_document = formData.get("is_test_document") === "on";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
// 验证必填字段
|
||||
if (!type_id || !status) {
|
||||
return Response.json(
|
||||
{
|
||||
error: "缺少必填字段",
|
||||
fieldErrors: {
|
||||
type_id: !type_id ? "文档类型不能为空" : null,
|
||||
status: !status ? "状态不能为空" : null
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 更新文档
|
||||
await updateDocument(id, {
|
||||
type_id,
|
||||
document_number: document_number || null,
|
||||
status,
|
||||
is_test_document,
|
||||
remark: remark || null
|
||||
});
|
||||
|
||||
// 重定向回文档列表
|
||||
return redirect("/documents");
|
||||
} catch (error) {
|
||||
console.error("更新文档失败:", error);
|
||||
return Response.json({ error: "更新文档失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 文档编辑页面组件
|
||||
export default function DocumentEdit() {
|
||||
const { document, documentTypes } = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 状态
|
||||
const [localStatus, setLocalStatus] = useState<DocumentStatus>(document.status);
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setLocalStatus(e.target.value as DocumentStatus);
|
||||
};
|
||||
|
||||
// 获取文档类型名称
|
||||
const getDocumentTypeName = (typeId: string): string => {
|
||||
const docType = documentTypes.find((type: DocumentType) => type.id === typeId);
|
||||
return docType ? docType.name : "未知类型";
|
||||
};
|
||||
|
||||
// 渲染状态徽章
|
||||
const renderStatusBadge = (status: DocumentStatus) => {
|
||||
const statusClasses: Record<DocumentStatus, string> = {
|
||||
[DocumentStatus.PENDING]: "status-badge status-pending",
|
||||
[DocumentStatus.PROCESSING]: "status-badge status-processing",
|
||||
[DocumentStatus.PASS]: "status-badge status-pass",
|
||||
[DocumentStatus.WARNING]: "status-badge status-warning",
|
||||
[DocumentStatus.FAIL]: "status-badge status-fail"
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={statusClasses[status]}>
|
||||
{STATUS_LABELS[status]}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="document-edit-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">修改文档信息</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate("/documents")}
|
||||
className="mr-2"
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
form="edit-form"
|
||||
>
|
||||
保存修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文档信息 */}
|
||||
<Card className="mb-4">
|
||||
<div className="document-info">
|
||||
<div className="document-icon">
|
||||
<i className="ri-file-pdf-line text-red-500"></i>
|
||||
</div>
|
||||
<div className="document-details">
|
||||
<div className="document-name">{document.name}</div>
|
||||
<div className="document-meta">
|
||||
<div className="meta-item">
|
||||
<i className="ri-file-list-line"></i>
|
||||
<span>{getDocumentTypeName(document.type_id)}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<i className="ri-time-line"></i>
|
||||
<span>{document.upload_time}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<i className="ri-hard-drive-line"></i>
|
||||
<span>{formatFileSize(document.file_size)}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
{renderStatusBadge(document.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="alert alert-info mb-4">
|
||||
<i className="ri-information-line mr-2"></i> 您可以修改此文档的基本信息,但不能更改文档内容。如需修改内容,请删除后重新上传新文档。
|
||||
</div>
|
||||
|
||||
<Form id="edit-form" method="post">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-id" className="form-label">文档类型 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="type-id"
|
||||
name="type_id"
|
||||
className="form-select"
|
||||
defaultValue={document.type_id}
|
||||
required
|
||||
>
|
||||
{documentTypes.map((type: DocumentType) => (
|
||||
<option key={type.id} value={type.id}>{type.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="text-sm text-secondary mt-1">更改文档类型将重新应用对应的评查规则</div>
|
||||
{actionData?.fieldErrors?.type_id && (
|
||||
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.type_id}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="document-number" className="form-label">文档编号</label>
|
||||
<input
|
||||
type="text"
|
||||
id="document-number"
|
||||
name="document_number"
|
||||
className="form-input"
|
||||
placeholder="请输入合同编号、许可证号等"
|
||||
defaultValue={document.document_number || ""}
|
||||
/>
|
||||
<div className="text-sm text-secondary mt-1">如无编号可留空</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="status" className="form-label">状态 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="form-select"
|
||||
value={localStatus}
|
||||
onChange={handleStatusChange}
|
||||
required
|
||||
>
|
||||
<option value={DocumentStatus.PENDING}>{STATUS_LABELS[DocumentStatus.PENDING]}</option>
|
||||
<option value={DocumentStatus.PROCESSING}>{STATUS_LABELS[DocumentStatus.PROCESSING]}</option>
|
||||
<option value={DocumentStatus.PASS}>{STATUS_LABELS[DocumentStatus.PASS]}</option>
|
||||
<option value={DocumentStatus.WARNING}>{STATUS_LABELS[DocumentStatus.WARNING]}</option>
|
||||
<option value={DocumentStatus.FAIL}>{STATUS_LABELS[DocumentStatus.FAIL]}</option>
|
||||
</select>
|
||||
<div className="text-sm text-secondary mt-1">更改状态可能会影响此文档在列表中的显示和排序</div>
|
||||
{actionData?.fieldErrors?.status && (
|
||||
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.status}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="form-label">文档属性</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<label className="switch mr-2" htmlFor="is-test-document">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is-test-document"
|
||||
name="is_test_document"
|
||||
defaultChecked={document.is_test_document}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
<span className="sr-only">标记为测试文档</span>
|
||||
</label>
|
||||
<span>标记为测试文档(不计入正式统计)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-span-2">
|
||||
<label htmlFor="remark" className="form-label">备注信息</label>
|
||||
<textarea
|
||||
id="remark"
|
||||
name="remark"
|
||||
className="form-textarea"
|
||||
placeholder="可输入文档的相关描述或备注信息"
|
||||
rows={3}
|
||||
defaultValue={document.remark || ""}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 文档预览 */}
|
||||
<Card
|
||||
title="文档预览"
|
||||
className="mb-4"
|
||||
>
|
||||
<div className="document-preview">
|
||||
<div className="preview-toolbar">
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-pdf-line text-red-500 mr-1"></i>
|
||||
<span>{document.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-download-line"
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preview-content">
|
||||
<div className="preview-placeholder">
|
||||
<i className="ri-file-pdf-line"></i>
|
||||
<p>预览功能暂不可用</p>
|
||||
<p className="text-xs mt-2">PDF文件需要外部查看器支持</p>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="ri-external-link-line"
|
||||
className="mt-4"
|
||||
>
|
||||
在新窗口打开
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 修改历史 */}
|
||||
<Card title="修改历史">
|
||||
<div className="history-timeline">
|
||||
{document.history.map((item: HistoryItem, index: number) => (
|
||||
<div className="timeline-item" key={`${item.time}-${index}`}>
|
||||
<div className="timeline-time">{item.time}</div>
|
||||
<div className="timeline-content">
|
||||
<div><strong>{item.user}</strong> {item.action}</div>
|
||||
<div className="text-xs text-secondary mt-1">{item.details}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误边界
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">文档编辑页面加载失败。请检查文档ID是否正确,或稍后重试。</p>
|
||||
<Button
|
||||
type="primary"
|
||||
to="/documents"
|
||||
>
|
||||
返回文档列表
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+52
-39
@@ -30,21 +30,11 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
// 文件类型定义
|
||||
export enum FileType {
|
||||
CONTRACT = "1",
|
||||
LICENSE = "2",
|
||||
PUNISHMENT = "3",
|
||||
OTHER = "4"
|
||||
}
|
||||
// 文件类型定义为字符串类型,以适应从API动态获取的ID
|
||||
export type FileType = string;
|
||||
|
||||
// 文件类型标签映射
|
||||
export const FILE_TYPE_LABELS: Record<FileType, string> = {
|
||||
[FileType.CONTRACT]: "合同文档",
|
||||
[FileType.LICENSE]: "专卖许可证",
|
||||
[FileType.PUNISHMENT]: "行政处罚决定书",
|
||||
[FileType.OTHER]: "其他文档"
|
||||
};
|
||||
// 动态构建的文件类型标签映射
|
||||
export const FILE_TYPE_LABELS: Record<string, string> = {};
|
||||
|
||||
// 优先级定义
|
||||
export enum Priority {
|
||||
@@ -85,7 +75,7 @@ export enum StepStatus {
|
||||
|
||||
// 上传的文件信息接口
|
||||
export interface UploadedFile {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
@@ -346,6 +336,19 @@ export default function FilesUpload() {
|
||||
const [queueFiles, setQueueFiles] = useState<Document[]>(documents);
|
||||
const [documentTypesState] = useState<DocumentType[]>(documentTypes);
|
||||
|
||||
// 构建文件类型标签映射
|
||||
useEffect(() => {
|
||||
// 清空之前的映射
|
||||
Object.keys(FILE_TYPE_LABELS).forEach(key => {
|
||||
delete FILE_TYPE_LABELS[key];
|
||||
});
|
||||
|
||||
// 使用从API获取的文档类型构建新的映射
|
||||
documentTypes.forEach(type => {
|
||||
FILE_TYPE_LABELS[type.id.toString()] = type.name;
|
||||
});
|
||||
}, [documentTypes]);
|
||||
|
||||
// 上传完成后的文件信息列表
|
||||
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
|
||||
|
||||
@@ -388,7 +391,7 @@ export default function FilesUpload() {
|
||||
|
||||
// 获取所有未完成的文档ID
|
||||
const incompleteIds = queueFiles
|
||||
.filter(file => file.status !== DocumentStatus.COMPLETED)
|
||||
.filter(file => file.status !== DocumentStatus.COMPLETED && file.id)
|
||||
.map(file => file.id);
|
||||
|
||||
console.log('未完成的文档ID:', incompleteIds);
|
||||
@@ -435,12 +438,17 @@ export default function FilesUpload() {
|
||||
|
||||
// 处理文件类型变化
|
||||
const handleFileTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newFileType = e.target.value as FileType;
|
||||
setFileType(newFileType);
|
||||
|
||||
// 如果已经有选中的文件,且选择了文件类型,则开始上传
|
||||
if (currentFiles.length > 0 && newFileType) {
|
||||
startUpload(currentFiles);
|
||||
const value = e.target.value;
|
||||
// 确保只有选择了有效的文件类型才进行设置
|
||||
if (value) {
|
||||
setFileType(value as FileType);
|
||||
|
||||
// 如果已经有选中的文件,且选择了文件类型,则开始上传
|
||||
if (currentFiles.length > 0) {
|
||||
startUpload(currentFiles);
|
||||
}
|
||||
} else {
|
||||
setFileType("");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -510,7 +518,7 @@ export default function FilesUpload() {
|
||||
|
||||
// 创建新的文件对象
|
||||
const newFile: UploadedFile = {
|
||||
id: response.result.id.toString(),
|
||||
id: response.result.id,
|
||||
name: response.result.file_name,
|
||||
size: response.result.file_size,
|
||||
type: file.type,
|
||||
@@ -537,14 +545,18 @@ export default function FilesUpload() {
|
||||
setUploadSpeed("完成");
|
||||
|
||||
// 更新队列
|
||||
const newDocuments: Document[] = uploadedFiles.map(file => ({
|
||||
id: parseInt(file.id),
|
||||
name: file.name,
|
||||
type_id: parseInt(fileType),
|
||||
file_size: file.size,
|
||||
status: DocumentStatus.CUTTING,
|
||||
created_at: new Date().toISOString()
|
||||
}));
|
||||
const newDocuments: Document[] = uploadedFiles.map(file => {
|
||||
// 确保id能够被正确解析为数字
|
||||
const id = file.id;
|
||||
return {
|
||||
id,
|
||||
name: file.name,
|
||||
type_id: fileType ? parseInt(fileType) : 0,
|
||||
file_size: file.size,
|
||||
status: DocumentStatus.CUTTING,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
setQueueFiles(prev => [...newDocuments, ...prev]);
|
||||
|
||||
@@ -587,7 +599,7 @@ export default function FilesUpload() {
|
||||
setProcessingSteps(updatedSteps);
|
||||
|
||||
// 获取文件ID列表
|
||||
const fileIds = files.map(file => parseInt(file.id));
|
||||
const fileIds = files.map(file => file.id).filter(id => id > 0);
|
||||
|
||||
console.log('开始处理文件,设置文件处理进度定时器');
|
||||
|
||||
@@ -802,8 +814,8 @@ export default function FilesUpload() {
|
||||
};
|
||||
|
||||
// 获取文档类型名称
|
||||
const getDocumentTypeName = (typeId: number) => {
|
||||
const type = documentTypesState.find(t => t.id === typeId);
|
||||
const getDocumentTypeName = (codeId: number) => {
|
||||
const type = documentTypesState.find(t => t.id === codeId);
|
||||
return type ? type.name : '未知类型';
|
||||
};
|
||||
|
||||
@@ -920,10 +932,9 @@ export default function FilesUpload() {
|
||||
disabled={uploadStage !== "idle"}
|
||||
>
|
||||
<option value="">请选择文件类型</option>
|
||||
<option value={FileType.CONTRACT}>{FILE_TYPE_LABELS[FileType.CONTRACT]}</option>
|
||||
<option value={FileType.LICENSE}>{FILE_TYPE_LABELS[FileType.LICENSE]}</option>
|
||||
<option value={FileType.PUNISHMENT}>{FILE_TYPE_LABELS[FileType.PUNISHMENT]}</option>
|
||||
<option value={FileType.OTHER}>{FILE_TYPE_LABELS[FileType.OTHER]}</option>
|
||||
{documentTypes.map(type => (
|
||||
<option key={type.id} value={type.id}>{type.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{actionData?.errors?.fileType && (
|
||||
@@ -1007,7 +1018,9 @@ export default function FilesUpload() {
|
||||
</li>
|
||||
<li className="file-info-item">
|
||||
<span className="file-info-label">文件类型:</span>
|
||||
<span className="file-info-value">{FILE_TYPE_LABELS[file.fileType]}</span>
|
||||
<span className="file-info-value">
|
||||
{FILE_TYPE_LABELS[file.fileType] || getDocumentTypeName(parseInt(file.fileType))}
|
||||
</span>
|
||||
</li>
|
||||
<li className="file-info-item">
|
||||
<span className="file-info-label">审核规则:</span>
|
||||
|
||||
@@ -227,13 +227,17 @@ export default function PromptsIndex() {
|
||||
{
|
||||
title: "类型",
|
||||
key: "template_type",
|
||||
width: "100px",
|
||||
width: "120px",
|
||||
render: (_: unknown, record: PromptTemplateUI) => {
|
||||
let typeText = '';
|
||||
let typeClass = '';
|
||||
|
||||
switch (record.template_type) {
|
||||
case 'Extraction':
|
||||
case 'LLM_Extraction':
|
||||
typeText = '抽取';
|
||||
typeClass = 'type-extraction';
|
||||
break;
|
||||
case 'VLM_Extraction':
|
||||
typeText = '抽取';
|
||||
typeClass = 'type-extraction';
|
||||
break;
|
||||
@@ -400,7 +404,8 @@ export default function PromptsIndex() {
|
||||
name="type"
|
||||
value={searchParams.get('type') || ''}
|
||||
options={[
|
||||
{ value: "Extraction", label: "抽取(Extraction)" },
|
||||
{ value: "LLM_Extraction", label: "LLM抽取(LLM_Extraction)" },
|
||||
{ value: "VLM_Extraction", label: "VLM抽取(VLM_Extraction)" },
|
||||
{ value: "Evaluation", label: "评估(Evaluation)" },
|
||||
{ value: "Summary", label: "摘要(Summary)" },
|
||||
{ value: "Common", label: "通用(Common)" }
|
||||
|
||||
@@ -52,7 +52,7 @@ interface ActionData {
|
||||
};
|
||||
formData?: {
|
||||
template_name: string;
|
||||
template_type: "Extraction" | "Evaluation" | "Summary" | "Common";
|
||||
template_type: 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
|
||||
description: string;
|
||||
template_content: string;
|
||||
variables: string;
|
||||
@@ -108,7 +108,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const template_name = formData.get("template_name") as string;
|
||||
const template_type = formData.get("template_type") as "Extraction" | "Evaluation" | "Summary" | "Common";
|
||||
const template_type = formData.get("template_type") as 'LLM_Extraction' | 'VLM_Extraction' | 'Evaluation' | 'Summary' | 'Common';
|
||||
const description = formData.get("description") as string;
|
||||
const template_content = formData.get("template_content") as string;
|
||||
const variables = formData.get("variables") as string;
|
||||
@@ -488,7 +488,8 @@ export default function PromptsNew() {
|
||||
>
|
||||
<option value="">请选择模板类型</option>
|
||||
<option value="Common">通用(Common) - 适用于多种场景的通用提示词</option>
|
||||
<option value="Extraction">抽取(Extraction) - 从文档抽取结构化信息</option>
|
||||
<option value="LLM_Extraction">LLM抽取(LLM_Extraction) - 使用LLM从文档中抽取结构化信息</option>
|
||||
<option value="VLM_Extraction">VLM抽取(VLM_Extraction) - 使用VLM从文档中抽取结构化信息</option>
|
||||
<option value="Evaluation">评估(Evaluation) - 对文档内容进行评估</option>
|
||||
<option value="Summary">摘要(Summary) - 生成文档内容摘要</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user