完善列表和编辑页面的数据验证和交互,实现服务端和客户端两重数据验证
This commit is contained in:
@@ -60,7 +60,8 @@ export interface DocumentTypeGroup {
|
||||
// 搜索参数
|
||||
export interface DocumentTypeSearchParams {
|
||||
name?: string;
|
||||
group_id?: string;
|
||||
ruleType?: string;
|
||||
groupId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
@@ -239,13 +240,18 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
}
|
||||
|
||||
// 如果有分组ID筛选条件
|
||||
if (searchParams.group_id) {
|
||||
filter['evaluation_point_groups_ids'] = `cs.{${searchParams.group_id}}`;
|
||||
if (searchParams.ruleType) {
|
||||
filter['evaluation_point_groups_ids'] = `cs.[${searchParams.ruleType}]`;
|
||||
}
|
||||
|
||||
if (searchParams.groupId) {
|
||||
// 如果groupId存在,则将groupId作为子级评查点分组ID
|
||||
filter['evaluation_point_groups_ids'] = `cs.[${searchParams.groupId}]`;
|
||||
}
|
||||
|
||||
params.filter = filter;
|
||||
|
||||
// console.log('获取文档类型列表,参数:', params);
|
||||
console.log('获取文档类型列表,参数:', params);
|
||||
const response = await postgrestGet<DocumentType[]>('document_types', params);
|
||||
|
||||
if (response.error) {
|
||||
@@ -529,8 +535,8 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO): P
|
||||
if (!groupId || isNaN(parseInt(groupId, 10))) {
|
||||
return { error: '无效的评查点分组ID', status: 400 };
|
||||
}
|
||||
const groupIds = parseInt(groupId, 10); // 修改为数组形式
|
||||
// const groupIds = [parseInt(groupId, 10)]; // 修改为数组形式
|
||||
// const groupIds = parseInt(groupId, 10); // 修改为数组形式
|
||||
const groupIds = [parseInt(groupId, 10)]; // 修改为数组形式
|
||||
|
||||
// 构建提示词配置 - 确保所有字段都有明确的设置
|
||||
const promptConfig: Record<string, number | null> = {
|
||||
@@ -580,13 +586,13 @@ export async function createDocumentType(documentType: DocumentTypeCreateDTO): P
|
||||
description: documentType.description || '',
|
||||
evaluation_point_groups_ids: groupIds,
|
||||
prompt_config: promptConfig,
|
||||
code: documentType.code || null
|
||||
// code: documentType.code || null
|
||||
};
|
||||
|
||||
// console.log('创建文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
|
||||
// console.log('创建文档类型请求数据:', apiDocumentType);
|
||||
// if(apiDocumentType){
|
||||
// throw new Error('测试错误');
|
||||
// throw new Error('测试错误');
|
||||
// }
|
||||
|
||||
// 发送创建请求
|
||||
@@ -660,7 +666,7 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU
|
||||
const promptConfig: Record<string, number | null> = {
|
||||
llm_extract_template: null,
|
||||
vlm_extract_template: null,
|
||||
evaluation_template: null,
|
||||
// evaluation_template: null,
|
||||
execution_template: null,
|
||||
summary_template: null
|
||||
};
|
||||
@@ -707,7 +713,7 @@ export async function updateDocumentType(id: string, documentType: DocumentTypeU
|
||||
};
|
||||
|
||||
console.log('更新文档类型请求数据:', JSON.stringify(apiDocumentType, null, 2));
|
||||
|
||||
// throw new Error('测试错误');
|
||||
// 发送更新请求
|
||||
const response = await postgrestPut<DocumentType, typeof apiDocumentType>(
|
||||
'document_types',
|
||||
|
||||
@@ -121,17 +121,17 @@ export async function getReviewPoints(fileId: string) {
|
||||
return { error: evaluationResultsResponse.error, status: evaluationResultsResponse.status };
|
||||
}
|
||||
|
||||
const evaluationResultsData = extractApiData<EvaluationResult[]>(evaluationResultsResponse.data);
|
||||
const evaluationResultsData = extractApiData<EvaluationResult[]>(evaluationResultsResponse.data) || [];
|
||||
|
||||
if (!evaluationResultsData || !Array.isArray(evaluationResultsData)) {
|
||||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
|
||||
if (Array.isArray(evaluationResultsData) && evaluationResultsData.length <= 0) {
|
||||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 },error: '获取评查结果数据失败' };
|
||||
}
|
||||
|
||||
// 收集所有评查点ID,用于查询评查点详情
|
||||
const evaluationPointIds = evaluationResultsData.map(item => item.evaluation_point_id).filter(Boolean);
|
||||
|
||||
if (evaluationPointIds.length === 0) {
|
||||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } };
|
||||
return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 },error: '获取评查点ID失败' };
|
||||
}
|
||||
|
||||
// 步骤2:根据evaluation_point_id查询evaluation_points表
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
interface FileInfoProps {
|
||||
fileInfo: {
|
||||
fileName: string;
|
||||
@@ -54,6 +55,10 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleExportReport = () => {
|
||||
alert('导出评查报告功能');
|
||||
};
|
||||
@@ -83,7 +88,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
{/* 返回上一级 */}
|
||||
<button
|
||||
className="ant-btn ant-btn-default flex items-center"
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => handleBack()}
|
||||
>
|
||||
<i className="ri-arrow-left-line mr-1"></i> 返回
|
||||
</button>
|
||||
|
||||
@@ -146,16 +146,18 @@ export function MessageModal({
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
role="presentation"
|
||||
aria-label="关闭对话框"
|
||||
>
|
||||
<div
|
||||
<div
|
||||
className={`message-modal message-modal-${type} ${isClosing ? 'closing' : ''}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="message-modal-title"
|
||||
aria-describedby="message-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
@@ -198,12 +200,14 @@ export function MessageModal({
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
className="message-modal-button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
{cancelText && (
|
||||
<button
|
||||
className="message-modal-button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
interface SearchBoxProps {
|
||||
placeholder?: string;
|
||||
@@ -17,6 +17,11 @@ export function SearchBox({
|
||||
className = '',
|
||||
name = 'keyword'
|
||||
}: SearchBoxProps) {
|
||||
const [inputValue, setInputValue] = useState(defaultValue);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
@@ -25,28 +30,67 @@ export function SearchBox({
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
|
||||
// 对于没有按钮的输入框,我们希望在输入时就触发搜索
|
||||
if (className.includes('form-input-only')) {
|
||||
onSearch(e.target.value);
|
||||
onSearch(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setInputValue('');
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = '';
|
||||
inputRef.current.focus();
|
||||
}
|
||||
onSearch('');
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovering(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovering(false);
|
||||
};
|
||||
|
||||
const isIconOnly = buttonText === '';
|
||||
const isFilterControl = className.includes('filter-control');
|
||||
const hasButton = !className.includes('form-input-only');
|
||||
const searchBoxClass = `search-box ${className} ${isFilterControl ? 'search-box-row' : ''}`;
|
||||
const showClearButton = inputValue && isHovering;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={searchBoxClass}>
|
||||
<input
|
||||
type="text"
|
||||
id={name}
|
||||
name={name}
|
||||
className={`form-input ${isFilterControl ? 'flex-1' : ''}`}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{!className.includes('form-input-only') && (
|
||||
<div
|
||||
className={`relative ${hasButton ? 'flex-1' : 'w-full'}`}
|
||||
ref={containerRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id={name}
|
||||
name={name}
|
||||
className={`form-input w-full ${hasButton ? 'rounded-r-none' : ''}`}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
{showClearButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="search-box-clear"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<i className="ri-close-circle-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasButton && (
|
||||
<button
|
||||
type="submit"
|
||||
className={`search-button ${isIconOnly ? "icon-only-btn" : ""}`}
|
||||
|
||||
@@ -338,7 +338,7 @@ export default function ConfigNew() {
|
||||
case 'name':
|
||||
if(value.trim() === ""){
|
||||
return "配置名称不能为空";
|
||||
}else if(/^[a-zA-Z_]+$/.test(value)){
|
||||
}else if(!/^[a-zA-Z_]+$/.test(value)){
|
||||
return "配置名称只能包含英文字母和下划线";
|
||||
}else if(value.length > 100){
|
||||
return "配置名称不能超过100个字符";
|
||||
@@ -574,7 +574,7 @@ export default function ConfigNew() {
|
||||
{/* 配置名称和状态 */}
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name" className="form-label required">配置名称</label>
|
||||
<label htmlFor="name" className="form-label ">配置名称 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
@@ -583,7 +583,6 @@ export default function ConfigNew() {
|
||||
value={formValues.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入配置名称,如database_connection"
|
||||
required
|
||||
/>
|
||||
{touchedFields.name && formErrors.name && (
|
||||
<div className="error-message">{formErrors.name}</div>
|
||||
@@ -619,7 +618,7 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 所属模块 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type" className="form-label required">所属模块</label>
|
||||
<label htmlFor="type" className="form-label">所属模块 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="type"
|
||||
@@ -633,7 +632,7 @@ export default function ConfigNew() {
|
||||
onChange={handleInputChange}
|
||||
name="type"
|
||||
placeholder="请输入或选择所属模块"
|
||||
required
|
||||
|
||||
/>
|
||||
{touchedFields.type && formErrors.type && (
|
||||
<div className="error-message">{formErrors.type}</div>
|
||||
@@ -657,7 +656,7 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 环境 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="environment" className="form-label required">环境</label>
|
||||
<label htmlFor="environment" className="form-label">环境 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="environment"
|
||||
@@ -671,7 +670,6 @@ export default function ConfigNew() {
|
||||
onChange={handleInputChange}
|
||||
name="environment"
|
||||
placeholder="请输入或选择环境"
|
||||
required
|
||||
/>
|
||||
{touchedFields.environment && formErrors.environment && (
|
||||
<div className="error-message">{formErrors.environment}</div>
|
||||
@@ -695,7 +693,7 @@ export default function ConfigNew() {
|
||||
|
||||
{/* 配置数据 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="config" className="form-label required">配置数据 (JSON)</label>
|
||||
<label htmlFor="config" className="form-label ">配置数据 (JSON) <span className="text-red-500">*</span></label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" style={{ minHeight: '390px' }}>
|
||||
{/* 左侧JSON编辑区 */}
|
||||
<div className="h-full">
|
||||
@@ -705,7 +703,7 @@ export default function ConfigNew() {
|
||||
className={`json-editor ${touchedFields.config && formErrors.config ? 'input-error' : ''}`}
|
||||
value={formValues.config}
|
||||
onChange={handleConfigDataChange}
|
||||
required
|
||||
|
||||
placeholder='请输入JSON格式的配置数据'
|
||||
/>
|
||||
<div className="editor-actions">
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { getRuleTypes, getRuleGroupsByType, type RuleType, type RuleGroup } from "~/api/evaluation_points/rules";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
getAllEvaluationPointGroups,
|
||||
type DocumentTypeUI,
|
||||
type DocumentTypeSearchParams,
|
||||
type DocumentTypeGroup
|
||||
} from "~/api/document-types/document-types";
|
||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||
|
||||
|
||||
// 引入CSS样式
|
||||
export function links() {
|
||||
return [
|
||||
@@ -39,6 +41,7 @@ interface LoaderData {
|
||||
currentPage: number;
|
||||
error?: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
ruleTypes: RuleType[];
|
||||
}
|
||||
|
||||
// 加载函数 - 获取文档类型列表
|
||||
@@ -46,60 +49,51 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const group_id = url.searchParams.get('group_id') || undefined;
|
||||
const ruleType = url.searchParams.get('ruleType') || undefined;
|
||||
const groupId = url.searchParams.get('groupId') || undefined;
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||||
|
||||
// 构建搜索参数
|
||||
const searchParams: DocumentTypeSearchParams = {
|
||||
name,
|
||||
group_id,
|
||||
ruleType,
|
||||
groupId,
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
|
||||
// 并行获取文档类型数据和所有评查点分组
|
||||
const [typesResult, groupsResult] = await Promise.all([
|
||||
getDocumentTypes(searchParams),
|
||||
getAllEvaluationPointGroups()
|
||||
]);
|
||||
// 并行获取文档类型数据和父级评查点分组
|
||||
const ruleTypesResponse = await getRuleTypes();
|
||||
if(ruleTypesResponse.error){
|
||||
console.error("获取父级评查点分组失败:", ruleTypesResponse.error);
|
||||
}
|
||||
const ruleTypes = ruleTypesResponse.error ? [] : ruleTypesResponse.data;
|
||||
|
||||
const typesResponse = await getDocumentTypes(searchParams);
|
||||
if(typesResponse.error){
|
||||
console.error("获取文档类型失败:", typesResponse.error);
|
||||
throw new Error(typesResponse.error);
|
||||
}
|
||||
const typesResult = typesResponse.data?.types || [];
|
||||
|
||||
// console.log('文档类型数据:', typesResult.data?.types);
|
||||
// console.log('评查点分组数据:', groupsResult.data);
|
||||
// console.log('父级评查点分组:', groupsResult.data);
|
||||
|
||||
if (typesResult.error) {
|
||||
return json<LoaderData>(
|
||||
{
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
error: typesResult.error,
|
||||
groups: groupsResult.data || []
|
||||
},
|
||||
{ status: typesResult.status || 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return json<LoaderData>({
|
||||
types: typesResult.data?.types || [],
|
||||
total: typesResult.data?.total || 0,
|
||||
return Response.json({
|
||||
types: typesResult,
|
||||
total: typesResponse.data?.total || typesResult.length,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
groups: groupsResult.data || []
|
||||
ruleTypes
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载文档类型列表失败:", error);
|
||||
return json<LoaderData>(
|
||||
return Response.json(
|
||||
{
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
error: "加载文档类型列表失败",
|
||||
groups: []
|
||||
},
|
||||
{ status: 500 }
|
||||
error: error || "加载文档类型列表失败",
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -138,13 +132,60 @@ export default function DocumentTypesList() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, pageSize: initialPageSize, currentPage: initialPage, error, groups } = useLoaderData<LoaderData>();
|
||||
const { types, total, error, ruleTypes } = useLoaderData<LoaderData>();
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
const group_id = searchParams.get('group_id') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || String(initialPage), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(initialPageSize), 10);
|
||||
const currentPage = parseInt(searchParams.get('page') || String(1), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10);
|
||||
|
||||
// 判断是否禁用子级评查分组选择,true表示禁用,false表示不禁用
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 当评查点类型变化时,加载对应的子级评查分组
|
||||
useEffect(() => {
|
||||
// 如果选择了"全部"或未选择,则清空子级评查分组
|
||||
if (!ruleTypeParam || ruleTypeParam === 'all') {
|
||||
setRuleGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前类型的子级评查分组
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
console.error('加载子级规则组失败:', response.error);
|
||||
setRuleGroups([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子级规则组出错:', error);
|
||||
toastService.error('加载子级规则组出错:' + error);
|
||||
setRuleGroups([]);
|
||||
} finally {
|
||||
setLoadingGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRuleGroups();
|
||||
}, [ruleTypeParam]);
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
useEffect(() => {
|
||||
if(error){
|
||||
toastService.error(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
|
||||
// 处理名称搜索
|
||||
const handleNameSearch = (value: string) => {
|
||||
@@ -158,29 +199,51 @@ export default function DocumentTypesList() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理分组筛选
|
||||
const handleGroupChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { value } = e.target;
|
||||
// 处理筛选变更
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value) {
|
||||
newParams.set('group_id', value);
|
||||
} else {
|
||||
newParams.delete('group_id');
|
||||
|
||||
// 如果是子级评查分组选择,但是当前应该被禁用,则不处理
|
||||
if (name === 'groupId' && isRuleGroupSelectDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
newParams.set(name, value);
|
||||
|
||||
// 如果是评查点类型变更,清空子级评查分组选择
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
// 如果选择了"全部"或空值,也清空子级评查分组选择
|
||||
if (value === '' || value === 'all') {
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newParams.delete(name);
|
||||
|
||||
// 如果清除评查点类型,也清除规则组
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换筛选条件时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理重置筛选
|
||||
const handleReset = () => {
|
||||
const nameInput = document.querySelector('input[name="name"]');
|
||||
const nameInput = document.querySelector('input[placeholder="请输入文档类型名称"]');
|
||||
if (nameInput) {
|
||||
(nameInput as HTMLInputElement).value = '';
|
||||
}
|
||||
const groupIdInput = document.querySelector('select[name="group_id"]');
|
||||
if (groupIdInput) {
|
||||
(groupIdInput as HTMLSelectElement).value = '';
|
||||
}
|
||||
|
||||
// 重置所有筛选条件
|
||||
setSearchParams(new URLSearchParams());
|
||||
};
|
||||
|
||||
@@ -351,35 +414,12 @@ export default function DocumentTypesList() {
|
||||
}
|
||||
noActionDivider={true}
|
||||
>
|
||||
<SearchFilter
|
||||
label="类型名称"
|
||||
placeholder="请输入文档类型名称"
|
||||
value={name}
|
||||
onSearch={handleNameSearch}
|
||||
className="flex-1 min-w-[200px]"
|
||||
instantSearch={true}
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="关联分组"
|
||||
name="group_id"
|
||||
value={group_id}
|
||||
options={[
|
||||
...(groups || []).map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleGroupChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
|
||||
{/* <FilterSelect
|
||||
label="评查点类型"
|
||||
label="父级评查分组"
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
options={[
|
||||
...ruleTypes.map((type: ApiRuleType) => ({
|
||||
...(ruleTypes || []).map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}))
|
||||
@@ -389,7 +429,7 @@ export default function DocumentTypesList() {
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="所属规则组"
|
||||
label="所属子级评查分组"
|
||||
name="groupId"
|
||||
value={searchParams.get('groupId') || ''}
|
||||
options={[
|
||||
@@ -401,7 +441,16 @@ export default function DocumentTypesList() {
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<SearchFilter
|
||||
label="类型名称"
|
||||
placeholder="请输入文档类型名称"
|
||||
value={name}
|
||||
onSearch={handleNameSearch}
|
||||
className="flex-1 min-w-[200px]"
|
||||
instantSearch={true}
|
||||
/>
|
||||
|
||||
</FilterPanel>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -7,6 +7,7 @@ import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
|
||||
import { getAllRuleGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType } from "~/api/document-types/document-types";
|
||||
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: documentTypesNewStyles }];
|
||||
@@ -42,8 +43,23 @@ const TEMPLATE_TYPES = {
|
||||
SUMMARY: "Summary"
|
||||
};
|
||||
|
||||
// 定义动作返回的数据类型
|
||||
interface ActionData {
|
||||
result?: boolean;
|
||||
errors?: {
|
||||
name?: string;
|
||||
groups?: string;
|
||||
general?: string;
|
||||
llmExtractionTemplate?: string;
|
||||
vlmExtractionTemplate?: string;
|
||||
evaluationTemplate?: string;
|
||||
summaryTemplate?: string;
|
||||
};
|
||||
values?: Record<string, string | string[]>;
|
||||
}
|
||||
|
||||
// 加载函数 - 获取数据
|
||||
|
||||
// 获取数据
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
@@ -54,6 +70,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const ruleGroupsResponse = await getAllRuleGroups();
|
||||
if (ruleGroupsResponse.error) {
|
||||
console.error("获取评查点分组失败:", ruleGroupsResponse.error);
|
||||
// throw new Error(ruleGroupsResponse.error);
|
||||
}
|
||||
|
||||
// ruleGroupsResponse.data已经是树形结构数据,getAllRuleGroups内部已处理好parent-children关系
|
||||
@@ -96,26 +113,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
llmExtractionTemplates: [],
|
||||
vlmExtractionTemplates: [],
|
||||
evaluationTemplates: [],
|
||||
summaryTemplates: []
|
||||
summaryTemplates: [],
|
||||
error: error || "加载数据失败"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 定义动作返回的数据类型
|
||||
interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
name?: string;
|
||||
groups?: string;
|
||||
general?: string;
|
||||
llmExtractionTemplate?: string;
|
||||
vlmExtractionTemplate?: string;
|
||||
evaluationTemplate?: string;
|
||||
summaryTemplate?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 动作函数 - 处理表单提交
|
||||
// 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string | null;
|
||||
@@ -159,7 +163,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
// 如果有错误,返回错误信息
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({ errors });
|
||||
return Response.json({ errors, result: false });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -198,7 +202,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
} catch (error) {
|
||||
console.error("保存文档类型失败:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
result: false,
|
||||
errors: {
|
||||
general: error instanceof Error ? error.message : "保存文档类型失败"
|
||||
}
|
||||
@@ -235,12 +239,28 @@ export default function DocumentTypeNew() {
|
||||
});
|
||||
|
||||
// 添加本地验证错误状态
|
||||
const [localErrors, setLocalErrors] = useState<ActionData["errors"]>({} as ActionData["errors"]);
|
||||
const [formErrors, setFormErrors] = useState<ActionData["errors"]>({} as ActionData["errors"]);
|
||||
|
||||
// 表单引用
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// 字段是否被触摸过(用于确定何时显示错误)
|
||||
const [touchedFields, setTouchedFields] = useState({
|
||||
name: false,
|
||||
llmExtractionTemplate: false,
|
||||
vlmExtractionTemplate: false,
|
||||
evaluationTemplate: false,
|
||||
summaryTemplate: false,
|
||||
groups: false
|
||||
});
|
||||
|
||||
// 从actionData初始化本地错误
|
||||
useEffect(() => {
|
||||
if (actionData?.errors) {
|
||||
setLocalErrors(actionData.errors);
|
||||
if (!actionData?.result) {
|
||||
setFormErrors(actionData?.errors);
|
||||
if (actionData?.errors?.general) {
|
||||
toastService.error(actionData?.errors?.general || "保存文档类型失败");
|
||||
}
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
@@ -281,6 +301,26 @@ export default function DocumentTypeNew() {
|
||||
}
|
||||
}, [documentType, ruleGroups]);
|
||||
|
||||
// 验证表单字段
|
||||
const validateField = (field: string, value: string | string[]): string => {
|
||||
switch (field) {
|
||||
case 'name':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "文档类型名称不能为空" : "";
|
||||
case 'llmExtractionTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : "";
|
||||
case 'vlmExtractionTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择vlm抽取提示词模板" : "";
|
||||
case 'evaluationTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择评查提示词模板" : "";
|
||||
case 'summaryTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择总结提示词模板" : "";
|
||||
case 'groups':
|
||||
return Array.isArray(value) && value.length === 0 ? "请至少选择一个关联的评查点分组" : "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理分组勾选
|
||||
const handleGroupCheckChange = (
|
||||
groupId: string,
|
||||
@@ -292,18 +332,16 @@ export default function DocumentTypeNew() {
|
||||
if (isChecked) {
|
||||
// 只添加当前选中的分组
|
||||
newSelectedGroups = [groupId];
|
||||
|
||||
// 如果选择的是父分组,不自动选择子分组
|
||||
// 如果选择的是子分组,不影响父分组状态
|
||||
}
|
||||
// 如果取消选中,则清空选择(在单选模式下可能不需要,但保留逻辑以防万一)
|
||||
|
||||
setFormData(prev => ({ ...prev, selectedGroups: newSelectedGroups }));
|
||||
|
||||
// 清除groups相关的错误
|
||||
if (localErrors?.groups) {
|
||||
setLocalErrors(prev => ({...prev, groups: undefined}));
|
||||
}
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, groups: true}));
|
||||
|
||||
// 实时验证
|
||||
const error = validateField('groups', newSelectedGroups);
|
||||
setFormErrors(prev => ({...prev, groups: error}));
|
||||
};
|
||||
|
||||
// 修复展开/折叠功能
|
||||
@@ -322,39 +360,78 @@ export default function DocumentTypeNew() {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// 根据name属性映射到对应的formData字段
|
||||
let fieldName = name;
|
||||
|
||||
if (name === 'llm_extraction_template') {
|
||||
setFormData(prev => ({ ...prev, llmExtractionTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.llmExtractionTemplate) {
|
||||
setLocalErrors(prev => ({...prev, llmExtractionTemplate: undefined}));
|
||||
}
|
||||
fieldName = 'llmExtractionTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, llmExtractionTemplate: true}));
|
||||
} else if (name === 'vlm_extraction_template') {
|
||||
setFormData(prev => ({ ...prev, vlmExtractionTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.vlmExtractionTemplate) {
|
||||
setLocalErrors(prev => ({...prev, vlmExtractionTemplate: undefined}));
|
||||
}
|
||||
fieldName = 'vlmExtractionTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, vlmExtractionTemplate: true}));
|
||||
} else if (name === 'evaluation_template') {
|
||||
setFormData(prev => ({ ...prev, evaluationTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.evaluationTemplate) {
|
||||
setLocalErrors(prev => ({...prev, evaluationTemplate: undefined}));
|
||||
}
|
||||
fieldName = 'evaluationTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, evaluationTemplate: true}));
|
||||
} else if (name === 'summary_template') {
|
||||
setFormData(prev => ({ ...prev, summaryTemplateId: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.summaryTemplate) {
|
||||
setLocalErrors(prev => ({...prev, summaryTemplate: undefined}));
|
||||
}
|
||||
fieldName = 'summaryTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, summaryTemplate: true}));
|
||||
} else if (name === 'name') {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
// 清除相关错误
|
||||
if (localErrors?.name) {
|
||||
setLocalErrors(prev => ({...prev, name: undefined}));
|
||||
}
|
||||
} else {
|
||||
// 其他表单字段(description等)
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, name: true}));
|
||||
}
|
||||
|
||||
setFormData(prev => ({ ...prev, [fieldName]: value }));
|
||||
|
||||
// 实时验证
|
||||
if (name === 'name') {
|
||||
const error = validateField(name, value);
|
||||
setFormErrors(prev => ({...prev, name: error}));
|
||||
} else if (name === 'llm_extraction_template') {
|
||||
const error = validateField('llmExtractionTemplate', value);
|
||||
setFormErrors(prev => ({...prev, llmExtractionTemplate: error}));
|
||||
} else if (name === 'vlm_extraction_template') {
|
||||
const error = validateField('vlmExtractionTemplate', value);
|
||||
setFormErrors(prev => ({...prev, vlmExtractionTemplate: error}));
|
||||
} else if (name === 'evaluation_template') {
|
||||
const error = validateField('evaluationTemplate', value);
|
||||
setFormErrors(prev => ({...prev, evaluationTemplate: error}));
|
||||
} else if (name === 'summary_template') {
|
||||
const error = validateField('summaryTemplate', value);
|
||||
setFormErrors(prev => ({...prev, summaryTemplate: error}));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理表单提交前验证
|
||||
const handleBeforeSubmit = (e: React.FormEvent) => {
|
||||
// 标记所有字段为已触摸
|
||||
setTouchedFields({
|
||||
name: true,
|
||||
llmExtractionTemplate: true,
|
||||
vlmExtractionTemplate: true,
|
||||
evaluationTemplate: true,
|
||||
summaryTemplate: true,
|
||||
groups: true
|
||||
});
|
||||
|
||||
// 验证所有字段
|
||||
const errors = {
|
||||
name: validateField('name', formData.name),
|
||||
llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId),
|
||||
vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId),
|
||||
evaluationTemplate: validateField('evaluationTemplate', formData.evaluationTemplateId),
|
||||
summaryTemplate: validateField('summaryTemplate', formData.summaryTemplateId),
|
||||
groups: validateField('groups', formData.selectedGroups)
|
||||
};
|
||||
|
||||
setFormErrors(errors);
|
||||
|
||||
// 如果有错误,阻止提交
|
||||
if (errors.name || errors.llmExtractionTemplate || errors.vlmExtractionTemplate ||
|
||||
errors.evaluationTemplate || errors.summaryTemplate || errors.groups) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -384,16 +461,16 @@ export default function DocumentTypeNew() {
|
||||
|
||||
{/* 表单内容 */}
|
||||
<Card>
|
||||
<Form id="type-form" method="post" noValidate>
|
||||
<Form id="type-form" method="post" noValidate ref={formRef} onSubmit={handleBeforeSubmit}>
|
||||
{/* 如果是编辑模式,添加隐藏的ID字段 */}
|
||||
{formData.id && <input type="hidden" name="id" value={formData.id} />}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{/* 错误提示 */}
|
||||
{localErrors?.general && (
|
||||
<div className="error-message general-error error-show">
|
||||
{formErrors?.general && (
|
||||
<div className="error-message general-error">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
{localErrors.general}
|
||||
{formErrors.general}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -406,16 +483,16 @@ export default function DocumentTypeNew() {
|
||||
type="text"
|
||||
id="type-name"
|
||||
name="name"
|
||||
className={`form-input ${localErrors?.name ? 'input-error' : ''}`}
|
||||
className={`form-input ${touchedFields.name && formErrors?.name ? 'input-error' : ''}`}
|
||||
placeholder="请输入文档类型名称"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
{localErrors?.name && (
|
||||
<div className="error-message error-show">{localErrors.name}</div>
|
||||
{touchedFields.name && formErrors?.name && (
|
||||
<div className="error-message">{formErrors.name}</div>
|
||||
)}
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
</div>
|
||||
|
||||
{/* 类型描述 */}
|
||||
@@ -440,7 +517,7 @@ export default function DocumentTypeNew() {
|
||||
<select
|
||||
id="llm-extraction-template"
|
||||
name="llm_extraction_template"
|
||||
className={`form-select ${localErrors?.llmExtractionTemplate ? 'input-error' : ''}`}
|
||||
className={`form-select ${touchedFields.llmExtractionTemplate && formErrors?.llmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.llmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
@@ -451,8 +528,8 @@ export default function DocumentTypeNew() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.llmExtractionTemplate && (
|
||||
<div className="error-message error-show">{localErrors.llmExtractionTemplate}</div>
|
||||
{touchedFields.llmExtractionTemplate && formErrors?.llmExtractionTemplate && (
|
||||
<div className="error-message">{formErrors.llmExtractionTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于从此类文档中抽取信息的llm提示词模板</div>
|
||||
</div>
|
||||
@@ -463,7 +540,7 @@ export default function DocumentTypeNew() {
|
||||
<select
|
||||
id="vlm-extraction-template"
|
||||
name="vlm_extraction_template"
|
||||
className={`form-select ${localErrors?.vlmExtractionTemplate ? 'input-error' : ''}`}
|
||||
className={`form-select ${touchedFields.vlmExtractionTemplate && formErrors?.vlmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.vlmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
@@ -474,8 +551,8 @@ export default function DocumentTypeNew() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.vlmExtractionTemplate && (
|
||||
<div className="error-message error-show">{localErrors.vlmExtractionTemplate}</div>
|
||||
{touchedFields.vlmExtractionTemplate && formErrors?.vlmExtractionTemplate && (
|
||||
<div className="error-message">{formErrors.vlmExtractionTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于从此类文档中抽取信息的vlm提示词模板</div>
|
||||
</div>
|
||||
@@ -486,7 +563,7 @@ export default function DocumentTypeNew() {
|
||||
<select
|
||||
id="evaluation-template"
|
||||
name="evaluation_template"
|
||||
className={`form-select ${localErrors?.evaluationTemplate ? 'input-error' : ''}`}
|
||||
className={`form-select ${touchedFields.evaluationTemplate && formErrors?.evaluationTemplate ? 'input-error' : ''}`}
|
||||
value={formData.evaluationTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
@@ -497,8 +574,8 @@ export default function DocumentTypeNew() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.evaluationTemplate && (
|
||||
<div className="error-message error-show">{localErrors.evaluationTemplate}</div>
|
||||
{touchedFields.evaluationTemplate && formErrors?.evaluationTemplate && (
|
||||
<div className="error-message">{formErrors.evaluationTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于评估此类文档内容的提示词模板</div>
|
||||
</div>
|
||||
@@ -509,7 +586,7 @@ export default function DocumentTypeNew() {
|
||||
<select
|
||||
id="summary-template"
|
||||
name="summary_template"
|
||||
className={`form-select ${localErrors?.summaryTemplate ? 'input-error' : ''}`}
|
||||
className={`form-select ${touchedFields.summaryTemplate && formErrors?.summaryTemplate ? 'input-error' : ''}`}
|
||||
value={formData.summaryTemplateId}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
@@ -520,8 +597,8 @@ export default function DocumentTypeNew() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{localErrors?.summaryTemplate && (
|
||||
<div className="error-message error-show">{localErrors.summaryTemplate}</div>
|
||||
{touchedFields.summaryTemplate && formErrors?.summaryTemplate && (
|
||||
<div className="error-message">{formErrors.summaryTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于生成此类文档摘要的提示词模板</div>
|
||||
</div>
|
||||
@@ -534,7 +611,7 @@ export default function DocumentTypeNew() {
|
||||
关联评查点分组 <span className="text-red-500">*</span>
|
||||
</legend>
|
||||
<div
|
||||
className={`checkbox-group ${localErrors?.groups ? 'group-error' : ''}`}
|
||||
className={`checkbox-group ${touchedFields.groups && formErrors?.groups ? 'group-error' : ''}`}
|
||||
aria-labelledby="checkpoint-groups-label"
|
||||
role="group"
|
||||
>
|
||||
@@ -599,10 +676,10 @@ export default function DocumentTypeNew() {
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-tip">选择与此文档类型关联的评查点分组,文档上传后将应用这些分组中的评查点进行审核</div>
|
||||
{localErrors?.groups && (
|
||||
<div className="error-message error-show">{localErrors.groups}</div>
|
||||
{touchedFields.groups && formErrors?.groups && (
|
||||
<div className="error-message">{formErrors.groups}</div>
|
||||
)}
|
||||
<div className="form-tip">选择与此文档类型关联的评查点分组,文档上传后将应用这些分组中的评查点进行审核</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-7
@@ -44,6 +44,7 @@ import {
|
||||
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||
import { type ReviewPoint } from '~/components/reviews';
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
|
||||
|
||||
/**
|
||||
@@ -202,17 +203,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||
// console.log("id-------",id);
|
||||
if (!id) {
|
||||
return Response.json({ error: '评查ID不能为空' }, { status: 400 });
|
||||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||||
}
|
||||
|
||||
|
||||
// 获取评查点数据
|
||||
const reviewData = await getReviewPoints(id);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
if ('error' in reviewData && reviewData.error) {
|
||||
console.error("获取评查点数据错误:", reviewData.error);
|
||||
return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 });
|
||||
return Response.json({ result: false, message: reviewData.error });
|
||||
}
|
||||
|
||||
// 确保reviewData有效且具有预期的属性
|
||||
@@ -225,24 +226,42 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
statistics: reviewData.stats
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确");
|
||||
return Response.json({ error: '返回的评查数据格式不正确' }, { status: 500 });
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
return Response.json({ result: false, message: '返回的评查数据格式不正确' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评查数据失败:', error);
|
||||
return Response.json({ error: '获取评查数据失败' }, { status: 500 });
|
||||
return Response.json({ result: false, message: '获取评查数据失败' });
|
||||
}
|
||||
}
|
||||
|
||||
export default function ReviewDetails() {
|
||||
const navigate = useNavigate();
|
||||
const { document, reviewPoints, statistics, reviewInfo } = useLoaderData<typeof loader>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
|
||||
messageService.show({
|
||||
title: '错误',
|
||||
message: loaderData.message,
|
||||
type: 'error',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
onConfirm: () => {
|
||||
navigate(-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
},[loaderData, navigate]);
|
||||
|
||||
|
||||
// 模拟获取评查数据
|
||||
useEffect(() => {
|
||||
if (!document) return;
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function RulesFiles() {
|
||||
|
||||
// 处理初始加载数据loader的错误
|
||||
useEffect(() => {
|
||||
if(!result) {
|
||||
if(result === false && message) {
|
||||
toastService.error(message);
|
||||
}
|
||||
}, [result, message]);
|
||||
|
||||
@@ -198,8 +198,10 @@ export default function RulesIndex() {
|
||||
useEffect(() => {
|
||||
if(loaderData.error) {
|
||||
toastService.error(loaderData.error);
|
||||
}else if(loaderData.ruleTypes.length === 0){
|
||||
toastService.error("评查点类型数据为空");
|
||||
}
|
||||
}, [loaderData.error]);
|
||||
}, [loaderData.error,loaderData.ruleTypes]);
|
||||
|
||||
// 当评查点类型变化时,加载对应的规则组
|
||||
useEffect(() => {
|
||||
|
||||
@@ -65,8 +65,25 @@
|
||||
|
||||
/* 清除按钮 */
|
||||
.search-box-clear {
|
||||
@apply absolute right-3 top-1/2 transform -translate-y-1/2
|
||||
text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-200;
|
||||
@apply absolute right-3 top-1/2 transform
|
||||
text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-200
|
||||
border-none bg-transparent p-0 outline-none;
|
||||
}
|
||||
|
||||
/* 当搜索框内有按钮时,调整清除按钮位置和输入框右侧内边距 */
|
||||
.search-box:not(.form-input-only) .form-input {
|
||||
@apply pr-8;
|
||||
}
|
||||
|
||||
/* 修正当搜索框右侧有搜索按钮时,清除图标的位置,确保它显示在输入框内部 */
|
||||
.search-box:not(.form-input-only) .search-box-clear {
|
||||
@apply right-20;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 清除按钮的图标样式 */
|
||||
.search-box-clear i {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
/* 带边框的搜索框 */
|
||||
|
||||
@@ -38,22 +38,16 @@
|
||||
}
|
||||
|
||||
.document-type-new-page .error-message {
|
||||
@apply text-sm text-red-600 mt-1 font-medium;
|
||||
@apply text-sm text-red-600 mt-1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.document-type-new-page .error-message::before {
|
||||
/* .document-type-new-page .error-message::before {
|
||||
content: "⚠️";
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
} */
|
||||
|
||||
.document-type-new-page .error-show {
|
||||
display: flex !important;
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.document-type-new-page .general-error {
|
||||
@apply flex items-center p-3 mb-4 bg-red-50 rounded-md text-red-600 text-sm;
|
||||
|
||||
Reference in New Issue
Block a user