diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index abfeacc..4ce2043 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -161,7 +161,7 @@ export async function getReviewPoints(fileId: string) { return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } }; } - // 查询评查点组 + // 步骤3:查询评查点组 const groupsParams: PostgrestParams = { select: '*', filter: { @@ -180,7 +180,7 @@ export async function getReviewPoints(fileId: string) { return { data: [], stats: { total: 0, success: 0, warning: 0, error: 0, score: 0 } }; } - // 从audit_status表中 获取 需人工审核 的那些评查点的数据 + // 步骤4:从audit_status表中 获取 需人工审核 的那些评查点的数据 // console.log('evaluationPointsData1112------', evaluationPointsData.find(point => point.post_action === 'manual')); const manualReviewPoints = evaluationPointsData.filter(point => point.post_action === 'manual'); const manualReviewPointsIds = manualReviewPoints.map(point => point.id); @@ -315,7 +315,7 @@ export async function getReviewPoints(fileId: string) { actionContent: point.action_config || '', // actionContent: '用户提前在评查点输入过的修改内容', - legalBasis: point.references_laws || {} + legalBasis: point.references_laws || {}, // legalBasis: { // name: '中华人民共和国食品安全法', // content: '中华人民共和国食品安全法', @@ -326,6 +326,9 @@ export async function getReviewPoints(fileId: string) { // } // ] // } + + // 评查配置: point.evaluation_config + evaluationConfig: point.evaluation_config || {} }; }); diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index d6a86f1..ca82396 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -69,10 +69,11 @@ interface FilePreviewProps { reviewPoints?: ReviewPoint[]; // 设为可选 activeReviewPointResultId: string | null; targetPage?: number; // 新增目标页码参数 + isStructuredView?: boolean; // 是否显示结构化视图 } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export function FilePreview({ fileContent, activeReviewPointResultId, targetPage }: FilePreviewProps) { +export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false }: FilePreviewProps) { const [zoomLevel, setZoomLevel] = useState(100); // const [highlightsVisible, setHighlightsVisible] = useState(true); const contentRef = useRef(null); @@ -173,6 +174,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage // 处理页面跳转 const prevTargetPageRef = useRef(undefined); useEffect(() => { + // 调试信息:记录组件状态 + // console.log(`FilePreview更新 - isStructuredView:${isStructuredView}, targetPage:${targetPage}, activeReviewPointResultId:${activeReviewPointResultId}, numPages:${numPages}`); + // 如果有目标页码,并且与上次相同,提示用户 if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){ toastService.success(`已跳转至目标页码`); @@ -181,9 +185,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) { prevTargetPageRef.current = targetPage; let newTargetPage = targetPage; - // let newTargetPage = targetPage; - // console.log("targetPage:", targetPage); - // console.log("fileContent:", fileContent); // 页码偏移量 try { @@ -197,13 +198,18 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误')); } - const pageElement = document.getElementById(`page-${newTargetPage}`); + const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`; + // console.log(`尝试跳转到元素ID: ${pageElementId}`); + + const pageElement = document.getElementById(pageElementId); if (pageElement) { console.log(`跳转到第${newTargetPage}页,对应评查点结果ID: ${activeReviewPointResultId}`); pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else { + console.warn(`未找到页面元素: ${pageElementId}`); } } - }, [targetPage, numPages, fileContent, activeReviewPointResultId]); + }, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView]); // 获取评查点对应的样式类 // const getHighlightClass = (status: string) => { @@ -302,9 +308,12 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距 }; + // 为结构化视图和普通视图创建不同的ID + const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`; + // 为每一页创建组件 pages.push( -
+
{/* 页码标识,显示在页面上方 */}
第 {i} 页
@@ -365,7 +374,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage // 渲染文档内容 const renderDocumentContent = () => { - return ( + // 如果路径无效,显示错误信息 + if (!fileContent.path) { + return ( +
+

无法加载文件:路径无效

