feat: 1.修改提示词模板的不用角色的操作权限。

2. 对接数据看板的数据。
3. 添加入口模块管理的页面。
This commit is contained in:
2025-11-21 17:16:07 +08:00
parent 3850d05bdd
commit dab0835605
13 changed files with 1877 additions and 297 deletions
+405
View File
@@ -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">
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>
);
}