合并评查点新增代码

This commit is contained in:
2025-04-13 15:09:01 +08:00
parent 414fb8ebac
commit 60680cd4bf
11 changed files with 772 additions and 941 deletions
+526
View File
@@ -1,6 +1,27 @@
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
/**
* 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 评查点列表查询参数
*/
@@ -927,4 +948,509 @@ export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGr
status: 500
};
}
}
// 定义提取配置类型
interface ExtractionConfigType {
llm: {
fields: string[];
prompt_setting: {
type: string;
template: string;
};
};
vlm: {
fields: Array<string | { name: string; type: string }>;
prompt_setting: {
type: string;
template: string;
};
};
regex: {
fields: Array<{ field: string; pattern: string }>;
};
}
// 定义转换后的评查点数据类型
interface FormattedEvaluationPoint {
id: number; // 修改为只接受 number 类型
name: string;
code: string;
risk: string;
is_enabled: boolean;
description: string;
references_laws: Record<string, unknown> | null;
evaluation_point_groups_pid: number | null;
evaluation_point_groups_id: number | null;
extraction_config: ExtractionConfigType;
evaluation_config: {
logicType: string;
customLogic: string;
rules: Array<{
id: string;
type: string;
config: Record<string, unknown>;
}>;
};
pass_message: string;
fail_message: string;
suggestion_message: string;
suggestion_message_type: string;
post_action: string;
action_config: string;
score: number;
}
/**
* 将 API 返回的数据格式转换为前端需要的格式
* @param apiRule API 返回的评查点数据
* @returns 转换后的前端格式数据
*/
export function convertApiRuleToFormData(apiRule: ApiRule): FormattedEvaluationPoint {
// 提取旧格式的字段数据
const extractFields = (): ExtractionConfigType => {
const fields: ExtractionConfigType = {
llm: { fields: [], prompt_setting: { type: 'system', template: '' } },
vlm: { fields: [], prompt_setting: { type: 'system', template: '' } },
regex: { fields: [] }
};
if (!apiRule.extraction_config) return fields;
// 处理不同的字段类型
if (apiRule.extraction_config.type === 'OCR+LLM' || apiRule.extraction_config.type === 'LLM') {
fields.llm.fields = apiRule.extraction_config.fields || [];
if (apiRule.extraction_config.prompt_setting) {
fields.llm.prompt_setting = {
type: apiRule.extraction_config.prompt_setting.type || 'system',
template: apiRule.extraction_config.prompt_setting.template || ''
};
}
} else if (apiRule.extraction_config.type === 'VLM') {
fields.vlm.fields = apiRule.extraction_config.fields || [];
if (apiRule.extraction_config.prompt_setting) {
fields.vlm.prompt_setting = {
type: apiRule.extraction_config.prompt_setting.type || 'system',
template: apiRule.extraction_config.prompt_setting.template || ''
};
}
} else if (apiRule.extraction_config.type === 'REGEX') {
// 将字符串字段转换为 regex 格式对象
fields.regex.fields = apiRule.extraction_config.fields.map(field => ({
field,
pattern: ''
}));
}
return fields;
};
// 转换后的数据
const formattedData: FormattedEvaluationPoint = {
id: typeof apiRule.id === 'string' ? parseInt(apiRule.id, 10) : apiRule.id, // 确保 id 是数字
name: apiRule.name,
code: apiRule.code,
risk: apiRule.risk,
is_enabled: apiRule.is_enabled,
description: apiRule.description,
references_laws: apiRule.references_laws,
evaluation_point_groups_pid: apiRule.evaluation_point_groups?.first_name ? null : null,
evaluation_point_groups_id: apiRule.evaluation_point_groups_id,
extraction_config: extractFields(),
evaluation_config: {
logicType: apiRule.evaluation_config?.logicType || 'and',
customLogic: '',
rules: apiRule.evaluation_config?.rules || []
},
pass_message: apiRule.pass_message || '',
fail_message: apiRule.fail_message || '',
suggestion_message: apiRule.suggestion_message || '',
suggestion_message_type: apiRule.suggestion_message_type || 'warning',
post_action: apiRule.post_action || 'none',
action_config: apiRule.action_config || '',
score: 0
};
return formattedData;
}
/**
* 获取单个评查点数据
* @param id 评查点ID
* @returns 评查点数据
*/
export async function getEvaluationPoint(id: number): Promise<{
data?: FormattedEvaluationPoint;
error?: string;
status?: number;
}> {
try {
console.log(`获取评查点数据,ID: ${id}`);
// 使用 postgrestGet 替代直接调用 fetch
const postgrestParams: PostgrestParams = {
select: `*`,
filter: {
'id': `eq.${id}`
}
};
const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('evaluation_points', postgrestParams);
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<ApiRule[]>(response.data);
if (extractedData && Array.isArray(extractedData) && extractedData.length > 0) {
// 转换数据为前端格式
const formattedData = convertApiRuleToFormData(extractedData[0]);
return { data: formattedData };
} else {
return {
error: '获取数据失败: 返回数据为空',
status: 404
};
}
} catch (error) {
console.error('获取评查点数据失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点数据失败',
status: 500
};
}
}
/**
* 获取评查点组数据
* @returns 评查点组列表
*/
export async function getEvaluationPointGroups(): Promise<{
data?: Array<{
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
created_at: string;
updated_at: string;
}>;
error?: string;
status?: number;
}> {
try {
console.log("获取评查点组数据");
// 使用 postgrestGet 替代直接调用 fetch
const postgrestParams: PostgrestParams = {
select: `*`
};
// 定义评查点组类型
type EvaluationPointGroupType = {
id: number;
pid: number;
code: string;
name: string;
description: string;
is_enabled: boolean;
created_at?: string;
updated_at?: string;
};
const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('evaluation_point_groups', postgrestParams);
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<EvaluationPointGroupType[]>(response.data);
if (extractedData) {
// 确保每个组都有 created_at 和 updated_at 字段
const currentTime = new Date().toISOString();
const formattedGroups = extractedData.map(group => ({
...group,
created_at: group.created_at || currentTime,
updated_at: group.updated_at || currentTime
}));
return { data: formattedGroups };
} else {
return {
error: '获取评查点组数据失败: 返回数据为空',
status: 404
};
}
} catch (error) {
console.error('获取评查点组数据失败:', error);
return {
error: error instanceof Error ? error.message : '获取评查点组数据失败',
status: 500
};
}
}
// 用于评查点输入的接口
interface EvaluationPointInput {
id?: number | string;
name?: string;
code?: string;
risk?: string;
is_enabled?: boolean;
description?: string;
references_laws?: Record<string, unknown> | null;
evaluation_point_groups_pid?: number | null;
evaluation_point_groups_id?: number | null;
extraction_config?: {
llm?: {
fields?: string[];
prompt_setting?: {
type?: string;
template?: string;
};
};
vlm?: {
fields?: Array<string | { name: string, type: string }>;
prompt_setting?: {
type?: string;
template?: string;
};
};
regex?: {
fields?: Array<{ field: string, pattern: string }>;
};
};
evaluation_config?: {
logicType?: string;
customLogic?: string;
rules?: Array<{
id: string;
type: string;
config: Record<string, unknown>;
}>;
};
pass_message?: string;
fail_message?: string;
suggestion_message?: string;
suggestion_message_type?: string;
post_action?: string;
action_config?: string;
score?: number;
}
/**
* 保存评查点数据
* @param evaluationPoint 评查点数据
* @param isEditMode 是否为编辑模式
* @returns 保存结果
*/
export async function saveEvaluationPoint(evaluationPoint: EvaluationPointInput, isEditMode: boolean): Promise<{
data?: FormattedEvaluationPoint[];
error?: string;
status?: number;
}> {
try {
console.log(`${isEditMode ? '更新' : '创建'}评查点数据`);
// 创建一个符合数据库模式的数据副本
const cleanedData = {
id: evaluationPoint.id,
name: evaluationPoint.name?.trim(),
code: evaluationPoint.code?.trim(),
risk: evaluationPoint.risk || 'low',
is_enabled: evaluationPoint.is_enabled !== undefined ? evaluationPoint.is_enabled : true,
description: evaluationPoint.description || '',
references_laws: evaluationPoint.references_laws || null,
evaluation_point_groups_pid: evaluationPoint.evaluation_point_groups_pid || null,
evaluation_point_groups_id: evaluationPoint.evaluation_point_groups_id || null,
extraction_config: {
llm: {
fields: Array.isArray(evaluationPoint.extraction_config?.llm?.fields) ?
[...evaluationPoint.extraction_config.llm.fields] : [],
prompt_setting: {
type: evaluationPoint.extraction_config?.llm?.prompt_setting?.type || 'system',
template: evaluationPoint.extraction_config?.llm?.prompt_setting?.template || ''
}
},
vlm: {
fields: Array.isArray(evaluationPoint.extraction_config?.vlm?.fields) ?
[...evaluationPoint.extraction_config.vlm.fields] : [],
prompt_setting: {
type: evaluationPoint.extraction_config?.vlm?.prompt_setting?.type || 'system',
template: evaluationPoint.extraction_config?.vlm?.prompt_setting?.template || ''
}
},
regex: {
fields: Array.isArray(evaluationPoint.extraction_config?.regex?.fields) ?
[...evaluationPoint.extraction_config.regex.fields] : []
}
},
evaluation_config: {
logicType: evaluationPoint.evaluation_config?.logicType || 'and',
customLogic: evaluationPoint.evaluation_config?.customLogic || '',
rules: Array.isArray(evaluationPoint.evaluation_config?.rules) ?
evaluationPoint.evaluation_config.rules.map((rule) => ({
id: rule.id || '1',
type: rule.type || '',
config: rule.config || {}
})) : []
},
pass_message: evaluationPoint.pass_message || '文档检查通过,符合规范要求。',
fail_message: evaluationPoint.fail_message || '文档存在以下问题,请修改后重新提交。',
suggestion_message: evaluationPoint.suggestion_message || '',
suggestion_message_type: evaluationPoint.suggestion_message_type || 'warning',
post_action: evaluationPoint.post_action || 'none',
action_config: evaluationPoint.action_config || '',
score: evaluationPoint.score !== undefined ? Number(evaluationPoint.score) : 0
};
// 如果是新建模式,则删除id字段
if (!isEditMode) {
delete cleanedData.id;
}
// 确保配置对象中的规则配置被正确处理
if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) {
cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules
.filter(rule => rule && rule.type) // 确保规则有类型
.map(rule => {
// 根据规则类型确保config中有必要的字段
const config = { ...rule.config };
// 移除辅助字段(同rules.new.tsx中的逻辑)
switch (rule.type) {
case 'exists':
if (!Array.isArray(config.fields)) config.fields = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.selectedFields;
delete config.existsLogic;
break;
case 'consistency':
if (!Array.isArray(config.pairs)) config.pairs = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.logicRelation;
delete config.initialSourceField;
delete config.initialTargetField;
delete config.initialCompareMethod;
break;
case 'format':
if (!config.field) config.field = '';
if (!config.formatType) config.formatType = 'date';
if (!config.parameters) config.parameters = '';
delete config.availableFields;
delete config.checkField;
delete config.formatParams;
break;
case 'logic':
if (!Array.isArray(config.conditions)) config.conditions = [];
if (!config.logic) config.logic = 'and';
delete config.availableFields;
delete config.logicRelation;
delete config.initialField;
delete config.initialOperator;
delete config.initialValue;
break;
case 'regex':
if (!config.field) config.field = '';
if (!config.pattern) config.pattern = '';
if (!config.matchType) config.matchType = 'match';
delete config.availableFields;
delete config.checkField;
delete config.regexPattern;
break;
case 'ai':
if (!config.model) config.model = 'qwen14b';
if (typeof config.temperature !== 'number') config.temperature = 0.1;
if (!config.prompt) config.prompt = '';
delete config.availableFields;
break;
case 'code':
if (!config.language) config.language = 'javascript';
if (!config.code) config.code = '';
delete config.availableFields;
break;
}
return {
id: rule.id,
type: rule.type,
config
};
});
}
console.log("准备发送到API的数据大小:", JSON.stringify(cleanedData).length, "字节");
// 使用 postgrest-client 替代直接 fetch 调用
let response;
if (isEditMode) {
// 更新操作
response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
`evaluation_points`,
cleanedData,
{id: cleanedData.id!}
);
} else {
// 创建操作
response = await postgrestPost<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>(
'evaluation_points',
cleanedData
);
}
// 处理错误响应
if (response.error) {
return {
error: response.error,
status: response.status
};
}
// 使用 extractApiData 统一处理响应数据
const extractedData = extractApiData<ApiRule | ApiRule[]>(response.data);
if (extractedData) {
// 转换数据格式后返回
if (Array.isArray(extractedData)) {
return {
data: extractedData.map(rule => convertApiRuleToFormData(rule))
};
} else {
return {
data: [convertApiRuleToFormData(extractedData)]
};
}
} else {
return {
error: `${isEditMode ? '更新' : '创建'}评查点数据失败: 返回数据为空`,
status: 404
};
}
} catch (error) {
console.error("保存评查点失败:", error);
return {
error: error instanceof Error ? error.message : '保存评查点失败',
status: 500
};
}
}
+79
View File
@@ -51,4 +51,83 @@ export async function uploadFileToServer(
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : '上传失败' };
}
}
/**
* 上传文件到文档审核系统
*/
export async function uploadDocumentToServer(
binaryData: ArrayBuffer,
fileName: string,
fileType: string,
typeId: string | number,
priority: string,
documentNumber?: string | null,
remark?: string | null,
isTestDocument: boolean = false
): Promise<{
success: boolean;
result?: {
id: number;
file_name: string;
file_size: number;
file_url: string;
type_id: number;
type_description: string;
document_number: string | null;
storage_type: string;
is_test_document: boolean;
remark: string | null;
background_processing: boolean;
evaluation_level: string;
};
error: string | null;
}> {
try {
// 创建FormData对象
const formData = new FormData();
// 将二进制数据转换为Blob并添加到FormData
const blob = new Blob([binaryData], { type: fileType });
formData.append('file', blob, fileName);
// 将信息添加到一个JSON对象中
const uploadInfo = {
type_id: Number(typeId),
evaluation_level: priority,
document_number: documentNumber || null,
remark: remark || null,
is_test_document: isTestDocument
};
// 添加JSON字符串到FormData
formData.append('upload_info', JSON.stringify(uploadInfo));
// 发送请求
const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', {
method: 'POST',
headers: {
'X-File-Name': encodeURIComponent(fileName)
},
body: formData
});
if (!response.ok) {
const errorText = await response.text();
console.error(`上传失败 (${response.status}): ${errorText}`);
return {
success: false,
error: `上传失败: ${response.status} ${response.statusText}`
};
}
const data = await response.json();
return data;
} catch (error) {
console.error('上传错误:', error);
return {
success: false,
error: error instanceof Error ? error.message : '上传失败'
};
}
}
+1 -1
View File
@@ -503,7 +503,7 @@ function preprocessData(data: Record<string, unknown>): Record<string, unknown>
export async function postgrestPut<T, D extends object>(
endpoint: string,
data: D,
filters?: Record<string, string>
filters?: Record<string | number, string | number>
): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
try {
// 确保端点没有前导斜杠
+1 -1
View File
@@ -84,7 +84,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
{
id: 'rule-new',
title: '新增评查点',
path: '/rules/new',
path: '/rules-new',
icon: 'ri-add-circle-line'
},
{
+2 -2
View File
@@ -238,7 +238,7 @@ export function ReviewSettings({
useEffect(() => {
// 如果已经初始化过,则跳过此次处理
if (initializedRef.current) {
console.log("ReviewSettings已初始化,跳过后续初始化处理");
// console.log("ReviewSettings已初始化,跳过后续初始化处理");
return;
}
@@ -1717,7 +1717,7 @@ export function ReviewSettings({
useEffect(() => {
// 如果有初始数据,在组件挂载后主动发送一次完整规则配置
if (initialDataRef.current && onChange) {
console.log("组件挂载后发送初始完整配置");
// console.log("组件挂载后发送初始完整配置");
setTimeout(() => generateEvaluationConfig(), 100);
}
}, [generateEvaluationConfig, onChange]);
+71 -83
View File
@@ -9,6 +9,7 @@ import { FileProgress} from "~/components/ui/FileProgress";
import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps";
import uploadStyles from "~/styles/pages/files_upload.css?url";
import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload";
import { uploadFileToBinary, uploadDocumentToServer } from "~/api/files";
export function links() {
return [
@@ -109,99 +110,34 @@ interface FileUploadResponse {
error: string | null;
}
// 将文件转换为二进制数据
async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
reject(new Error('无法将文件转换为二进制格式'));
}
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
// 读取文件为 ArrayBuffer (二进制格式)
reader.readAsArrayBuffer(file);
});
}
// 模拟上传文件到服务器的API
async function uploadFileToServer(
binaryData: ArrayBuffer,
fileName: string,
fileType: string,
documentType: FileType,
priority: Priority
priority: Priority,
documentNumber: string | null,
remark: string | null,
isTestDocument: boolean
): Promise<FileUploadResponse> {
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
try {
// 创建FormData对象,将文件和其他信息一起提交
const formData = new FormData();
// 使用封装的上传函数
const response = await uploadDocumentToServer(
binaryData,
fileName,
fileType,
documentType,
PRIORITY_TO_CHINESE[priority],
documentNumber,
remark,
isTestDocument
);
// 将二进制数据转换为Blob并添加到FormData
const blob = new Blob([binaryData], { type: fileType });
formData.append('file', blob, fileName);
// 将 type_id 和 priority 添加到一个JSON对象中
const uploadInfo = {
type_id: Number(documentType), // 确保 type_id 是数字值
evaluation_level: PRIORITY_TO_CHINESE[priority] // 转换为中文优先级
};
// 添加 JSON 字符串到 FormData
formData.append('upload_info', JSON.stringify(uploadInfo));
// 创建HTTP请求的参数
const requestParams = {
method: 'POST',
url: 'http://172.16.0.55:8000/admin/documents/upload',
headers: {
// FormData会自动设置Content-Type为multipart/form-data
'X-File-Name': encodeURIComponent(fileName)
},
body: formData
};
// 打印 FormData 内容的正确方式
console.log('[模拟API] 请求参数:', {
url: requestParams.url,
headers: requestParams.headers,
uploadInfo, // 直接打印原始对象更有帮助
formDataEntries: Array.from(formData.entries()).map(([key, value]) => {
if (!(value instanceof Blob)) {
return { key, value };
}
}),
fileName
});
// 实际API调用 - 在生产环境中实现
const response = await fetch(requestParams.url, {
method: requestParams.method,
headers: requestParams.headers,
body: requestParams.body
});
if (!response.ok) {
// 获取更多错误信息
const errorText = await response.text();
console.error(`上传失败 (${response.status}): ${errorText}`);
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
return response;
} catch (error) {
console.error('[模拟API] 上传错误:', error);
return {
@@ -318,8 +254,11 @@ export default function FilesUpload() {
const { documents, documentTypes } = useLoaderData<LoaderData>();
// 状态管理
const [isTestDocument, setIsTestDocument] = useState(false);
const [fileType, setFileType] = useState<FileType | "">("");
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
const [documentNumber, setDocumentNumber] = useState<string>("");
const [remark, setRemark] = useState<string>("");
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
@@ -506,7 +445,10 @@ export default function FilesUpload() {
file.name,
file.type,
fileType as FileType,
priority
priority,
documentNumber || null,
remark || null,
isTestDocument
);
if (!response.success || !response.result) {
@@ -801,6 +743,8 @@ export default function FilesUpload() {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 格式化文件大小显示
const formatFileSize = (bytes: number) => {
@@ -920,7 +864,7 @@ export default function FilesUpload() {
<Form method="post" encType="multipart/form-data" ref={formRef}>
{/* 文件类型选择 */}
<Card title={<h3></h3>} className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="form-group">
<label htmlFor="file-type-select" className="form-label"> <span className="text-red-500">*</span></label>
<select
@@ -959,6 +903,37 @@ export default function FilesUpload() {
</select>
<div className="form-tip"></div>
</div>
<div className="form-group">
<label htmlFor="docNumber" className="form-label"></label>
<input
type="text"
id="docNumber"
name="docNumber"
className="form-input w-full"
placeholder="请输入合同编号、许可证号等"
value={documentNumber}
onChange={(e) => setDocumentNumber(e.target.value)}
disabled={uploadStage !== "idle"}
/>
<div className="form-tip"></div>
</div>
</div>
<div className="grid grid-cols-1 mt-4">
<div className="form-group">
<label className="form-label" htmlFor="docRemark">
</label>
<textarea
id="docRemark"
name="docRemark"
className="form-textarea w-full"
placeholder="可输入文档的相关描述或备注信息"
rows={2}
value={remark}
onChange={(e) => setRemark(e.target.value)}
disabled={uploadStage !== "idle"}
></textarea>
</div>
</div>
</Card>
@@ -975,6 +950,17 @@ export default function FilesUpload() {
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
shouldPreventFileSelect={!fileType}
/>
<div className="switch-container">
<label className="switch" aria-label="标记为测试文档">
<input
type="checkbox"
checked={isTestDocument}
onChange={e => setIsTestDocument(e.target.checked)}
/>
<span className="slider"></span>
</label>
<span></span>
</div>
</>
)}
@@ -1082,6 +1068,8 @@ export default function FilesUpload() {
</div>
</div>
)}
</Card>
</Form>
-103
View File
@@ -1,103 +0,0 @@
import { type MetaFunction, LinksFunction } from "@remix-run/node";
import { useState } from "react";
import { BasicInfo } from "~/components/rules/new/BasicInfo";
import { ExtractionSettings } from "~/components/rules/new/ExtractionSettings";
import { ReviewSettings, RuleContext } from "~/components/rules/new/ReviewSettings";
import { ActionButtons } from "~/components/rules/new/ActionButtons";
import { PageHeader } from "~/components/rules/new/PageHeader";
import rulesStyles from "~/styles/rules.css";
export const meta: MetaFunction = () => {
return [
{ title: "新增评查点 - 中国烟草AI合同及卷宗审核系统" },
{
name: "description",
content: "创建新的评查点,设置规则参数"
}
];
};
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: rulesStyles }
];
export const handle = {
breadcrumb: "新增评查点"
};
export default function RuleNew() {
// 用于保存抽取字段的状态,在抽取设置和评查设置组件之间共享
const [extractionFields, setExtractionFields] = useState<string[]>([]);
const updateExtractionFields = (fields: string[]) => {
setExtractionFields(fields);
};
const handleSave = () => {
// 实现保存逻辑
console.log('保存评查点');
};
const handleSaveDraft = () => {
// 实现保存草稿逻辑
console.log('保存为草稿');
};
const handleExtractionChange = (data: Record<string, unknown>) => {
// 使用合并后的所有字段列表
if (data.allFields && Array.isArray(data.allFields)) {
updateExtractionFields(data.allFields);
return;
}
// 旧版本兼容逻辑
if (data.fields) {
// 尝试获取合并的字段列表
if (Array.isArray(data.fields)) {
updateExtractionFields(data.fields);
} else {
const fieldData = data.fields as Record<string, string[]>;
const currentMethod = data.extractionMethod as string;
// 提取当前抽取方法的字段
if (fieldData[currentMethod]) {
updateExtractionFields(fieldData[currentMethod]);
}
}
} else if (data.regexFields) {
// 处理正则字段情况
const regexFields = data.regexFields as { id: string; fieldName: string; regex: string }[];
const fieldNames = regexFields
.map(field => field.fieldName)
.filter(name => name.trim() !== '');
updateExtractionFields(fieldNames);
}
};
return (
<RuleContext.Provider value={{ extractionFields, updateExtractionFields }}>
<div className="px-4 py-6 bg-white border-b border-gray-200 shadow-sm">
<PageHeader
title="新增评查点"
onSave={handleSave}
/>
</div>
<div className="container py-6">
<div className="px-4">
<BasicInfo />
<ExtractionSettings onChange={handleExtractionChange} />
<ReviewSettings />
<ActionButtons
onSave={handleSave}
onSaveDraft={handleSaveDraft}
/>
</div>
</div>
</RuleContext.Provider>
);
}
@@ -36,18 +36,14 @@ import rulesStyles from "~/styles/rules.css?url";
import { useNavigate, useLocation } from "@remix-run/react";
// 导入评查点模型定义和常量
import type {
EvaluationPoint,
LogicOperator,
CompareMethod,
FormatType,
ComparisonOperator,
MatchType,
ProgrammingLanguage
EvaluationPoint
} from "~/models/evaluation_points";
import { EVALUATION_OPTIONS } from "~/models/evaluation_points";
import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
// 导入RuleContext上下文
import { RuleContext } from "~/contexts/RuleContext";
// 导入API函数
import { getEvaluationPoint, getEvaluationPointGroups, saveEvaluationPoint } from "~/api/evaluation_points/rules";
export const meta: MetaFunction = () => {
return [
@@ -59,76 +55,14 @@ export const meta: MetaFunction = () => {
];
};
export function links() {
return [{ rel: "stylesheet", href: rulesStyles }];
}
export const handle = {
breadcrumb: "评查点管理"
};
// 添加规则配置接口
interface BaseRuleConfig {
availableFields?: string[];
export function links() {
return [{ rel: "stylesheet", href: rulesStyles }];
}
interface ExistsRuleConfig extends BaseRuleConfig {
fields: string[];
logic: LogicOperator;
selectedFields?: string[];
existsLogic?: string;
}
interface ConsistencyRuleConfig extends BaseRuleConfig {
pairs: Array<{sourceField: string; targetField: string; compareMethod: CompareMethod}>;
logic: LogicOperator;
logicRelation?: string;
initialSourceField?: string;
initialTargetField?: string;
initialCompareMethod?: string;
}
interface FormatRuleConfig extends BaseRuleConfig {
field: string;
formatType: FormatType;
parameters: string;
checkField?: string;
formatParams?: string;
}
interface LogicRuleConfig extends BaseRuleConfig {
conditions: {
field: string;
operator: ComparisonOperator;
value: string;
}[];
logic: LogicOperator;
logicRelation?: string;
initialField?: string;
initialOperator?: string;
initialValue?: string;
}
interface RegexRuleConfig extends BaseRuleConfig {
field: string;
pattern: string;
matchType: MatchType;
checkField?: string;
regexPattern?: string;
}
interface AIRuleConfig extends BaseRuleConfig {
model: string;
temperature: number;
prompt: string;
}
interface CodeRuleConfig extends BaseRuleConfig {
language: ProgrammingLanguage;
code: string;
}
type RuleConfig = ExistsRuleConfig | ConsistencyRuleConfig | FormatRuleConfig | LogicRuleConfig | RegexRuleConfig | AIRuleConfig | CodeRuleConfig;
export default function RuleNew() {
const navigate = useNavigate();
@@ -235,33 +169,31 @@ export default function RuleNew() {
try {
setIsLoading(true);
console.log(`获取评查点数据,ID: ${id}`);
const response = await fetch(`http://172.16.0.119:9000/admin/evaluation_points?id=eq.${id}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
console.log(response);
if (response.ok) {
const data = await response.json();
if (data.data && data.data[0]) {
setFormData(data.data[0]);
// 初始化extractionFields
const extractedFields = extractFieldsFromFormData(data.data[0]);
setExtractionFields(extractedFields);
// 设置编辑模式的实例键
setInstanceKey(`edit_${id}_${Date.now()}`);
} else {
console.error('获取数据失败: 返回数据为空');
alert('获取数据失败: 返回数据为空');
resetFormData();
navigate('/rules');
}
const response = await getEvaluationPoint(id);
if (response.error) {
console.error('获取评查点数据失败:', response.error);
alert(`获取评查点数据失败: ${response.error}`);
resetFormData();
navigate('/rules');
return;
}
if (response.data) {
setFormData(response.data as EvaluationPoint);
// 初始化extractionFields
const extractedFields = extractFieldsFromFormData(response.data as EvaluationPoint);
setExtractionFields(extractedFields);
// 设置编辑模式的实例键
setInstanceKey(`edit_${id}_${Date.now()}`);
} else {
throw new Error(`响应状态: ${response.status}`);
console.error('获取数据失败: 返回数据为空');
alert('获取数据失败: 返回数据为空');
resetFormData();
navigate('/rules');
}
} catch (error) {
console.error('获取评查点数据失败:', error);
@@ -281,17 +213,20 @@ export default function RuleNew() {
const fetchEvaluationPointGroups = useCallback(async () => {
try {
console.log("获取评查点组数据");
const response = await fetch("http://172.16.0.119:9000/admin/evaluation_point_groups", {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
console.log(response);
if (response.ok) {
const data = await response.json();
setEvaluationPointGroups(data.data);
const response = await getEvaluationPointGroups();
if (response.error) {
console.error('获取评查点组数据失败:', response.error);
alert(`获取评查点组数据失败: ${response.error}\n将使用默认数据`);
return;
}
if (response.data) {
setEvaluationPointGroups(response.data);
} else {
console.error('获取评查点组数据失败: 返回数据为空');
alert('获取评查点组数据失败: 返回数据为空\n将使用默认数据');
}
} catch (error) {
console.error('获取评查点组数据失败:', error);
@@ -317,198 +252,18 @@ export default function RuleNew() {
// 显示保存中状态
setIsLoading(true);
// 根据模式决定是创建还是更新
const apiMethod = isEditMode ? 'PATCH' : 'POST';
const apiUrl = isEditMode
? `http://172.16.0.119:9000/admin/evaluation_points?id=eq.${formData.id}`
: 'http://172.16.0.119:9000/admin/evaluation_points';
try {
// 创建一个符合数据库模式的数据副本
const cleanedData = {
id: formData.id,
name: formData.name?.trim(),
code: formData.code?.trim(),
risk: formData.risk || 'low',
is_enabled: formData.is_enabled !== undefined ? formData.is_enabled : true,
description: formData.description || '',
references_laws: formData.references_laws || null,
evaluation_point_groups_pid: formData.evaluation_point_groups_pid || null,
evaluation_point_groups_id: formData.evaluation_point_groups_id || null,
extraction_config: {
llm: {
fields: Array.isArray(formData.extraction_config?.llm?.fields) ?
[...formData.extraction_config.llm.fields] : [],
prompt_setting: {
type: formData.extraction_config?.llm?.prompt_setting?.type || 'system',
template: formData.extraction_config?.llm?.prompt_setting?.template || ''
}
},
vlm: {
fields: Array.isArray(formData.extraction_config?.vlm?.fields) ?
[...formData.extraction_config.vlm.fields] : [],
prompt_setting: {
type: formData.extraction_config?.vlm?.prompt_setting?.type || 'system',
template: formData.extraction_config?.vlm?.prompt_setting?.template || ''
}
},
regex: {
fields: Array.isArray(formData.extraction_config?.regex?.fields) ?
[...formData.extraction_config.regex.fields] : []
}
},
evaluation_config: {
logicType: formData.evaluation_config?.logicType || 'and',
customLogic: formData.evaluation_config?.customLogic || '',
rules: Array.isArray(formData.evaluation_config?.rules) ?
formData.evaluation_config.rules.map(rule => ({
id: rule.id || '1',
type: rule.type || '',
config: rule.config || {}
})) : []
},
pass_message: formData.pass_message || '文档检查通过,符合规范要求。',
fail_message: formData.fail_message || '文档存在以下问题,请修改后重新提交。',
suggestion_message: formData.suggestion_message || '',
suggestion_message_type: formData.suggestion_message_type || 'warning',
post_action: formData.post_action || 'none',
action_config: formData.action_config || '',
score: formData.score !== undefined ? Number(formData.score) : 0
};
// 确保rules中的每个配置对象都被正确处理
if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) {
cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules
.filter(rule => rule && rule.type) // 确保规则有类型
.map(rule => {
// 根据规则类型确保config中有必要的字段
const config = { ...rule.config } as RuleConfig;
switch (rule.type) {
case 'exists':
if (!Array.isArray((config as ExistsRuleConfig).fields)) (config as ExistsRuleConfig).fields = [];
if (!(config as ExistsRuleConfig).logic) (config as ExistsRuleConfig).logic = 'and';
// 删除不必要的字段
delete (config as ExistsRuleConfig & {availableFields?: string}).availableFields;
delete (config as ExistsRuleConfig).selectedFields;
delete (config as ExistsRuleConfig).existsLogic;
break;
case 'consistency':
if (!Array.isArray((config as ConsistencyRuleConfig).pairs)) (config as ConsistencyRuleConfig).pairs = [];
if (!(config as ConsistencyRuleConfig).logic) (config as ConsistencyRuleConfig).logic = 'and';
delete (config as ConsistencyRuleConfig & {availableFields?: string}).availableFields;
delete (config as ConsistencyRuleConfig).logicRelation;
delete (config as ConsistencyRuleConfig).initialSourceField;
delete (config as ConsistencyRuleConfig).initialTargetField;
delete (config as ConsistencyRuleConfig).initialCompareMethod;
break;
case 'format':
if (!(config as FormatRuleConfig).field) (config as FormatRuleConfig).field = '';
if (!(config as FormatRuleConfig).formatType) (config as FormatRuleConfig).formatType = 'date';
if (!(config as FormatRuleConfig).parameters) (config as FormatRuleConfig).parameters = '';
delete (config as FormatRuleConfig & {availableFields?: string}).availableFields;
delete (config as FormatRuleConfig).checkField;
delete (config as FormatRuleConfig).formatParams;
break;
case 'logic':
if (!Array.isArray((config as LogicRuleConfig).conditions)) (config as LogicRuleConfig).conditions = [];
if (!(config as LogicRuleConfig).logic) (config as LogicRuleConfig).logic = 'and';
delete (config as LogicRuleConfig & {availableFields?: string}).availableFields;
delete (config as LogicRuleConfig).logicRelation;
delete (config as LogicRuleConfig).initialField;
delete (config as LogicRuleConfig).initialOperator;
delete (config as LogicRuleConfig).initialValue;
break;
case 'regex':
if (!(config as RegexRuleConfig).field) (config as RegexRuleConfig).field = '';
if (!(config as RegexRuleConfig).pattern) (config as RegexRuleConfig).pattern = '';
if (!(config as RegexRuleConfig).matchType) (config as RegexRuleConfig).matchType = 'match';
delete (config as RegexRuleConfig & {availableFields?: string}).availableFields;
delete (config as RegexRuleConfig).checkField;
delete (config as RegexRuleConfig).regexPattern;
break;
case 'ai':
if (!(config as AIRuleConfig).model) (config as AIRuleConfig).model = 'qwen14b';
if (typeof (config as AIRuleConfig).temperature !== 'number') (config as AIRuleConfig).temperature = 0.1;
if (!(config as AIRuleConfig).prompt) (config as AIRuleConfig).prompt = '';
delete (config as AIRuleConfig & {availableFields?: string}).availableFields;
break;
case 'code':
if (!(config as CodeRuleConfig).language) (config as CodeRuleConfig).language = 'javascript';
if (!(config as CodeRuleConfig).code) (config as CodeRuleConfig).code = '';
delete (config as CodeRuleConfig & {availableFields?: string}).availableFields;
break;
}
return {
id: rule.id,
type: rule.type,
config
};
});
}
// 如果是新建模式,则删除id字段
if (!isEditMode) {
delete cleanedData.id;
}
// 确保extraction_config和evaluation_config是有效的JSON对象
// 通过先序列化再解析来验证
try {
JSON.parse(JSON.stringify(cleanedData.extraction_config));
} catch (e) {
throw new Error("extraction_config 格式无效");
}
try {
JSON.parse(JSON.stringify(cleanedData.evaluation_config));
} catch (e) {
throw new Error("evaluation_config 格式无效");
}
// 检查JSON字符串长度,如果太长可能会被截断
const jsonData = JSON.stringify(cleanedData);
const maxLength = 65536; // 通常PostgreSQL的jsonb列可以存储的最大长度
if (jsonData.length > maxLength) {
throw new Error(`数据大小超过限制 (${jsonData.length} > ${maxLength})`);
}
console.log("准备提交到API的数据:", cleanedData);
console.log("JSON数据长度:", jsonData.length);
// 发送数据到API
fetch(apiUrl, {
method: apiMethod,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(cleanedData)
})
.then(async response => {
// 尝试解析响应内容
let responseData;
try {
responseData = await response.json();
} catch (e) {
responseData = { code: 500, msg: "响应解析失败" };
// 调用API保存数据
saveEvaluationPoint(formData, isEditMode)
.then(response => {
if (response.error) {
console.error("保存评查点失败:", response.error);
alert(`保存评查点失败: ${response.error}`);
return;
}
// 根据响应码处理不同情况
if (responseData.code === 0) {
// 成功情况
console.log("保存成功:", responseData);
if (response.data) {
// 获取新创建或更新的评查点ID
const savedPointId = responseData.data[0]?.id;
const savedPointId = response.data[0]?.id;
if (savedPointId) {
// 显示成功消息
@@ -522,46 +277,17 @@ export default function RuleNew() {
navigate('/rules');
}
} else {
// 处理各种错误情况
console.error("API错误:", responseData);
// 根据错误码显示不同的错误消息
switch (responseData.code) {
case 400:
alert(`参数错误: ${responseData.msg}`);
break;
case 401:
alert(`未授权: ${responseData.msg}`);
break;
case 403:
alert(`禁止访问: ${responseData.msg}`);
break;
case 404:
alert(`未找到资源: ${responseData.msg}`);
break;
case 409:
alert(`资源冲突: ${responseData.msg}`);
break;
case 500:
alert(`服务器错误: ${responseData.msg}`);
break;
default:
alert(`保存失败: ${responseData.msg || '未知错误'}`);
}
alert(`保存成功,但返回数据为空。正在返回列表页面。`);
navigate('/rules');
}
})
.catch(error => {
console.error("网络或解析错误:", error);
alert(`网络或解析错误: ${error.message || '未知错误'}`);
console.error("保存评查点出错:", error);
alert(`保存评查点出错: ${error instanceof Error ? error.message : '未知错误'}`);
})
.finally(() => {
setIsLoading(false);
});
} catch (error) {
console.error("数据处理错误:", error);
alert(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`);
setIsLoading(false);
}
}
const handleSaveDraft = () => {
@@ -606,7 +332,7 @@ export default function RuleNew() {
};
const handleReviewSettingsChange = (data: Record<string, unknown>) => {
console.log("评查设置变更:", data);
// console.log("评查设置变更:", data);
// 检查数据中是否包含evaluation_config对象
if (data.evaluation_config) {
-418
View File
@@ -1,418 +0,0 @@
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>
);
}
+1 -1
View File
@@ -448,7 +448,7 @@ export default function RulesIndex() {
width: "10%",
render: (_: unknown, record: Rule) => (
<div className="operations-cell">
<Link to={`/rules/new?id=${record.id}`} className="operation-btn">
<Link to={`/rules-new?id=${record.id}`} className="operation-btn">
<i className="ri-edit-line"></i>
</Link>
<button className="operation-btn" onClick={() => handleCopy(record)}>
+34 -1
View File
@@ -26,13 +26,46 @@
/* 表单样式 */
.file-upload-page .form-group {
@apply mb-4;
@apply mb-2;
}
/* 复选框样式 */
.file-upload-page .switch-container {
@apply flex items-center mt-2;
}
.file-upload-page .switch {
@apply relative inline-block w-10 h-5 mr-2;
}
.file-upload-page .switch input {
@apply opacity-0 w-0 h-0;
}
.file-upload-page .slider {
@apply absolute cursor-pointer inset-0 bg-gray-300 rounded-full transition-all duration-300;
}
.file-upload-page .slider:before {
@apply absolute content-[''] h-4 w-4 left-0.5 bottom-0.5 bg-white rounded-full transition-all duration-300;
}
.file-upload-page input:checked + .slider {
@apply bg-[var(--primary-color)];
}
.file-upload-page input:checked + .slider:before {
@apply transform translate-x-5;
}
.file-upload-page .form-label {
@apply block text-sm font-medium text-gray-700 mb-2;
}
.file-upload-page .form-textarea {
@apply block w-full px-3 py-2 text-base rounded-md shadow-sm focus:outline-none border;
}
.file-upload-page .form-tip {
@apply text-xs text-gray-500 mt-1;
}