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; accessDenied?: boolean; } function resolveModuleLogoUrl(path?: string | null): string | null { if (!path) { return null; } if (path.startsWith("/images/") || path.startsWith("http://") || path.startsWith("https://")) { return path; } return `${DOCUMENT_URL}${path}`; } // 🔑 客户端加载函数 - 在浏览器端执行,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, accessDenied: false, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "加载入口模块列表失败"; console.error("加载入口模块列表失败:", error); return { modules: [], total: 0, pageSize: 10, currentPage: 1, error: errorMessage, accessDenied: errorMessage.includes('无权限') || errorMessage.includes('权限') || errorMessage.includes('403'), }; } } // 地区选项 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>(new Set()); // 获取加载器数据 const loaderData = useLoaderData(); const { modules, total, error, accessDenied } = 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) { if (accessDenied) { toastService.warning('您当前只有入口模块页面可见权限,没有列表读取权限'); return; } toastService.error(error); } }, [error, accessDenied]); // 处理名称搜索 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) => { 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) => ( {record.name} ) }, { key: 'description', title: '描述', width: '250px', render: (_: any, record: EntryModule) => ( {record.description || '-'} ) }, { key: 'logo', title: 'Logo图片', width: '150px', render: (_: any, record: EntryModule) => { const logoUrl = resolveModuleLogoUrl(record.path); if (!logoUrl) { return 未上传; } const hasFailed = failedImages.has(record.id!); return (
{!hasFailed ? ( <>
{record.name} { // 使用 React 状态管理加载失败的图片 setFailedImages(prev => new Set(prev).add(record.id!)); }} />
) : (
加载失败
)}
); } }, { key: 'areas', title: '适用地区', width: '200px', render: (_: any, record: EntryModule) => (
{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) => ( {areaConfig.area} )) ) : ( 未设置 )}
) }, { 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) => (
{ canViewModule && } {canDeleteModule && ( )}
) } ]; return (
{/* 页面头部 */}

入口模块管理

管理系统入口模块,包括Logo图片和适用地区设置

{/* ✅ 仅在有创建权限时显示新建按钮 */} {canCreateModule && ( )}
{/* 筛选面板 */} {accessDenied ? (
当前账号没有入口模块列表权限
请在“角色权限管理”中为当前角色授予 `entry_module:list:read` 及相关权限后再访问。
) : ( )} {/* 分页 */} {!accessDenied && total > 0 && ( )} ); }