feat: 1.修改提示词模板的不用角色的操作权限。
2. 对接数据看板的数据。 3. 添加入口模块管理的页面。
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
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: "新建/编辑入口模块"
|
||||
};
|
||||
|
||||
// 定义加载器返回的数据类型
|
||||
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">
|
||||
支持 JPG、PNG、GIF 格式,大小不超过 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user