From 0397139ad844d48ba5d370fbb0428764bbb1bc39 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Tue, 3 Jun 2025 12:16:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=88=E5=90=8C=E5=92=8C?= =?UTF-8?q?=E5=8D=B7=E5=AE=97=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/document-types/document-types.ts | 12 + app/api/evaluation_points/reviews.ts | 2 +- app/api/evaluation_points/rules-files.ts | 8 +- app/api/files/documents.ts | 22 +- app/api/files/files-upload.ts | 47 +++- app/api/home/home.ts | 263 ++++++++++++++++++-- app/components/layout/Layout.tsx | 50 +++- app/components/layout/Sidebar.tsx | 110 ++++++++- app/root.tsx | 2 +- app/routes/_index.tsx | 25 +- app/routes/documents._index.tsx | 300 +++++++++++++---------- app/routes/files.upload.tsx | 249 +++++++++++++------ app/routes/home.tsx | 237 ++++++++++++------ app/routes/login.tsx | 4 +- app/routes/rule-groups._index.tsx | 38 +-- app/routes/rule-groups.new.tsx | 41 +++- app/routes/rules-files.tsx | 177 ++++++++----- app/routes/rules-new.tsx | 1 + app/routes/rules._index.tsx | 21 +- app/styles/main.css | 18 +- 20 files changed, 1190 insertions(+), 437 deletions(-) diff --git a/app/api/document-types/document-types.ts b/app/api/document-types/document-types.ts index 1d75661..dddf68c 100644 --- a/app/api/document-types/document-types.ts +++ b/app/api/document-types/document-types.ts @@ -64,6 +64,7 @@ export interface DocumentTypeSearchParams { groupId?: string; page?: number; pageSize?: number; + reviewType?: string; } @@ -249,6 +250,17 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = filter['evaluation_point_groups_ids'] = `cs.[${searchParams.groupId}]`; } + // 根据 reviewType 添加过滤条件 + if (searchParams.reviewType) { + if (searchParams.reviewType === 'contract') { + // 如果是合同类型,只显示id=1的文档类型 + filter['id'] = 'eq.1'; + } else if (searchParams.reviewType === 'record') { + // 如果是卷宗类型,只显示id=2或id=3的文档类型 + filter['id'] = 'in.(2,3)'; + } + } + params.filter = filter; // console.log('获取文档类型列表,参数:', params); diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index c7f0681..2a4df4c 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -1,7 +1,7 @@ import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client"; import {getDocument} from "~/api/files/documents"; import dayjs from "dayjs"; -import { formatDate } from "~/utils"; +// import { formatDate } from "~/utils"; /** * 从不同格式的 API 响应中提取数据 diff --git a/app/api/evaluation_points/rules-files.ts b/app/api/evaluation_points/rules-files.ts index 3104366..3a481a3 100644 --- a/app/api/evaluation_points/rules-files.ts +++ b/app/api/evaluation_points/rules-files.ts @@ -265,8 +265,14 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P // 添加筛选条件 const filter: Record = {}; + // 处理文件类型筛选 if (searchParams.fileType) { - filter['type_id'] = `eq.${searchParams.fileType}`; + // 特殊处理 'record' 类型,表示 type_id 为 2 或 3 + if (searchParams.fileType === 'record') { + filter['type_id'] = 'in.(2,3)'; + } else { + filter['type_id'] = `eq.${searchParams.fileType}`; + } } if (searchParams.reviewStatus) { diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 9bb7c18..177b50f 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -37,6 +37,7 @@ export interface DocumentSearchParams { dateTo?: string; page?: number; pageSize?: number; + reviewType?: string; } /** @@ -210,7 +211,12 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro } if (searchParams.auditStatus) { - filter['audit_status'] = `eq.${searchParams.auditStatus}`; + // 处理"待审核"状态 - 特殊处理 audit_status = 0 的情况,同时包含 null 值 + if (searchParams.auditStatus === '0') { + filter['or'] = `(audit_status.eq.0,audit_status.is.null)`; + } else { + filter['audit_status'] = `eq.${searchParams.auditStatus}`; + } } if (searchParams.fileStatus) { @@ -236,6 +242,20 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro } } + // 根据 reviewType 添加过滤条件 + if (searchParams.reviewType) { + // 如果已经有文档类型过滤,则不再添加 reviewType 的过滤 + if (!searchParams.documentType) { + if (searchParams.reviewType === 'contract') { + // 如果是合同类型,只显示 type_id=1 的文档 + filter['type_id'] = 'eq.1'; + } else if (searchParams.reviewType === 'record') { + // 如果是卷宗类型,只显示 type_id=2 或 type_id=3 的文档 + filter['type_id'] = 'in.(2,3)'; + } + } + } + // console.log('filter-----', filter); params.filter = filter; diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index 669195e..e02b70a 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -2,7 +2,6 @@ import { postgrestGet, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; // import { API_BASE_URL } from '../client'; - /** * 从不同格式的 API 响应中提取数据 * @param responseData API 响应数据 @@ -222,9 +221,10 @@ export async function uploadDocumentToServer( /** * 获取当天的文档列表 + * @param reviewType 审核类型(可选) * @returns 文档列表 */ -export async function getTodayDocuments(): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getTodayDocuments(reviewType?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { try { const today = dayjs().startOf('day').format('YYYY-MM-DD'); // console.log('查询当天文档,日期范围:', today); @@ -245,7 +245,8 @@ export async function getTodayDocuments(): Promise<{data: Document[]; error?: ne ocr_result, extracted_results, sumary, - remark + remark, + audit_status `, order: 'created_at.desc', filter: { @@ -253,6 +254,23 @@ export async function getTodayDocuments(): Promise<{data: Document[]; error?: ne } }; + // 根据reviewType添加过滤条件 + if (reviewType === 'contract') { + // 如果是合同类型,只显示type_id=1的文档 + if (params.filter) { + params.filter['type_id'] = 'eq.1'; + } else { + params.filter = { 'type_id': 'eq.1' }; + } + } else if (reviewType === 'record') { + // 如果是卷宗类型,只显示type_id=2或type_id=3的文档 + if (params.filter) { + params.filter['type_id'] = 'in.(2,3)'; + } else { + params.filter = { 'type_id': 'in.(2,3)' }; + } + } + // console.log('发送请求参数:', params); const response = await postgrestGet('documents', params); // console.log('API 响应:', response); @@ -282,14 +300,33 @@ export async function getTodayDocuments(): Promise<{data: Document[]; error?: ne /** * 获取文档类型列表 + * @param reviewType 审核类型(可选) * @returns 文档类型列表 */ -export async function getDocumentTypes(): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> { +export async function getDocumentTypes(reviewType?: string): Promise<{data: DocumentType[]; error?: never} | {data?: never; error: string; status?: number}> { try { const params: PostgrestParams = { - select: 'id, name' + select: 'id, name', + filter: {} // 初始化为空对象 }; + // 根据reviewType添加过滤条件 + if (reviewType === 'contract') { + // 如果是合同类型,只显示id=1的文档类型 + if (params.filter) { + params.filter['id'] = 'eq.1'; + } else { + params.filter = { 'id': 'eq.1' }; + } + } else if (reviewType === 'record') { + // 如果是卷宗类型,只显示id=2或id=3的文档类型 + if (params.filter) { + params.filter['id'] = 'in.(2,3)'; + } else { + params.filter = { 'id': 'in.(2,3)' }; + } + } + const response = await postgrestGet('document_types', params); if (response.error) { diff --git a/app/api/home/home.ts b/app/api/home/home.ts index 42fb186..f9584d5 100644 --- a/app/api/home/home.ts +++ b/app/api/home/home.ts @@ -1,4 +1,3 @@ -import { log } from "node:console"; import { postgrestGet, type PostgrestParams } from "../postgrest-client"; import dayjs from 'dayjs'; @@ -77,11 +76,27 @@ interface HomeStatistics { }; } +/** + * 通过传入的 reviewType 参数构建类型过滤条件 + * @param reviewType 文档类型 + * @returns 过滤条件字符串 + */ +function buildTypeFilter(reviewType: string | null): string { + let typeFilter = ''; + if (reviewType === 'contract') { + typeFilter = 'type_id.eq.1'; + } else if (reviewType === 'record') { + typeFilter = '(type_id.eq.2,type_id.eq.3)'; + } + return typeFilter; +} + /** * 获取主页数据 + * @param reviewType 从客户端传入的 reviewType 值 * @returns 主页数据 */ -export async function getHomeData(): Promise { +export async function getHomeData(reviewType?: string | null): Promise { try { // 获取当前日期和时间相关值 const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'); @@ -90,6 +105,12 @@ export async function getHomeData(): Promise { const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss'); const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss'); + // console.log('传入的 reviewType', reviewType); + + // 基于 reviewType 构建类型过滤条件 + const typeFilter = buildTypeFilter(reviewType || null); + // console.log('构建的 typeFilter', typeFilter); + // 通用API响应处理函数 const handleApiResponse = async ( apiCall: Promise<{ @@ -128,6 +149,24 @@ export async function getHomeData(): Promise { is_test_document: `eq.false` } }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + // 确保 filter 已初始化 + if (!todayPendingParams.filter) { + todayPendingParams.filter = {}; + } + todayPendingParams.filter.or = typeFilter + ',' + todayPendingParams.filter.or; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!todayPendingParams.filter) { + todayPendingParams.filter = {}; + } + todayPendingParams.filter[field] = `${op}.${value}`; + } + } + const todayPendingCount = await handleApiResponse<{ count: number }[]>( postgrestGet('documents', todayPendingParams), '获取今日待审核文件数量失败', @@ -144,6 +183,20 @@ export async function getHomeData(): Promise { is_test_document: `eq.false` } }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + thisMonthReviewedParams.or = typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!thisMonthReviewedParams.filter) { + thisMonthReviewedParams.filter = {}; + } + thisMonthReviewedParams.filter[field] = `${op}.${value}`; + } + } + const thisMonthReviewedCount = await handleApiResponse<{ count: number }[]>( postgrestGet('documents', thisMonthReviewedParams), '获取本月已审核文件数量失败', @@ -161,6 +214,24 @@ export async function getHomeData(): Promise { is_test_document: `eq.false` } }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + // 确保 filter 已初始化 + if (!lastMonthReviewedParams.filter) { + lastMonthReviewedParams.filter = {}; + } + lastMonthReviewedParams.filter.or = lastMonthReviewedParams.filter.or + ',' + typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!lastMonthReviewedParams.filter) { + lastMonthReviewedParams.filter = {}; + } + lastMonthReviewedParams.filter[field] = `${op}.${value}`; + } + } + const lastMonthReviewedCount = await handleApiResponse<{ count: number }[]>( postgrestGet('documents', lastMonthReviewedParams), '获取上月已审核文件数量失败', @@ -190,6 +261,20 @@ export async function getHomeData(): Promise { is_test_document: `eq.false` } }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + thisMonthTotalParams.or = typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!thisMonthTotalParams.filter) { + thisMonthTotalParams.filter = {}; + } + thisMonthTotalParams.filter[field] = `${op}.${value}`; + } + } + const thisMonthTotalCount = await handleApiResponse<{ count: number }[]>( postgrestGet('documents', thisMonthTotalParams), '获取本月审核通过数量失败', @@ -212,6 +297,20 @@ export async function getHomeData(): Promise { is_test_document: `eq.false` } }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + lastMonthTotalParams.or = typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!lastMonthTotalParams.filter) { + lastMonthTotalParams.filter = {}; + } + lastMonthTotalParams.filter[field] = `${op}.${value}`; + } + } + const lastMonthTotalCount = await handleApiResponse<{ count: number }[]>( postgrestGet('documents', lastMonthTotalParams), '获取上月审核通过数量失败', @@ -246,36 +345,157 @@ export async function getHomeData(): Promise { // console.log('本月通过率-------', monthlyPassRate); // 4. 检查出的问题总数(从评估结果表中统计) - const thisMonthIssuesParams: PostgrestParams = { + // 使用新的数据库函数 count_evaluation_results_by_type 获取指定类型文档的问题数量 + let thisMonthIssuesCount = 0; + let lastMonthIssuesCount = 0; + + // 根据 reviewType 设置要查询的文档类型 + if (reviewType === 'contract') { + // 合同类型 - 直接查询类型 1 + const typeToQuery = 1; + + // 调用数据库函数获取本月指定类型的问题数量 + const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=${typeToQuery}&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, { + select: '*', + filter: {} + }), + '获取本月问题数据失败', + [] + ); + + // 本月问题数量 + thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0; + + // 调用数据库函数获取上月指定类型的问题数量 + const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=${typeToQuery}&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, { + select: '*', + filter: {} + }), + '获取上月问题数据失败', + [] + ); + + // 上月问题数量 + lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0; + + } else if (reviewType === 'record') { + // 记录类型 - 需要查询类型 2 和类型 3,并合并结果 + + // 查询类型 2 的本月问题数量 + const thisMonthType2Response = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=2&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, { + select: '*', + filter: {} + }), + '获取本月类型2问题数据失败', + [] + ); + + // 查询类型 3 的本月问题数量 + const thisMonthType3Response = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=3&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, { + select: '*', + filter: {} + }), + '获取本月类型3问题数据失败', + [] + ); + + // 合并本月两种类型的问题数量 + const thisMonthType2Count = thisMonthType2Response[0]?.count || 0; + const thisMonthType3Count = thisMonthType3Response[0]?.count || 0; + thisMonthIssuesCount = thisMonthType2Count + thisMonthType3Count; + + // 查询类型 2 的上月问题数量 + const lastMonthType2Response = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=2&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, { + select: '*', + filter: {} + }), + '获取上月类型2问题数据失败', + [] + ); + + // 查询类型 3 的上月问题数量 + const lastMonthType3Response = await handleApiResponse<{ count: number }[]>( + postgrestGet(`rpc/count_evaluation_results_by_type?type_val=3&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, { + select: '*', + filter: {} + }), + '获取上月类型3问题数据失败', + [] + ); + + // 合并上月两种类型的问题数量 + const lastMonthType2Count = lastMonthType2Response[0]?.count || 0; + const lastMonthType3Count = lastMonthType3Response[0]?.count || 0; + lastMonthIssuesCount = lastMonthType2Count + lastMonthType3Count; + + } else { + // 如果没有指定类型,则使用原来的查询方式获取所有类型的问题数量 + const thisMonthIssuesParams: PostgrestParams = { select: 'count', filter: { - and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`, - 'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 + and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`, + 'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 } - }; - const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( + }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + thisMonthIssuesParams.or = typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!thisMonthIssuesParams.filter) { + thisMonthIssuesParams.filter = {}; + } + thisMonthIssuesParams.filter[field] = `${op}.${value}`; + } + } + + const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( postgrestGet('evaluation_results', thisMonthIssuesParams), '获取本月问题数据失败', [] - ); - // 本月问题数量 - const thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0; - - // 上月问题数量 - const lastMonthIssuesParams: PostgrestParams = { + ); + + // 本月问题数量 + thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0; + + // 上月问题数量 + const lastMonthIssuesParams: PostgrestParams = { select: 'count', filter: { - and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`, - 'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 + and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`, + 'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 } - }; - const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( + }; + + // 添加类型过滤条件 + if (typeFilter) { + if (typeFilter.startsWith('(')) { + lastMonthIssuesParams.or = typeFilter; + } else { + const [field, op, value] = typeFilter.split('.'); + if (!lastMonthIssuesParams.filter) { + lastMonthIssuesParams.filter = {}; + } + lastMonthIssuesParams.filter[field] = `${op}.${value}`; + } + } + + const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>( postgrestGet('evaluation_results', lastMonthIssuesParams), '获取上月问题数据失败', [] - ); - // 上月问题数量 - const lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0; + ); + + // 上月问题数量 + lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0; + } // 计算问题数量同比增长 let issuesGrowthValue = 0; @@ -286,6 +506,9 @@ export async function getHomeData(): Promise { const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100; issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1))); issuesGrowthIsUp = issuesGrowth >= 0; + }else if(lastMonthIssuesCount == 0 && thisMonthIssuesCount > 0){ + issuesGrowthValue = 100; + issuesGrowthIsUp = true; } // 返回统计结果 return { diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index 3c1cfa3..2af5764 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -5,6 +5,16 @@ import { Breadcrumb } from './Breadcrumb'; import { useMatches, useLocation } from '@remix-run/react'; import type { UserRole } from '~/root'; +// 定义应用模块类型 +type AppModule = 'contract' | 'record' | 'model'; + +// 应用模块与reviewType的映射 +const REVIEW_TYPE_TO_APP: Record = { + 'contract': 'contract', // 合同管理 + 'record': 'record', // 案卷智能评查 + 'model': 'model' // 智慧法务大模型 +}; + interface LayoutProps { children: React.ReactNode; userRole?: UserRole; @@ -24,6 +34,7 @@ interface Match { export function Layout({ children, userRole = 'developer' }: LayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [selectedApp, setSelectedApp] = useState('contract'); const matches = useMatches() as Match[]; const location = useLocation(); @@ -36,12 +47,25 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) { match.handle && match.handle.hideBreadcrumb === true ); - // 从本地存储中获取侧边栏状态 + // 从sessionStorage中获取侧边栏状态和reviewType useEffect(() => { + // 从localStorage获取侧边栏状态 const savedState = localStorage.getItem('sidebarCollapsed'); if (savedState) { setSidebarCollapsed(savedState === 'true'); } + + // 从sessionStorage获取reviewType并设置对应的应用模块 + if (typeof window !== 'undefined') { + try { + const reviewType = sessionStorage.getItem('reviewType'); + if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) { + setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]); + } + } catch (error) { + console.error('获取reviewType失败:', error); + } + } }, []); const toggleSidebar = () => { @@ -50,6 +74,12 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) { localStorage.setItem('sidebarCollapsed', String(newState)); }; + // 切换应用模块 + // const changeAppModule = (appId: AppModule) => { + // setSelectedApp(appId); + // localStorage.setItem('selectedApp', appId); + // }; + // 如果是无布局页面,只渲染内容 if (shouldHideSidebar) { return <>{children}; @@ -61,10 +91,26 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) { collapsed={sidebarCollapsed} onToggle={toggleSidebar} userRole={userRole} + selectedApp={selectedApp} />
- {/*
*/} + {/* 应用模块选择器 */} + {/*
+ {APP_MODULES.map(app => ( + + ))} +
*/} +
{!shouldHideBreadcrumb && } {children} diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 9115539..73a40ee 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -16,11 +16,92 @@ interface SidebarProps { onToggle: () => void; collapsed: boolean; userRole: UserRole; + selectedApp?: string; // 添加所选应用模块参数 } -export function Sidebar({ onToggle, collapsed, userRole }: SidebarProps) { +// 定义不同应用模块下显示的菜单项ID +const APP_MENU_MAP = { + 'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'system-settings'], + 'record': ['home', 'file-management', 'rule-management', 'system-settings'], + 'model': ['home'] +}; + +// 应用模块名称映射 +const APP_NAME_MAP: Record = { + 'contract': '合同管理', + 'record': '案卷智能评查', + 'model': '智慧法务大模型' +}; + +// 应用模块图标映射 +const APP_ICON_MAP: Record = { + 'contract': 'ri-file-list-2-fill', + 'record': 'ri-folder-shared-fill', + 'model': 'ri-robot-2-fill' +}; + +export function Sidebar({ onToggle, collapsed, userRole, selectedApp = 'contract' }: SidebarProps) { const location = useLocation(); const [expandedMenus, setExpandedMenus] = useState>({}); + const [currentApp, setCurrentApp] = useState(selectedApp); + + // 从 sessionStorage 获取 reviewType 并设置当前应用模块 + useEffect(() => { + // 初始加载时获取 reviewType + const updateReviewType = () => { + if (typeof window !== 'undefined') { + const reviewType = sessionStorage.getItem('reviewType'); + if (reviewType) { + setCurrentApp(reviewType); + } + } + }; + + // 首次执行 + updateReviewType(); + + // 设置轮询,每秒检查一次 reviewType 变化 + const intervalId = setInterval(updateReviewType, 1000); + + // 添加自定义事件监听 + const handleReviewTypeChange = () => { + updateReviewType(); + }; + + // 监听 sessionStorage 变化 + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'reviewType' && e.newValue) { + setCurrentApp(e.newValue); + } + }; + + // 添加事件监听器 + window.addEventListener('reviewTypeChange', handleReviewTypeChange); + window.addEventListener('storage', handleStorageChange); + + return () => { + clearInterval(intervalId); + window.removeEventListener('reviewTypeChange', handleReviewTypeChange); + window.removeEventListener('storage', handleStorageChange); + }; + }, []); + + // 监听路由变化,重新检查 reviewType + useEffect(() => { + if (typeof window !== 'undefined') { + const reviewType = sessionStorage.getItem('reviewType'); + if (reviewType) { + setCurrentApp(reviewType); + } + } + }, [location.pathname]); + + // 监听 selectedApp 属性变化 + useEffect(() => { + if (selectedApp) { + setCurrentApp(selectedApp); + } + }, [selectedApp]); const menuItems: MenuItem[] = [ { @@ -187,12 +268,21 @@ export function Sidebar({ onToggle, collapsed, userRole }: SidebarProps) { // console.log('子菜单点击:', child.title, '路径:', child.path); }; - // 根据用户角色过滤菜单项 + // 获取当前应用模式下应显示的菜单ID列表 + const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract']; + + // 根据用户角色和当前应用模式过滤菜单项 const filteredMenuItems = menuItems.filter(item => { // 如果菜单项需要特定角色但用户没有 if (item.requiredRole && item.requiredRole !== userRole) { return false; } + + // 检查当前菜单是否在所选应用模式中显示 + if (!visibleMenuIds.includes(item.id)) { + return false; + } + return true; }); @@ -213,17 +303,17 @@ export function Sidebar({ onToggle, collapsed, userRole }: SidebarProps) {
- {/* {!collapsed && ( -
-
- + {!collapsed && ( +
+
+ + {APP_NAME_MAP[currentApp] || '合同管理'}
-
-

系统管理员

-

超级管理员

+
+ 当前模块: {APP_NAME_MAP[currentApp] || '合同管理'}
- )} */} + )}
{filteredMenuItems.map((item) => ( diff --git a/app/root.tsx b/app/root.tsx index b14edbc..44c05d6 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -74,7 +74,7 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User const session = await sessionStorage.getSession(); session.set("isAuthenticated", isAuthenticated); session.set("userRole", userRole); - + console.log("session-----", session.get("userRole")); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session), diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 4074072..d444af1 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -66,15 +66,18 @@ export default function Index() { }, []); // 处理模块点击 - const handleModuleClick = (path: string) => { - // console.log("导航到路径:", path); + const handleModuleClick = (path: string, reviewType: string) => { + // 将reviewType存入sessionStorage + if (typeof window !== 'undefined') { + sessionStorage.setItem('reviewType', reviewType); + } navigate(path); }; // 处理键盘事件 - const handleKeyDown = (path: string, e: React.KeyboardEvent) => { + const handleKeyDown = (path: string, reviewType: string, e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { - handleModuleClick(path); + handleModuleClick(path, reviewType); } }; @@ -134,8 +137,8 @@ export default function Index() { {/* 合同管理模块 */}
handleModuleClick('/contract-template/search')} - onKeyDown={(e) => handleKeyDown('/contract-template/search', e)} + onClick={() => handleModuleClick('/contract-template/search', 'contract')} + onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)} role="button" tabIndex={0} aria-label="合同管理" @@ -147,8 +150,8 @@ export default function Index() { {/* 案卷智能评查模块 */}
handleModuleClick('/home')} - onKeyDown={(e) => handleKeyDown('/home', e)} + onClick={() => handleModuleClick('/home', 'record')} + onKeyDown={(e) => handleKeyDown('/home', 'record', e)} role="button" tabIndex={0} aria-label="案卷智能评查" @@ -160,8 +163,8 @@ export default function Index() { {/* 智慧法务大模型模块 */}
handleModuleClick('/prompts')} - onKeyDown={(e) => handleKeyDown('/prompts', e)} + onClick={() => handleModuleClick('/prompts', 'model')} + onKeyDown={(e) => handleKeyDown('/prompts', 'model', e)} role="button" tabIndex={0} aria-label="智慧法务大模型" @@ -178,4 +181,4 @@ export default function Index() {
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index cca7479..f8054a5 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; @@ -34,63 +34,28 @@ export const meta: MetaFunction = () => { // 数据加载器 export const loader = async ({ request }: LoaderFunctionArgs) => { - // 获取URL查询参数 + // 获取URL查询参数,只保留必要的分页参数 const url = new URL(request.url); - const search = url.searchParams.get("search") || ""; - const documentType = url.searchParams.get("documentType") || ""; - const auditStatus = url.searchParams.get("auditStatus") || ""; - const documentNumber = url.searchParams.get("documentNumber") || ""; - const fileStatus = url.searchParams.get("fileStatus") || ""; - const dateFrom = url.searchParams.get("dateFrom") || ""; - const dateTo = url.searchParams.get("dateTo") || ""; const page = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); - // 构建搜索参数 - const searchParams = { - name: search || undefined, - documentNumber: documentNumber || undefined, - documentType: documentType || undefined, - auditStatus: auditStatus || undefined, - fileStatus: fileStatus || undefined, - dateFrom: dateFrom || undefined, - dateTo: dateTo || undefined, + // 获取文档类型列表,用于筛选条件 + const typesResponse = await getDocumentTypes({ pageSize: 500 }); + const documentTypes = typesResponse.data?.types || []; + const documentTypeOptions = documentTypes.map(type => ({ + value: type.id, + label: type.name + })); + + // 初始返回空数据,将在客户端根据 sessionStorage 中的 reviewType 加载实际数据 + return Response.json({ + documents: [], + total: 0, page, - pageSize - }; - - 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 - })); - - // console.log('typesResponse-----',JSON.stringify(documentsResponse.data?.documents[1],null,2)); - 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, + documentTypeOptions, + initialLoad: true // 标记这是初始加载 + }); }; // 定义action返回的数据类型 @@ -199,8 +164,14 @@ export default function DocumentsIndex() { const fetcher = useFetcher(); const navigate = useNavigate(); + // 存储从 sessionStorage 获取的 reviewType + const [reviewType, setReviewType] = useState(null); + // 添加页面加载状态管理 const [isLoadingData, setIsLoadingData] = useState(true); + const [documents, setDocuments] = useState([]); + const [total, setTotal] = useState(0); + const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions); const dataCache = useRef(null); // 从URL获取当前筛选条件 @@ -214,8 +185,80 @@ export default function DocumentsIndex() { const currentPage = parseInt(searchParams.get("page") || "1", 10); const pageSize = parseInt(searchParams.get("pageSize") || "10", 10); - // 获取API返回的数据 - const { documents, total, documentTypeOptions } = loaderData; + // 客户端数据请求 + const fetchData = useCallback(async (storedReviewType: string) => { + setIsLoadingData(true); + loadingBarService.show(); + + try { + // 构建搜索参数 + const searchParams = { + name: search || undefined, + documentNumber: documentNumber || undefined, + documentType: documentType || undefined, + auditStatus: auditStatus || undefined, + fileStatus: fileStatus || undefined, + dateFrom: dateFrom || undefined, + dateTo: dateTo || undefined, + reviewType: storedReviewType || undefined, + page: currentPage, + pageSize + }; + + // 获取文档列表 + const documentsResponse = await getDocuments(searchParams); + if (documentsResponse.error) { + throw new Error(documentsResponse.error); + } + + // 获取经过过滤的文档类型列表 + const filteredTypesResponse = await getDocumentTypes({ + pageSize: 500, + reviewType: storedReviewType || undefined + }); + const filteredDocumentTypes = filteredTypesResponse.data?.types || []; + const filteredOptions = filteredDocumentTypes.map(type => ({ + value: type.id, + label: type.name + })); + + // 更新状态 + setDocuments(documentsResponse.data?.documents || []); + setTotal(documentsResponse.data?.total || 0); + setFilteredDocumentTypeOptions(filteredOptions); + + } catch (error) { + console.error('获取文档列表失败:', error); + toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setIsLoadingData(false); + loadingBarService.hide(); + } + }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize]); + + // 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据 + useEffect(() => { + try { + if (typeof window !== 'undefined') { + const storedReviewType = sessionStorage.getItem('reviewType'); + setReviewType(storedReviewType); + + // 如果有 reviewType,则加载数据 + if (storedReviewType) { + fetchData(storedReviewType); + } + } + } catch (error) { + console.error('获取 sessionStorage 中的 reviewType 失败:', error); + } + }, [fetchData]); + + // 监听 URL 参数变化,重新获取数据 + useEffect(() => { + if (reviewType) { + fetchData(reviewType); + } + }, [searchParams, fetchData, reviewType]); // 使用并更新缓存数据 useEffect(() => { @@ -231,10 +274,6 @@ export default function DocumentsIndex() { // 设置缓存数据 dataCache.current = loaderData; - // 数据加载完成后,执行额外的延迟以确保UI效果 - setIsLoadingData(false); - loadingBarService.hide(); - // 处理loader错误 if (loaderData.error) { toastService.error(loaderData.error); @@ -808,84 +847,73 @@ export default function DocumentsIndex() {
{/* 搜索筛选区 */} - - - {/* */} - - } - noActionDivider={true} - > -
- + + + + } + noActionDivider={true} + > +
+ - - - - - - - - - handleDateChange('dateFrom', value)} - onEndDateChange={(value) => handleDateChange('dateTo', value)} - simple={true} - colorMode="light" - /> -
-
- + + + + + + + + + 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 db8b26f..4a6be3d 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -71,6 +71,7 @@ export const PRIORITY_LABELS: Record = { }; // 优先级中文映射 +// eslint-disable-next-line @typescript-eslint/no-unused-vars const PRIORITY_TO_CHINESE: Record = { [Priority.NORMAL]: "普通", [Priority.HIGH]: "优先", @@ -109,60 +110,74 @@ export interface UploadedFile { }; } -// 模拟上传文件到服务器的API -async function uploadFileToServer( - binaryData: ArrayBuffer, - fileName: string, - fileType: string, - documentType: FileType, +// 修改文件上传函数部分,解决类型问题 +async function handleFileUpload( + binaryData: ArrayBuffer, + fileName: string, + fileType: string, + documentType: FileType, priority: Priority, documentNumber: string | null, remark: string | null, isTestDocument: boolean ): Promise { - // 在实际应用中,这里会使用fetch或axios发送请求到后端API - // console.log(`[API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); + // try { + // // 使用封装的上传函数 + // const response = await uploadDocumentToServer( + // binaryData, + // fileName, + // fileType, + // documentType, + // PRIORITY_TO_CHINESE[priority], + // documentNumber, + // remark, + // isTestDocument + // ); + + // if (response.error) { + // console.error('[API] 上传错误:', response.error); + // return { + // success: false, + // error: response.error + // }; + // } + + // // 确保返回有效的FileUploadResponse对象 + // // console.log('上传成功:', response.data); + // if (response.data) { + // return response.data; + // } + + // // 如果没有数据,则返回错误 + // // console.log('上传失败:', response.error); + // return { + // success: false, + // error: '上传失败,未获取到响应数据' + // }; + // } catch (error) { + // console.error('[API] 上传错误:', error); + // return { + // success: false, + // error: error instanceof Error ? error.message : '上传失败' + // }; + // } - try { - // 使用封装的上传函数 - const response = await uploadDocumentToServer( - binaryData, - fileName, - fileType, - documentType, - PRIORITY_TO_CHINESE[priority], - documentNumber, - remark, - isTestDocument - ); - - if (response.error) { - console.error('[API] 上传错误:', response.error); - return { - success: false, - error: response.error - }; - } - - // 确保返回有效的FileUploadResponse对象 - // console.log('上传成功:', response.data); - if (response.data) { - return response.data; - } - - // 如果没有数据,则返回错误 - // console.log('上传失败:', response.error); - return { - success: false, - error: '上传失败,未获取到响应数据' - }; - } catch (error) { - console.error('[API] 上传错误:', error); - return { - success: false, - error: error instanceof Error ? error.message : '上传失败' - }; + const response = await uploadDocumentToServer( + binaryData, + fileName, + fileType, + documentType, + priority, + documentNumber, + remark, + isTestDocument + ); + + if (response.error || !response.data) { + throw new Error(response.error || '上传失败'); } + + return response.data; } // 定义action返回数据的类型 @@ -242,6 +257,8 @@ export async function loader({ request }: LoaderFunctionArgs) { // console.log('loader: 开始加载数据...'); const url = new URL(request.url); const mode = url.searchParams.get("mode") || "create"; + + // 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤 // 并行加载文档和文档类型 const [documentsResponse, typesResponse] = await Promise.all([ getTodayDocuments(), @@ -271,8 +288,12 @@ export async function loader({ request }: LoaderFunctionArgs) { // 文件上传页面组件 export default function FilesUpload() { + // 获取 sessionStorage 中的 reviewType 值 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [reviewType, setReviewType] = useState(null); + // 使用 useLoaderData 获取初始数据 - const { documents, documentTypes } = useLoaderData(); + const loaderData = useLoaderData(); // 状态管理 // 高级上传设置 @@ -285,9 +306,10 @@ export default function FilesUpload() { const [currentFiles, setCurrentFiles] = useState([]); // 合同文件上传状态 - const [isContractType, setIsContractType] = useState(false); - const [contractMainFiles, setContractMainFiles] = useState([]); - const [contractAttachmentFiles, setContractAttachmentFiles] = useState([]); + // 这些变量暂时未使用,但保留以备将来扩展 + // const [isContractType, setIsContractType] = useState(false); + // const [contractMainFiles, setContractMainFiles] = useState([]); + // const [contractAttachmentFiles, setContractAttachmentFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); @@ -302,8 +324,76 @@ export default function FilesUpload() { const navigate = useNavigate(); // 队列文件状态 - const [queueFiles, setQueueFiles] = useState(documents); - const [documentTypesState] = useState(documentTypes); + const [queueFiles, setQueueFiles] = useState([]); + const [documentTypesState, setDocumentTypesState] = useState([]); + + // 在组件挂载时从 sessionStorage 获取 reviewType + useEffect(() => { + try { + // 在客户端环境中执行 + if (typeof window !== 'undefined') { + const storedReviewType = sessionStorage.getItem('reviewType'); + setReviewType(storedReviewType); + + // 根据 reviewType 过滤文档类型和文档列表 + filterDocumentTypes(storedReviewType, loaderData.documentTypes); + filterDocuments(storedReviewType); + } + } catch (error) { + console.error('获取 sessionStorage 中的 reviewType 失败:', error); + } + }, [loaderData]); + + // 过滤文档类型列表 + const filterDocumentTypes = (reviewType: string | null, types: DocumentType[]) => { + if (!reviewType) { + // 如果没有特定的 reviewType,使用原始数据 + setDocumentTypesState(types); + return; + } + + let filteredTypes: DocumentType[] = []; + + if (reviewType === 'contract') { + // 只保留 id=1 的选项 + filteredTypes = types.filter(type => type.id === 1); + } else if (reviewType === 'record') { + // 只保留 id=2 和 id=3 的选项 + filteredTypes = types.filter(type => type.id === 2 || type.id === 3); + } else { + // 如果reviewType不匹配任何条件,使用原始数据 + filteredTypes = types; + } + + setDocumentTypesState(filteredTypes); + }; + + // 过滤文档列表 + const filterDocuments = async (reviewType: string | null) => { + if (!reviewType) { + // 如果没有特定的 reviewType,使用原始数据 + setQueueFiles(loaderData.documents); + return; + } + + try { + // 使用 reviewType 获取过滤后的文档列表 + const response = await getTodayDocuments(reviewType); + + if (response.error) { + console.error('过滤文档列表失败:', response.error); + // 失败时使用原始数据 + setQueueFiles(loaderData.documents); + return; + } + + setQueueFiles(response.data || []); + } catch (error) { + console.error('过滤文档列表失败:', error); + // 出错时使用原始数据 + setQueueFiles(loaderData.documents); + } + }; // 构建文件类型标签映射 useEffect(() => { @@ -312,11 +402,11 @@ export default function FilesUpload() { delete FILE_TYPE_LABELS[key]; }); - // 使用从API获取的文档类型构建新的映射 - documentTypes.forEach(type => { + // 使用过滤后的文档类型构建新的映射 + documentTypesState.forEach(type => { FILE_TYPE_LABELS[type.id.toString()] = type.name; }); - }, [documentTypes]); + }, [documentTypesState]); // 上传完成后的文件信息列表 const [completedFiles, setCompletedFiles] = useState([]); @@ -462,7 +552,7 @@ export default function FilesUpload() { setFileTypeError(null); // 检查是否选择了合同类型 - const selectedType = documentTypes.find(t => t.id.toString() === value); + const selectedType = loaderData.documentTypes.find(t => t.id.toString() === value); const isContract = !!(selectedType && selectedType.name.includes('合同')); // console.log('【调试-handleFileTypeChange】文件类型检查:', { // selectedType, @@ -471,11 +561,11 @@ export default function FilesUpload() { // currentFiles: currentFiles.length // }); - setIsContractType(isContract); + // setIsContractType(isContract); // 重置文件状态 - setContractMainFiles([]); - setContractAttachmentFiles([]); + // setContractMainFiles([]); + // setContractAttachmentFiles([]); setCurrentFiles([]); // 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传 @@ -490,20 +580,21 @@ export default function FilesUpload() { } else { setFileType(""); - setIsContractType(false); + // setIsContractType(false); // 如果用户选择了空选项,显示错误信息 setFileTypeError("上传文件之前请选择文件类型"); } }; - // 处理合同主文件选择 + // 处理合同主文件选择 - 暂时未使用,保留以备将来扩展 + /* const handleContractMainFilesSelected = (files: FileList) => { try { // console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length); // 检查组件是否已卸载 if (!isMountedRef.current) { - // console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理'); + console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理'); return; } @@ -535,7 +626,7 @@ export default function FilesUpload() { // console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); - setContractMainFiles(validFiles); + // setContractMainFiles(validFiles); } else { console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载'); } @@ -546,8 +637,10 @@ export default function FilesUpload() { console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error); } }; + */ - // 处理合同附件选择 + // 处理合同附件选择 - 暂时未使用,保留以备将来扩展 + /* const handleContractAttachmentFilesSelected = (files: FileList) => { try { // console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length); @@ -586,7 +679,7 @@ export default function FilesUpload() { // console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); - setContractAttachmentFiles(validFiles); + // setContractAttachmentFiles(validFiles); } else { console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载'); } @@ -597,8 +690,10 @@ export default function FilesUpload() { console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error); } }; + */ - // 检查并准备上传 + // 检查并准备上传 - 暂时未使用,保留以备将来扩展 + /* const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => { try { // console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', { @@ -621,7 +716,7 @@ export default function FilesUpload() { } // 检查是否为合同类型 - const selectedType = documentTypes.find(t => t.id.toString() === fileType); + const selectedType = loaderData.documentTypes.find(t => t.id.toString() === fileType); const isContract = !!(selectedType && selectedType.name.includes('合同')); // console.log('【调试-checkAndPrepareUpload】文件类型检查', { @@ -721,6 +816,7 @@ export default function FilesUpload() { } } }; + */ // 开始上传文件 const startUpload = async (files: File[]) => { @@ -822,7 +918,7 @@ export default function FilesUpload() { // console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`); // 使用Promise.race添加超时处理 - const uploadPromise = uploadFileToServer( + const uploadPromise = handleFileUpload( binaryData, file.name, file.type, @@ -840,7 +936,7 @@ export default function FilesUpload() { }); // 使用Promise.race处理超时 - response = await Promise.race([uploadPromise, timeoutPromise]); + const uploadResult = await Promise.race([uploadPromise, timeoutPromise]); // 再次检查组件是否已卸载 if (!isMountedRef.current) { @@ -848,6 +944,13 @@ export default function FilesUpload() { return; } + // 检查上传结果 + if (!uploadResult.success || !uploadResult.result) { + throw new Error(uploadResult.error || '上传失败'); + } + + response = uploadResult; + // console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response); } catch (error) { // 检查组件是否已卸载 @@ -1215,8 +1318,8 @@ export default function FilesUpload() { setCompletedFiles([]); // 重置合同文件状态 - setContractMainFiles([]); - setContractAttachmentFiles([]); + // setContractMainFiles([]); + // setContractAttachmentFiles([]); // 重置步骤状态 setProcessingSteps([ @@ -1493,7 +1596,7 @@ export default function FilesUpload() { disabled={uploadStage !== "idle"} > - {documentTypes.map(type => ( + {documentTypesState.map(type => ( ))} diff --git a/app/routes/home.tsx b/app/routes/home.tsx index a0351da..cc02fd7 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -7,7 +7,7 @@ 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/sys_overview.css?url"; -import { getDocuments, type DocumentUI } from "~/api/files/documents"; +import { getDocuments, type DocumentUI, type DocumentSearchParams } from "~/api/files/documents"; import { useState, useEffect } from "react"; import { getHomeData } from "~/api/home/home"; import dayjs from 'dayjs'; @@ -52,27 +52,20 @@ export async function loader() { // } try { - const documentSearchParams = { - page: 1, - pageSize: 10, - order: 'updated_at.desc' - }; - - // 获取最近文档数据 - const responseDocuments = await getDocuments(documentSearchParams); - if (responseDocuments.error) { - console.error('获取最近文档数据失败', responseDocuments.error); - return Response.json({ error: responseDocuments.error }, { status: responseDocuments.status || 500 }); - } - const recentFiles = responseDocuments.data?.documents || []; - // console.log("recentFiles-------",recentFiles); - - - const homeData = await getHomeData(); - // console.log("homeData-------",homeData); - - - return Response.json({ homeData, recentFiles }); + // 返回默认值,实际数据将在客户端根据 sessionStorage 加载 + return Response.json({ + homeData: { + todayPendingFiles: 0, + monthlyReviewedFiles: 0, + monthlyReviewGrowth: { value: 0, isUp: true }, + monthlyPassRate: 0, + passRateGrowth: { value: 0, isUp: true }, + issuesDetected: 0, + issuesGrowth: { value: 0, isUp: true } + }, + recentFiles: [], + reviewType: null + }); } catch (error) { // 错误处理 console.error('Failed to fetch dashboard data:', error); @@ -84,13 +77,14 @@ export async function loader() { } export default function Home() { - const { homeData, recentFiles: initialRecentFiles } = useLoaderData(); - // 使用useState存储最近文档数据,初始值为loader加载的数据 + const { homeData: initialHomeData, recentFiles: initialRecentFiles } = useLoaderData(); const [recentFiles, setRecentFiles] = useState(initialRecentFiles || []); + const [homeData, setHomeData] = useState(initialHomeData); const [currentDateTime, setCurrentDateTime] = useState({ date: '', time: '' }); + const [isLoading, setIsLoading] = useState(true); // 更新当前时间 useEffect(() => { @@ -113,38 +107,132 @@ export default function Home() { return () => clearInterval(timerID); }, []); + // 在客户端挂载时,根据 sessionStorage 中的 reviewType 加载正确的数据 + useEffect(() => { + const loadData = async () => { + try { + setIsLoading(true); + // 从 sessionStorage 获取 reviewType + const reviewType = sessionStorage.getItem('reviewType'); + + // 加载主页数据 + const newHomeData = await getHomeData(reviewType || undefined); + setHomeData(newHomeData); + + // 加载文档数据 + const docs = await loadDocuments(reviewType); + setRecentFiles(docs); + + setIsLoading(false); + } catch (error) { + console.error('加载数据失败:', error); + setIsLoading(false); + } + }; + + loadData(); + }, []); // 仅在组件挂载时执行一次 + + // 加载文档数据的函数 + const loadDocuments = async (reviewType: string | null) => { + try { + const documentSearchParams: DocumentSearchParams = { + page: 1, + pageSize: 10 + }; + + // 根据 reviewType 添加过滤条件 + if (reviewType === 'contract') { + documentSearchParams.documentType = '1'; + + const response = await getDocuments(documentSearchParams); + if (!response.error && response.data) { + return response.data.documents; + } + } else if (reviewType === 'record') { + // 获取类型 2 的文档 + const response1 = await getDocuments({ + ...documentSearchParams, + documentType: '2' + }); + + // 获取类型 3 的文档 + const response2 = await getDocuments({ + ...documentSearchParams, + documentType: '3' + }); + + if (!response1.error && !response2.error && response1.data && response2.data) { + // 合并文档并排序 + const mergedDocs = [...response1.data.documents, ...response2.data.documents]; + mergedDocs.sort((a, b) => + new Date(b.updatedAt || '').getTime() - new Date(a.updatedAt || '').getTime() + ); + + // 限制数量 + return mergedDocs.slice(0, documentSearchParams.pageSize); + } + } else { + // 没有特定类型,获取所有文档 + const response = await getDocuments(documentSearchParams); + if (!response.error && response.data) { + return response.data.documents; + } + } + return []; // 默认返回空数组 + } catch (error) { + console.error('加载文档数据失败:', error); + return []; + } + }; + + // 监听 sessionStorage 中 reviewType 的变化 + useEffect(() => { + const handleStorageChange = async () => { + const currentReviewType = sessionStorage.getItem('reviewType'); + const previousReviewType = sessionStorage.getItem('previousReviewType'); + + // 如果 reviewType 发生变化 + if (currentReviewType !== previousReviewType) { + setIsLoading(true); + + // 更新主页数据 + const newHomeData = await getHomeData(currentReviewType || undefined); + setHomeData(newHomeData); + + // 更新文档数据 + const docs = await loadDocuments(currentReviewType); + setRecentFiles(docs); + + // 保存当前 reviewType 为上一次的值,用于比较 + sessionStorage.setItem('previousReviewType', currentReviewType || ''); + + setIsLoading(false); + } + }; + + // 设置初始的 previousReviewType + const initialReviewType = sessionStorage.getItem('reviewType'); + sessionStorage.setItem('previousReviewType', initialReviewType || ''); + + // 设置定期检查 + const checkInterval = setInterval(handleStorageChange, 1000); + + return () => { + clearInterval(checkInterval); + }; + }, []); + // 修改useEffect定时器,每10秒自动获取最近文档数据 // 按照定时器更新最近文档 useEffect(() => { - // 定义一个函数用于获取最新的文档数据 + // 避免在加载状态下进行自动更新 + if (isLoading) return; + const fetchLatestDocuments = async () => { - try { - const documentSearchParams = { - page: 1, - pageSize: 10, - order: 'updated_at.desc' - }; - - // console.log('定时获取最新文档数据...'); - const responseDocuments = await getDocuments(documentSearchParams); - - if (responseDocuments.error) { - console.error('获取最近文档数据失败', responseDocuments.error); - return; - } - - // 获取新的文档数据 - const newRecentFiles = responseDocuments.data?.documents || []; - - // 检查数据是否有变化 - if (JSON.stringify(newRecentFiles) !== JSON.stringify(recentFiles)) { - // console.log('文档数据已更新,直接更新状态'); - // 直接更新状态,不需要刷新页面 - setRecentFiles(newRecentFiles); - } - } catch (error) { - console.error('自动获取文档数据失败:', error); - } + const reviewType = sessionStorage.getItem('reviewType'); + const docs = await loadDocuments(reviewType); + setRecentFiles(docs); }; // 设置10秒的定时器 @@ -152,10 +240,9 @@ export default function Home() { // 组件卸载时清除定时器 return () => { - // console.log('清除文档数据自动更新定时器'); clearInterval(timerID); }; - }, []); // 不再依赖recentFiles,避免循环依赖 + }, [isLoading]); // 仅依赖 isLoading 状态 return (
@@ -188,24 +275,24 @@ export default function Home() { value={homeData.todayPendingFiles} icon="ri-inbox-line" /> - - - + + +
@@ -216,10 +303,6 @@ export default function Home() { - {/* */} - - {/* */} -
@@ -257,7 +340,7 @@ export default function Home() { {(() => { const fileStatus = file.fileStatus || "-"; const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || - fileProcessingStatusOptions[0]; + fileProcessingStatusOptions[0]; const isSpinning = fileStatus !== "Processed"; return (
diff --git a/app/routes/login.tsx b/app/routes/login.tsx index cb2db47..c8f4b6b 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -21,6 +21,8 @@ export async function action({ request }: ActionFunctionArgs) { const username = formData.get("username") as string; const password = formData.get("password") as string; const userRole = formData.get("userRole") as UserRole || 'common'; + + console.log("userRole-----", userRole); // 简单的登录验证,实际应用中应该进行真正的身份验证 if (!username || !password) { @@ -116,7 +118,7 @@ export default function Login() { required > - {/* */} +
diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 25bc48b..52ce0ed 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -1,5 +1,5 @@ import { type MetaFunction } from "@remix-run/node"; -import { useLoaderData, Link, useNavigate, useSearchParams } from "@remix-run/react"; +import { useLoaderData, Link, useNavigate, useSearchParams, useRouteLoaderData } from "@remix-run/react"; import { useState, useEffect } from "react"; import indexStyles from "~/styles/pages/rule-groups_index.css?url"; import { Card } from "~/components/ui/Card"; @@ -36,6 +36,7 @@ export async function loader() { export default function RuleGroupsIndex() { const { groups: initialGroups } = useLoaderData(); + const rootData = useRouteLoaderData("root") as { userRole: string }; const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [expandedGroups, setExpandedGroups] = useState([]); @@ -43,6 +44,7 @@ export default function RuleGroupsIndex() { const [loading, setLoading] = useState>({}); const [filteredChildrenMap, setFilteredChildrenMap] = useState>({}); const [initialLoading, setInitialLoading] = useState(true); + const userRole = rootData?.userRole || 'common'; // 初始加载时自动加载所有子分组 useEffect(() => { @@ -524,15 +526,17 @@ export default function RuleGroupsIndex() { onClick={() => navigate(`/rule-groups/new?id=${record.id}`)} className="operation-btn" > - 编辑 - - + {userRole !== 'common' && ( + + )}
) } @@ -566,13 +570,15 @@ export default function RuleGroupsIndex() { > 收起全部 - + {userRole !== 'common' && ( + + )}
diff --git a/app/routes/rule-groups.new.tsx b/app/routes/rule-groups.new.tsx index fc8c318..6d57946 100644 --- a/app/routes/rule-groups.new.tsx +++ b/app/routes/rule-groups.new.tsx @@ -1,6 +1,6 @@ -// app/routes/rule-groups.new.tsx + import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node"; -import { useLoaderData, useActionData, useNavigation, Form } from "@remix-run/react"; +import { useLoaderData, useActionData, useNavigation, Form, useRouteLoaderData } from "@remix-run/react"; import { useEffect, useState, useRef } from "react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; @@ -232,6 +232,12 @@ export default function RuleGroupNew() { const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; + const rootData = useRouteLoaderData("root") as { userRole: string }; + const userRole = rootData?.userRole || 'common'; + + + // 判断表单是否为只读模式 + const isReadOnly = userRole === 'common'; // 解构数据 const { group, parentGroups, isEdit, error } = data; @@ -369,6 +375,12 @@ export default function RuleGroupNew() { // 处理表单提交前验证 const handleBeforeSubmit = (e: React.FormEvent) => { + // 如果是只读模式,阻止提交 + if (isReadOnly) { + e.preventDefault(); + return; + } + // 标记所有字段为已触摸 setTouchedFields({ name: true, @@ -409,7 +421,7 @@ export default function RuleGroupNew() { {/* 页面头部 */}
-

{isEdit ? "编辑评查点分组" : "新增评查点分组"}

+

{isEdit ? (isReadOnly ? "查看评查点分组" : "编辑评查点分组") : "新增评查点分组"}

创建新的评查点分组,用于组织管理评查点

@@ -420,13 +432,15 @@ export default function RuleGroupNew() { > 返回列表 - + {!isReadOnly && ( + + )}
@@ -472,6 +486,7 @@ export default function RuleGroupNew() { value="primary" checked={formValues.groupType === "primary"} onChange={handleGroupTypeChange} + disabled={isReadOnly} /> 一级分组 @@ -484,6 +499,7 @@ export default function RuleGroupNew() { value="secondary" checked={formValues.groupType === "secondary"} onChange={handleGroupTypeChange} + disabled={isReadOnly} /> 二级分组 @@ -503,6 +519,7 @@ export default function RuleGroupNew() { className={`form-select ${touchedFields.parentId && formErrors.parentId ? 'error' : ''}`} value={formValues.parentId} onChange={handleChange} + disabled={isReadOnly} > {parentGroups @@ -535,6 +552,7 @@ export default function RuleGroupNew() { value={formValues.code} onChange={handleChange} placeholder="请输入分组编码,如contract-base" + readOnly={isReadOnly} /> {touchedFields.code && formErrors.code && (
{formErrors.code}
@@ -555,6 +573,7 @@ export default function RuleGroupNew() { value={formValues.name} onChange={handleChange} placeholder="请输入分组名称,如合同基本要素检查" + readOnly={isReadOnly} /> {touchedFields.name && formErrors.name && (
{formErrors.name}
@@ -583,6 +602,7 @@ export default function RuleGroupNew() { value={formValues.description} onChange={handleChange} placeholder="请输入分组描述,包括适用场景、分组目的等" + readOnly={isReadOnly} >
详细描述有助于其他用户了解该分组的用途
@@ -596,6 +616,7 @@ export default function RuleGroupNew() { className="form-select" value={formValues.status} onChange={handleChange} + disabled={isReadOnly} > diff --git a/app/routes/rules-files.tsx b/app/routes/rules-files.tsx index 000e93a..989299d 100644 --- a/app/routes/rules-files.tsx +++ b/app/routes/rules-files.tsx @@ -1,6 +1,6 @@ import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react"; -import { useEffect } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { FileIcon } from "~/components/ui/FileIcon"; @@ -13,7 +13,8 @@ import rulesFilesStyles from "~/styles/pages/rules-files.css?url"; import { getReviewFiles, type ReviewFileUI, - updateDocumentAuditStatus + updateDocumentAuditStatus, + type DocumentSearchParams } from "~/api/evaluation_points/rules-files"; import { getDocumentTypes } from "~/api/document-types/document-types"; import { toastService } from "~/components/ui/Toast"; @@ -55,14 +56,8 @@ export const REVIEW_STATUS_LABELS: Record = { // 加载评查文件列表 export async function loader({ request }: LoaderFunctionArgs) { + // 获取分页参数 const url = new URL(request.url); - const fileType = url.searchParams.get("fileType") || ""; - const reviewStatus = url.searchParams.get("reviewStatus") || ""; - const dateRange = url.searchParams.get("dateRange") || ""; - const dateFrom = url.searchParams.get("dateFrom") || ""; - const dateTo = url.searchParams.get("dateTo") || ""; - const keyword = url.searchParams.get("keyword") || ""; - const sortOrder = url.searchParams.get("sortOrder") || "upload_time_desc"; const currentPage = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); @@ -71,36 +66,14 @@ export async function loader({ request }: LoaderFunctionArgs) { const typesResponse = await getDocumentTypes({pageSize:500}); const documentTypes = typesResponse.data?.types || []; - // 获取文件列表 - const searchParams = { - fileType, - reviewStatus, - dateRange, - dateFrom, - dateTo, - keyword, - sortOrder, - page: currentPage, - pageSize, - }; - - // console.log('rules-filessearchParams-----',searchParams); - - const filesResponse = await getReviewFiles(searchParams); - if (filesResponse.error) { - console.error('获取评查文件列表失败:', filesResponse.error); - return Response.json({ result: false, message: filesResponse.error }, { status: filesResponse.status || 500 }); - } - - const files = filesResponse.data?.files || []; - const totalCount = filesResponse.data?.total || 0; - + // 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据 return Response.json({ - files, + files: [], documentTypes, - totalCount, + totalCount: 0, currentPage, pageSize, + initialLoad: true }); } catch (error) { console.error('加载评查文件列表失败:', error); @@ -110,10 +83,17 @@ export async function loader({ request }: LoaderFunctionArgs) { export default function RulesFiles() { const navigate = useNavigate(); - const { files, documentTypes, totalCount, currentPage, pageSize, result, message } = useLoaderData(); + const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, result, message } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const dateFrom = searchParams.get('dateFrom') || ''; const dateTo = searchParams.get('dateTo') || ''; + + // 添加状态管理 + const [files, setFiles] = useState(initialFiles); + const [documentTypes, setDocumentTypes] = useState(allDocumentTypes); + const [totalCount, setTotalCount] = useState(initialTotal); + const [isLoading, setIsLoading] = useState(true); + const [reviewType, setReviewType] = useState(null); // 处理初始加载数据loader的错误 useEffect(() => { @@ -122,6 +102,87 @@ export default function RulesFiles() { } }, [result, message]); + // 客户端数据请求 + const fetchData = useCallback(async (params: Record) => { + setIsLoading(true); + + try { + // 构建搜索参数 + const searchParams: DocumentSearchParams = { + fileType: params.fileType || undefined, + reviewStatus: params.reviewStatus || undefined, + dateFrom: params.dateFrom || undefined, + dateTo: params.dateTo || undefined, + keyword: params.keyword || undefined, + sortOrder: params.sortOrder || 'upload_time_desc', + page: parseInt(params.page || "1", 10), + pageSize: parseInt(params.pageSize || "10", 10) + }; + + // 根据 reviewType 添加类型过滤 + if (reviewType === 'contract') { + searchParams.fileType = '1'; + } else if (reviewType === 'record') { + // 在 API 层处理 type_id 为 2 或 3 的过滤 + searchParams.fileType = 'record'; + } + + // 如果用户手动选择了文件类型,优先使用用户选择的 + if (params.fileType) { + searchParams.fileType = params.fileType; + } + + // 获取文件列表 + const filesResponse = await getReviewFiles(searchParams); + if (filesResponse.error) { + throw new Error(filesResponse.error); + } + + setFiles(filesResponse.data?.files || []); + setTotalCount(filesResponse.data?.total || 0); + } catch (error) { + console.error('获取评查文件列表失败:', error); + toastService.error('获取评查文件列表失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setIsLoading(false); + } + }, [reviewType]); + + // 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据 + useEffect(() => { + try { + if (typeof window !== 'undefined') { + const storedReviewType = sessionStorage.getItem('reviewType'); + setReviewType(storedReviewType); + + // 根据 reviewType 过滤文档类型选项 + if (storedReviewType) { + if (storedReviewType === 'contract') { + // 只保留 id=1 的选项 + const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 1); + setDocumentTypes(filteredTypes); + } else if (storedReviewType === 'record') { + // 只保留 id=2 和 id=3 的选项 + const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 2 || type.id === 3); + setDocumentTypes(filteredTypes); + } + + // 加载数据 + fetchData(Object.fromEntries(searchParams.entries())); + } + } + } catch (error) { + console.error('获取 sessionStorage 中的 reviewType 失败:', error); + } + }, [allDocumentTypes, fetchData, searchParams]); + + // 监听 URL 参数变化,重新获取数据 + useEffect(() => { + if (reviewType) { + fetchData(Object.fromEntries(searchParams.entries())); + } + }, [searchParams, fetchData, reviewType]); + // 处理筛选条件变更 const handleFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -512,28 +573,30 @@ export default function RulesFiles() { {/* 文件列表 */} - - - - {/* 分页组件 */} - {totalCount > 0 && ( - +
+
- )} + + {/* 分页组件 */} + {totalCount > 0 && ( + + )} + ); diff --git a/app/routes/rules-new.tsx b/app/routes/rules-new.tsx index 6b6e87a..0ecb9db 100644 --- a/app/routes/rules-new.tsx +++ b/app/routes/rules-new.tsx @@ -713,6 +713,7 @@ export default function RuleNew() { // 从sessionStorage获取用户角色 if (typeof window !== 'undefined') { const userRoleFromSession = sessionStorage.getItem('userRole') as UserRole || 'common'; + // console.log("userRoleFromSession-----",userRoleFromSession); setUserRole(userRoleFromSession); } diff --git a/app/routes/rules._index.tsx b/app/routes/rules._index.tsx index fa31785..dbb9a65 100644 --- a/app/routes/rules._index.tsx +++ b/app/routes/rules._index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; -import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher } from "@remix-run/react"; +import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher, useRouteLoaderData } from "@remix-run/react"; import { Button } from '~/components/ui/Button'; import { Card } from '~/components/ui/Card'; import { Tag } from '~/components/ui/Tag'; @@ -45,7 +45,6 @@ export type LoaderData = { pageSize: number; totalPages: number; ruleTypes: ApiRuleType[]; // 添加评查点类型 - userRole: UserRole; // 添加用户角色 }; // API返回的数据映射到前端模型 @@ -121,18 +120,12 @@ export async function loader({ request }: LoaderFunctionArgs) { const totalCount = response.data?.totalCount || 0; const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule)); - // 从sessionStorage获取用户角色 - const userRoleFromSession = typeof document !== 'undefined' - ? sessionStorage.getItem('userRole') || 'common' - : 'common'; - return Response.json({ rules, totalCount, currentPage: params.page, pageSize: params.pageSize, - ruleTypes, - userRole: userRoleFromSession as UserRole + ruleTypes }, { headers: { "Cache-Control": "max-age=60, s-maxage=180" @@ -186,15 +179,13 @@ const priorityLabels = { export default function RulesIndex() { const loaderData = useLoaderData(); - const { rules, totalCount, currentPage, pageSize, userRole } = loaderData; + const rootData = useRouteLoaderData("root") as { userRole: UserRole }; + const { rules, totalCount, currentPage, pageSize } = loaderData; const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const fetcher = useFetcher(); - // 检查用户是否为开发者角色 - const isDeveloper = userRole === 'developer'; - // 状态管理 const [ruleGroups, setRuleGroups] = useState([]); const [loadingGroups, setLoadingGroups] = useState(false); @@ -205,6 +196,10 @@ export default function RulesIndex() { // 判断是否禁用规则组选择 const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0; + // 检查用户是否为开发者角色 + const userRole = rootData?.userRole || 'common'; + const isDeveloper = userRole === 'developer'; + // 使用useEffect监听loaderData.error变化并显示Toast useEffect(() => { if(loaderData.error) { diff --git a/app/styles/main.css b/app/styles/main.css index d9838da..0c7c161 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -200,15 +200,29 @@ /* === 主内容区域 === */ .main-content { - @apply flex-1 ml-[240px] transition-all duration-300 min-h-screen flex flex-col; + @apply ml-[240px] flex-1 transition-all duration-300 flex flex-col; } .main-content.sidebar-collapsed { @apply ml-20; } + /* 应用模块选择器 */ + .app-module-selector { + @apply bg-white shadow-sm; + } + + .app-module-btn { + @apply transition-all duration-200 text-gray-700 hover:bg-gray-50 font-medium; + } + + .app-module-btn.active { + @apply bg-green-50 text-green-700 border border-green-200; + } + + /* 内容容器 */ .content-container { - @apply flex-1 p-5 overflow-auto; + @apply p-6 bg-gray-50 flex-1 overflow-auto; } /* === 面包屑导航 === */