418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
|
||
import { useLoaderData, useActionData, Form, useSubmit, useNavigate } from '@remix-run/react';
|
||
import { Button } from '~/components/ui/Button';
|
||
import { Card } from '~/components/ui/Card';
|
||
import { Breadcrumb } from '~/components/layout/Breadcrumb';
|
||
import type { Rule, RuleType, RulePriority } from '~/models/rule';
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "中国烟草AI合同及卷宗审核系统 - 评查规则详情" },
|
||
{ name: "description", content: "评查规则详情编辑页面" }
|
||
];
|
||
};
|
||
|
||
export const handle = {
|
||
breadcrumb: '编辑评查点'
|
||
};
|
||
|
||
interface LoaderData {
|
||
rule: Rule;
|
||
ruleTypes: { label: string; value: RuleType }[];
|
||
rulePriorities: { label: string; value: RulePriority }[];
|
||
groupOptions: { label: string; value: string }[];
|
||
}
|
||
|
||
export async function loader({ params }: LoaderFunctionArgs) {
|
||
const { ruleId } = params;
|
||
|
||
// 判断是否为新建规则
|
||
const isNewRule = ruleId === 'new';
|
||
|
||
// 模拟数据,实际项目中应从API获取
|
||
const rule: Rule = isNewRule ? {
|
||
id: '',
|
||
name: '',
|
||
description: '',
|
||
content: '',
|
||
type: 'text',
|
||
priority: 'medium',
|
||
groupId: '',
|
||
groupName: '',
|
||
isActive: true,
|
||
createdAt: '',
|
||
updatedAt: ''
|
||
} : {
|
||
id: ruleId,
|
||
name: '许可证编号格式检查',
|
||
description: '检查烟草专卖零售许可证编号是否符合"烟零许(年份)序号号"的标准格式',
|
||
content: '许可证编号应当符合"烟零许(年份)序号号"的标准格式,如"烟零许(2023)12345号"',
|
||
type: 'regex',
|
||
priority: 'high',
|
||
groupId: '1',
|
||
groupName: '专卖许可证规则组',
|
||
isActive: true,
|
||
createdAt: '2023-10-15 09:30',
|
||
updatedAt: '2023-12-10 14:20'
|
||
};
|
||
|
||
// 规则类型选项
|
||
const ruleTypes = [
|
||
{ label: '文本匹配', value: 'text' },
|
||
{ label: '正则表达式', value: 'regex' },
|
||
{ label: '数值范围', value: 'range' },
|
||
{ label: '日期检查', value: 'date' },
|
||
{ label: 'AI智能检查', value: 'ai' }
|
||
];
|
||
|
||
// 规则优先级选项
|
||
const rulePriorities = [
|
||
{ label: '低', value: 'low' },
|
||
{ label: '中', value: 'medium' },
|
||
{ label: '高', value: 'high' },
|
||
{ label: '关键', value: 'critical' }
|
||
];
|
||
|
||
// 规则组选项
|
||
const groupOptions = [
|
||
{ label: '专卖许可证规则组', value: '1' },
|
||
{ label: '合同协议规则组', value: '2' },
|
||
{ label: '财务票据规则组', value: '3' },
|
||
{ label: '采购订单规则组', value: '4' },
|
||
{ label: '销售报表规则组', value: '5' }
|
||
];
|
||
|
||
return Response.json({
|
||
rule,
|
||
ruleTypes,
|
||
rulePriorities,
|
||
groupOptions
|
||
});
|
||
}
|
||
|
||
interface ActionData {
|
||
success?: boolean;
|
||
errors?: {
|
||
name?: string;
|
||
description?: string;
|
||
content?: string;
|
||
type?: string;
|
||
priority?: string;
|
||
groupId?: string;
|
||
general?: string;
|
||
};
|
||
}
|
||
|
||
export async function action({ request, params }: ActionFunctionArgs) {
|
||
const { ruleId } = params;
|
||
const formData = await request.formData();
|
||
const isNewRule = ruleId === 'new';
|
||
|
||
// 获取表单数据
|
||
const name = formData.get('name')?.toString() || '';
|
||
const description = formData.get('description')?.toString() || '';
|
||
const content = formData.get('content')?.toString() || '';
|
||
const type = formData.get('type')?.toString() || '';
|
||
const priority = formData.get('priority')?.toString() || '';
|
||
const groupId = formData.get('groupId')?.toString() || '';
|
||
const isActive = formData.get('isActive') === 'true';
|
||
|
||
// 表单验证
|
||
const errors: ActionData['errors'] = {};
|
||
|
||
if (!name.trim()) {
|
||
errors.name = '规则名称不能为空';
|
||
}
|
||
|
||
if (!content.trim()) {
|
||
errors.content = '规则内容不能为空';
|
||
}
|
||
|
||
if (!type) {
|
||
errors.type = '必须选择规则类型';
|
||
}
|
||
|
||
if (!priority) {
|
||
errors.priority = '必须选择规则优先级';
|
||
}
|
||
|
||
if (!groupId) {
|
||
errors.groupId = '必须选择规则所属组';
|
||
}
|
||
|
||
if (Object.keys(errors).length > 0) {
|
||
return Response.json({ errors });
|
||
}
|
||
|
||
// 模拟API保存操作,实际项目中应调用API
|
||
try {
|
||
// 在这里调用API进行保存
|
||
console.log('保存规则:', {
|
||
id: isNewRule ? 'new-id' : ruleId,
|
||
name,
|
||
description,
|
||
content,
|
||
type,
|
||
priority,
|
||
groupId,
|
||
isActive
|
||
});
|
||
|
||
// 成功后重定向到规则列表页
|
||
return redirect('/rules');
|
||
} catch (error) {
|
||
return Response.json({
|
||
errors: {
|
||
general: '保存规则失败,请重试'
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
export default function RuleDetail() {
|
||
const { rule, ruleTypes, rulePriorities, groupOptions } = useLoaderData<typeof loader>();
|
||
const actionData = useActionData<typeof action>();
|
||
const navigate = useNavigate();
|
||
const submit = useSubmit();
|
||
|
||
const [formData, setFormData] = useState({
|
||
name: rule.name,
|
||
description: rule.description,
|
||
content: rule.content,
|
||
type: rule.type,
|
||
priority: rule.priority,
|
||
groupId: rule.groupId,
|
||
isActive: rule.isActive
|
||
});
|
||
|
||
const isNewRule = !rule.id;
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value } = e.target;
|
||
setFormData(prev => ({ ...prev, [name]: value }));
|
||
};
|
||
|
||
const handleSwitchChange = (name: string, checked: boolean) => {
|
||
setFormData(prev => ({ ...prev, [name]: checked }));
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
navigate('/rules');
|
||
};
|
||
|
||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
|
||
// 使用useSubmit提交表单
|
||
const formElement = e.currentTarget;
|
||
submit(formElement, { method: 'post' });
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<Breadcrumb
|
||
items={[
|
||
{ title: '评查规则', to: '/rules' },
|
||
{ title: isNewRule ? '新增规则' : '编辑规则', to: `/rules/${rule.id}` }
|
||
]}
|
||
/>
|
||
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h2 className="text-xl font-medium">{isNewRule ? '新增评查规则' : '编辑评查规则'}</h2>
|
||
</div>
|
||
|
||
<Card>
|
||
<Form method="post" onSubmit={handleSubmit}>
|
||
{actionData?.errors?.general && (
|
||
<div className="error-message mb-4">
|
||
<i className="ri-error-warning-line mr-1"></i>
|
||
{actionData.errors.general}
|
||
</div>
|
||
)}
|
||
|
||
<div className="form-section mb-6">
|
||
<h3 className="form-section-title">基本信息</h3>
|
||
|
||
<div className="form-row">
|
||
<div className="form-group col-span-6">
|
||
<label htmlFor="name" className="form-label required">规则名称</label>
|
||
<input
|
||
type="text"
|
||
id="name"
|
||
name="name"
|
||
className={`form-input ${actionData?.errors?.name ? 'error' : ''}`}
|
||
value={formData.name}
|
||
onChange={handleChange}
|
||
required
|
||
/>
|
||
{actionData?.errors?.name && (
|
||
<div className="form-error">{actionData.errors.name}</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="form-group col-span-6">
|
||
<label htmlFor="groupId" className="form-label required">所属规则组</label>
|
||
<select
|
||
id="groupId"
|
||
name="groupId"
|
||
className={`form-select ${actionData?.errors?.groupId ? 'error' : ''}`}
|
||
value={formData.groupId}
|
||
onChange={handleChange}
|
||
required
|
||
>
|
||
<option value="">选择规则组</option>
|
||
{groupOptions.map((option: { value: string; label: string }) => (
|
||
<option key={option.value} value={option.value}>{option.label}</option>
|
||
))}
|
||
</select>
|
||
{actionData?.errors?.groupId && (
|
||
<div className="form-error">{actionData.errors.groupId}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<div className="form-group col-span-12">
|
||
<label htmlFor="description" className="form-label">规则描述</label>
|
||
<textarea
|
||
id="description"
|
||
name="description"
|
||
className="form-textarea"
|
||
rows={3}
|
||
value={formData.description}
|
||
onChange={handleChange}
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-section mb-6">
|
||
<h3 className="form-section-title">规则设置</h3>
|
||
|
||
<div className="form-row">
|
||
<div className="form-group col-span-4">
|
||
<label htmlFor="type" className="form-label required">规则类型</label>
|
||
<select
|
||
id="type"
|
||
name="type"
|
||
className={`form-select ${actionData?.errors?.type ? 'error' : ''}`}
|
||
value={formData.type}
|
||
onChange={handleChange}
|
||
required
|
||
>
|
||
<option value="">选择规则类型</option>
|
||
{ruleTypes.map((option: { value: string; label: string }) => (
|
||
<option key={option.value} value={option.value}>{option.label}</option>
|
||
))}
|
||
</select>
|
||
{actionData?.errors?.type && (
|
||
<div className="form-error">{actionData.errors.type}</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="form-group col-span-4">
|
||
<label htmlFor="priority" className="form-label required">规则优先级</label>
|
||
<select
|
||
id="priority"
|
||
name="priority"
|
||
className={`form-select ${actionData?.errors?.priority ? 'error' : ''}`}
|
||
value={formData.priority}
|
||
onChange={handleChange}
|
||
required
|
||
>
|
||
<option value="">选择优先级</option>
|
||
{rulePriorities.map((option: { value: string; label: string }) => (
|
||
<option key={option.value} value={option.value}>{option.label}</option>
|
||
))}
|
||
</select>
|
||
{actionData?.errors?.priority && (
|
||
<div className="form-error">{actionData.errors.priority}</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="form-group col-span-4">
|
||
<label htmlFor="isActive" className="form-label">状态</label>
|
||
<div className="flex items-center h-10 mt-1">
|
||
<label className="switch" aria-label="切换规则状态">
|
||
<input
|
||
type="checkbox"
|
||
name="isActive"
|
||
checked={formData.isActive}
|
||
onChange={(e) => handleSwitchChange('isActive', e.target.checked)}
|
||
/>
|
||
<span className="slider round"></span>
|
||
</label>
|
||
<input type="hidden" name="isActive" value={formData.isActive ? 'true' : 'false'} />
|
||
<span className="ml-2">{formData.isActive ? '启用' : '禁用'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<div className="form-group col-span-12">
|
||
<label htmlFor="content" className="form-label required">规则内容</label>
|
||
<textarea
|
||
id="content"
|
||
name="content"
|
||
className={`form-textarea code-editor ${actionData?.errors?.content ? 'error' : ''}`}
|
||
rows={8}
|
||
value={formData.content}
|
||
onChange={handleChange}
|
||
required
|
||
></textarea>
|
||
{actionData?.errors?.content && (
|
||
<div className="form-error">{actionData.errors.content}</div>
|
||
)}
|
||
{formData.type === 'regex' && (
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
<i className="ri-information-line mr-1"></i>
|
||
输入正则表达式,用于匹配文档内容
|
||
</div>
|
||
)}
|
||
{formData.type === 'ai' && (
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
<i className="ri-information-line mr-1"></i>
|
||
请使用自然语言描述规则检查的要求,AI将自动理解并执行检查
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-section mb-6">
|
||
<h3 className="form-section-title">测试工具</h3>
|
||
|
||
<div className="p-4 bg-gray-50 rounded-md">
|
||
<div className="mb-4">
|
||
<label htmlFor="testContent" className="form-label">测试内容</label>
|
||
<textarea
|
||
id="testContent"
|
||
className="form-textarea"
|
||
rows={4}
|
||
placeholder="粘贴待测试的文本内容..."
|
||
></textarea>
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
<Button type="default">
|
||
<i className="ri-test-tube-line mr-1"></i>
|
||
测试规则
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-2">
|
||
<Button type="default" onClick={handleCancel}>
|
||
取消
|
||
</Button>
|
||
<Button type="primary">
|
||
{isNewRule ? '创建规则' : '保存修改'}
|
||
</Button>
|
||
</div>
|
||
</Form>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|