Files
leaudit-platform-frontend/app/routes/entry-modules.new.tsx
T
LiangShiyong d09d5b709d Merge branch 'PingChuan' into shiy-login
# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
2025-11-22 15:57:22 +08:00

410 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 } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { toastService } from "~/components/ui/Toast";
import { Modal } from "~/components/ui/Modal";
import {
getEntryModuleById,
createEntryModule,
updateEntryModule,
type EntryModule
} from "~/api/entry-modules/entry-modules";
import { API_BASE_URL, DOCUMENT_URL } from "~/config/api-config";
// 页面元数据
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;
frontendJWT?: string | null;
}
// 加载函数 - 获取入口模块数据(编辑模式)
export async function loader({ request }: LoaderFunctionArgs) {
try {
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const url = new URL(request.url);
const id = url.searchParams.get('id');
if (id) {
const moduleResponse = await getEntryModuleById(parseInt(id), frontendJWT);
if (moduleResponse.error) {
throw new Error(moduleResponse.error);
}
return Response.json({
module: moduleResponse.data,
frontendJWT
});
}
return Response.json({ frontendJWT });
} catch (error) {
console.error("加载入口模块失败:", error);
return Response.json(
{
error: error || "加载入口模块失败",
status: 500
}
);
}
}
// 地区选项
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, frontendJWT } = useLoaderData<LoaderData>();
const id = searchParams.get('id');
const isEditMode = !!id;
// 表单状态
const [name, setName] = useState(module?.name || '');
const [description, setDescription] = useState(module?.description || '');
const [selectedAreas, setSelectedAreas] = useState<string[]>(module?.areas || []);
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 [showConfirmModal, setShowConfirmModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 处理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;
};
// 上传logo图片
const uploadLogo = async (): Promise<string | null> => {
if (!logoFile) return module?.path || null;
try {
const formData = new FormData();
formData.append('file', logoFile);
formData.append('folder', 'entryModule');
const response = await fetch(`${API_BASE_URL}/admin/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${frontendJWT}`
},
body: formData
});
if (!response.ok) {
throw new Error('图片上传失败');
}
const result = await response.json();
console.log('图片上传结果:', result);
// 根据后端返回的数据结构提取路径
if (result.data?.path) {
return result.data.path;
} else if (result.path) {
return result.path;
} else {
throw new Error('未获取到图片路径');
}
} catch (error) {
console.error('上传logo失败:', error);
throw error;
}
};
// 处理表单提交
const handleSubmit = async () => {
if (!validateForm()) return;
setIsSubmitting(true);
try {
// 上传logo
let logoPath = module?.path || null;
if (logoFile) {
logoPath = await uploadLogo();
}
const moduleData = {
name: name.trim(),
description: description.trim() || undefined,
path: logoPath,
areas: selectedAreas
};
let result;
if (isEditMode) {
result = await updateEntryModule(parseInt(id!), moduleData, frontendJWT);
} else {
result = await createEntryModule(moduleData, frontendJWT);
}
if (result.error) {
toastService.error(result.error);
return;
}
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 = () => {
setShowConfirmModal(true);
};
// 确认取消
const confirmCancel = () => {
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 ? '编辑入口模块' : '新建入口模块'}
</h1>
<p className="text-sm text-gray-600 mt-1">
{isEditMode ? '修改入口模块信息' : '创建新的入口模块'}
</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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</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 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>
</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()}
>
{logoPreview ? '更换图片' : '上传图片'}
</Button>
<span className="text-sm text-gray-500">
JPGPNGGIF 5MB
</span>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleLogoChange}
className="hidden"
/>
{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 cursor-pointer"
>
<input
type="checkbox"
checked={selectedAreas.includes(option.value)}
onChange={() => handleAreaToggle(option.value)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<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>
<Button
type="primary"
onClick={handleSubmit}
loading={isSubmitting}
disabled={isSubmitting}
>
{isSubmitting ? '提交中...' : (isEditMode ? '保存' : '创建')}
</Button>
</div>
</Card>
{/* 取消确认模态框 */}
<Modal
isOpen={showConfirmModal}
onClose={() => setShowConfirmModal(false)}
title="确认取消"
size="small"
footer={
<div className="flex justify-end space-x-3">
<Button
type="default"
onClick={() => setShowConfirmModal(false)}
>
</Button>
<Button
type="primary"
danger
onClick={confirmCancel}
>
</Button>
</div>
}
>
<p className="text-gray-700"></p>
</Modal>
</div>
);
}