+
+ ); + } + + // 获取文件扩展名 + const fileExtension = fileContent.path.split('.').pop()?.toLowerCase(); + + // PDF内容渲染 + const renderPdfContent = () => (
); + + // 结构化数据渲染 + const renderStructuredData = () => ( +
+
结构化数据:
+ {fileContent.ocrResult ? ( +
+
+              {JSON.stringify(fileContent.ocrResult, null, 2)}
+            
+
+ ) : ( +
+

无结构化数据可显示

+
+ )} +
+ ); + + // 根据文件类型选择不同的渲染方式 + if (fileExtension === 'pdf') { + // 结构化视图模式:显示PDF和结构化数据 + if (isStructuredView) { + return ( +
+ {renderPdfContent()} + {renderStructuredData()} +
+ ); + } + // 普通模式:仅显示PDF + return renderPdfContent(); + } else { + // 非PDF文件显示不支持消息 + return ( +
+

暂不支持预览此类型的文件:{fileExtension}

+
+ ); + } }; return (
-
+
- - 文件预览 + + {isStructuredView ? '附件预览' : '文件预览'}
@@ -423,7 +488,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
- {numPages && / {numPages}} + {numPages && / {numPages}}
- {/* */} - {"比例:"+zoomLevel+"%"} + {"比例:"+zoomLevel+"%"}
diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 186ac70..e37288b 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -55,6 +55,16 @@ export interface ReviewPoint { }; postAction?: string; actionContent?: string; + evaluationConfig?: { + rules?: Array<{ + type: string; + config?: { + fields?: string[]; + pairs?: Array<{ sourceField?: string; targetField?: string }>; + logic?: string; + }; + }>; + }; } // 统计数据类型 @@ -444,84 +454,160 @@ export function ReviewPointsList({ * @returns 评查点主要内容组件 */ const renderContent = (reviewPoint: ReviewPoint, result?: boolean) => { + // 获取evaluationConfig中type为consistency的规则 评查点一致性规则组的规则 + const consistencyRules = reviewPoint.evaluationConfig?.rules?.filter(rule => rule.type === 'consistency') || []; + + // 获取所有consistency规则中的fields + const allConsistencyFields: string[][] = []; + consistencyRules.forEach(rule => { + if (rule.config?.fields) { + allConsistencyFields.push(rule.config.fields); + }else if (rule.config?.pairs) { + // 处理pairs情况,提取sourceField和targetField + const fields: string[] = []; + rule.config.pairs.forEach(pair => { + if (pair.sourceField) fields.push(pair.sourceField); + if (pair.targetField) fields.push(pair.targetField); + }); + if (fields.length > 0) { + allConsistencyFields.push(fields); + } + } + }); + + // 对content进行排序 + const contentEntries = Object.entries(reviewPoint.content); + + // 按照consistency规则分组 + const groupedContent: Record> = { + 'default': [] // 默认组,存放不属于任何consistency规则的项 + }; + + // 为每个consistency规则创建分组 + allConsistencyFields.forEach((fields, index) => { + groupedContent[`consistency_${index}`] = []; + }); + + // 将content按照规则分组 + contentEntries.forEach(entry => { + const [key, value] = entry; + + // 检查是否属于某个consistency规则 + let assigned = false; + allConsistencyFields.forEach((fields, index) => { + if (fields.includes(key)) { + groupedContent[`consistency_${index}`].push(entry); + assigned = true; + } + }); + + // 如果不属于任何规则,放入默认组 + if (!assigned) { + groupedContent['default'].push(entry); + } + }); + return ( <> - {/* 修改评查结果的结构之后,显示新的结构 */} - {Object.entries(reviewPoint.content).map(([key, value], index) => !(result && value.value?.toString().trim() == '') && ( -
{ - e.stopPropagation(); - console.log(`单独点击${key}----`, reviewPoint); - const valuePage = parseInt(value.page as string); - const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); - // 检查value中的page属性是否存在,优先取value中的page - if (valuePage > 0) { - console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage); - onReviewPointSelect(reviewPoint.id, valuePage); + {/* 渲染各个分组 */} + {Object.entries(groupedContent).map(([groupKey, entries], groupIndex) => { + if (entries.length === 0) return null; + + // 非默认组添加边框 + const isDefaultGroup = groupKey === 'default'; + + return ( +
+ {/* 分组标题,只有非默认组显示 */} + {/* {!isDefaultGroup && ( +
+ 规则组 {groupIndex} +
+ )} */} + + {/* 渲染组内内容 */} + {entries.map(([key, value], index) => + !(result && value.value?.toString().trim() == '') && ( +
{ + e.stopPropagation(); + console.log(`单独点击${key}----`, reviewPoint); + const valuePage = parseInt(value.page as string); + const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); + // 检查value中的page属性是否存在,优先取value中的page + if (valuePage > 0) { + console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage); + onReviewPointSelect(reviewPoint.id, valuePage); - } else if(contentPage && contentPage > 0) { - console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage); - onReviewPointSelect(reviewPoint.id, contentPage); - }else { - toastService.error(`无法找到"${key}"对应的索引内容`); - console.log(`单独点击${key}--------没有对应页码`); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - const valuePage = parseInt(value.page as string); - const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); - // 检查value中的page属性是否存在,优先取value中的page - if (valuePage > 0) { - onReviewPointSelect(reviewPoint.id, valuePage); - } else if(contentPage && contentPage > 0) { - onReviewPointSelect(reviewPoint.id, contentPage); - } else { - toastService.error(`无法找到"${key}"对应的索引内容`); - console.log(`单独点击${key}--------没有对应页码`); - } - } - }} - role="button" - tabIndex={0} - aria-label={`查看${key}内容详情`} - onMouseLeave={(e) => { - // 获取容器内的滚动区域元素 - const scrollContainer = e.currentTarget.querySelector('.text-container'); - if (scrollContainer) { - // 在文本缩回之前重置滚动位置 - scrollContainer.scrollTop = 0; - } - }} - > - {/*
*/} -
- - {key} - - - {parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : } - {value.value?.toString().trim() ? '' : '缺失'} - -
+ } else if(contentPage && contentPage > 0) { + console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage); + onReviewPointSelect(reviewPoint.id, contentPage); + }else { + toastService.error(`无法找到"${key}"对应的索引内容`); + console.log(`单独点击${key}--------没有对应页码`); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const valuePage = parseInt(value.page as string); + const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); + // 检查value中的page属性是否存在,优先取value中的page + if (valuePage > 0) { + onReviewPointSelect(reviewPoint.id, valuePage); + } else if(contentPage && contentPage > 0) { + onReviewPointSelect(reviewPoint.id, contentPage); + } else { + toastService.error(`无法找到"${key}"对应的索引内容`); + console.log(`单独点击${key}--------没有对应页码`); + } + } + }} + role="button" + tabIndex={0} + aria-label={`查看${key}内容详情`} + onMouseLeave={(e) => { + // 获取容器内的滚动区域元素 + const scrollContainer = e.currentTarget.querySelector('.text-container'); + if (scrollContainer) { + // 在文本缩回之前重置滚动位置 + scrollContainer.scrollTop = 0; + } + }} + > + {/*
*/} +
+ + {key} + + + {parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : } + {value.value?.toString().trim() ? '' : '缺失'} + +
-
-

