完成文档类型增删改查
This commit is contained in:
+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}
|
||||
|
||||
Reference in New Issue
Block a user