优化评查详情,新增信息提示框组件
This commit is contained in:
@@ -251,61 +251,61 @@ export async function getReviewPoints(fileId: string) {
|
||||
}
|
||||
|
||||
// 提取页码数组
|
||||
let contentPage: Record<string, object> = {};
|
||||
let contentPage: Record<string, string> = {};
|
||||
// console.log('result-------', result.evaluated_results?.result);
|
||||
// console.log('datacontent-------', data);
|
||||
if (data && typeof data === 'object') {
|
||||
try {
|
||||
const dataObj = data as Record<string, string>;
|
||||
// 检查是否是预期的格式 {'立案报告表-完整性检查':'缺失部分内容'}
|
||||
for (const key in dataObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||||
// 使用'-'分割获取前缀(如'立案报告表')
|
||||
const prefix = key.split('-')[0];
|
||||
// console.log('prefix-------', prefix);
|
||||
// 检查document.data中的ocrResult是否存在这个key
|
||||
if (documentData.data?.ocrResult &&
|
||||
typeof documentData.data.ocrResult === 'object') {
|
||||
// try {
|
||||
// const dataObj = data as Record<string, string>;
|
||||
// // 检查是否是预期的格式 {'立案报告表-完整性检查':'缺失部分内容'}
|
||||
// for (const key in dataObj) {
|
||||
// if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||||
// // 使用'-'分割获取前缀(如'立案报告表')
|
||||
// const prefix = key.split('-')[0];
|
||||
// // console.log('prefix-------', prefix);
|
||||
// // 检查document.data中的ocrResult是否存在这个key
|
||||
// if (documentData.data?.ocrResult &&
|
||||
// typeof documentData.data.ocrResult === 'object') {
|
||||
|
||||
// ocrResult可能有嵌套的ocr_result属性
|
||||
let ocrData: OcrData = documentData.data.ocrResult as OcrData;
|
||||
// // ocrResult可能有嵌套的ocr_result属性
|
||||
// let ocrData: OcrData = documentData.data.ocrResult as OcrData;
|
||||
|
||||
// 检查是否有嵌套的ocr_result对象
|
||||
if ('ocr_result' in ocrData &&
|
||||
ocrData.ocr_result &&
|
||||
typeof ocrData.ocr_result === 'object') {
|
||||
ocrData = ocrData.ocr_result as OcrData;
|
||||
}
|
||||
// // 检查是否有嵌套的ocr_result对象
|
||||
// if ('ocr_result' in ocrData &&
|
||||
// ocrData.ocr_result &&
|
||||
// typeof ocrData.ocr_result === 'object') {
|
||||
// ocrData = ocrData.ocr_result as OcrData;
|
||||
// }
|
||||
|
||||
for (const ocrKey in ocrData) {
|
||||
// 如果找到匹配的key
|
||||
if (ocrKey === prefix &&
|
||||
ocrData[ocrKey] &&
|
||||
typeof ocrData[ocrKey] === 'object' &&
|
||||
'pages' in ocrData[ocrKey]) {
|
||||
// for (const ocrKey in ocrData) {
|
||||
// // 如果找到匹配的key
|
||||
// if (ocrKey === prefix &&
|
||||
// ocrData[ocrKey] &&
|
||||
// typeof ocrData[ocrKey] === 'object' &&
|
||||
// 'pages' in ocrData[ocrKey]) {
|
||||
|
||||
// 获取pages数组
|
||||
const pages = ocrData[ocrKey].pages;
|
||||
if (Array.isArray(pages)) {
|
||||
// 存储每个key对应的页码数组
|
||||
contentPage[key] = pages;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// // 4-22 更改数据结构:通过拿到的data数据(每一个key对应一个object),将object中的page提取出来
|
||||
// try{
|
||||
// const dataObj = data as Record<string, object>;
|
||||
// for (const key in dataObj) {
|
||||
// if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||||
// contentPage[key] = dataObj[key];
|
||||
// // 获取pages数组
|
||||
// const pages = ocrData[ocrKey].pages;
|
||||
// if (Array.isArray(pages)) {
|
||||
// // 存储每个key对应的页码数组
|
||||
// contentPage[key] = pages;
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// 4-22 更改数据结构:通过拿到的data数据(每一个key对应一个object),将object中的page提取出来
|
||||
try{
|
||||
const dataObj = data as Record<string, {page: number | string,value: string}>;
|
||||
for (const key in dataObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
|
||||
contentPage[key] = dataObj[key].page.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解析评查点data失败:', e);
|
||||
contentPage = {};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* - 操作按钮: 提供一键替换和人工审核功能
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toastService } from '../ui/Toast';
|
||||
// import { toastService } from '../ui/Toast';
|
||||
|
||||
/**
|
||||
@@ -34,13 +35,13 @@ export interface ReviewPoint {
|
||||
title: string;
|
||||
groupName: string;
|
||||
status: string;
|
||||
content: Record<string, string | { page?: number, value?: string }>;
|
||||
content: Record<string, { page?: number | string, value?: object }>;
|
||||
suggestion: string;
|
||||
needsHumanReview?: boolean;
|
||||
humanReviewNote?: string;
|
||||
humanReviewBy?: string;
|
||||
humanReviewTime?: string;
|
||||
contentPage?: Record<string, number[]>;
|
||||
contentPage?: Record<string, number | string>;
|
||||
position?: {
|
||||
section: string;
|
||||
index: number;
|
||||
@@ -448,24 +449,27 @@ export function ReviewPointsList({
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`通过:单独点击${key}----`, reviewPoint);
|
||||
console.log(`单独点击${key}----`, reviewPoint);
|
||||
// 检查value中的page属性是否存在
|
||||
if (value && typeof value === 'object' && value.page) {
|
||||
if (value && typeof value === 'object' && value.page && parseInt(value.page as string) > 0) {
|
||||
// 获取当前 key 对应的第一个页码并跳转
|
||||
console.log(`通过:单独点击${key}----页码---`, value.page);
|
||||
console.log(`单独点击${key}----页码---`, value.page);
|
||||
onReviewPointSelect(reviewPoint.id, parseInt(value.page as string));
|
||||
|
||||
onReviewPointSelect(reviewPoint.id, value.page);
|
||||
} else {
|
||||
console.log(`通过:单独点击${key}--------没有对应页码`);
|
||||
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (value && typeof value === 'object' && value.page) {
|
||||
onReviewPointSelect(reviewPoint.id, value.page);
|
||||
if (value && typeof value === 'object' && value.page && parseInt(value.page as string) > 0) {
|
||||
onReviewPointSelect(reviewPoint.id, parseInt(value.page as string));
|
||||
} else {
|
||||
console.log(`通过:单独点击${key}--------没有对应页码`);
|
||||
toastService.error(`无法找到"${key}"对应的索引内容`);
|
||||
console.log(`单独点击${key}--------没有对应页码`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -474,11 +478,16 @@ export function ReviewPointsList({
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs w-15 ${value ? 'text-error' : 'text-warning'}`}>
|
||||
{value ? '' : '缺失'}
|
||||
<span className="text-xs pr-5">{key}</span>
|
||||
<span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
|
||||
{value.value?.toString().trim() ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left select-text">
|
||||
{(value.value?.toString().trim() === '')
|
||||
? <span className="invisible">占位符</span>
|
||||
: value.value?.toString() || ''}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -551,7 +560,7 @@ export function ReviewPointsList({
|
||||
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
{/* 修改评查结果的结构之前,先显示旧的结构 */}
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
@@ -584,7 +593,6 @@ export function ReviewPointsList({
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs w-15 ${value ? 'text-error' : 'text-warning'}`}>
|
||||
@@ -597,9 +605,9 @@ export function ReviewPointsList({
|
||||
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||||
{/* {renderContent(reviewPoint)} */}
|
||||
{renderContent(reviewPoint)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -655,7 +663,7 @@ export function ReviewPointsList({
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
<div>
|
||||
{/* 修改评查结果的结构之前,先显示旧的结构 */}
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
@@ -688,7 +696,7 @@ export function ReviewPointsList({
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs w-15 ${value ? 'text-error' : 'text-warning'}`}>
|
||||
@@ -701,19 +709,15 @@ export function ReviewPointsList({
|
||||
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||||
{/* {renderContent(reviewPoint)} */}
|
||||
{renderContent(reviewPoint)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 非通过状态,显示内容和修改建议
|
||||
const isErrorStatus = reviewPoint.result === false && reviewPoint.status === 'error';
|
||||
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -774,7 +778,7 @@ export function ReviewPointsList({
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
<div>
|
||||
{/* 修改评查结果的结构之前,先显示旧的结构 */}
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
|
||||
@@ -810,7 +814,6 @@ export function ReviewPointsList({
|
||||
tabIndex={0}
|
||||
aria-label={`查看${key}内容详情`}
|
||||
>
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
@@ -823,9 +826,9 @@ export function ReviewPointsList({
|
||||
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
))} */}
|
||||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||||
{/* {renderContent(reviewPoint)} */}
|
||||
{renderContent(reviewPoint)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -969,12 +972,10 @@ export function ReviewPointsList({
|
||||
|
||||
// 遍历contentPage中的每个key
|
||||
for (const key of Object.keys(reviewPoint.contentPage)) {
|
||||
const pageArr = reviewPoint.contentPage[key];
|
||||
// 如果数组存在且长度大于0
|
||||
if (pageArr && pageArr.length > 0) {
|
||||
// 返回第一个找到的有效页码,以及对应的key
|
||||
if (reviewPoint.contentPage[key] && parseInt(reviewPoint.contentPage[key] as string) > 0) {
|
||||
// 返回第一个找到的有效页码,以及对应的key
|
||||
return {
|
||||
pageIndex: pageArr[0],
|
||||
pageIndex: parseInt(reviewPoint.contentPage[key] as string),
|
||||
key,
|
||||
id: reviewPoint.id
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '~/components/ui/Button';
|
||||
interface ActionButtonsProps {
|
||||
onSave?: () => void;
|
||||
onSaveDraft?: () => void;
|
||||
@@ -23,13 +22,14 @@ export function ActionButtons({ onSave, onSaveDraft, isEditMode }: ActionButtons
|
||||
>
|
||||
<i className="ri-draft-line mr-1"></i> {isEditMode ? '另存为草稿' : '保存草稿'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ant-btn ant-btn-default min-w-[120px]"
|
||||
<Button
|
||||
type="default"
|
||||
className="min-w-[120px] focus:!ring-gray-300"
|
||||
onClick={() => window.history.back()}
|
||||
icon="ri-arrow-left-line"
|
||||
>
|
||||
<i className="ri-arrow-left-line mr-1"></i> 返回
|
||||
</button>
|
||||
返回
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+112
-25
@@ -1,5 +1,14 @@
|
||||
// app/components/ui/Modal.tsx
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import modalStyles from '~/styles/components/modal.css?url';
|
||||
|
||||
// 导出样式
|
||||
export function links() {
|
||||
return [{ rel: 'stylesheet', href: modalStyles }];
|
||||
}
|
||||
|
||||
// 模态框尺寸
|
||||
export type ModalSize = 'small' | 'medium' | 'large' | 'full';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -8,65 +17,143 @@ interface ModalProps {
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
width?: number | string;
|
||||
size?: ModalSize;
|
||||
className?: string;
|
||||
closeOnEsc?: boolean;
|
||||
closeOnBackdropClick?: boolean;
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, footer, width = 500 }: ModalProps) {
|
||||
// 点击ESC键关闭模态框
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleEsc);
|
||||
return () => window.removeEventListener('keydown', handleEsc);
|
||||
}, [isOpen, onClose]);
|
||||
export function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
width,
|
||||
size = 'medium',
|
||||
className = '',
|
||||
closeOnEsc = true,
|
||||
closeOnBackdropClick = true
|
||||
}: ModalProps) {
|
||||
// 引用模态框内容元素
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
// 保存之前的活动元素,以便关闭时恢复焦点
|
||||
const previousActiveElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
// 禁用背景滚动
|
||||
// 自动聚焦第一个可聚焦元素并保存先前的焦点元素
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
previousActiveElement.current = document.activeElement as HTMLElement;
|
||||
|
||||
if (contentRef.current) {
|
||||
const focusableElements = contentRef.current.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusableElements.length > 0) {
|
||||
(focusableElements[0] as HTMLElement).focus();
|
||||
} else {
|
||||
contentRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 当模态框打开时禁止背景滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
} else if (previousActiveElement.current) {
|
||||
// 当模态框关闭时,恢复之前的焦点
|
||||
previousActiveElement.current.focus();
|
||||
// 恢复背景滚动
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// 处理背景点击事件
|
||||
const handleBackdropClick = () => {
|
||||
if (closeOnBackdropClick) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取尺寸样式类
|
||||
const sizeClass = width ? '' : `modal-${size}`;
|
||||
|
||||
// 计算宽度样式
|
||||
const widthStyle = width ?
|
||||
{ width: typeof width === 'number' ? `${width}px` : width, maxWidth: typeof width === 'number' ? `${width}px` : width } : {};
|
||||
|
||||
// 使用useEffect添加键盘事件监听器
|
||||
useEffect(() => {
|
||||
const handleEscKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && closeOnEsc) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [closeOnEsc, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{ maxWidth: typeof width === 'number' ? `${width}px` : width }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
ref={contentRef}
|
||||
className={`modal-content ${sizeClass} ${className}`}
|
||||
style={widthStyle}
|
||||
tabIndex={-1}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
<h3 id="modal-title" className="modal-title">{title}</h3>
|
||||
<button
|
||||
className="text-gray-400 hover:text-gray-500"
|
||||
className="modal-close"
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body py-4">
|
||||
<div className="modal-body">
|
||||
{children}
|
||||
</div>
|
||||
{footer && (
|
||||
<div className="modal-footer flex justify-end space-x-2 pt-4 border-t border-gray-100">
|
||||
<div className="modal-footer">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 背景点击处理 */}
|
||||
<button
|
||||
className="modal-backdrop-button"
|
||||
onClick={handleBackdropClick}
|
||||
aria-label="关闭模态框"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'default',
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export function Toast({
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
||||
const [messageLines, setMessageLines] = useState<number>(1);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// 在客户端渲染时获取 portal 容器
|
||||
useEffect(() => {
|
||||
@@ -89,7 +90,7 @@ export function Toast({
|
||||
|
||||
// 自动关闭
|
||||
useEffect(() => {
|
||||
if (isOpen && autoClose) {
|
||||
if (isOpen && autoClose && !isHovered) {
|
||||
// 根据消息长度调整显示时间,长消息显示更长时间
|
||||
const adjustedDelay = Math.min(
|
||||
autoCloseDelay + (message.length > 100 ? 2000 : 0),
|
||||
@@ -101,7 +102,16 @@ export function Toast({
|
||||
}, adjustedDelay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, autoClose, autoCloseDelay, handleClose, message]);
|
||||
}, [isOpen, autoClose, autoCloseDelay, handleClose, message, isHovered]);
|
||||
|
||||
// 鼠标悬停处理
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
// 渲染图标
|
||||
const renderIcon = () => {
|
||||
@@ -138,10 +148,10 @@ export function Toast({
|
||||
return createPortal(
|
||||
<div
|
||||
className={`toast toast-${type} ${isClosing ? 'closing' : ''} ${className} ${messageLines > 3 ? 'toast-multiline' : ''}`}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="toast-content">
|
||||
<div className="toast-icon-wrapper">
|
||||
@@ -155,6 +165,7 @@ export function Toast({
|
||||
className="toast-close"
|
||||
onClick={handleClose}
|
||||
aria-label="关闭"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
|
||||
+132
-109
@@ -1,14 +1,17 @@
|
||||
import { json, type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useSubmit, Link } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { useLoaderData, useSearchParams, useFetcher, Link } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { Modal } from "~/components/ui/Modal";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Tag } from "~/components/ui/Tag";
|
||||
import { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem } from "~/api/system_setting/config-lists";
|
||||
import configListsStyles from "~/styles/pages/config-lists_index.css?url";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: configListsStyles }
|
||||
@@ -54,16 +57,13 @@ export const MODULE_LABELS: Record<ConfigModule, string> = {
|
||||
[ConfigModule.NOTIFICATION]: '通知'
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
configs: ConfigItem[];
|
||||
totalCount: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
types: string[];
|
||||
environments: string[];
|
||||
// 操作响应
|
||||
interface ActionResponse {
|
||||
result: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get("name") || "";
|
||||
@@ -95,7 +95,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
throw new Error(optionsResponse.error || "获取配置选项失败");
|
||||
}
|
||||
|
||||
return json<LoaderData>({
|
||||
return Response.json({
|
||||
configs: configsResponse.data,
|
||||
totalCount: configsResponse.total,
|
||||
currentPage,
|
||||
@@ -110,8 +110,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载配置列表失败:', error);
|
||||
throw new Response('加载配置列表失败', { status: 500 });
|
||||
}
|
||||
return Response.json({
|
||||
error: error || '加载配置列表失败',
|
||||
status: 500
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
@@ -120,7 +123,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const configId = formData.get('configId');
|
||||
|
||||
if (!configId) {
|
||||
return json({ success: false, error: "缺少配置ID" }, { status: 400 });
|
||||
return Response.json({ result: false, message: "缺少配置ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 进行更新启用和禁用的状态
|
||||
@@ -130,37 +133,48 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
const response = await updateConfigStatus(parseInt(configId as string), is_active);
|
||||
|
||||
if (!response.success) {
|
||||
return json({ success: false, error: response.error }, { status: 500 });
|
||||
if (response.error) {
|
||||
return Response.json({ result: false, message: response.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
return Response.json({ result: true, message: is_active ? '启用成功' : '禁用成功' });
|
||||
}
|
||||
|
||||
return json({ success: false, error: "未知操作" }, { status: 400 });
|
||||
return Response.json({ result: false, message: "未知操作" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('操作配置失败:', error);
|
||||
return json({ success: false, error: "操作失败" }, { status: 500 });
|
||||
return Response.json({ result: false, message: error || "操作失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载配置列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default function ConfigListsIndex() {
|
||||
const { configs, totalCount, currentPage, pageSize, types, environments } = useLoaderData<typeof loader>();
|
||||
const { configs, totalCount, currentPage, pageSize, types, environments, error } = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const submit = useSubmit();
|
||||
const fetcher = useFetcher<ActionResponse>();
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState<ConfigItem | null>(null);
|
||||
|
||||
// 处理loader错误
|
||||
useEffect(() => {
|
||||
if(error) {
|
||||
toastService.error(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast
|
||||
useEffect(() => {
|
||||
if(fetcher.state === 'idle' && fetcher.data) {
|
||||
if(fetcher.data.result) {
|
||||
toastService.success(fetcher.data.message);
|
||||
} else if (fetcher.data.message) {
|
||||
toastService.error(fetcher.data.message);
|
||||
}
|
||||
}
|
||||
}, [fetcher.state,fetcher.data]);
|
||||
|
||||
// 处理筛选条件变化
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
@@ -177,13 +191,11 @@ export default function ConfigListsIndex() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 搜索配置名称
|
||||
const handleConfigNameSearch = (value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (value) {
|
||||
newParams.set('name', value);
|
||||
} else {
|
||||
newParams.delete('name');
|
||||
}
|
||||
|
||||
value ? newParams.set('name', value) : newParams.delete('name');
|
||||
|
||||
// 搜索时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
@@ -191,28 +203,40 @@ export default function ConfigListsIndex() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理启用和禁用状态
|
||||
const handleToggleStatus = (config: ConfigItem) => {
|
||||
if (window.confirm(`确定要${config.is_active ? '禁用' : '启用'}该配置吗?`)) {
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'toggleStatus');
|
||||
formData.append('configId', config.id.toString());
|
||||
formData.append('is_active', String(!config.is_active));
|
||||
|
||||
submit(formData, { method: 'post' });
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: '提示',
|
||||
message: `确定要${config.is_active ? '禁用' : '启用'}该配置吗?`,
|
||||
type: config.is_active ? 'warning' : 'success',
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
onConfirm: () => {
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'toggleStatus');
|
||||
formData.append('configId', config.id.toString());
|
||||
formData.append('is_active', String(!config.is_active));
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理查看详情
|
||||
const handleViewDetail = (config: ConfigItem) => {
|
||||
setSelectedConfig(config);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
// 处理分页
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理每页条数变更
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('pageSize', size.toString());
|
||||
@@ -223,17 +247,10 @@ export default function ConfigListsIndex() {
|
||||
// 处理重置筛选
|
||||
const handleReset = () => {
|
||||
const nameInput = document.querySelector('input[placeholder="请输入配置名称"]') as HTMLInputElement;
|
||||
const typeSelect = document.querySelector('select[name="type"]') as HTMLInputElement;
|
||||
const environmentSelect = document.querySelector('select[name="environment"]') as HTMLInputElement;
|
||||
const statusSelect = document.querySelector('select[name="is_active"]') as HTMLInputElement;
|
||||
if(nameInput) nameInput.value = ''
|
||||
|
||||
setSearchParams(new URLSearchParams());
|
||||
|
||||
if(nameInput) nameInput.value = ''
|
||||
if(typeSelect) typeSelect.value = ''
|
||||
if(environmentSelect) environmentSelect.value = ''
|
||||
if(statusSelect) statusSelect.value = ''
|
||||
|
||||
};
|
||||
|
||||
// 关闭详情模态框
|
||||
@@ -355,7 +372,7 @@ export default function ConfigListsIndex() {
|
||||
label="所属模块"
|
||||
name="type"
|
||||
value={searchParams.get('type') || ''}
|
||||
options={[ ...types.map(type => ({ value: type, label: type }))]}
|
||||
options={[ ...types.map((type: string) => ({ value: type, label: type }))]}
|
||||
onChange={handleFilterChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
@@ -364,7 +381,7 @@ export default function ConfigListsIndex() {
|
||||
label="环境"
|
||||
name="environment"
|
||||
value={searchParams.get('environment') || ''}
|
||||
options={[ ...environments.map(env => ({ value: env, label: env }))]}
|
||||
options={[ ...environments.map((env: string) => ({ value: env, label: env }))]}
|
||||
onChange={handleFilterChange}
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
@@ -409,70 +426,76 @@ export default function ConfigListsIndex() {
|
||||
|
||||
{/* 配置详情模态框 */}
|
||||
{showDetailModal && selectedConfig && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-3xl w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">查看配置详情</h3>
|
||||
<button className="text-gray-500 hover:text-gray-700" onClick={closeDetailModal}>
|
||||
<i className="ri-close-line text-xl"></i>
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={showDetailModal}
|
||||
onClose={closeDetailModal}
|
||||
title="查看配置详情"
|
||||
width="800px"
|
||||
footer={
|
||||
<Button type="default" onClick={closeDetailModal}>关闭</Button>
|
||||
}
|
||||
>
|
||||
<div className="config-detail-content">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置名称</div>
|
||||
<div className="config-detail-value">{selectedConfig.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-content">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置名称</div>
|
||||
<div className="config-detail-value">{selectedConfig.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">所属模块</div>
|
||||
<div className="config-detail-value">{selectedConfig.type}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">环境</div>
|
||||
<div className="config-detail-value">
|
||||
<span className="env-tag">
|
||||
{selectedConfig.environment}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">状态</div>
|
||||
<div className="config-detail-value">
|
||||
<Tag color={selectedConfig.is_active ? 'green' : 'red'}>
|
||||
{selectedConfig.is_active ? '已启用' : '已禁用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置数据</div>
|
||||
<pre className="config-detail-code">
|
||||
{JSON.stringify(selectedConfig.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">创建时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.created_at}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">更新时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.updated_at}</div>
|
||||
</div>
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">所属模块</div>
|
||||
<div className="config-detail-value">{selectedConfig.type}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">环境</div>
|
||||
<div className="config-detail-value">
|
||||
<span className="env-tag">
|
||||
{selectedConfig.environment}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button type="default" onClick={closeDetailModal}>关闭</Button>
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">状态</div>
|
||||
<div className="config-detail-value">
|
||||
<Tag color={selectedConfig.is_active ? 'green' : 'red'}>
|
||||
{selectedConfig.is_active ? '已启用' : '已禁用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">配置数据</div>
|
||||
<pre className="config-detail-code">
|
||||
{JSON.stringify(selectedConfig.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">创建时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.created_at}</div>
|
||||
</div>
|
||||
|
||||
<div className="config-detail-item">
|
||||
<div className="config-detail-label">更新时间</div>
|
||||
<div className="config-detail-value">{selectedConfig.updated_at}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误边界
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载配置列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+441
-180
@@ -1,11 +1,12 @@
|
||||
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { Form, useActionData, useLoaderData, useNavigation } 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 { ConfigModule, MODULE_LABELS, ENVIRONMENT_LABELS } from "./config-lists._index";
|
||||
import { ENVIRONMENT_LABELS } from "./config-lists._index";
|
||||
import { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists";
|
||||
import configNewStyles from "~/styles/pages/config-lists_new.css?url";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: configNewStyles }
|
||||
@@ -39,6 +40,7 @@ export const EXTENDED_ENVIRONMENT_LABELS: Record<string, string> = {
|
||||
[ExtendedConfigEnvironment.COMMON]: '通用'
|
||||
};
|
||||
|
||||
// 新增配置表单数据
|
||||
interface ConfigData {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -49,41 +51,16 @@ interface ConfigData {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 加载器数据类型
|
||||
interface LoaderData {
|
||||
config?: ConfigData;
|
||||
isEdit: boolean;
|
||||
types: string[];
|
||||
environments: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
let config: ConfigData | undefined = undefined;
|
||||
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
if (optionsResponse.error) {
|
||||
throw new Error(optionsResponse.error);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
// 获取配置详情
|
||||
const detailResponse = await getConfigDetail(id);
|
||||
if (detailResponse.error) {
|
||||
throw new Error(detailResponse.error);
|
||||
}
|
||||
config = detailResponse.data;
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
config,
|
||||
isEdit: !!config,
|
||||
types: optionsResponse.data?.types || [],
|
||||
environments: optionsResponse.data?.environments || []
|
||||
});
|
||||
}
|
||||
|
||||
// 新增配置表单数据
|
||||
interface ActionData {
|
||||
success?: boolean;
|
||||
errors?: {
|
||||
@@ -93,81 +70,7 @@ interface ActionData {
|
||||
config?: string;
|
||||
general?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const name = formData.get("name") as string;
|
||||
const type = formData.get("type") as string;
|
||||
const environment = formData.get("environment") as string;
|
||||
const config = formData.get("config") as string;
|
||||
const is_active = formData.get("is_active") === "true";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 表单验证
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "配置名称不能为空";
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
errors.type = "请选择所属模块";
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
errors.environment = "请选择环境";
|
||||
}
|
||||
|
||||
if (!config || config.trim() === "") {
|
||||
errors.config = "配置数据不能为空";
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(config);
|
||||
} catch (e) {
|
||||
errors.config = "配置数据必须是有效的JSON格式";
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({ errors });
|
||||
}
|
||||
|
||||
try {
|
||||
const configData = {
|
||||
name,
|
||||
type,
|
||||
environment,
|
||||
config: JSON.parse(config),
|
||||
is_active,
|
||||
remark
|
||||
};
|
||||
|
||||
if (id) {
|
||||
// 更新配置
|
||||
const response = await updateConfig(id, configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else {
|
||||
// 创建配置
|
||||
const response = await createConfig(configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect("/config-lists");
|
||||
} catch (error) {
|
||||
console.error("保存配置失败:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: {
|
||||
general: "保存配置失败,请稍后重试"
|
||||
}
|
||||
});
|
||||
}
|
||||
values?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 配置模板常量
|
||||
@@ -205,29 +108,212 @@ const CONFIG_TEMPLATES = {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
let config: ConfigData | undefined = undefined;
|
||||
|
||||
try {
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
if (optionsResponse.error) {
|
||||
throw new Error(optionsResponse.error);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
// 获取配置详情
|
||||
const detailResponse = await getConfigDetail(id);
|
||||
if (detailResponse.error) {
|
||||
throw new Error(detailResponse.error);
|
||||
}
|
||||
config = detailResponse.data;
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
config,
|
||||
isEdit: !!config,
|
||||
types: optionsResponse.data?.types || [],
|
||||
environments: optionsResponse.data?.environments || [],
|
||||
error: undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载配置数据失败:", error);
|
||||
return Response.json({
|
||||
config: undefined,
|
||||
isEdit: false,
|
||||
types: [],
|
||||
environments: [],
|
||||
error: error instanceof Error ? error.message : "加载配置数据失败"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const name = formData.get("name") as string;
|
||||
const type = formData.get("type") as string;
|
||||
const environment = formData.get("environment") as string;
|
||||
const config = formData.get("config") as string;
|
||||
const is_active = formData.get("is_active") === "true";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 表单验证
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "配置名称不能为空";
|
||||
}else if(/^[a-zA-Z_]+$/.test(name)){
|
||||
errors.name = "配置名称只能包含英文字母和下划线";
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
errors.type = "请选择或输入所属模块";
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
errors.environment = "请选择环境";
|
||||
}
|
||||
|
||||
if (!config || config.trim() === "") {
|
||||
errors.config = "配置数据不能为空";
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(config);
|
||||
} catch (e) {
|
||||
errors.config = "配置数据必须是有效的JSON格式";
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({
|
||||
errors,
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const configData = {
|
||||
name,
|
||||
type,
|
||||
environment,
|
||||
config: JSON.parse(config),
|
||||
is_active,
|
||||
remark
|
||||
};
|
||||
|
||||
if (id) {
|
||||
// 更新配置
|
||||
const response = await updateConfig(id, configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else {
|
||||
// 创建配置
|
||||
const response = await createConfig(configData);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存成功,显示成功提示并重定向
|
||||
toastService.success("保存成功");
|
||||
return redirect("/config-lists");
|
||||
} catch (error) {
|
||||
console.error("保存配置失败:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: {
|
||||
general: error instanceof Error ? error.message : "保存配置失败,请稍后重试"
|
||||
},
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function ConfigNew() {
|
||||
const { config, isEdit, types, environments } = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [configDataValue, setConfigDataValue] = useState("");
|
||||
const { config, isEdit, types, environments, error } = data;
|
||||
|
||||
// 表单状态管理
|
||||
const [formValues, setFormValues] = useState<{
|
||||
name: string;
|
||||
type: string;
|
||||
environment: string;
|
||||
config: string;
|
||||
is_active: boolean;
|
||||
remark: string;
|
||||
}>({
|
||||
name: config?.name || "",
|
||||
type: config?.type || "",
|
||||
environment: config?.environment || "",
|
||||
config: config?.config ? JSON.stringify(config.config, null, 2) : "",
|
||||
is_active: config?.is_active ?? true,
|
||||
remark: config?.remark || "",
|
||||
});
|
||||
|
||||
// 表单验证错误状态
|
||||
const [formErrors, setFormErrors] = useState<{
|
||||
name?: string;
|
||||
type?: string;
|
||||
environment?: string;
|
||||
config?: string;
|
||||
general?: string;
|
||||
}>({});
|
||||
|
||||
// 字段是否被触摸过(用于确定何时显示错误)
|
||||
const [touchedFields, setTouchedFields] = useState<{
|
||||
name: boolean;
|
||||
type: boolean;
|
||||
environment: boolean;
|
||||
config: boolean;
|
||||
}>({
|
||||
name: false,
|
||||
type: false,
|
||||
environment: false,
|
||||
config: false
|
||||
});
|
||||
|
||||
// 示例JSON状态
|
||||
const [exampleJsonValue, setExampleJsonValue] = useState("");
|
||||
|
||||
// 标签选择状态
|
||||
const [selectedModule, setSelectedModule] = useState<string>("");
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<string>("");
|
||||
|
||||
// 在 ConfigNew 组件中添加状态来跟踪当前选中的模板
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<keyof typeof CONFIG_TEMPLATES | null>(null);
|
||||
|
||||
// 从 actionData 初始化表单错误
|
||||
useEffect(() => {
|
||||
if (actionData?.errors) {
|
||||
setFormErrors(actionData.errors);
|
||||
}
|
||||
|
||||
// 如果提交后有错误,则将所有字段标记为已触摸
|
||||
if (actionData?.errors && Object.keys(actionData.errors).length > 0) {
|
||||
setTouchedFields({
|
||||
name: true,
|
||||
type: true,
|
||||
environment: true,
|
||||
config: true
|
||||
});
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// 根据加载的配置数据初始化表单
|
||||
useEffect(() => {
|
||||
// 初始化配置数据
|
||||
if (config) {
|
||||
setConfigDataValue(JSON.stringify(config.config, null, 2));
|
||||
setSelectedModule(config.type);
|
||||
setSelectedEnvironment(config.environment);
|
||||
setFormValues({
|
||||
name: config.name,
|
||||
type: config.type,
|
||||
environment: config.environment,
|
||||
config: JSON.stringify(config.config, null, 2),
|
||||
is_active: config.is_active,
|
||||
remark: config.remark || ""
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化示例JSON
|
||||
@@ -246,46 +332,213 @@ export default function ConfigNew() {
|
||||
}, null, 2));
|
||||
}, [config]);
|
||||
|
||||
// 处理JSON数据变更
|
||||
// 验证表单字段
|
||||
const validateField = (field: string, value: string): string => {
|
||||
switch (field) {
|
||||
case 'name':
|
||||
if(value.trim() === ""){
|
||||
return "配置名称不能为空";
|
||||
}else if(/^[a-zA-Z_]+$/.test(value)){
|
||||
return "配置名称只能包含英文字母和下划线";
|
||||
}else if(value.length > 100){
|
||||
return "配置名称不能超过100个字符";
|
||||
}
|
||||
return "";
|
||||
case 'type':
|
||||
return value.trim() === "" ? "请选择所属模块" : "";
|
||||
case 'environment':
|
||||
return value.trim() === "" ? "请选择环境" : "";
|
||||
case 'config':
|
||||
if (value.trim() === "") {
|
||||
return "配置数据不能为空";
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return "";
|
||||
} catch (e) {
|
||||
return "配置数据必须是有效的JSON格式";
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理字段改变
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// 标记字段为已触摸
|
||||
if (['name', 'type', 'environment', 'config'].includes(name)) {
|
||||
setTouchedFields(prev => ({
|
||||
...prev,
|
||||
[name]: true
|
||||
}));
|
||||
}
|
||||
|
||||
// 实时验证
|
||||
const error = validateField(name, value);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
[name]: error
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理配置数据变更(JSON编辑器)
|
||||
const handleConfigDataChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setConfigDataValue(value);
|
||||
const { value } = e.target;
|
||||
|
||||
if (value.trim() === "") {
|
||||
setJsonError(null);
|
||||
return;
|
||||
}
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
config: value
|
||||
}));
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
setJsonError(null);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setJsonError(`配置数据必须是有效的JSON格式: ${error.message}`);
|
||||
} else {
|
||||
setJsonError("配置数据必须是有效的JSON格式");
|
||||
}
|
||||
}
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({
|
||||
...prev,
|
||||
config: true
|
||||
}));
|
||||
|
||||
// 实时验证
|
||||
const error = validateField('config', value);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
config: error
|
||||
}));
|
||||
};
|
||||
|
||||
// 格式化JSON
|
||||
const handleFormatJson = () => {
|
||||
|
||||
if (configDataValue.trim() === "") return;
|
||||
if (formValues.config.trim() === "") return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(configDataValue);
|
||||
setConfigDataValue(JSON.stringify(parsed, null, 2));
|
||||
setJsonError(null);
|
||||
const parsed = JSON.parse(formValues.config);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
config: formatted
|
||||
}));
|
||||
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
config: ""
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setJsonError(`当前不是有效的JSON,无法格式化: ${error.message}`);
|
||||
} else {
|
||||
setJsonError("当前不是有效的JSON,无法格式化");
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
config: `当前不是有效的JSON,无法格式化: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理模块类型选择
|
||||
const handleModuleSelect = (moduleType: string) => {
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
type: moduleType
|
||||
}));
|
||||
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({
|
||||
...prev,
|
||||
type: true
|
||||
}));
|
||||
|
||||
// 清除错误
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
type: ""
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理环境选择
|
||||
const handleEnvironmentSelect = (env: string) => {
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
environment: env
|
||||
}));
|
||||
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({
|
||||
...prev,
|
||||
environment: true
|
||||
}));
|
||||
|
||||
// 清除错误
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
environment: ""
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理模板选择
|
||||
const handleTemplateSelect = (templateKey: keyof typeof CONFIG_TEMPLATES) => {
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES[templateKey], null, 2));
|
||||
setSelectedTemplate(templateKey);
|
||||
};
|
||||
|
||||
// 处理启用状态变更
|
||||
const handleActiveChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
is_active: e.target.checked
|
||||
}));
|
||||
};
|
||||
|
||||
// 提交前验证
|
||||
const handleBeforeSubmit = (e: React.FormEvent) => {
|
||||
// 标记所有字段为已触摸
|
||||
setTouchedFields({
|
||||
name: true,
|
||||
type: true,
|
||||
environment: true,
|
||||
config: true
|
||||
});
|
||||
|
||||
// 验证所有字段
|
||||
const errors = {
|
||||
name: validateField('name', formValues.name),
|
||||
type: validateField('type', formValues.type),
|
||||
environment: validateField('environment', formValues.environment),
|
||||
config: validateField('config', formValues.config)
|
||||
};
|
||||
|
||||
setFormErrors(errors);
|
||||
|
||||
// 如果有错误,阻止提交
|
||||
if (errors.name || errors.type || errors.environment || errors.config) {
|
||||
e.preventDefault();
|
||||
|
||||
// 滚动到第一个错误字段
|
||||
if (formRef.current) {
|
||||
const firstErrorField = Object.keys(errors).find(key => !!errors[key as keyof typeof errors]);
|
||||
if (firstErrorField) {
|
||||
const element = formRef.current.querySelector(`[name="${firstErrorField}"]`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果加载数据时出错,显示错误信息
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">加载出错</h1>
|
||||
<p className="mb-4">{error}</p>
|
||||
<Button type="primary" to="/config-lists">返回列表</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-new-page">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -302,14 +555,20 @@ export default function ConfigNew() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actionData?.errors?.general && (
|
||||
{formErrors.general && (
|
||||
<div className="mb-4 w-full">
|
||||
<div className="error-message general-error">{actionData.errors.general}</div>
|
||||
<div className="error-message general-error">{formErrors.general}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="config-form-card">
|
||||
<Form method="post" id="configForm" className="config-form">
|
||||
<Form
|
||||
method="post"
|
||||
id="configForm"
|
||||
className="config-form"
|
||||
ref={formRef}
|
||||
onSubmit={handleBeforeSubmit}
|
||||
>
|
||||
{config?.id && <input type="hidden" name="id" value={config.id} />}
|
||||
|
||||
{/* 配置名称和状态 */}
|
||||
@@ -320,13 +579,14 @@ export default function ConfigNew() {
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
className={`form-input ${actionData?.errors?.name ? 'input-error' : ''}`}
|
||||
defaultValue={config?.name || ''}
|
||||
className={`form-input ${touchedFields.name && formErrors.name ? 'input-error' : ''}`}
|
||||
value={formValues.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入配置名称,如database_connection"
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.name && (
|
||||
<div className="error-message">{actionData.errors.name}</div>
|
||||
{touchedFields.name && formErrors.name && (
|
||||
<div className="error-message">{formErrors.name}</div>
|
||||
)}
|
||||
<div className="form-help">
|
||||
唯一标识符,配置名称应使用英文,推荐使用下划线命名方式
|
||||
@@ -343,7 +603,8 @@ export default function ConfigNew() {
|
||||
name="is_active"
|
||||
value="true"
|
||||
className="form-checkbox"
|
||||
defaultChecked={config?.is_active !== false}
|
||||
checked={formValues.is_active}
|
||||
onChange={handleActiveChange}
|
||||
/>
|
||||
<label htmlFor="is_active" className="form-checkbox-label">
|
||||
启用此配置
|
||||
@@ -362,27 +623,28 @@ export default function ConfigNew() {
|
||||
<input
|
||||
type="hidden"
|
||||
name="type"
|
||||
value={selectedModule}
|
||||
value={formValues.type}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="typeDisplay"
|
||||
className={`form-input ${actionData?.errors?.type ? 'input-error' : ''}`}
|
||||
value={selectedModule}
|
||||
onChange={(e) => setSelectedModule(e.target.value)}
|
||||
className={`form-input ${touchedFields.type && formErrors.type ? 'input-error' : ''}`}
|
||||
value={formValues.type}
|
||||
onChange={handleInputChange}
|
||||
name="type"
|
||||
placeholder="请输入或选择所属模块"
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.type && (
|
||||
<div className="error-message">{actionData.errors.type}</div>
|
||||
{touchedFields.type && formErrors.type && (
|
||||
<div className="error-message">{formErrors.type}</div>
|
||||
)}
|
||||
<div className="tag-buttons mt-2">
|
||||
{types.map((type: string) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
className={`tag-button ${selectedModule === type ? 'active' : ''}`}
|
||||
onClick={() => setSelectedModule(type)}
|
||||
className={`tag-button ${formValues.type === type ? 'active' : ''}`}
|
||||
onClick={() => handleModuleSelect(type)}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
@@ -399,27 +661,28 @@ export default function ConfigNew() {
|
||||
<input
|
||||
type="hidden"
|
||||
name="environment"
|
||||
value={selectedEnvironment}
|
||||
value={formValues.environment}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="environmentDisplay"
|
||||
className={`form-input ${actionData?.errors?.environment ? 'input-error' : ''}`}
|
||||
value={selectedEnvironment}
|
||||
onChange={(e) => setSelectedEnvironment(e.target.value)}
|
||||
className={`form-input ${touchedFields.environment && formErrors.environment ? 'input-error' : ''}`}
|
||||
value={formValues.environment}
|
||||
onChange={handleInputChange}
|
||||
name="environment"
|
||||
placeholder="请输入或选择环境"
|
||||
required
|
||||
/>
|
||||
{actionData?.errors?.environment && (
|
||||
<div className="error-message">{actionData.errors.environment}</div>
|
||||
{touchedFields.environment && formErrors.environment && (
|
||||
<div className="error-message">{formErrors.environment}</div>
|
||||
)}
|
||||
<div className="tag-buttons mt-2">
|
||||
{environments.map((env: string) => (
|
||||
<button
|
||||
key={env}
|
||||
type="button"
|
||||
className={`tag-button ${selectedEnvironment === env ? 'active' : ''}`}
|
||||
onClick={() => setSelectedEnvironment(env)}
|
||||
className={`tag-button ${formValues.environment === env ? 'active' : ''}`}
|
||||
onClick={() => handleEnvironmentSelect(env)}
|
||||
>
|
||||
{env}
|
||||
</button>
|
||||
@@ -439,8 +702,8 @@ export default function ConfigNew() {
|
||||
<textarea
|
||||
id="config"
|
||||
name="config"
|
||||
className={`json-editor ${(actionData?.errors?.config || jsonError) ? 'input-error' : ''}`}
|
||||
value={configDataValue}
|
||||
className={`json-editor ${touchedFields.config && formErrors.config ? 'input-error' : ''}`}
|
||||
value={formValues.config}
|
||||
onChange={handleConfigDataChange}
|
||||
required
|
||||
placeholder='请输入JSON格式的配置数据'
|
||||
@@ -457,8 +720,8 @@ export default function ConfigNew() {
|
||||
<i className="ri-braces-line mr-1"></i> 格式化JSON
|
||||
</Button>
|
||||
</div>
|
||||
{(actionData?.errors?.config || jsonError) && (
|
||||
<div className="error-message">{actionData?.errors?.config || jsonError}</div>
|
||||
{touchedFields.config && formErrors.config && (
|
||||
<div className="error-message">{formErrors.config}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -498,8 +761,7 @@ export default function ConfigNew() {
|
||||
className={`${selectedTemplate === 'database' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.database, null, 2));
|
||||
setSelectedTemplate('database');
|
||||
handleTemplateSelect('database');
|
||||
}}
|
||||
>
|
||||
数据库配置
|
||||
@@ -510,8 +772,7 @@ export default function ConfigNew() {
|
||||
className={`${selectedTemplate === 'file' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.file, null, 2));
|
||||
setSelectedTemplate('file');
|
||||
handleTemplateSelect('file');
|
||||
}}
|
||||
>
|
||||
文件存储配置
|
||||
@@ -522,8 +783,7 @@ export default function ConfigNew() {
|
||||
className={`${selectedTemplate === 'ai' ? 'border-[#00684a] text-[#00684a] focus:ring-0' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setExampleJsonValue(JSON.stringify(CONFIG_TEMPLATES.ai, null, 2));
|
||||
setSelectedTemplate('ai');
|
||||
handleTemplateSelect('ai');
|
||||
}}
|
||||
>
|
||||
AI服务配置
|
||||
@@ -545,7 +805,8 @@ export default function ConfigNew() {
|
||||
id="remark"
|
||||
name="remark"
|
||||
className="form-textarea"
|
||||
defaultValue={config?.remark || ''}
|
||||
value={formValues.remark}
|
||||
onChange={handleInputChange}
|
||||
rows={2}
|
||||
placeholder="请输入配置备注信息"
|
||||
/>
|
||||
|
||||
+131
-108
@@ -13,6 +13,7 @@ import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/docum
|
||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
|
||||
// 导入样式
|
||||
export function links() {
|
||||
@@ -56,77 +57,49 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
pageSize
|
||||
};
|
||||
|
||||
// 获取文档列表
|
||||
const documentsResponse = await getDocuments(searchParams);
|
||||
// console.log('documentsResponse---1--',JSON.stringify(documentsResponse,null,2));
|
||||
if (documentsResponse.error) {
|
||||
throw new Error(documentsResponse.error);
|
||||
try {
|
||||
// 获取文档列表
|
||||
const documentsResponse = await getDocuments(searchParams);
|
||||
// console.log('documentsResponse---1--',JSON.stringify(documentsResponse,null,2));
|
||||
if (documentsResponse.error) {
|
||||
throw new Error(documentsResponse.error);
|
||||
}
|
||||
|
||||
// 获取文档类型列表,用于筛选条件,设置较大的pageSize确保获取所有数据
|
||||
const typesResponse = await getDocumentTypes({ pageSize: 500 });
|
||||
// console.log('typesResponse-----',typesResponse);
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
const documentTypeOptions = documentTypes.map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
return Response.json({
|
||||
documents: documentsResponse.data?.documents || [],
|
||||
total: documentsResponse.data?.total || 0,
|
||||
page,
|
||||
pageSize,
|
||||
documentTypeOptions
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
return Response.json({
|
||||
error: '获取文档列表失败',
|
||||
status: 500
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
// 获取文档类型列表,用于筛选条件,设置较大的pageSize确保获取所有数据
|
||||
const typesResponse = await getDocumentTypes({ pageSize: 500 });
|
||||
// console.log('typesResponse-----',typesResponse);
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
const documentTypeOptions = documentTypes.map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
return Response.json({
|
||||
documents: documentsResponse.data?.documents || [],
|
||||
total: documentsResponse.data?.total || 0,
|
||||
page,
|
||||
pageSize,
|
||||
documentTypeOptions
|
||||
});
|
||||
};
|
||||
|
||||
// 定义action返回的数据类型
|
||||
interface ActionResponse {
|
||||
success: boolean;
|
||||
result: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 处理表单提交和删除等操作
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const action = formData.get("_action");
|
||||
|
||||
if (action === "delete") {
|
||||
const id = formData.get("id") as string;
|
||||
const response = await deleteDocument(id);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, message: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
return Response.json({ success: true, message: "文档已成功删除" });
|
||||
}
|
||||
|
||||
if (action === "batchDelete") {
|
||||
const ids = formData.getAll("ids") as string[];
|
||||
|
||||
// 批量删除处理
|
||||
const results = await Promise.all(ids.map(id => deleteDocument(id)));
|
||||
const failures = results.filter(r => r.error);
|
||||
|
||||
if (failures.length > 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
|
||||
}
|
||||
|
||||
// 未知操作
|
||||
return Response.json({ success: false, message: "未知操作" }, { status: 400 });
|
||||
};
|
||||
|
||||
// 审核状态筛选选项
|
||||
const auditStatusOptions = [
|
||||
// { value: "", label: "全部" },
|
||||
{ value: "-2", label: "警告" },
|
||||
// { value: "-2", label: "警告" },
|
||||
{ value: "-1", label: "不通过" },
|
||||
{ value: "0", label: "待审核" },
|
||||
{ value: "1", label: "通过" },
|
||||
@@ -172,19 +145,49 @@ const formatFileSize = (bytes: number) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 获取文档类型标签背景颜色
|
||||
// 此函数已不再需要,改用 FileTypeTag 组件
|
||||
// const getDocumentTypeTagColor = (type: string): string => {
|
||||
// const colorMap: Record<string, string> = {
|
||||
// "sales-contract": "blue",
|
||||
// "purchase-contract": "green",
|
||||
// "license": "purple",
|
||||
// "punishment": "yellow",
|
||||
// "agreement": "orange",
|
||||
// "default": "gray"
|
||||
// };
|
||||
// return colorMap[type] || colorMap.default;
|
||||
// };
|
||||
// 处理表单提交和删除等操作
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const action = formData.get("_action");
|
||||
|
||||
if (action === "delete") {
|
||||
const id = formData.get("id") as string;
|
||||
const response = await deleteDocument(id);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ result: false, message: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
return Response.json({ result: true, message: "文档已成功删除" });
|
||||
}
|
||||
|
||||
if (action === "batchDelete") {
|
||||
const ids = formData.getAll("ids") as string[];
|
||||
|
||||
// 批量删除处理
|
||||
const results = await Promise.all(ids.map(id => deleteDocument(id)));
|
||||
const failures = results.filter(r => r.error);
|
||||
|
||||
if (failures.length > 0) {
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
return Response.json({ result: true, message: `已成功删除${ids.length}个文档` });
|
||||
}
|
||||
|
||||
// 未知操作
|
||||
return Response.json({ result: false, message: "未知操作" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('处理表单提交和删除等操作失败:', error);
|
||||
return Response.json({
|
||||
result: false,
|
||||
error: error instanceof Error ? error.message : "处理表单提交和删除等操作失败"
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export default function DocumentsIndex() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -207,6 +210,24 @@ export default function DocumentsIndex() {
|
||||
// 获取API返回的数据
|
||||
const { documents, total, documentTypeOptions } = loaderData;
|
||||
|
||||
// 处理loader错误
|
||||
useEffect(() => {
|
||||
if (loaderData.error) {
|
||||
toastService.error(loaderData.error);
|
||||
}
|
||||
}, [loaderData.error]);
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle') {
|
||||
if (fetcher.data.result) {
|
||||
toastService.success(fetcher.data.message);
|
||||
} else if (fetcher.data.message) {
|
||||
toastService.error(fetcher.data.message);
|
||||
}
|
||||
}
|
||||
}, [fetcher.data, fetcher.state]);
|
||||
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number) => {
|
||||
searchParams.set("page", page.toString());
|
||||
@@ -365,7 +386,7 @@ export default function DocumentsIndex() {
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -377,19 +398,25 @@ export default function DocumentsIndex() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`确定要删除文档"${name}"吗?`)) {
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("id", id);
|
||||
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: `确定要删除文档"${name}"吗?`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("id", id);
|
||||
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
// alert('请至少选择一个文档');
|
||||
toastService.error('请至少选择一个文档');
|
||||
return;
|
||||
}
|
||||
@@ -400,26 +427,32 @@ export default function DocumentsIndex() {
|
||||
);
|
||||
|
||||
if (hasProcessingFiles) {
|
||||
// alert('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
toastService.error('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) {
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'batchDelete');
|
||||
|
||||
// 添加所有选中的ID
|
||||
selectedRowKeys.forEach(id => {
|
||||
formData.append('ids', id);
|
||||
});
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
// 清空选中行
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确认删除选中的 ${selectedRowKeys.length} 个文档?`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {
|
||||
// 使用fetcher提交表单
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'batchDelete');
|
||||
|
||||
// 添加所有选中的ID
|
||||
selectedRowKeys.forEach(id => {
|
||||
formData.append('ids', id);
|
||||
});
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
|
||||
// 清空选中行
|
||||
setSelectedRowKeys([]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理文件状态变更
|
||||
@@ -548,16 +581,6 @@ export default function DocumentsIndex() {
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
||||
};
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle') {
|
||||
if (fetcher.data.success) {
|
||||
toastService.success(fetcher.data.message);
|
||||
} else if (fetcher.data.message) {
|
||||
toastService.error(fetcher.data.message);
|
||||
}
|
||||
}
|
||||
}, [fetcher.data, fetcher.state]);
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
|
||||
+169
-66
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useLoaderData, useActionData, useNavigate, Form } from "@remix-run/react";
|
||||
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -27,7 +27,18 @@ export const handle = {
|
||||
breadcrumb: "文档编辑"
|
||||
};
|
||||
|
||||
|
||||
// 定义action返回数据类型
|
||||
export interface ActionData {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
fieldErrors?: {
|
||||
document_number?: string;
|
||||
audit_status?: string;
|
||||
remark?: string;
|
||||
general?: string;
|
||||
};
|
||||
values?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 文档审核状态定义
|
||||
enum DocumentAuditStatus {
|
||||
@@ -120,21 +131,19 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const isTest = formData.get("is_test_document") === "on";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
// 验证必填字段
|
||||
// if (!type || auditStatus === undefined || isNaN(auditStatus)) {
|
||||
// return Response.json(
|
||||
// {
|
||||
// error: "缺少必填字段",
|
||||
// fieldErrors: {
|
||||
// type_id: !type ? "文档类型不能为空" : null,
|
||||
// audit_status: (auditStatus === undefined || isNaN(auditStatus)) ? "审核状态不能为空" : null
|
||||
// }
|
||||
// },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
// 表单验证
|
||||
const errors: ActionData["fieldErrors"] = {};
|
||||
|
||||
// console.log('提交更新:', { type, documentNumber, auditStatus, isTest, remark });
|
||||
if (isNaN(auditStatus)) {
|
||||
errors.audit_status = "审核状态必须选择";
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({
|
||||
fieldErrors: errors,
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 更新文档
|
||||
const updateResponse = await updateDocument(id, {
|
||||
@@ -146,26 +155,25 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
});
|
||||
|
||||
if (updateResponse.error) {
|
||||
console.error('更新文档失败1:', updateResponse.error);
|
||||
console.error('更新文档失败:', updateResponse.error);
|
||||
return Response.json({
|
||||
error: updateResponse.error,
|
||||
message: "更新文档失败,请检查提交的数据是否正确"
|
||||
fieldErrors: {
|
||||
general: "更新文档失败,请检查提交的数据是否正确"
|
||||
},
|
||||
values: Object.fromEntries(formData) as Record<string, string>
|
||||
}, { status: updateResponse.status || 500 });
|
||||
}
|
||||
|
||||
// toastService.success('更新文档成功');
|
||||
|
||||
// 重定向回文档列表
|
||||
// return redirect("/documents");
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "更新文档成功"
|
||||
});
|
||||
toastService.success('更新文档成功');
|
||||
return redirect("/documents");
|
||||
} catch (error) {
|
||||
console.error("更新文档失败2:", error);
|
||||
return Response.json({
|
||||
error: "更新文档失败",
|
||||
message: error instanceof Error ? error.message : "发生未知错误"
|
||||
fieldErrors: {
|
||||
general: error instanceof Error ? error.message : "发生未知错误"
|
||||
}
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -173,20 +181,117 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 文档编辑页面组件
|
||||
export default function DocumentEdit() {
|
||||
const { document, documentTypes } = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const actionData = useActionData<ActionData>();
|
||||
const navigate = useNavigate();
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// 表单状态管理 - 使用受控组件
|
||||
const [formValues, setFormValues] = useState({
|
||||
documentNumber: document.documentNumber || "",
|
||||
auditStatus: document.auditStatus,
|
||||
isTest: document.isTest || false,
|
||||
remark: document.remark || ""
|
||||
});
|
||||
|
||||
// 表单验证错误状态
|
||||
const [formErrors, setFormErrors] = useState<{
|
||||
documentNumber?: string;
|
||||
auditStatus?: string;
|
||||
remark?: string;
|
||||
general?: string;
|
||||
}>({});
|
||||
|
||||
// 字段是否被触摸过(用于确定何时显示错误)
|
||||
const [touchedFields, setTouchedFields] = useState({
|
||||
documentNumber: false,
|
||||
auditStatus: false,
|
||||
remark: false
|
||||
});
|
||||
|
||||
// 从 actionData 初始化表单错误
|
||||
useEffect(() => {
|
||||
// console.log('actionData', actionData);
|
||||
if (actionData?.error) {
|
||||
toastService.error('更新文档失败:' + actionData.error);
|
||||
}
|
||||
if (actionData?.success) {
|
||||
toastService.success('更新文档成功');
|
||||
if (actionData?.fieldErrors) {
|
||||
// general 是loader的时候返回的错误信息
|
||||
setFormErrors(actionData.fieldErrors);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// 验证表单字段
|
||||
const validateField = (field: string, value: string | number | undefined): string => {
|
||||
switch (field) {
|
||||
case 'auditStatus': {
|
||||
const statusValue = typeof value === 'string' ? parseInt(value) : value;
|
||||
return statusValue === undefined || isNaN(statusValue as number) ? "审核状态必须选择" : "";
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理字段改变
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
|
||||
if (type === 'checkbox') {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
[name === 'is_test_document' ? 'isTest' : name]: checked
|
||||
}));
|
||||
} else {
|
||||
setFormValues(prev => ({
|
||||
...prev,
|
||||
[name === 'document_number' ? 'documentNumber' :
|
||||
name === 'audit_status' ? 'auditStatus' : name]:
|
||||
name === 'audit_status' ? (parseInt(value)) : value
|
||||
}));
|
||||
}
|
||||
|
||||
// 标记字段为已触摸
|
||||
if (name === 'document_number' || name === 'audit_status' || name === 'remark') {
|
||||
const fieldName = name === 'document_number' ? 'documentNumber' :
|
||||
name === 'audit_status' ? 'auditStatus' : name;
|
||||
setTouchedFields(prev => ({
|
||||
...prev,
|
||||
[fieldName]: true
|
||||
}));
|
||||
}
|
||||
|
||||
// 实时验证
|
||||
if (name === 'audit_status') {
|
||||
const statusValue = parseInt(value);
|
||||
const error = validateField('auditStatus', statusValue);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
auditStatus: error
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理表单提交前验证
|
||||
const handleBeforeSubmit = (e: React.FormEvent) => {
|
||||
// 标记所有字段为已触摸
|
||||
setTouchedFields({
|
||||
documentNumber: true,
|
||||
auditStatus: true,
|
||||
remark: true
|
||||
});
|
||||
|
||||
// 验证所有字段
|
||||
const errors = {
|
||||
auditStatus: validateField('auditStatus', formValues.auditStatus)
|
||||
};
|
||||
|
||||
setFormErrors(errors);
|
||||
|
||||
// 如果有错误,阻止提交
|
||||
if (errors.auditStatus) {
|
||||
toastService.error('审核状态不能为空');
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentLoadSuccess = ({numPages}: {numPages: number}) => {
|
||||
setNumPages(numPages);
|
||||
@@ -242,10 +347,8 @@ export default function DocumentEdit() {
|
||||
<div
|
||||
className="page-wrapper flex justify-center"
|
||||
style={{
|
||||
// transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
||||
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
||||
maxWidth: '100%', // 限制最大宽度
|
||||
position: 'relative',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{/* 渲染PDF页面组件 */}
|
||||
@@ -270,14 +373,6 @@ export default function DocumentEdit() {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 状态
|
||||
const [localStatus, setLocalStatus] = useState<number>(document.auditStatus);
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setLocalStatus(parseInt(e.target.value));
|
||||
};
|
||||
|
||||
// 获取文档类型名称
|
||||
const getDocumentTypeName = (typeId: string): string => {
|
||||
const docType = documentTypes.find((type: DocType) => type.id.toString() === typeId);
|
||||
@@ -299,8 +394,6 @@ export default function DocumentEdit() {
|
||||
|
||||
// 在新窗口打开文档预览
|
||||
const openPreview = () => {
|
||||
// 假设有一个预览URL的格式,比如 /preview?path=xxx
|
||||
// console.log('documentstest', document);
|
||||
const urlBefore = 'http://172.18.0.100:9000/docauditai/'
|
||||
const previewUrl = `${urlBefore}${document.path}`;
|
||||
window.open(previewUrl, '_blank');
|
||||
@@ -368,7 +461,15 @@ export default function DocumentEdit() {
|
||||
<i className="ri-information-line mr-2"></i> 您可以修改此文档的基本信息,但不能更改文档内容。如需修改内容,请删除后重新上传新文档。
|
||||
</div>
|
||||
|
||||
<Form id="edit-form" method="post">
|
||||
{/* 错误提示 */}
|
||||
{formErrors.general && (
|
||||
<div className="general-error mb-4">
|
||||
<i className="ri-error-warning-line mr-2"></i>
|
||||
{formErrors.general}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form id="edit-form" method="post" ref={formRef} onSubmit={handleBeforeSubmit}>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-id" className="form-label">文档类型 <span className="text-red-500">*</span></label>
|
||||
@@ -376,8 +477,7 @@ export default function DocumentEdit() {
|
||||
id="type-id"
|
||||
name="type_id"
|
||||
className="form-select"
|
||||
defaultValue={document.type}
|
||||
// disabled={document.fileStatus !== 'Processed'}
|
||||
value={document.type}
|
||||
disabled={true}
|
||||
required
|
||||
>
|
||||
@@ -386,9 +486,6 @@ export default function DocumentEdit() {
|
||||
))}
|
||||
</select>
|
||||
<div className="text-sm text-secondary mt-1">更改文档类型将重新应用对应的评查规则</div>
|
||||
{actionData?.fieldErrors?.type_id && (
|
||||
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.type_id}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -399,7 +496,8 @@ export default function DocumentEdit() {
|
||||
name="document_number"
|
||||
className="form-input"
|
||||
placeholder="请输入合同编号、许可证号等"
|
||||
defaultValue={document.documentNumber || ""}
|
||||
value={formValues.documentNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className="text-sm text-secondary mt-1">如无编号可留空</div>
|
||||
</div>
|
||||
@@ -409,19 +507,20 @@ export default function DocumentEdit() {
|
||||
<select
|
||||
id="audit-status"
|
||||
name="audit_status"
|
||||
className="form-select"
|
||||
value={localStatus}
|
||||
onChange={handleStatusChange}
|
||||
className={`form-select ${touchedFields.auditStatus && formErrors.auditStatus ? 'error' : ''}`}
|
||||
value={formValues.auditStatus}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
{/* <option value='全部'>请选择审核状态</option> */}
|
||||
<option value={DocumentAuditStatus.WAITING}>{STATUS_LABELS[DocumentAuditStatus.WAITING]}</option>
|
||||
<option value={DocumentAuditStatus.PROCESSING}>{STATUS_LABELS[DocumentAuditStatus.PROCESSING]}</option>
|
||||
<option value={DocumentAuditStatus.PASS}>{STATUS_LABELS[DocumentAuditStatus.PASS]}</option>
|
||||
<option value={DocumentAuditStatus.FAIL}>{STATUS_LABELS[DocumentAuditStatus.FAIL]}</option>
|
||||
</select>
|
||||
<div className="text-sm text-secondary mt-1">更改状态可能会影响此文档在列表中的显示和排序</div>
|
||||
{actionData?.fieldErrors?.audit_status && (
|
||||
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.audit_status}</div>
|
||||
{touchedFields.auditStatus && formErrors.auditStatus && (
|
||||
<div className="text-red-500 text-sm mt-1">{formErrors.auditStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -433,7 +532,8 @@ export default function DocumentEdit() {
|
||||
type="checkbox"
|
||||
id="is-test-document"
|
||||
name="is_test_document"
|
||||
defaultChecked={document.isTest}
|
||||
checked={formValues.isTest}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
<span className="sr-only">标记为测试文档</span>
|
||||
@@ -450,7 +550,8 @@ export default function DocumentEdit() {
|
||||
className="form-textarea"
|
||||
placeholder="可输入文档的相关描述或备注信息"
|
||||
rows={3}
|
||||
defaultValue={document.remark || ""}
|
||||
value={formValues.remark}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,9 +589,11 @@ export default function DocumentEdit() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 预览窗口 */}
|
||||
{loadError ?(<div className="text-red-500">
|
||||
{loadError}
|
||||
</div>):(
|
||||
{loadError ? (
|
||||
<div className="text-red-500">
|
||||
{loadError}
|
||||
</div>
|
||||
) : (
|
||||
renderDocumentContent()
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 表单处理
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
|
||||
|
||||
// 提取表单数据
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
@@ -156,6 +156,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const parentId = groupType === "secondary" ? formData.get("parentId") as string : null;
|
||||
|
||||
// 表单验证
|
||||
// action是处于服务端的表单提交方法,这里再次验证表单数据也是出于安全考虑,防止客户端验证被绕过从而提交非法数据
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
if (!name || name.trim() === "") {
|
||||
|
||||
+31
-47
@@ -1,5 +1,6 @@
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { FileIcon } from "~/components/ui/FileIcon";
|
||||
@@ -50,7 +51,7 @@ export const REVIEW_STATUS_LABELS: Record<string, string> = {
|
||||
'pending': '待人工确认'
|
||||
};
|
||||
|
||||
|
||||
// 加载评查文件列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const fileType = url.searchParams.get("fileType") || "";
|
||||
@@ -86,7 +87,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const filesResponse = await getReviewFiles(searchParams);
|
||||
if (filesResponse.error) {
|
||||
console.error('获取评查文件列表失败:', filesResponse.error);
|
||||
throw new Response('获取评查文件列表失败', { status: filesResponse.status || 500 });
|
||||
return Response.json({ result: false, message: filesResponse.error }, { status: filesResponse.status || 500 });
|
||||
}
|
||||
|
||||
const files = filesResponse.data?.files || [];
|
||||
@@ -101,29 +102,24 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载评查文件列表失败:', error);
|
||||
throw new Response('加载评查文件列表失败', { status: 500 });
|
||||
return Response.json({ result: false, message: error instanceof Error ? error.message : '加载评查文件列表失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 提取renderErrorBoundary函数作为命名导出
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-normal text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载评查文件列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 在文件中定义一个与路由文件名匹配的命名函数组件
|
||||
export default function RulesFiles() {
|
||||
const navigate = useNavigate();
|
||||
const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
|
||||
const { files, documentTypes, totalCount, currentPage, pageSize, result, message } = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const dateFrom = searchParams.get('dateFrom') || '';
|
||||
const dateTo = searchParams.get('dateTo') || '';
|
||||
|
||||
// 处理初始加载数据loader的错误
|
||||
useEffect(() => {
|
||||
if(!result) {
|
||||
toastService.error(message);
|
||||
}
|
||||
}, [result, message]);
|
||||
|
||||
// 处理筛选条件变更
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -178,12 +174,12 @@ export default function RulesFiles() {
|
||||
try {
|
||||
const response = await updateDocumentAuditStatus(fileId, 2);
|
||||
if (response.error) {
|
||||
console.error('更新文件审核状态失败:', response.error);
|
||||
// 尽管更新失败,仍然导航到文件详情页
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态时出错:', error);
|
||||
// 尽管发生错误,仍然导航到文件详情页
|
||||
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,9 +189,10 @@ export default function RulesFiles() {
|
||||
|
||||
// 渲染问题摘要
|
||||
const renderIssues = (file: ReviewFileUI) => {
|
||||
// 如果评查状态为通过(说明所有评查结果为true),显示"所有评查点均通过"
|
||||
// 如果文件状态为完成
|
||||
if (file.status === 'Processed') {
|
||||
if (file.reviewStatus === 'pass') {
|
||||
// 如果没有问题,显示"所有评查点均通过"
|
||||
if (file.warningCount <= 0 && file.failCount <= 0) {
|
||||
return (
|
||||
<div className="text-sm text-success">
|
||||
<i className="ri-check-double-line mr-1"></i>所有评查点均通过
|
||||
@@ -213,7 +210,7 @@ export default function RulesFiles() {
|
||||
// }
|
||||
|
||||
// 显示问题列表
|
||||
if (file.reviewStatus !== 'pass' && file.reviewStatus !== 'fail' && file.issues && file.issues.length > 0) {
|
||||
if (file.issues && file.issues.length > 0) {
|
||||
// 最多显示2个问题
|
||||
const displayIssues = file.issues.slice(0, 2);
|
||||
|
||||
@@ -292,19 +289,11 @@ export default function RulesFiles() {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
const newParams = new URLSearchParams();
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -314,22 +303,6 @@ export default function RulesFiles() {
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
// 评查状态选项
|
||||
// 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 columns = [
|
||||
{
|
||||
@@ -570,4 +543,15 @@ export default function RulesFiles() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误边界
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-normal text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载评查文件列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+107
-128
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useSubmit, Link, useNavigate } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher } from "@remix-run/react";
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { Card } from '~/components/ui/Card';
|
||||
import { Tag } from '~/components/ui/Tag';
|
||||
@@ -12,10 +12,11 @@ import type { TagColor } from '~/components/ui/Tag';
|
||||
import { Table } from '~/components/ui/Table';
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterPanel';
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
duplicateRule,
|
||||
getRuleTypes,
|
||||
getRuleGroupsByType,
|
||||
type RuleType as ApiRuleType,
|
||||
@@ -60,6 +61,11 @@ interface ApiRule {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ActionResponse {
|
||||
result: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function mapApiRuleToModel(apiRule: ApiRule): Rule {
|
||||
return {
|
||||
id: apiRule.id,
|
||||
@@ -109,30 +115,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error('API返回数据为空');
|
||||
}
|
||||
|
||||
const apiRules = response.data.rules;
|
||||
const totalCount = response.data.totalCount;
|
||||
const apiRules = response.data?.rules || [];
|
||||
const totalCount = response.data?.totalCount || 0;
|
||||
const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = Math.ceil(totalCount / params.pageSize);
|
||||
|
||||
// 验证页码范围
|
||||
if (params.page < 1 || (totalCount > 0 && params.page > totalPages)) {
|
||||
const newUrl = new URL(request.url);
|
||||
newUrl.searchParams.set('page', '1');
|
||||
return redirect(newUrl.pathname + newUrl.search);
|
||||
}
|
||||
|
||||
|
||||
return Response.json({
|
||||
rules,
|
||||
totalCount,
|
||||
currentPage: params.page,
|
||||
pageSize: params.pageSize,
|
||||
totalPages,
|
||||
ruleTypes
|
||||
}, {
|
||||
headers: {
|
||||
@@ -142,7 +133,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载评查点列表失败:', error);
|
||||
throw new Response('加载评查点列表失败', { status: 500 });
|
||||
return Response.json({
|
||||
error: error || '加载评查点列表失败',
|
||||
status: 500
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +146,7 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
const ruleId = formData.get('ruleId');
|
||||
|
||||
if (!ruleId) {
|
||||
return Response.json({ success: false, error: "缺少评查点ID" }, { status: 400 });
|
||||
return Response.json({ result: false, message: "缺少评查点ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -160,66 +154,20 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
// 调用API删除评查点
|
||||
console.log(`删除评查点 ${ruleId}`);
|
||||
|
||||
try {
|
||||
const deleteResponse = await deleteRule(ruleId as string);
|
||||
|
||||
if (deleteResponse.error) {
|
||||
throw new Error(deleteResponse.error);
|
||||
}
|
||||
|
||||
// 删除成功,获取当前URL
|
||||
const url = new URL(request.url);
|
||||
// 返回重定向响应,以刷新页面数据
|
||||
return redirect(`${url.pathname}${url.search}`);
|
||||
} catch (error) {
|
||||
console.error('删除评查点失败:', error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "删除失败"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
if (_action === 'duplicate') {
|
||||
// 复制评查点
|
||||
console.log(`复制评查点 ${ruleId}`);
|
||||
const deleteResponse = await deleteRule(ruleId as string);
|
||||
|
||||
try {
|
||||
const duplicateResponse = await duplicateRule(ruleId as string);
|
||||
|
||||
if (duplicateResponse.error) {
|
||||
throw new Error(duplicateResponse.error);
|
||||
}
|
||||
|
||||
// 复制成功,获取当前URL
|
||||
const url = new URL(request.url);
|
||||
// 返回重定向响应,以刷新页面数据
|
||||
return redirect(`${url.pathname}${url.search}`);
|
||||
} catch (error) {
|
||||
console.error('复制评查点失败:', error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "复制失败"
|
||||
}, { status: 500 });
|
||||
if (deleteResponse.error) {
|
||||
return Response.json({ result: false, message: deleteResponse.error }, { status: deleteResponse.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ result: true, message: "评查点删除成功" }, { status: 200 });
|
||||
}
|
||||
|
||||
return Response.json({ success: false, error: "未知操作" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('操作评查点失败:', error);
|
||||
return Response.json({ success: false, error: "操作失败" }, { status: 500 });
|
||||
return Response.json({ result: false, message: error instanceof Error ? error.message : "操作失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载评查点列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 规则优先级的描述标签映射
|
||||
const priorityLabels = {
|
||||
@@ -233,24 +181,30 @@ export default function RulesIndex() {
|
||||
const { rules, totalCount, currentPage, pageSize } = loaderData;
|
||||
const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const submit = useSubmit();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetcher = useFetcher<ActionResponse>();
|
||||
|
||||
// 状态管理
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [ruleToDelete, setRuleToDelete] = useState<Rule | null>(null);
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
|
||||
// 判断是否禁用规则组选择
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !searchParams.get('ruleType') || ruleGroups.length === 0;
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
|
||||
// 判断是否禁用规则组选择
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 使用useEffect监听loaderData.error变化并显示Toast
|
||||
useEffect(() => {
|
||||
if(loaderData.error) {
|
||||
toastService.error(loaderData.error);
|
||||
}
|
||||
}, [loaderData.error]);
|
||||
|
||||
// 当评查点类型变化时,加载对应的规则组
|
||||
useEffect(() => {
|
||||
const selectedType = searchParams.get('ruleType');
|
||||
|
||||
// 如果选择了"全部"或未选择,则清空规则组
|
||||
if (!selectedType || selectedType === 'all') {
|
||||
if (!ruleTypeParam || ruleTypeParam === 'all') {
|
||||
setRuleGroups([]);
|
||||
return;
|
||||
}
|
||||
@@ -259,7 +213,7 @@ export default function RulesIndex() {
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(selectedType);
|
||||
const response = await getRuleGroupsByType(ruleTypeParam);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
@@ -275,8 +229,24 @@ export default function RulesIndex() {
|
||||
};
|
||||
|
||||
loadRuleGroups();
|
||||
}, [searchParams.get('ruleType')]);
|
||||
|
||||
}, [ruleTypeParam]);
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle') {
|
||||
if (fetcher.data.result) {
|
||||
toastService.success(fetcher.data.message);
|
||||
} else if (!fetcher.data.result) {
|
||||
if(fetcher.data.message.includes("evaluation_results_evaluation_point_id_fkey")) {
|
||||
toastService.error("对表“evaluation_points”进行更新或删除违反了表“evaluation results”上的外键约束“evaluations results_evaluation _point_id_fkey”");
|
||||
} else {
|
||||
toastService.error(fetcher.data.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [fetcher.data,fetcher.state]);
|
||||
|
||||
// 筛选评查点
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
@@ -313,6 +283,7 @@ export default function RulesIndex() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 搜索评查点
|
||||
const handleSearch = (keyword: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (keyword) {
|
||||
@@ -327,30 +298,26 @@ export default function RulesIndex() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 删除评查点
|
||||
const handleDeleteClick = (rule: Rule) => {
|
||||
console.log("handleDELETEclick",rule)
|
||||
setRuleToDelete(rule);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!ruleToDelete) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'delete');
|
||||
formData.append('ruleId', ruleToDelete.id);
|
||||
|
||||
submit(formData, { method: 'post' });
|
||||
setShowDeleteConfirm(false);
|
||||
setRuleToDelete(null);
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: `确认删除评查点【${rule.name}】吗?`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("ruleId", rule.id);
|
||||
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 复制评查点
|
||||
const handleCopy = (rule: Rule) => {
|
||||
// const formData = new FormData();
|
||||
// formData.append('_action', 'duplicate');
|
||||
// formData.append('ruleId', rule.id);
|
||||
|
||||
// submit(formData, { method: 'post' });
|
||||
navigate(`/rules-new?id=${rule.id}&mode=copy`);
|
||||
};
|
||||
|
||||
@@ -360,6 +327,7 @@ export default function RulesIndex() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理每页条数变化
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('pageSize', size.toString());
|
||||
@@ -368,9 +336,13 @@ export default function RulesIndex() {
|
||||
};
|
||||
|
||||
// 处理重置筛选
|
||||
// const handleReset = () => {
|
||||
// setSearchParams(new URLSearchParams());
|
||||
// };
|
||||
const handleReset = () => {
|
||||
const input = document.querySelector('input[placeholder="输入评查点名称或编码"]');
|
||||
if (input) {
|
||||
(input as HTMLInputElement).value = '';
|
||||
}
|
||||
setSearchParams(new URLSearchParams());
|
||||
};
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
@@ -475,7 +447,15 @@ export default function RulesIndex() {
|
||||
</div>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<FilterPanel>
|
||||
<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="ruleType"
|
||||
@@ -487,7 +467,7 @@ export default function RulesIndex() {
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[20%]"
|
||||
className="mr-3 w-[15%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
@@ -514,15 +494,16 @@ export default function RulesIndex() {
|
||||
{ value: "false", label: "禁用" }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[20%]"
|
||||
className="mr-3 w-[15%]"
|
||||
/>
|
||||
|
||||
<SearchFilter
|
||||
label="搜索"
|
||||
placeholder="输入评查点名称或编码"
|
||||
value={searchParams.get('keyword') || ''}
|
||||
buttonText="搜索"
|
||||
onSearch={handleSearch}
|
||||
className="w-[30%]"
|
||||
className="min-w-[200px] flex-1"
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
@@ -552,19 +533,17 @@ export default function RulesIndex() {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
{showDeleteConfirm && ruleToDelete && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-lg font-medium mb-4">确认删除</h3>
|
||||
<p className="mb-6">确定要删除评查点“{ruleToDelete.name}”吗?</p>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="default" onClick={() => setShowDeleteConfirm(false)}>取消</Button>
|
||||
<Button type="danger" onClick={confirmDelete}>删除</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误边界
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载评查点列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 模态框样式
|
||||
* 包含动画效果、尺寸变体和响应式布局
|
||||
*/
|
||||
|
||||
/* 模态框背景遮罩 */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 模态框容器 */
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
animation: slideUpIn 0.3s ease forwards;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 模态框尺寸变体 */
|
||||
.modal-small {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal-medium {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal-large {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.modal-full {
|
||||
max-width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
/* 模态框头部 */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer !important;
|
||||
color: #999;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* .modal-close:focus {
|
||||
outline: 2px solid #4f46e5;
|
||||
outline-offset: 2px;
|
||||
} */
|
||||
|
||||
/* 模态框内容区域 */
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
max-height: calc(90vh - 120px); /* 减去header和footer的高度 */
|
||||
}
|
||||
|
||||
/* 模态框底部 */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background-color: #fafafa;
|
||||
border-radius: 0 0 8px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUpIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
.modal-backdrop {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-small,
|
||||
.modal-medium,
|
||||
.modal-large {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body,
|
||||
.modal-footer {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
/* 导入组件样式 */
|
||||
@import './components/badge.css';
|
||||
@import './components/modal.css';
|
||||
@import './components/button.css';
|
||||
@import './components/card.css';
|
||||
@import './components/form.css';
|
||||
|
||||
@@ -199,3 +199,7 @@ input:checked + .slider:before {
|
||||
.error-container {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.document-edit-page .form-select.error {
|
||||
@apply border-red-500 focus:ring-0 focus:shadow-none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user