合并评查点新增代码
This commit is contained in:
@@ -1,6 +1,27 @@
|
|||||||
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
|
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client';
|
||||||
import dayjs from 'dayjs';
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评查点列表查询参数
|
* 评查点列表查询参数
|
||||||
*/
|
*/
|
||||||
@@ -928,3 +949,508 @@ export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定义提取配置类型
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,3 +52,82 @@ export async function uploadFileToServer(
|
|||||||
return { success: false, error: error instanceof Error ? error.message : '上传失败' };
|
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 : '上传失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -503,7 +503,7 @@ function preprocessData(data: Record<string, unknown>): Record<string, unknown>
|
|||||||
export async function postgrestPut<T, D extends object>(
|
export async function postgrestPut<T, D extends object>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: D,
|
data: D,
|
||||||
filters?: Record<string, string>
|
filters?: Record<string | number, string | number>
|
||||||
): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
|
): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> {
|
||||||
try {
|
try {
|
||||||
// 确保端点没有前导斜杠
|
// 确保端点没有前导斜杠
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
|||||||
{
|
{
|
||||||
id: 'rule-new',
|
id: 'rule-new',
|
||||||
title: '新增评查点',
|
title: '新增评查点',
|
||||||
path: '/rules/new',
|
path: '/rules-new',
|
||||||
icon: 'ri-add-circle-line'
|
icon: 'ri-add-circle-line'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export function ReviewSettings({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果已经初始化过,则跳过此次处理
|
// 如果已经初始化过,则跳过此次处理
|
||||||
if (initializedRef.current) {
|
if (initializedRef.current) {
|
||||||
console.log("ReviewSettings已初始化,跳过后续初始化处理");
|
// console.log("ReviewSettings已初始化,跳过后续初始化处理");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1717,7 +1717,7 @@ export function ReviewSettings({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果有初始数据,在组件挂载后主动发送一次完整规则配置
|
// 如果有初始数据,在组件挂载后主动发送一次完整规则配置
|
||||||
if (initialDataRef.current && onChange) {
|
if (initialDataRef.current && onChange) {
|
||||||
console.log("组件挂载后发送初始完整配置");
|
// console.log("组件挂载后发送初始完整配置");
|
||||||
setTimeout(() => generateEvaluationConfig(), 100);
|
setTimeout(() => generateEvaluationConfig(), 100);
|
||||||
}
|
}
|
||||||
}, [generateEvaluationConfig, onChange]);
|
}, [generateEvaluationConfig, onChange]);
|
||||||
|
|||||||
+71
-83
@@ -9,6 +9,7 @@ import { FileProgress} from "~/components/ui/FileProgress";
|
|||||||
import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps";
|
import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps";
|
||||||
import uploadStyles from "~/styles/pages/files_upload.css?url";
|
import uploadStyles from "~/styles/pages/files_upload.css?url";
|
||||||
import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload";
|
import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload";
|
||||||
|
import { uploadFileToBinary, uploadDocumentToServer } from "~/api/files";
|
||||||
|
|
||||||
export function links() {
|
export function links() {
|
||||||
return [
|
return [
|
||||||
@@ -109,99 +110,34 @@ interface FileUploadResponse {
|
|||||||
error: string | null;
|
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
|
// 模拟上传文件到服务器的API
|
||||||
async function uploadFileToServer(
|
async function uploadFileToServer(
|
||||||
binaryData: ArrayBuffer,
|
binaryData: ArrayBuffer,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
fileType: string,
|
fileType: string,
|
||||||
documentType: FileType,
|
documentType: FileType,
|
||||||
priority: Priority
|
priority: Priority,
|
||||||
|
documentNumber: string | null,
|
||||||
|
remark: string | null,
|
||||||
|
isTestDocument: boolean
|
||||||
): Promise<FileUploadResponse> {
|
): Promise<FileUploadResponse> {
|
||||||
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
|
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
|
||||||
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
|
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
|
||||||
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建FormData对象,将文件和其他信息一起提交
|
// 使用封装的上传函数
|
||||||
const formData = new FormData();
|
const response = await uploadDocumentToServer(
|
||||||
|
binaryData,
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
documentType,
|
||||||
|
PRIORITY_TO_CHINESE[priority],
|
||||||
|
documentNumber,
|
||||||
|
remark,
|
||||||
|
isTestDocument
|
||||||
|
);
|
||||||
|
|
||||||
// 将二进制数据转换为Blob并添加到FormData
|
return response;
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[模拟API] 上传错误:', error);
|
console.error('[模拟API] 上传错误:', error);
|
||||||
return {
|
return {
|
||||||
@@ -318,8 +254,11 @@ export default function FilesUpload() {
|
|||||||
const { documents, documentTypes } = useLoaderData<LoaderData>();
|
const { documents, documentTypes } = useLoaderData<LoaderData>();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
|
const [isTestDocument, setIsTestDocument] = useState(false);
|
||||||
const [fileType, setFileType] = useState<FileType | "">("");
|
const [fileType, setFileType] = useState<FileType | "">("");
|
||||||
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
|
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
|
||||||
|
const [documentNumber, setDocumentNumber] = useState<string>("");
|
||||||
|
const [remark, setRemark] = useState<string>("");
|
||||||
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
||||||
@@ -506,7 +445,10 @@ export default function FilesUpload() {
|
|||||||
file.name,
|
file.name,
|
||||||
file.type,
|
file.type,
|
||||||
fileType as FileType,
|
fileType as FileType,
|
||||||
priority
|
priority,
|
||||||
|
documentNumber || null,
|
||||||
|
remark || null,
|
||||||
|
isTestDocument
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.success || !response.result) {
|
if (!response.success || !response.result) {
|
||||||
@@ -802,6 +744,8 @@ export default function FilesUpload() {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 格式化文件大小显示
|
// 格式化文件大小显示
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
@@ -920,7 +864,7 @@ export default function FilesUpload() {
|
|||||||
<Form method="post" encType="multipart/form-data" ref={formRef}>
|
<Form method="post" encType="multipart/form-data" ref={formRef}>
|
||||||
{/* 文件类型选择 */}
|
{/* 文件类型选择 */}
|
||||||
<Card title={<h3>选择文件类型</h3>} className="mb-4">
|
<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">
|
<div className="form-group">
|
||||||
<label htmlFor="file-type-select" className="form-label">文件类型 <span className="text-red-500">*</span></label>
|
<label htmlFor="file-type-select" className="form-label">文件类型 <span className="text-red-500">*</span></label>
|
||||||
<select
|
<select
|
||||||
@@ -959,6 +903,37 @@ export default function FilesUpload() {
|
|||||||
</select>
|
</select>
|
||||||
<div className="form-tip">优先级影响文档在队列中的处理顺序</div>
|
<div className="form-tip">优先级影响文档在队列中的处理顺序</div>
|
||||||
</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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -975,6 +950,17 @@ export default function FilesUpload() {
|
|||||||
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
|
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
|
||||||
shouldPreventFileSelect={!fileType}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useNavigate, useLocation } from "@remix-run/react";
|
||||||
// 导入评查点模型定义和常量
|
// 导入评查点模型定义和常量
|
||||||
import type {
|
import type {
|
||||||
EvaluationPoint,
|
EvaluationPoint
|
||||||
LogicOperator,
|
|
||||||
CompareMethod,
|
|
||||||
FormatType,
|
|
||||||
ComparisonOperator,
|
|
||||||
MatchType,
|
|
||||||
ProgrammingLanguage
|
|
||||||
} from "~/models/evaluation_points";
|
} from "~/models/evaluation_points";
|
||||||
import { EVALUATION_OPTIONS } from "~/models/evaluation_points";
|
import { EVALUATION_OPTIONS } from "~/models/evaluation_points";
|
||||||
import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
|
import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
|
||||||
// 导入RuleContext上下文
|
// 导入RuleContext上下文
|
||||||
import { RuleContext } from "~/contexts/RuleContext";
|
import { RuleContext } from "~/contexts/RuleContext";
|
||||||
|
// 导入API函数
|
||||||
|
import { getEvaluationPoint, getEvaluationPointGroups, saveEvaluationPoint } from "~/api/evaluation_points/rules";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@@ -59,76 +55,14 @@ export const meta: MetaFunction = () => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function links() {
|
|
||||||
return [{ rel: "stylesheet", href: rulesStyles }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: "评查点管理"
|
breadcrumb: "评查点管理"
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加规则配置接口
|
export function links() {
|
||||||
interface BaseRuleConfig {
|
return [{ rel: "stylesheet", href: rulesStyles }];
|
||||||
availableFields?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
export default function RuleNew() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -235,21 +169,22 @@ export default function RuleNew() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log(`获取评查点数据,ID: ${id}`);
|
console.log(`获取评查点数据,ID: ${id}`);
|
||||||
const response = await fetch(`http://172.16.0.119:9000/admin/evaluation_points?id=eq.${id}`, {
|
|
||||||
method: 'GET',
|
const response = await getEvaluationPoint(id);
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
if (response.error) {
|
||||||
'Content-Type': 'application/json'
|
console.error('获取评查点数据失败:', response.error);
|
||||||
|
alert(`获取评查点数据失败: ${response.error}`);
|
||||||
|
resetFormData();
|
||||||
|
navigate('/rules');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
console.log(response);
|
if (response.data) {
|
||||||
if (response.ok) {
|
setFormData(response.data as EvaluationPoint);
|
||||||
const data = await response.json();
|
|
||||||
if (data.data && data.data[0]) {
|
|
||||||
setFormData(data.data[0]);
|
|
||||||
|
|
||||||
// 初始化extractionFields
|
// 初始化extractionFields
|
||||||
const extractedFields = extractFieldsFromFormData(data.data[0]);
|
const extractedFields = extractFieldsFromFormData(response.data as EvaluationPoint);
|
||||||
setExtractionFields(extractedFields);
|
setExtractionFields(extractedFields);
|
||||||
|
|
||||||
// 设置编辑模式的实例键
|
// 设置编辑模式的实例键
|
||||||
@@ -260,9 +195,6 @@ export default function RuleNew() {
|
|||||||
resetFormData();
|
resetFormData();
|
||||||
navigate('/rules');
|
navigate('/rules');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error(`响应状态: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取评查点数据失败:', error);
|
console.error('获取评查点数据失败:', error);
|
||||||
alert(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
alert(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
@@ -281,17 +213,20 @@ export default function RuleNew() {
|
|||||||
const fetchEvaluationPointGroups = useCallback(async () => {
|
const fetchEvaluationPointGroups = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
console.log("获取评查点组数据");
|
console.log("获取评查点组数据");
|
||||||
const response = await fetch("http://172.16.0.119:9000/admin/evaluation_point_groups", {
|
|
||||||
method: 'GET',
|
const response = await getEvaluationPointGroups();
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
if (response.error) {
|
||||||
'Content-Type': 'application/json'
|
console.error('获取评查点组数据失败:', response.error);
|
||||||
|
alert(`获取评查点组数据失败: ${response.error}\n将使用默认数据`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
console.log(response);
|
if (response.data) {
|
||||||
if (response.ok) {
|
setEvaluationPointGroups(response.data);
|
||||||
const data = await response.json();
|
} else {
|
||||||
setEvaluationPointGroups(data.data);
|
console.error('获取评查点组数据失败: 返回数据为空');
|
||||||
|
alert('获取评查点组数据失败: 返回数据为空\n将使用默认数据');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取评查点组数据失败:', error);
|
console.error('获取评查点组数据失败:', error);
|
||||||
@@ -317,198 +252,18 @@ export default function RuleNew() {
|
|||||||
// 显示保存中状态
|
// 显示保存中状态
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// 根据模式决定是创建还是更新
|
// 调用API保存数据
|
||||||
const apiMethod = isEditMode ? 'PATCH' : 'POST';
|
saveEvaluationPoint(formData, isEditMode)
|
||||||
const apiUrl = isEditMode
|
.then(response => {
|
||||||
? `http://172.16.0.119:9000/admin/evaluation_points?id=eq.${formData.id}`
|
if (response.error) {
|
||||||
: 'http://172.16.0.119:9000/admin/evaluation_points';
|
console.error("保存评查点失败:", response.error);
|
||||||
|
alert(`保存评查点失败: ${response.error}`);
|
||||||
try {
|
return;
|
||||||
// 创建一个符合数据库模式的数据副本
|
|
||||||
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 {
|
if (response.data) {
|
||||||
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: "响应解析失败" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据响应码处理不同情况
|
|
||||||
if (responseData.code === 0) {
|
|
||||||
// 成功情况
|
|
||||||
console.log("保存成功:", responseData);
|
|
||||||
|
|
||||||
// 获取新创建或更新的评查点ID
|
// 获取新创建或更新的评查点ID
|
||||||
const savedPointId = responseData.data[0]?.id;
|
const savedPointId = response.data[0]?.id;
|
||||||
|
|
||||||
if (savedPointId) {
|
if (savedPointId) {
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
@@ -522,46 +277,17 @@ export default function RuleNew() {
|
|||||||
navigate('/rules');
|
navigate('/rules');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 处理各种错误情况
|
alert(`保存成功,但返回数据为空。正在返回列表页面。`);
|
||||||
console.error("API错误:", responseData);
|
navigate('/rules');
|
||||||
|
|
||||||
// 根据错误码显示不同的错误消息
|
|
||||||
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 || '未知错误'}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("网络或解析错误:", error);
|
console.error("保存评查点出错:", error);
|
||||||
alert(`网络或解析错误: ${error.message || '未知错误'}`);
|
alert(`保存评查点出错: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error("数据处理错误:", error);
|
|
||||||
alert(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveDraft = () => {
|
const handleSaveDraft = () => {
|
||||||
@@ -606,7 +332,7 @@ export default function RuleNew() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewSettingsChange = (data: Record<string, unknown>) => {
|
const handleReviewSettingsChange = (data: Record<string, unknown>) => {
|
||||||
console.log("评查设置变更:", data);
|
// console.log("评查设置变更:", data);
|
||||||
|
|
||||||
// 检查数据中是否包含evaluation_config对象
|
// 检查数据中是否包含evaluation_config对象
|
||||||
if (data.evaluation_config) {
|
if (data.evaluation_config) {
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -448,7 +448,7 @@ export default function RulesIndex() {
|
|||||||
width: "10%",
|
width: "10%",
|
||||||
render: (_: unknown, record: Rule) => (
|
render: (_: unknown, record: Rule) => (
|
||||||
<div className="operations-cell">
|
<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> 编辑
|
<i className="ri-edit-line"></i> 编辑
|
||||||
</Link>
|
</Link>
|
||||||
<button className="operation-btn" onClick={() => handleCopy(record)}>
|
<button className="operation-btn" onClick={() => handleCopy(record)}>
|
||||||
|
|||||||
@@ -26,13 +26,46 @@
|
|||||||
|
|
||||||
/* 表单样式 */
|
/* 表单样式 */
|
||||||
.file-upload-page .form-group {
|
.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 {
|
.file-upload-page .form-label {
|
||||||
@apply block text-sm font-medium text-gray-700 mb-2;
|
@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 {
|
.file-upload-page .form-tip {
|
||||||
@apply text-xs text-gray-500 mt-1;
|
@apply text-xs text-gray-500 mt-1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user