Files
leaudit-platform-frontend/app/routes/entry-modules.new.tsx
T
DocAuditAI Dev ebcaf05625 revert: reset to 32bee87 for clean text_bbox baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:14:11 +08:00

399 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from "react";
import { useNavigate, useSearchParams, useLoaderData, ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { toastService } from "~/components/ui/Toast";
import { usePermission } from "~/hooks/usePermission";
import {
getEntryModuleById,
createEntryModule,
updateEntryModule,
uploadEntryModuleImage,
type EntryModule,
type AreaConfig
} from "~/api/entry-modules/entry-modules";
import { DOCUMENT_URL } from "~/config/api-config";
import entryModulesStyles from "~/styles/pages/entry-modules.css?url";
// 引入CSS样式
export function links() {
return [
{ rel: "stylesheet", href: entryModulesStyles }
];
}
// 页面元数据
export const meta: MetaFunction = () => {
return [
{ title: "入口模块编辑 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "创建或编辑入口模块" },
];
};
export const handle = {
breadcrumb: "新建/编辑入口模块",
previousRoute: {
title: "入口模块管理",
to: "/entry-modules"
}
};
// 定义加载器返回的数据类型
interface LoaderData {
module?: EntryModule;
error?: string;
}
// 🔑 客户端加载函数 - 在浏览器端执行,axios-client 会自动添加 JWT
export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
try {
const url = new URL(request.url);
const id = url.searchParams.get('id');
if (id) {
// ✅ 不需要传递 JWTaxios-client 会自动处理
const moduleResponse = await getEntryModuleById(parseInt(id));
if (moduleResponse.error) {
throw new Error(moduleResponse.error);
}
return {
module: moduleResponse.data
};
}
return {};
} catch (error) {
console.error("加载入口模块失败:", error);
return {
error: error instanceof Error ? error.message : "加载入口模块失败"
};
}
}
// 地区选项
const AREA_OPTIONS = [
{ value: "梅州", label: "梅州" },
{ value: "云浮", label: "云浮" },
{ value: "揭阳", label: "揭阳" },
{ value: "潮州", label: "潮州" },
{ value: "省局", label: "省局" }
];
// 入口模块新建/编辑组件
export default function EntryModuleNew() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { module, error } = useLoaderData<LoaderData>();
const id = searchParams.get('id');
const isEditMode = !!id;
// ✅ 使用权限 Hook
const { canCreate, canUpdate } = usePermission();
const canCreateModule = canCreate('entry_module');
const canUpdateModule = canUpdate('entry_module');
// ✅ 根据当前操作类型判断权限
const hasEditPermission = isEditMode ? canUpdateModule : canCreateModule;
const isReadOnly = !hasEditPermission;
// 表单状态
const [name, setName] = useState(module?.name || '');
const [description, setDescription] = useState(module?.description || '');
// 🔑 从 AreaConfig[] 提取地区名称数组
const [selectedAreas, setSelectedAreas] = useState<string[]>(
module?.areas ? module.areas.map(a => a.area) : []
);
const [logoFile, setLogoFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string | null>(
module?.path ? `${DOCUMENT_URL}${module.path}` : null
);
const [isSubmitting, setIsSubmitting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// ✅ 页面加载时检查权限并提示(仅在只读模式下提示)
useEffect(() => {
if (isReadOnly) {
if (isEditMode) {
toastService.info('当前为查看模式,您没有编辑权限');
} else {
toastService.warning('您没有创建入口模块的权限');
}
}
}, [isReadOnly, isEditMode]);
// 处理loader加载数据的时候的错误
useEffect(() => {
if (error) {
toastService.error(error);
}
}, [error]);
// 处理logo文件选择
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
toastService.error('请选择图片文件');
return;
}
// 验证文件大小(限制5MB
if (file.size > 5 * 1024 * 1024) {
toastService.error('图片大小不能超过5MB');
return;
}
setLogoFile(file);
// 生成预览
const reader = new FileReader();
reader.onload = (event) => {
setLogoPreview(event.target?.result as string);
};
reader.readAsDataURL(file);
}
};
// 处理地区选择
const handleAreaToggle = (area: string) => {
setSelectedAreas(prev => {
if (prev.includes(area)) {
return prev.filter(a => a !== area);
} else {
return [...prev, area];
}
});
};
// 验证表单
const validateForm = () => {
if (!name.trim()) {
toastService.error('请输入模块名称');
return false;
}
if (selectedAreas.length === 0) {
toastService.error('请至少选择一个适用地区');
return false;
}
return true;
};
// 处理表单提交
const handleSubmit = async () => {
// ✅ Runtime permission check
if (isEditMode && !canUpdateModule) {
toastService.warning('您没有修改权限,无法保存更改');
return;
}
if (!isEditMode && !canCreateModule) {
toastService.warning('您没有创建权限,无法新增入口模块');
return;
}
if (!validateForm()) return;
setIsSubmitting(true);
try {
// 🔑 步骤1: 先创建或更新模块(不包含图片上传)
// areas 字段会在 API 层自动转换为 AreaConfig[] 格式
const moduleData = {
name: name.trim(),
description: description.trim() || undefined,
// 创建时 path 为 null,编辑时保持原 path(图片上传接口会自动更新)
path: isEditMode ? module?.path : null,
areas: selectedAreas // 字符串数组,API会自动转换
};
// ✅ 不需要传递 JWTaxios-client 会自动处理
let result;
let moduleId: number;
if (isEditMode) {
result = await updateEntryModule(parseInt(id!), moduleData);
moduleId = parseInt(id!);
} else {
result = await createEntryModule(moduleData);
moduleId = result.data!.id!;
}
if (result.error) {
toastService.error(result.error);
return;
}
// 🔑 步骤2: 如果有新的logo文件,调用专用的图片上传接口
// 后端会自动更新数据库中的 path 字段,无需再手动调用 update
if (logoFile) {
const uploadResult = await uploadEntryModuleImage(moduleId, logoFile);
if (uploadResult.error) {
toastService.error(`模块${isEditMode ? '更新' : '创建'}成功,但图片上传失败: ${uploadResult.error}`);
return;
}
console.log('✅ 图片上传成功,访问URL:', uploadResult.data?.url);
}
toastService.success(isEditMode ? '更新成功!' : '创建成功!');
setTimeout(() => {
navigate('/entry-modules');
}, 1000);
} catch (error) {
console.error('提交失败:', error);
toastService.error(error instanceof Error ? error.message : '操作失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 处理取消
const handleCancel = () => {
navigate('/entry-modules');
};
return (
<div className="entry-modules-new-page">
<Card>
{/* 页面头部 */}
<div className="page-header">
<div>
<h1 className="text-2xl font-bold text-gray-900">
{isEditMode ? (isReadOnly ? '查看入口模块' : '编辑入口模块') : '新建入口模块'}
</h1>
<p className="text-sm text-gray-600 mt-1">
{isEditMode ? (isReadOnly ? '查看入口模块信息' : '修改入口模块信息') : '创建新的入口模块'}
</p>
</div>
</div>
{/* 表单内容 */}
<div className="form-content space-y-6 mt-6">
{/* 模块名称 */}
<div className="form-item">
<label className="form-label">
<span className="text-red-500 mr-1">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入模块名称,如:合同管理"
maxLength={255}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
disabled={isReadOnly}
/>
</div>
{/* 描述 */}
<div className="form-item">
<label className="form-label"></label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="请输入模块描述"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
rows={4}
disabled={isReadOnly}
/>
</div>
{/* Logo图片上传 */}
<div className="form-item">
<label className="form-label">Logo图片</label>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<Button
type="default"
icon="ri-upload-line"
onClick={() => fileInputRef.current?.click()}
disabled={isReadOnly}
>
{logoPreview ? '更换图片' : '上传图片'}
</Button>
<span className="text-sm text-gray-500">
JPGPNGGIF 5MB
</span>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleLogoChange}
className="hidden"
disabled={isReadOnly}
/>
{logoPreview && (
<div className="mt-3">
<div className="inline-block border border-gray-300 rounded p-2">
<img
src={logoPreview}
alt="Logo预览"
className="h-24 w-24 object-contain"
/>
</div>
</div>
)}
</div>
</div>
{/* 适用地区 */}
<div className="form-item">
<label className="form-label">
<span className="text-red-500 mr-1">*</span>
</label>
<div className="flex flex-wrap gap-3">
{AREA_OPTIONS.map(option => (
<label
key={option.value}
className={`flex items-center space-x-2 ${isReadOnly ? 'cursor-not-allowed' : 'cursor-pointer'}`}
>
<input
type="checkbox"
checked={selectedAreas.includes(option.value)}
onChange={() => handleAreaToggle(option.value)}
className="w-4 h-4 border-gray-300 rounded cursor-pointer"
disabled={isReadOnly}
/>
<span className="text-sm text-gray-700">{option.label}</span>
</label>
))}
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="form-actions mt-8 flex justify-end space-x-3">
<Button
type="default"
onClick={handleCancel}
disabled={isSubmitting}
>
</Button>
{/* ✅ 仅在有对应权限时显示保存/创建按钮 */}
{hasEditPermission && (
<Button
type="primary"
onClick={handleSubmit}
loading={isSubmitting}
disabled={isSubmitting}
>
{isSubmitting ? '提交中...' : (isEditMode ? '保存' : '创建')}
</Button>
)}
</div>
</Card>
</div>
);
}