修改时间范围组件,评查详情创建新的数据结构来适配新的返回格式

This commit is contained in:
2025-04-22 20:49:18 +08:00
parent cd2f060d87
commit 6261950356
14 changed files with 678 additions and 299 deletions
+5 -3
View File
@@ -9,6 +9,7 @@ import { FileProgress} from "~/components/ui/FileProgress";
import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps";
import uploadStyles from "~/styles/pages/files_upload.css?url";
import { messageService } from "~/components/ui/MessageModal";
import { toastService } from "~/components/ui/Toast";
import {
getTodayDocuments,
getDocumentTypes,
@@ -561,7 +562,7 @@ export default function FilesUpload() {
}
// 显示错误提示
messageService.error('文件上传失败:只能上传pdf文件。', {
messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, {
title: '文件上传失败',
onConfirm: () => {
resetUpload();
@@ -815,11 +816,12 @@ export default function FilesUpload() {
const response = await updateDocumentAuditStatus(record.id.toString(), 2);
if (response.error) {
console.error('更新文件审核状态失败:', response.error);
alert('更新文件审核状态失败:' + (response.error || '未知错误'));
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
}
} catch (error) {
console.error('更新文件审核状态时出错:', error);
alert('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
}
}
navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`);
+6 -6
View File
@@ -240,7 +240,7 @@ export default function ReviewDetails() {
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
const [activeReviewPointId, setActiveReviewPointId] = useState<string | null>(null);
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
// 模拟获取评查数据
@@ -285,16 +285,16 @@ export default function ReviewDetails() {
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
if (reviewPointId === activeReviewPointId && page) {
if (reviewPointId === activeReviewPointResultId && page) {
setTargetPage(undefined);
// 使用setTimeout确保状态更新后再设置新的targetPage
setTimeout(() => {
setActiveReviewPointId(reviewPointId);
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
}, 0);
} else {
// 正常设置activeReviewPointId和targetPage
setActiveReviewPointId(reviewPointId);
setActiveReviewPointResultId(reviewPointId);
setTargetPage(page);
}
};
@@ -483,7 +483,7 @@ export default function ReviewDetails() {
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointId={activeReviewPointId}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
/>
</div>
@@ -493,7 +493,7 @@ export default function ReviewDetails() {
<ReviewPointsList
reviewPoints={reviewData.reviewPoints}
statistics={reviewData.statistics}
activeReviewPointId={activeReviewPointId}
activeReviewPointResultId={activeReviewPointResultId}
onReviewPointSelect={handleReviewPointSelect}
onStatusChange={handleReviewPointStatusChange}
/>
+3 -2
View File
@@ -7,7 +7,7 @@ import { Button } from "~/components/ui/Button";
import { StatusDot } from "~/components/ui/StatusDot";
import { Table } from "~/components/ui/Table";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
// import { Pagination } from "~/components/ui/Pagination";
import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups";
export function links() {
@@ -539,7 +539,8 @@ export default function RuleGroupsIndex() {
];
return (
<div className="content-container rule-groups-page">
// <div className="content-container rule-groups-page">
<div className="rule-groups-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium flex">
+178 -40
View File
@@ -1,9 +1,10 @@
// app/routes/rule-groups.new.tsx
import { redirect, json, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
import { useLoaderData, useActionData, useNavigation, Form } from "@remix-run/react";
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { toastService } from "~/components/ui/Toast";
import ruleGroupsNewStyles from "~/styles/pages/rule-groups_new.css?url";
import {
getRuleGroups,
@@ -199,7 +200,7 @@ export async function action({ request }: ActionFunctionArgs) {
// 处理API响应
if (response.error) {
console.error("保存分组失败:", response.error);
return json<ActionData>({
return Response.json({
success: false,
errors: {
general: response.error
@@ -209,10 +210,11 @@ export async function action({ request }: ActionFunctionArgs) {
}
// 保存成功,重定向到列表页
toastService.success("保存成功");
return redirect("/rule-groups");
} catch (error) {
console.error("保存分组失败:", error);
return json<ActionData>({
return Response.json({
success: false,
errors: {
general: error instanceof Error ? error.message : "保存分组失败,请稍后重试"
@@ -230,31 +232,162 @@ export default function RuleGroupNew() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// 表单状态
const [groupType, setGroupType] = useState<"primary" | "secondary">("primary");
const [showParentSelect, setShowParentSelect] = useState(false);
// 解构数据
const { group, parentGroups, isEdit, error } = data;
// 初始化表单状态
// 表单状态管理 - 使用受控组件
const [formValues, setFormValues] = useState<{
groupType: "primary" | "secondary";
name: string;
code: string;
parentId: string;
description: string;
status: string;
}>({
groupType: group?.parentId ? "secondary" : "primary",
name: group?.name || "",
code: group?.code || "",
parentId: group?.parentId || "",
description: group?.description || "",
status: group?.status || "active",
});
// 表单验证错误状态
const [formErrors, setFormErrors] = useState<{
name?: string;
code?: string;
parentId?: string;
general?: string;
}>({});
// 表单引用
const formRef = useRef<HTMLFormElement>(null);
// 字段是否被触摸过(用于确定何时显示错误)
const [touchedFields, setTouchedFields] = useState<{
name: boolean;
code: boolean;
parentId: boolean;
}>({
name: false,
code: false,
parentId: false
});
// 从 actionData 初始化表单错误
useEffect(() => {
if (actionData?.errors) {
setFormErrors(actionData.errors);
}
}, [actionData]);
// 根据加载的组数据初始化表单
useEffect(() => {
if (group) {
if (group.parentId) {
setGroupType("secondary");
setShowParentSelect(true);
} else {
setGroupType("primary");
setShowParentSelect(false);
}
setFormValues({
groupType: group.parentId ? "secondary" : "primary",
name: group.name,
code: group.code,
parentId: group.parentId || "",
description: group.description || "",
status: group.status
});
}
}, [group]);
// 处理分组类型变更
// 验证表单字段
const validateField = (field: string, value: string) => {
switch (field) {
case 'name':
return value.trim() === "" ? "分组名称不能为空" : "";
case 'code':
if (value.trim() === "") {
return "分组编码不能为空";
} else if (!/^[a-zA-Z0-9-_]+$/.test(value)) {
return "分组编码只能包含字母、数字、连字符和下划线";
}
return "";
case 'parentId':
return formValues.groupType === "secondary" && value.trim() === ""
? "请选择上级分组"
: "";
default:
return "";
}
};
// 处理字段改变
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormValues(prev => ({
...prev,
[name]: value
}));
// 标记字段为已触摸
if (['name', 'code', 'parentId'].includes(name)) {
setTouchedFields(prev => ({
...prev,
[name]: true
}));
}
// 实时验证
const error = validateField(name, value);
setFormErrors(prev => ({
...prev,
[name]: error
}));
};
// 处理分组类型更改
const handleGroupTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value as "primary" | "secondary";
setGroupType(value);
setShowParentSelect(value === "secondary");
setFormValues(prev => ({
...prev,
groupType: value
}));
// 如果切换为一级分组,清除父分组错误
if (value === "primary") {
setFormErrors(prev => ({
...prev,
parentId: ""
}));
} else if (value === "secondary" && touchedFields.parentId) {
// 如果切换为二级分组,且父分组字段已被触摸,重新验证
const error = validateField('parentId', formValues.parentId);
setFormErrors(prev => ({
...prev,
parentId: error
}));
}
};
// 处理表单提交前验证
const handleBeforeSubmit = (e: React.FormEvent) => {
// 标记所有字段为已触摸
setTouchedFields({
name: true,
code: true,
parentId: true
});
// 验证所有字段
const errors = {
name: validateField('name', formValues.name),
code: validateField('code', formValues.code),
parentId: validateField('parentId', formValues.parentId)
};
setFormErrors(errors);
// 如果有错误,阻止提交
if (errors.name || errors.code || (formValues.groupType === "secondary" && errors.parentId)) {
e.preventDefault();
}
};
// 如果加载数据时出错,显示错误信息
@@ -304,15 +437,15 @@ export default function RuleGroupNew() {
</div>
{/* 错误提示 */}
{actionData?.errors?.general && (
{formErrors.general && (
<div className="general-error">
<i className="ri-error-warning-line mr-2"></i>
{actionData.errors.general}
{formErrors.general}
</div>
)}
{/* 表单 */}
<Form method="post" id="group-form">
<Form method="post" id="group-form" ref={formRef} onSubmit={handleBeforeSubmit}>
{/* 如果是编辑模式,添加ID */}
{group?.id && <input type="hidden" name="id" value={group.id} />}
@@ -336,7 +469,7 @@ export default function RuleGroupNew() {
name="groupType"
className="radio-input"
value="primary"
checked={groupType === "primary"}
checked={formValues.groupType === "primary"}
onChange={handleGroupTypeChange}
/>
<span></span>
@@ -348,7 +481,7 @@ export default function RuleGroupNew() {
name="groupType"
className="radio-input"
value="secondary"
checked={groupType === "secondary"}
checked={formValues.groupType === "secondary"}
onChange={handleGroupTypeChange}
/>
<span></span>
@@ -358,7 +491,7 @@ export default function RuleGroupNew() {
</div>
{/* 上级分组选择 */}
{showParentSelect && (
{formValues.groupType === "secondary" && (
<div className="form-group">
<label htmlFor="parentId" className="form-label">
<span className="required-mark">*</span>
@@ -366,8 +499,9 @@ export default function RuleGroupNew() {
<select
id="parentId"
name="parentId"
className={`form-select ${actionData?.errors?.parentId ? 'error' : ''}`}
defaultValue={group?.parentId || ""}
className={`form-select ${touchedFields.parentId && formErrors.parentId ? 'error' : ''}`}
value={formValues.parentId}
onChange={handleChange}
>
<option value=""></option>
{parentGroups
@@ -378,8 +512,8 @@ export default function RuleGroupNew() {
</option>
))}
</select>
{actionData?.errors?.parentId && (
<div className="form-error">{actionData.errors.parentId}</div>
{touchedFields.parentId && formErrors.parentId && (
<div className="form-error">{formErrors.parentId}</div>
)}
<div className="form-tip"></div>
</div>
@@ -396,12 +530,13 @@ export default function RuleGroupNew() {
type="text"
id="code"
name="code"
className={`form-input ${actionData?.errors?.code ? 'error' : ''}`}
defaultValue={group?.code || actionData?.values?.code || ""}
className={`form-input ${touchedFields.code && formErrors.code ? 'error' : ''}`}
value={formValues.code}
onChange={handleChange}
placeholder="请输入分组编码,如contract-base"
/>
{actionData?.errors?.code && (
<div className="form-error">{actionData.errors.code}</div>
{touchedFields.code && formErrors.code && (
<div className="form-error">{formErrors.code}</div>
)}
<div className="form-tip">线</div>
</div>
@@ -415,12 +550,13 @@ export default function RuleGroupNew() {
type="text"
id="name"
name="name"
className={`form-input ${actionData?.errors?.name ? 'error' : ''}`}
defaultValue={group?.name || actionData?.values?.name || ""}
className={`form-input ${touchedFields.name && formErrors.name ? 'error' : ''}`}
value={formValues.name}
onChange={handleChange}
placeholder="请输入分组名称,如合同基本要素检查"
/>
{actionData?.errors?.name && (
<div className="form-error">{actionData.errors.name}</div>
{touchedFields.name && formErrors.name && (
<div className="form-error">{formErrors.name}</div>
)}
<div className="form-tip">使30</div>
</div>
@@ -443,7 +579,8 @@ export default function RuleGroupNew() {
id="description"
name="description"
className="form-textarea"
defaultValue={group?.description || actionData?.values?.description || ""}
value={formValues.description}
onChange={handleChange}
placeholder="请输入分组描述,包括适用场景、分组目的等"
></textarea>
<div className="form-tip"></div>
@@ -456,7 +593,8 @@ export default function RuleGroupNew() {
id="status"
name="status"
className="form-select"
defaultValue={group?.status || actionData?.values?.status || "active"}
value={formValues.status}
onChange={handleChange}
>
<option value="active"></option>
<option value="inactive"></option>
@@ -472,7 +610,7 @@ export default function RuleGroupNew() {
id="sortOrder"
name="sortOrder"
className="form-input"
defaultValue={group?.sortOrder?.toString() || actionData?.values?.sortOrder || "0"}
defaultValue={group?.sortOrder?.toString() || "0"}
placeholder="请输入排序值,数字越小排序越靠前"
min="0"
/>
+83 -29
View File
@@ -3,7 +3,7 @@ import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { FileIcon } from "~/components/ui/FileIcon";
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
import { Table } from "~/components/ui/Table";
import { Tag } from "~/components/ui/Tag";
@@ -15,6 +15,7 @@ import {
updateDocumentAuditStatus
} from "~/api/evaluation_points/rules-files";
import { getDocumentTypes } from "~/api/document-types/document-types";
import { toastService } from "~/components/ui/Toast";
export const links = () => [
{ rel: "stylesheet", href: rulesFilesStyles }
@@ -55,6 +56,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
const fileType = url.searchParams.get("fileType") || "";
const reviewStatus = url.searchParams.get("reviewStatus") || "";
const dateRange = url.searchParams.get("dateRange") || "";
const dateFrom = url.searchParams.get("dateFrom") || "";
const dateTo = url.searchParams.get("dateTo") || "";
const keyword = url.searchParams.get("keyword") || "";
const sortOrder = url.searchParams.get("sortOrder") || "upload_time_desc";
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
@@ -70,6 +73,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
fileType,
reviewStatus,
dateRange,
dateFrom,
dateTo,
keyword,
sortOrder,
page: currentPage,
@@ -113,9 +118,11 @@ export function ErrorBoundary() {
// 在文件中定义一个与路由文件名匹配的命名函数组件
export default function RulesFiles() {
const navigate = useNavigate();
const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || '';
// 处理筛选条件变更
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
@@ -196,14 +203,14 @@ export default function RulesFiles() {
);
}
// 如果评查状态为通过,显示"所有评查点均通过"
if (file.reviewStatus === 'fail') {
return (
<div className="text-sm text-error">
<i className="ri-error-warning-line mr-1"></i>{file.score || 0}80
</div>
);
}
// 如果评查状态为通过,显示"统计分数为:{file.score || 0}。分数低于80分。"
// if (file.reviewStatus === 'fail') {
// return (
// <div className="text-sm text-error">
// <i className="ri-error-warning-line mr-1"></i>统计分数为:{file.score || 0}。分数低于80分。
// </div>
// );
// }
// 显示问题列表
if (file.reviewStatus !== 'pass' && file.reviewStatus !== 'fail' && file.issues && file.issues.length > 0) {
@@ -268,9 +275,38 @@ export default function RulesFiles() {
}, 100);
} catch (error) {
console.error('下载文件失败:', error);
alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 处理时间范围变更
const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => {
const newParams = new URLSearchParams(searchParams);
if(value) {
newParams.set(field, value);
} else {
newParams.delete(field);
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleReset = () => {
const newParams = new URLSearchParams(searchParams);
const searchInput = document.querySelector('input[name="keyword"]');
if(searchInput) {
(searchInput as HTMLInputElement).value = '';
}
// newParams.delete('keyword');
newParams.delete('dateFrom');
newParams.delete('dateTo');
newParams.delete('fileType');
// newParams.delete('reviewStatus');
newParams.delete('sortOrder');
newParams.set('page', '1');
setSearchParams(newParams);
};
// 文件类型选项
const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({
@@ -279,20 +315,20 @@ export default function RulesFiles() {
}));
// 评查状态选项
const reviewStatusOptions = [
{ value: 'pass', label: '通过' },
{ value: 'warning', label: '警告' },
{ value: 'fail', label: '不通过' },
{ value: 'pending', label: '待人工确认' }
];
// const reviewStatusOptions = [
// { value: 'pass', label: '通过' },
// { value: 'warning', label: '警告' },
// { value: 'fail', label: '不通过' },
// { value: 'pending', label: '待人工确认' }
// ];
// 时间范围选项
const dateRangeOptions = [
{ value: DateRange.TODAY, label: '今天' },
{ value: DateRange.WEEK, label: '本周' },
{ value: DateRange.MONTH, label: '本月' },
// { value: DateRange.CUSTOM, label: '自定义时间段' }
];
// const dateRangeOptions = [
// { value: DateRange.TODAY, label: '今天' },
// { value: DateRange.WEEK, label: '本周' },
// { value: DateRange.MONTH, label: '本月' },
// // { value: DateRange.CUSTOM, label: '自定义时间段' }
// ];
// 定义表格列配置
const columns = [
@@ -423,7 +459,7 @@ export default function RulesFiles() {
];
return (
<div className="p-6 review-files-page">
<div className="review-files-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
@@ -440,7 +476,15 @@ export default function RulesFiles() {
</div>
{/* 筛选区域 */}
<FilterPanel className="px-3 py-3" noActionDivider={true}>
<FilterPanel className="px-3 py-3" noActionDivider={true}
actions={
<>
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2 hover:!border-gray-300">
</Button>
</>
}
>
<FilterSelect
label="文件类型"
name="fileType"
@@ -450,23 +494,33 @@ export default function RulesFiles() {
className="mr-2 w-40"
/>
<FilterSelect
{/* <FilterSelect
label="评查状态"
name="reviewStatus"
value={searchParams.get('reviewStatus') || ''}
options={reviewStatusOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/>
/> */}
<FilterSelect
{/* <FilterSelect
label="时间范围"
name="dateRange"
value={searchParams.get('dateRange') || ''}
options={dateRangeOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/> */}
<DateRangeFilter
label="时间范围"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
/>
<FilterSelect
label="排序方式"
name="sortOrder"
@@ -485,7 +539,7 @@ export default function RulesFiles() {
placeholder="搜索文件名、合同编号"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
buttonText="搜索"
buttonText=""
className="mr-2 flex-1"
/>
</FilterPanel>
+1 -1
View File
@@ -465,7 +465,7 @@ export default function RulesIndex() {
];
return (
<div className="p-6 rules-page">
<div className="rules-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>