- {(value.value?.toString().trim() === '') - ? "" - : value.value?.toString() || ''} -

+
+

+ {(value.value?.toString().trim() === '') + ? "" + : value.value?.toString() || ''} +

+
+
+ ))}
-
- ))} + ); + })} ); }; @@ -533,7 +619,7 @@ export function ReviewPointsList({ * @returns 评查点内容与建议组件 */ const renderReviewPointContent = (reviewPoint: ReviewPoint) => { - + const handleManualReviewNotesChange = (reviewPointId: string, text: string) => { setManualReviewNotes(prev => ({ ...prev, @@ -541,8 +627,9 @@ export function ReviewPointsList({ })); }; - // 如果当前评查点不处于编辑状态,只显示简单信息 + // 如果当前评查点不处于编辑状态 TODO delete if (editingReviewPoint !== reviewPoint.id) { + // 根据result和status决定渲染哪种样式 if (reviewPoint.result === true) { // 已通过的评查点只显示基本信息和人工审核注释 @@ -893,11 +980,11 @@ export function ReviewPointsList({ > {/* 评查点标题和状态 */} {/* 评查点名称 pointName*/} -
{'评查点名称:' + reviewPoint.pointName}
+
{reviewPoint.pointName}
-
{reviewPoint.title}
+
{reviewPoint.title}
{/* 评查点所属分组 */} -
+ {/*
{renderStatusBadge(reviewPoint.status, reviewPoint.result)} {renderHumanReviewBadge(reviewPoint)} diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx index 8fbccb9..5147a0c 100644 --- a/app/components/reviews/ReviewTabs.tsx +++ b/app/components/reviews/ReviewTabs.tsx @@ -15,6 +15,7 @@ interface ReviewTabsProps { previousRoute?: string; path?: string; auditStatus?: number; + type?: string; }; onConfirmResults: () => void; } @@ -104,14 +105,16 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi > AI智能分析 */} - + 结构比对 + + )}
@@ -666,11 +686,18 @@ export function ExtractionSettings({ ].map((variable) => ( ))}
@@ -692,7 +719,7 @@ export function ExtractionSettings({ -
+
+
+ 请为每个字段选择适当的抽取类型,有助于提高识别准确率 +
{fields.vlm.map((field, index) => { const { fieldName, fieldType, typeName, badgeClass } = @@ -747,9 +777,7 @@ export function ExtractionSettings({ ); })}
-
- 请为每个字段选择适当的抽取类型,有助于提高识别准确率 -
+
@@ -843,7 +871,17 @@ export function ExtractionSettings({ className="var-tag" onClick={() => applyVariableToPrompt(variable, "vlm")} > - {variable} + {variable=='docType' ? '文档类型:{docType}': + variable=='fieldsList' ? '抽取字段列表:{fieldsList}': + variable=='companyName' ? '公司名称:{companyName}': + variable=='documentId' ? '文档编号:{documentId}': + variable=='date' ? '日期:{date}': + variable=='industry' ? '行业:{industry}': + variable=='contentType' ? '内容类型:{contentType}': + variable=='pageRange' ? '页面范围:{pageRange}': + variable=='colorMode' ? '色彩模式:{colorMode}': + variable=='ocrText' ? 'OCR文本:{ocrText}': + variable} ))}
diff --git a/app/components/ui/DateRangePicker.tsx b/app/components/ui/DateRangePicker.tsx index 03443e8..bcf2b34 100644 --- a/app/components/ui/DateRangePicker.tsx +++ b/app/components/ui/DateRangePicker.tsx @@ -13,6 +13,7 @@ export interface DateRangePickerProps { endId?: string; format?: string; placeholder?: string; + colorMode?: 'light' | 'dark' | 'auto'; } export function links() { @@ -57,7 +58,8 @@ export function DateRangePicker({ startId = "date-start", endId = "date-end", format = "yyyy-MM-dd", - placeholder = "请选择时间" + placeholder = "请选择时间", + colorMode = 'auto' }: DateRangePickerProps) { // 使用ref获取input元素 const startInputRef = useRef(null); @@ -121,8 +123,15 @@ export function DateRangePicker({ setFocusedInput(null); }; + // 获取颜色模式类名 + const getColorModeClass = () => { + if (colorMode === 'light') return 'color-mode-light'; + if (colorMode === 'dark') return 'color-mode-dark'; + return ''; // auto模式不添加额外类名 + }; + return ( -
+
@@ -200,7 +209,8 @@ export function SimpleDateRangePicker({ startId = "date-start-simple", endId = "date-end-simple", format = "yyyy-MM-dd", - placeholder = "请选择时间" + placeholder = "请选择时间", + colorMode = 'auto' }: Omit) { // 使用ref获取input元素 const startInputRef = useRef(null); @@ -264,8 +274,15 @@ export function SimpleDateRangePicker({ setFocusedInput(null); }; + // 获取颜色模式类名 + const getColorModeClass = () => { + if (colorMode === 'light') return 'color-mode-light'; + if (colorMode === 'dark') return 'color-mode-dark'; + return ''; // auto模式不添加额外类名 + }; + return ( -
+
{ @@ -47,26 +57,75 @@ export function FileTypeTag({ 'license': 'ri-vip-crown-line', 'punishment': 'ri-scales-line', 'agreement': 'ri-file-paper-line', + 'pdf': 'ri-file-pdf-line', + 'doc': 'ri-file-word-line', + 'docx': 'ri-file-word-line', + 'xls': 'ri-file-excel-line', + 'xlsx': 'ri-file-excel-line', + 'ppt': 'ri-file-ppt-line', + 'pptx': 'ri-file-ppt-line', + 'contract': 'ri-file-list-3-line', // 合同文档 + 'license-doc': 'ri-vip-crown-line', // 行政许可卷宗 + 'punishment-doc': 'ri-scales-line', // 行政处罚卷宗 + 'other': 'ri-file-paper-line', // 其他 }; return typeIconMap[type] || 'ri-file-text-line'; }; // 文档类型对应的文本 const getTypeText = () => { + // 如果提供了自定义文本,优先使用 if (text) return text; + // 如果提供了typeName,优先使用 + if (typeName) return typeName; + const typeTextMap: Record = { 'sales-contract': '销售合同', 'purchase-contract': '采购合同', 'license': '专卖许可证', 'punishment': '行政处罚决定书', 'agreement': '承包协议', + 'pdf': 'PDF', + 'doc': 'Word', + 'docx': 'Word', + 'xls': 'Excel', + 'xlsx': 'Excel', + 'ppt': 'PPT', + 'contract': '合同文档', // 合同文档 + 'license-doc': '行政许可卷宗', // 行政许可卷宗 + 'punishment-doc': '行政处罚卷宗', // 行政处罚卷宗 + 'other': '其他文档', // 其他 }; return typeTextMap[type] || type; }; + // 获取根据typeName判断的样式类名 + const getTypeNameClass = () => { + if (!typeName) return ''; + + // 根据typeName判断文档类型 + if (typeName.includes('合同')) { + return 'file-type-tag-contract'; + } else if (typeName.includes('许可') || typeName.includes('行政许可')) { + return 'file-type-tag-license-doc'; + } else if (typeName.includes('处罚') || typeName.includes('行政处罚')) { + return 'file-type-tag-punishment-doc'; + } else { + return 'file-type-tag-other'; + } + }; + // 获取文档类型对应的类名 const getTypeClass = () => { + // 如果有typeName,根据typeName判断样式 + if (typeName) { + const typeNameClass = getTypeNameClass(); + if (typeNameClass) { + return `file-type-tag ${typeNameClass}`; + } + } + // 如果有文件类型,优先使用文件类型决定样式 if (fileType) { const fileTypeClass = getFileTypeClass(fileType); @@ -108,8 +167,15 @@ export function FileTypeTag({ return !showIcon ? 'file-type-tag-no-icon' : ''; }; + // 获取颜色模式类名 + const getColorModeClass = () => { + if (colorMode === 'light') return 'color-mode-light'; + if (colorMode === 'dark') return 'color-mode-dark'; + return ''; // auto模式不添加额外类名 + }; + return ( - + {showIcon && } {getTypeText()} diff --git a/app/components/ui/FilterPanel.tsx b/app/components/ui/FilterPanel.tsx index bac14e4..f190bcc 100644 --- a/app/components/ui/FilterPanel.tsx +++ b/app/components/ui/FilterPanel.tsx @@ -143,6 +143,7 @@ interface DateRangeFilterProps { startLabel?: string; endLabel?: string; simple?: boolean; + colorMode?: 'light' | 'dark' | 'auto'; } /** @@ -168,7 +169,8 @@ export function DateRangeFilter({ className = '', startLabel = "从", endLabel = "至", - simple = false + simple = false, + colorMode = 'auto' }: DateRangeFilterProps) { return (
@@ -180,6 +182,7 @@ export function DateRangeFilter({ onStartDateChange={onStartDateChange} onEndDateChange={onEndDateChange} className="filter-control" + colorMode={colorMode} /> ) : ( )}
diff --git a/app/root.tsx b/app/root.tsx index 737af90..93ffaee 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,8 +8,15 @@ import { ScrollRestoration, isRouteErrorResponse, useRouteError, - type MetaFunction + type MetaFunction, + // useLoaderData } from "@remix-run/react"; +// import { +// LoaderFunctionArgs, +// redirect, +// createCookieSessionStorage, +// ActionFunctionArgs +// } from "@remix-run/node"; import { Layout } from "~/components/layout/Layout"; import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary"; import { MessageModalProvider } from "~/components/ui/MessageModal"; @@ -21,6 +28,104 @@ import messageModalStyles from "~/styles/components/message-modal.css?url"; import toastStyles from "~/styles/components/toast.css?url"; import LoadingBarContainer from "~/components/ui/LoadingBar"; import RouteChangeLoader from "~/components/ui/RouteChangeLoader"; +// import { useState, useEffect } from "react"; + +// 创建基于Cookie的会话存储 +// 在实际应用中,应该使用环境变量来设置密钥 +// const sessionStorage = createCookieSessionStorage({ +// cookie: { +// name: "__session", +// httpOnly: true, +// path: "/", +// sameSite: "lax", +// secrets: ["s3cr3t"], // 应该从环境变量读取 +// secure: process.env.NODE_ENV === "production", +// }, +// }); + +// // 获取会话对象 +// export async function getSession(request: Request) { +// const cookie = request.headers.get("Cookie"); +// return sessionStorage.getSession(cookie); +// } + +// // 获取用户登录状态 +// export async function getUserSession(request: Request) { +// const session = await getSession(request); +// return { +// isAuthenticated: session.get("isAuthenticated") === true, +// }; +// } + +// // 创建登录会话 +// export async function createUserSession(isAuthenticated: boolean, redirectTo: string) { +// const session = await sessionStorage.getSession(); +// session.set("isAuthenticated", isAuthenticated); + +// return redirect(redirectTo, { +// headers: { +// "Set-Cookie": await sessionStorage.commitSession(session), +// }, +// }); +// } + +// // 销毁会话(登出) +// export async function logout(request: Request) { +// const session = await getSession(request); + +// return redirect("/login", { +// headers: { +// "Set-Cookie": await sessionStorage.destroySession(session), +// }, +// }); +// } + +// // 添加action处理登录/登出请求 +// export async function action({ request }: ActionFunctionArgs) { +// const formData = await request.formData(); +// const intent = formData.get("intent"); + +// if (intent === "logout") { +// return logout(request); +// } + +// return null; +// } + +// // 添加loader函数进行全局认证检查 +// export async function loader({ request }: LoaderFunctionArgs) { +// // 获取当前路径 +// const url = new URL(request.url); +// const pathname = url.pathname; + +// // 排除不需要登录验证的路径 +// const publicPaths = ['/login', '/favicon.ico']; +// const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); + +// // 获取用户会话 +// const { isAuthenticated } = await getUserSession(request); + +// // 如果访问需要认证的路径但未登录,重定向到登录页 +// if (!isPublicPath && !isAuthenticated) { +// // 保存请求的URL,以便登录后重定向回来 +// const session = await getSession(request); +// session.set("redirectTo", pathname); + +// return redirect("/login", { +// headers: { +// "Set-Cookie": await sessionStorage.commitSession(session), +// }, +// }); +// } + +// // 如果已登录且访问登录页,重定向到首页 +// if (pathname === "/login" && isAuthenticated) { +// return redirect("/home"); +// } + +// // 向组件传递认证状态和当前路径 +// return Response.json({ isAuthenticated, pathname }); +// } // 添加客户端hydration错误处理 // if (typeof window !== "undefined") { @@ -58,6 +163,12 @@ export function links() { } export default function App() { + // const { pathname } = useLoaderData(); + + // // 确定哪些路径不需要Layout + // const noLayoutPaths = ['/login', '/home']; + // const needsLayout = !noLayoutPaths.includes(pathname); + return ( @@ -82,9 +193,18 @@ export default function App() { - + {/* {needsLayout ? ( + + + + ) : ( - + )} */} + + + + + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 24be06a..64825cb 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,16 +1,17 @@ // import React from 'react'; -import { type MetaFunction } from "@remix-run/node"; +import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag"; // import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag"; import { Tag } from "~/components/ui/Tag"; -import homeStyles from "~/styles/pages/home.css?url"; +import homeStyles from "~/styles/pages/sys_overview.css?url"; import { getDocuments, type DocumentUI } from "~/api/files/documents"; import { useState, useEffect } from "react"; import { getHomeData } from "~/api/home/home"; import dayjs from 'dayjs'; +import { getUserSession } from "~/root"; // 文件处理状态选项 const fileProcessingStatusOptions = [ @@ -41,8 +42,15 @@ export const meta: MetaFunction = () => { // passRate: number; // } -// 模拟数据,实际项目中应该从API获取 -export async function loader() { +// 添加认证检查 +export async function loader({ request }: LoaderFunctionArgs) { + // 检查用户登录状态 + // const { isAuthenticated } = await getUserSession(request); + + // if (!isAuthenticated) { + // return redirect("/login"); + // } + try { const documentSearchParams = { page: 1, diff --git a/app/routes/config-lists._index.tsx b/app/routes/config-lists._index.tsx index 0c56c4e..fef03c8 100644 --- a/app/routes/config-lists._index.tsx +++ b/app/routes/config-lists._index.tsx @@ -1,4 +1,4 @@ -import { json, type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; +import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; import { useLoaderData, useSearchParams, useFetcher, Link } from "@remix-run/react"; import { useState, useEffect } from "react"; import { Button } from "~/components/ui/Button"; diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index c068116..cca7479 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -75,7 +75,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { value: type.id, label: type.name })); - + + // console.log('typesResponse-----',JSON.stringify(documentsResponse.data?.documents[1],null,2)); return Response.json({ documents: documentsResponse.data?.documents || [], total: documentsResponse.data?.total || 0, @@ -643,10 +644,12 @@ export default function DocumentsIndex() {
{record.isTest && ( 测试 @@ -878,6 +881,7 @@ export default function DocumentsIndex() { onStartDateChange={(value) => handleDateChange('dateFrom', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)} simple={true} + colorMode="light" />
diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 3957646..917bd87 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -1527,7 +1527,7 @@ export default function FilesUpload() { id="docNumber" name="docNumber" className="form-input w-full" - placeholder="请输入合同编号、许可证号等" + placeholder="请输入卷宗编号、合同编号等" value={documentNumber} onChange={(e) => setDocumentNumber(e.target.value)} disabled={uploadStage !== "idle"} diff --git a/app/routes/home.tsx b/app/routes/home.tsx new file mode 100644 index 0000000..1007d7c --- /dev/null +++ b/app/routes/home.tsx @@ -0,0 +1,180 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, Form } from '@remix-run/react'; +import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect, json } from "@remix-run/node"; +import styles from "~/styles/pages/home.css?url"; +import { getUserSession, logout } from "~/root"; + +export const links = () => [ + { rel: "stylesheet", href: styles } +]; + +export const meta: MetaFunction = () => { + return [ + { title: "中国烟草AI合同及卷宗审核系统 - 首页" }, + { name: "description", content: "中国烟草AI合同及卷宗审核系统首页" }, + ]; +}; + +// 处理登出请求 +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const intent = formData.get("intent"); + + if (intent === "logout") { + return logout(request); + } + + return null; +} + +// 验证用户登录状态 +export async function loader({ request }: LoaderFunctionArgs) { + const { isAuthenticated } = await getUserSession(request); + + if (!isAuthenticated) { + return redirect("/login"); + } + + return json({ isAuthenticated }); +} + +export default function Home() { + const navigate = useNavigate(); + const [currentTime, setCurrentTime] = useState(''); + const [currentDate, setCurrentDate] = useState(''); + + // 更新日期时间 + useEffect(() => { + const updateDateTime = () => { + const now = new Date(); + // 格式化日期: YYYY/MM/DD + const date = now.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).replace(/\//g, '/'); + + // 格式化时间: HH:MM + const time = now.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + + setCurrentDate(date); + setCurrentTime(time); + }; + + // 初始化时间 + updateDateTime(); + + // 每分钟更新一次 + const interval = setInterval(updateDateTime, 60000); + + return () => clearInterval(interval); + }, []); + + // 处理模块点击 + const handleModuleClick = (path: string) => { + navigate(path); + }; + + // 处理键盘事件 + const handleKeyDown = (path: string, e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + handleModuleClick(path); + } + }; + + // 处理登出 + const handleLogout = () => { + // 使用Form组件提交登出请求 + const form = document.getElementById('logout-form') as HTMLFormElement; + if (form) { + form.submit(); + } + }; + + return ( +
+ {/* 登出表单 - 隐藏 */} +
+ +
+ + {/* 头部 */} +
+
+ 中国烟草 + 中国烟草 + CHINA TOBACCO +
+
+ {currentDate} {currentTime} +
+ 用户头像 + 系统管理员 + +
+
+
+ + {/* 主要内容 */} +
+

- 欢迎来到智慧法务平台 -

+ +
+ {/* 合同管理模块 */} +
handleModuleClick('/documents')} + onKeyDown={(e) => handleKeyDown('/documents', e)} + role="button" + tabIndex={0} + aria-label="合同管理" + > +
+ 合同管理 +
+ + {/* 案卷智能评查模块 */} +
handleModuleClick('/')} + onKeyDown={(e) => handleKeyDown('/', e)} + role="button" + tabIndex={0} + aria-label="案卷智能评查" + > +
+ 案卷智能评查 +
+ + {/* 智慧法务大模型模块 */} +
handleModuleClick('/prompts')} + onKeyDown={(e) => handleKeyDown('/prompts', e)} + role="button" + tabIndex={0} + aria-label="智慧法务大模型" + > +
+ 智慧法务大模型 +
+
+
+ + {/* 底部山水背景 */} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/login.tsx b/app/routes/login.tsx new file mode 100644 index 0000000..0388c0c --- /dev/null +++ b/app/routes/login.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; +import { type MetaFunction, type ActionFunctionArgs, redirect, json, type LoaderFunctionArgs } from "@remix-run/node"; +import styles from "~/styles/pages/login.css?url"; +import { createUserSession, getUserSession, getSession } from "~/root"; + +export const links = () => [ + { rel: "stylesheet", href: styles } +]; + +export const meta: MetaFunction = () => { + return [ + { title: "中国烟草AI合同及卷宗审核系统 - 登录" }, + { name: "description", content: "中国烟草AI合同及卷宗审核系统登录页面" }, + ]; +}; + +// 处理表单提交的action +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const username = formData.get("username") as string; + const password = formData.get("password") as string; + + // 简单的登录验证,实际应用中应该进行真正的身份验证 + if (!username || !password) { + return json({ error: "用户名和密码不能为空" }); + } + + // 在实际应用中,这里应该是对用户名和密码的验证逻辑 + // 简化起见,我们直接视为登录成功 + + // 获取session中存储的重定向URL,如果没有则默认到/home + const session = await getSession(request); + const redirectTo = session.get("redirectTo") || "/home"; + + // 创建登录会话并重定向 + return createUserSession(true, redirectTo); +} + +// 加载器,获取当前会话状态 +export async function loader({ request }: LoaderFunctionArgs) { + const { isAuthenticated } = await getUserSession(request); + + // 如果已登录,重定向到首页 + if (isAuthenticated) { + return redirect("/home"); + } + + return Response.json({ isAuthenticated }); +} + +export default function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const actionData = useActionData(); + const navigation = useNavigation(); + + // 判断是否正在提交表单 + const isSubmitting = navigation.state === "submitting"; + + return ( +
+
+
+ 中国烟草 +

中国烟草AI合同及卷宗审核系统

+
+ +
+

用户登录

+
+ {actionData?.error && ( +
{actionData.error}
+ )} + +
+ + setUsername(e.target.value)} + className="form-input" + placeholder="请输入用户名" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="form-input" + placeholder="请输入密码" + required + /> +
+ + +
+
+ +
+

© 2024 中国烟草 版权所有

+
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 6f2cc9d..41e8a74 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -194,7 +194,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 确保reviewData有效且具有预期的属性 if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) { - console.log("reviewData-------",JSON.stringify(reviewData.document?.type,null,2)); + // console.log("reviewData-------",JSON.stringify(reviewData.document?.type,null,2)); return Response.json({ previousRoute: previousRoute, document: reviewData.document, @@ -517,7 +517,7 @@ export default function ReviewDetails() { }; return ( -
+
{isLoading ? (
@@ -584,7 +584,8 @@ export default function ReviewDetails() { fileInfo={{ previousRoute: loaderData.previousRoute, path: document?.path, - auditStatus: document?.auditStatus + auditStatus: document?.auditStatus, + type: document?.type }} onConfirmResults={handleConfirmResults} > @@ -614,6 +615,43 @@ export default function ReviewDetails() {
)} + {/* 结构比对选项卡内容 */} + {activeTab === 'filecompare' && ( +
+ {/* 左侧:原文件预览 */} +
+ +
+ + {/* 中间:附件文件预览 */} +
+ +
+ + {/* 右侧:评查结果 */} +
+ +
+
+ )} + {/* AI智能分析选项卡内容 */} {activeTab === 'analysis' && ( handleDateChange('dateFrom', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)} simple={true} + colorMode="light" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/contract-icon.svg b/public/images/contract-icon.svg new file mode 100644 index 0000000..b5a92d6 --- /dev/null +++ b/public/images/contract-icon.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/images/mountains-bg.svg b/public/images/mountains-bg.svg new file mode 100644 index 0000000..fe86ec2 --- /dev/null +++ b/public/images/mountains-bg.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/review-icon.svg b/public/images/review-icon.svg new file mode 100644 index 0000000..8e07b2b --- /dev/null +++ b/public/images/review-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..f724d8e --- /dev/null +++ b/public/logo.png @@ -0,0 +1,2 @@ +This is a placeholder for the logo image. +Please replace with an actual logo image file. \ No newline at end of file