4fcc92a381
2. 合同模板对比接入monaco editor的效果。 3. 添加交叉评查的案卷类型的数据查询。 fix: 1. 修复文档列表的打开模态框蒙板层显示效果。
429 lines
13 KiB
TypeScript
429 lines
13 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
import { useSearchParams, useNavigate, useLoaderData, useRevalidator } from "@remix-run/react";
|
||
import { ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
|
||
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 { toastService } from "~/components/ui/Toast";
|
||
import { messageService } from "~/components/ui/MessageModal";
|
||
import { usePermission } from "~/hooks/usePermission";
|
||
import {
|
||
getEntryModules,
|
||
deleteEntryModule,
|
||
type EntryModule,
|
||
type EntryModuleSearchParams
|
||
} from "~/api/entry-modules/entry-modules";
|
||
import entryModulesStyles from "~/styles/pages/entry-modules.css?url";
|
||
import { DOCUMENT_URL } from "~/config/api-config";
|
||
|
||
// 引入CSS样式
|
||
export function links() {
|
||
return [
|
||
{ rel: "stylesheet", href: entryModulesStyles }
|
||
];
|
||
}
|
||
|
||
// 页面元数据
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "入口模块管理 - 中国烟草AI合同及卷宗审核系统" },
|
||
{ name: "description", content: "管理入口模块,包括查看、编辑和删除入口模块" },
|
||
];
|
||
};
|
||
|
||
// 面包屑配置
|
||
export const handle = {
|
||
breadcrumb: "入口模块管理"
|
||
};
|
||
|
||
|
||
// 定义加载器返回的数据类型
|
||
interface LoaderData {
|
||
modules: EntryModule[];
|
||
total: number;
|
||
pageSize: number;
|
||
currentPage: number;
|
||
error?: string;
|
||
}
|
||
|
||
// 🔑 客户端加载函数 - 在浏览器端执行,axios-client 会自动添加 JWT
|
||
export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
|
||
try {
|
||
const url = new URL(request.url);
|
||
const name = url.searchParams.get('name') || undefined;
|
||
const area = url.searchParams.get('area') || undefined;
|
||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||
|
||
// 构建搜索参数(注意:API使用page_size而不是pageSize)
|
||
const searchParams: EntryModuleSearchParams = {
|
||
name,
|
||
area,
|
||
page,
|
||
page_size: pageSize // API使用page_size
|
||
};
|
||
|
||
// ✅ 不需要传递 JWT,axios-client 会自动从 localStorage 读取并添加
|
||
const modulesResponse = await getEntryModules(searchParams);
|
||
|
||
if (modulesResponse.error) {
|
||
console.error("❌ [clientLoader] 获取入口模块失败:", modulesResponse.error);
|
||
throw new Error(modulesResponse.error);
|
||
}
|
||
|
||
const modulesResult = modulesResponse.data?.modules || [];
|
||
const totalCount = modulesResponse.data?.total || modulesResult.length;
|
||
|
||
return {
|
||
modules: modulesResult,
|
||
total: totalCount,
|
||
pageSize,
|
||
currentPage: page
|
||
};
|
||
} catch (error) {
|
||
console.error("加载入口模块列表失败:", error);
|
||
return {
|
||
modules: [],
|
||
total: 0,
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
error: error instanceof Error ? error.message : "加载入口模块列表失败"
|
||
};
|
||
}
|
||
}
|
||
|
||
// 地区选项
|
||
const AREA_OPTIONS = [
|
||
{ value: "梅州", label: "梅州" },
|
||
{ value: "云浮", label: "云浮" },
|
||
{ value: "揭阳", label: "揭阳" },
|
||
{ value: "潮州", label: "潮州" },
|
||
{ value: "省局", label: "省局" }
|
||
];
|
||
|
||
// 入口模块列表组件
|
||
export default function EntryModulesList() {
|
||
const navigate = useNavigate();
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const revalidator = useRevalidator();
|
||
|
||
// 记录加载失败的图片ID
|
||
const [failedImages, setFailedImages] = useState<Set<number>>(new Set());
|
||
|
||
// 获取加载器数据
|
||
const loaderData = useLoaderData<LoaderData>();
|
||
const { modules, total, error } = loaderData;
|
||
|
||
// ✅ 使用权限 Hook
|
||
const { canCreate, canUpdate, canDelete, canView } = usePermission();
|
||
const canCreateModule = canCreate('entry_module');
|
||
const canUpdateModule = canUpdate('entry_module');
|
||
const canDeleteModule = canDelete('entry_module');
|
||
const canViewModule = canView('entry_module');
|
||
|
||
// 获取搜索参数
|
||
const name = searchParams.get('name') || '';
|
||
const area = searchParams.get('area') || '';
|
||
const currentPage = parseInt(searchParams.get('page') || String(1), 10);
|
||
const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10);
|
||
|
||
// 处理loader加载数据的时候的错误
|
||
useEffect(() => {
|
||
if (error) {
|
||
toastService.error(error);
|
||
}
|
||
}, [error]);
|
||
|
||
// 处理名称搜索
|
||
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 handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||
const { name, value } = e.target;
|
||
const newParams = new URLSearchParams(searchParams);
|
||
|
||
if (value) {
|
||
newParams.set(name, value);
|
||
} else {
|
||
newParams.delete(name);
|
||
}
|
||
|
||
// 切换筛选条件时,重置到第一页
|
||
newParams.set('page', '1');
|
||
|
||
setSearchParams(newParams);
|
||
};
|
||
|
||
// 处理重置筛选
|
||
const handleReset = () => {
|
||
const nameInput = document.querySelector('input[placeholder="请输入入口模块名称"]');
|
||
if (nameInput) {
|
||
(nameInput as HTMLInputElement).value = '';
|
||
}
|
||
|
||
// 重置所有筛选条件
|
||
setSearchParams(new URLSearchParams());
|
||
};
|
||
|
||
// 处理删除入口模块
|
||
const handleDelete = async (id: number) => {
|
||
// ✅ 检查删除权限
|
||
if (!canDeleteModule) {
|
||
toastService.warning('您没有删除权限');
|
||
return;
|
||
}
|
||
|
||
messageService.show({
|
||
title: "确认删除",
|
||
message: "确定要删除该入口模块吗?此操作不可撤销。",
|
||
type: "warning",
|
||
confirmText: "删除",
|
||
cancelText: "取消",
|
||
confirmDelay: 3,
|
||
onConfirm: async () => {
|
||
setIsDeleting(true);
|
||
|
||
try {
|
||
// 直接调用 API 删除函数
|
||
const result = await deleteEntryModule(id);
|
||
|
||
if (result.success) {
|
||
toastService.success('删除成功!');
|
||
// 重新验证数据,刷新表格
|
||
revalidator.revalidate();
|
||
} else {
|
||
toastService.error(`删除失败: ${result.error || '未知错误'}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('删除入口模块失败:', error);
|
||
toastService.error(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
} finally {
|
||
setIsDeleting(false);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 处理编辑入口模块
|
||
const handleEdit = (id: number) => {
|
||
navigate(`/entry-modules/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 = [
|
||
{
|
||
key: 'name',
|
||
title: '模块名称',
|
||
width: '200px',
|
||
render: (_: any, record: EntryModule) => (
|
||
<span className="font-medium text-gray-900">{record.name}</span>
|
||
)
|
||
},
|
||
{
|
||
key: 'description',
|
||
title: '描述',
|
||
width: '250px',
|
||
render: (_: any, record: EntryModule) => (
|
||
<span className="text-gray-600">{record.description || '-'}</span>
|
||
)
|
||
},
|
||
{
|
||
key: 'logo',
|
||
title: 'Logo图片',
|
||
width: '150px',
|
||
render: (_: any, record: EntryModule) => {
|
||
if (!record.path) {
|
||
return <span className="text-gray-400">未上传</span>;
|
||
}
|
||
|
||
const logoUrl = `${DOCUMENT_URL}${record.path}`;
|
||
const hasFailed = failedImages.has(record.id!);
|
||
|
||
return (
|
||
<div className="flex items-center">
|
||
{!hasFailed ? (
|
||
<>
|
||
<a
|
||
href={logoUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="ml-2 text-blue-600 hover:underline text-sm"
|
||
>
|
||
<div className="h-10 w-10 bg-gray-100 rounded flex items-center justify-center overflow-hidden">
|
||
<img
|
||
src={logoUrl}
|
||
alt={record.name}
|
||
className="h-full w-full object-contain"
|
||
onError={() => {
|
||
// 使用 React 状态管理加载失败的图片
|
||
setFailedImages(prev => new Set(prev).add(record.id!));
|
||
}}
|
||
/>
|
||
</div>
|
||
</a>
|
||
</>
|
||
) : (
|
||
<div className="flex items-center">
|
||
<div className="h-8 w-8 bg-red-50 border border-red-200 rounded flex items-center justify-center">
|
||
<i className="ri-image-line text-red-400 text-lg"></i>
|
||
</div>
|
||
<span className="ml-2 text-red-500 text-sm">加载失败</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
key: 'areas',
|
||
title: '适用地区',
|
||
width: '200px',
|
||
render: (_: any, record: EntryModule) => (
|
||
<div className="flex flex-wrap gap-1">
|
||
{record.areas && record.areas.length > 0 ? (
|
||
record.areas
|
||
.filter(areaConfig => areaConfig.enabled !== false) // 只显示启用的地区
|
||
.sort((a, b) => a.sort_order - b.sort_order) // 按排序号排序
|
||
.map((areaConfig, index) => (
|
||
<span
|
||
key={index}
|
||
className="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded"
|
||
>
|
||
{areaConfig.area}
|
||
</span>
|
||
))
|
||
) : (
|
||
<span className="text-gray-400">未设置</span>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
{
|
||
key: 'created_at',
|
||
title: '创建时间',
|
||
width: '180px',
|
||
render: (_: any, record: EntryModule) =>
|
||
record.created_at ? new Date(record.created_at).toLocaleString('zh-CN') : '-'
|
||
},
|
||
{
|
||
key: 'actions',
|
||
title: '操作',
|
||
width: '180px',
|
||
render: (_: any, record: EntryModule) => (
|
||
<div className="operations-cell">
|
||
{ canViewModule &&
|
||
<button
|
||
onClick={() => handleEdit(record.id!)}
|
||
className="operation-btn"
|
||
title="查看/编辑入口模块"
|
||
>
|
||
<i className="ri-edit-line"></i> {canUpdateModule ? '编辑' : '查看'}
|
||
</button>
|
||
}
|
||
{canDeleteModule && (
|
||
<button
|
||
type="button"
|
||
className="operation-btn !text-[--color-error]"
|
||
onClick={() => handleDelete(record.id!)}
|
||
disabled={isDeleting}
|
||
title="删除入口模块"
|
||
>
|
||
<i className="ri-delete-bin-line"></i> 删除
|
||
</button>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
];
|
||
|
||
return (
|
||
<div className="entry-modules-page">
|
||
<Card>
|
||
{/* 页面头部 */}
|
||
<div className="page-header">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">入口模块管理</h1>
|
||
<p className="text-sm text-gray-600 mt-1">管理系统入口模块,包括Logo图片和适用地区设置</p>
|
||
</div>
|
||
{/* ✅ 仅在有创建权限时显示新建按钮 */}
|
||
{canCreateModule && (
|
||
<Button
|
||
type="primary"
|
||
icon="ri-add-line"
|
||
to="/entry-modules/new"
|
||
>
|
||
新建入口模块
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 筛选面板 */}
|
||
<FilterPanel onReset={handleReset}>
|
||
<FilterSelect
|
||
label="适用地区"
|
||
name="area"
|
||
value={area}
|
||
options={AREA_OPTIONS}
|
||
onChange={handleFilterChange}
|
||
/>
|
||
<SearchFilter
|
||
label="模块名称"
|
||
placeholder="请输入入口模块名称"
|
||
value={name}
|
||
onSearch={handleNameSearch}
|
||
className="filter-item-wide"
|
||
/>
|
||
</FilterPanel>
|
||
|
||
{/* 表格 */}
|
||
<Table
|
||
columns={columns}
|
||
dataSource={modules || []}
|
||
rowKey="id"
|
||
loading={false}
|
||
emptyText="暂无入口模块数据"
|
||
/>
|
||
|
||
{/* 分页 */}
|
||
{total > 0 && (
|
||
<Pagination
|
||
// pageSizeOptions={[10,20]}
|
||
currentPage={currentPage}
|
||
pageSize={pageSize}
|
||
total={total}
|
||
onChange={handlePageChange}
|
||
onPageSizeChange={handlePageSizeChange}
|
||
/>
|
||
)}
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|