添加合同和卷宗数据隔离

This commit is contained in:
2025-06-03 12:16:31 +08:00
parent b02978508d
commit 0397139ad8
20 changed files with 1190 additions and 437 deletions
+12
View File
@@ -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);
+1 -1
View File
@@ -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 响应中提取数据
+7 -1
View File
@@ -265,8 +265,14 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
// 添加筛选条件
const filter: Record<string, string> = {};
// 处理文件类型筛选
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) {
+21 -1
View File
@@ -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;
+42 -5
View File
@@ -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<Document[]>('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<DocumentType[]>('document_types', params);
if (response.error) {
+243 -20
View File
@@ -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<HomeStatistics> {
export async function getHomeData(reviewType?: string | null): Promise<HomeStatistics> {
try {
// 获取当前日期和时间相关值
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
@@ -90,6 +105,12 @@ export async function getHomeData(): Promise<HomeStatistics> {
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 <T>(
apiCall: Promise<{
@@ -128,6 +149,24 @@ export async function getHomeData(): Promise<HomeStatistics> {
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<HomeStatistics> {
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<HomeStatistics> {
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<HomeStatistics> {
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<HomeStatistics> {
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<HomeStatistics> {
// 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<HomeStatistics> {
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 {
+48 -2
View File
@@ -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<string, AppModule> = {
'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<AppModule>('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}
/>
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* <Header username="系统管理员" /> */}
{/* 应用模块选择器 */}
{/* <div className="app-module-selector py-2 px-4 border-b border-gray-100 flex items-center">
{APP_MODULES.map(app => (
<button
key={app.id}
onClick={() => changeAppModule(app.id as AppModule)}
className={`app-module-btn mr-4 py-2 px-4 rounded-md flex items-center ${
selectedApp === app.id ? 'bg-green-50 text-green-700 border border-green-200' : 'hover:bg-gray-50'
}`}
>
<i className={`${app.icon} mr-2`}></i>
<span>{app.name}</span>
</button>
))}
</div> */}
<div className="content-container">
{!shouldHideBreadcrumb && <Breadcrumb />}
{children}
+100 -10
View File
@@ -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<string, string> = {
'contract': '合同管理',
'record': '案卷智能评查',
'model': '智慧法务大模型'
};
// 应用模块图标映射
const APP_ICON_MAP: Record<string, string> = {
'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<Record<string, boolean>>({});
const [currentApp, setCurrentApp] = useState<string>(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) {
</button>
</div>
{/* {!collapsed && (
<div className="user-profile p-4 border-b border-gray-100 flex items-center">
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<span>管</span>
{!collapsed && (
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center text-green-700">
<i className={`${APP_ICON_MAP[currentApp] || 'ri-file-list-2-fill'} mr-2 text-xl`}></i>
<span className="font-medium">{APP_NAME_MAP[currentApp] || '合同管理'}</span>
</div>
<div className="ml-3">
<p className="text-sm font-medium">系统管理员</p>
<p className="text-xs text-gray-500">超级管理员</p>
<div className="text-xs text-gray-500 mt-1">
: {APP_NAME_MAP[currentApp] || '合同管理'}
</div>
</div>
)} */}
)}
<div className="py-4 px-[10px]">
{filteredMenuItems.map((item) => (
+1 -1
View File
@@ -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),
+14 -11
View File
@@ -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<HTMLDivElement>) => {
const handleKeyDown = (path: string, reviewType: string, e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
handleModuleClick(path);
handleModuleClick(path, reviewType);
}
};
@@ -134,8 +137,8 @@ export default function Index() {
{/* 合同管理模块 */}
<div
className="module-card"
onClick={() => 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() {
{/* 案卷智能评查模块 */}
<div
className="module-card"
onClick={() => 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() {
{/* 智慧法务大模型模块 */}
<div
className="module-card"
onClick={() => 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() {
</div>
);
}
}
+164 -136
View File
@@ -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<ActionResponse>();
const navigate = useNavigate();
// 存储从 sessionStorage 获取的 reviewType
const [reviewType, setReviewType] = useState<string | null>(null);
// 添加页面加载状态管理
const [isLoadingData, setIsLoadingData] = useState(true);
const [documents, setDocuments] = useState<DocumentUI[]>([]);
const [total, setTotal] = useState(0);
const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions);
const dataCache = useRef<typeof loaderData | null>(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() {
</div>
{/* 搜索筛选区 */}
<FilterPanel
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
{/* <Button
type="primary"
icon="ri-search-line"
onClick={() => {
// 保持当前筛选条件,刷新数据
// 在实际应用中,这里可能需要触发某些操作
}}
>
搜索
</Button> */}
</>
}
noActionDivider={true}
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
<SearchFilter
label="文档名称"
placeholder="请输入文档名称"
value={search}
onSearch={handleNameSearch}
instantSearch={true}
/>
<FilterPanel
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
</>
}
noActionDivider={true}
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
<SearchFilter
label="文档名称"
placeholder="请输入文档名称"
value={search}
onSearch={handleNameSearch}
instantSearch={true}
/>
<SearchFilter
label="文档编号"
placeholder="请输入文档编号"
value={documentNumber}
onSearch={handleDocumentNumberChange}
instantSearch={true}
/>
<FilterSelect
label="文档类型"
name="documentType"
value={documentType}
options={documentTypeOptions}
onChange={handleDocumentTypeChange}
/>
<FilterSelect
label="文件状态"
name="fileStatus"
value={fileStatus}
options={fileStatusOptions}
onChange={handleFileStatusChange}
/>
<FilterSelect
label="审核状态"
name="auditStatus"
value={auditStatus}
options={auditStatusOptions}
onChange={handleStatusChange}
/>
<DateRangeFilter
label="上传时间"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
colorMode="light"
/>
</div>
</FilterPanel>
<SearchFilter
label="文档编号"
placeholder="请输入文档编号"
value={documentNumber}
onSearch={handleDocumentNumberChange}
instantSearch={true}
/>
<FilterSelect
label="文档类型"
name="documentType"
value={documentType}
options={filteredDocumentTypeOptions}
onChange={handleDocumentTypeChange}
/>
<FilterSelect
label="文件状态"
name="fileStatus"
value={fileStatus}
options={fileStatusOptions}
onChange={handleFileStatusChange}
/>
<FilterSelect
label="审核状态"
name="auditStatus"
value={auditStatus}
options={auditStatusOptions}
onChange={handleStatusChange}
/>
<DateRangeFilter
label="上传时间"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
colorMode="light"
/>
</div>
</FilterPanel>
{/* 数据表格 */}
<Card>
+176 -73
View File
@@ -71,6 +71,7 @@ export const PRIORITY_LABELS: Record<Priority, string> = {
};
// 优先级中文映射
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const PRIORITY_TO_CHINESE: Record<Priority, string> = {
[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<FileUploadResponse> {
// 在实际应用中,这里会使用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<string | null>(null);
// 使用 useLoaderData 获取初始数据
const { documents, documentTypes } = useLoaderData<LoaderData>();
const loaderData = useLoaderData<LoaderData>();
// 状态管理
// 高级上传设置
@@ -285,9 +306,10 @@ export default function FilesUpload() {
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
// 合同文件上传状态
const [isContractType, setIsContractType] = useState<boolean>(false);
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
// 这些变量暂时未使用,但保留以备将来扩展
// const [isContractType, setIsContractType] = useState<boolean>(false);
// const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
// const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
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<Document[]>(documents);
const [documentTypesState] = useState<DocumentType[]>(documentTypes);
const [queueFiles, setQueueFiles] = useState<Document[]>([]);
const [documentTypesState, setDocumentTypesState] = useState<DocumentType[]>([]);
// 在组件挂载时从 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<UploadedFile[]>([]);
@@ -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"}
>
<option value=""></option>
{documentTypes.map(type => (
{documentTypesState.map(type => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
</select>
+160 -77
View File
@@ -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<typeof loader>();
// 使用useState存储最近文档数据,初始值为loader加载的数据
const { homeData: initialHomeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(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 (
<div className="dashboard-container">
@@ -188,24 +275,24 @@ export default function Home() {
value={homeData.todayPendingFiles}
icon="ri-inbox-line"
/>
<StatCard
title="本月已审核文件"
value={homeData.monthlyReviewedFiles}
icon="ri-file-search-line"
trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }}
/>
<StatCard
title="本月审核通过率"
value={homeData.monthlyPassRate}
icon="ri-percent-line"
trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }}
/>
<StatCard
title="本月问题检出数"
value={homeData.issuesDetected}
icon="ri-error-warning-line"
trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }}
/>
<StatCard
title="本月已审核文件"
value={homeData.monthlyReviewedFiles}
icon="ri-file-search-line"
trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }}
/>
<StatCard
title="本月审核通过率"
value={homeData.monthlyPassRate}
icon="ri-percent-line"
trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }}
/>
<StatCard
title="本月问题检出数"
value={homeData.issuesDetected}
icon="ri-error-warning-line"
trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }}
/>
</div>
</Card>
@@ -216,10 +303,6 @@ export default function Home() {
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" />
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
{/* <ShortcutItem icon="ri-file-chart-line" label="评查详情" to="/reviews" /> */}
<ShortcutItem icon="ri-file-list-line" label="文档类型" to="/document-types" />
{/* <ShortcutItem icon="ri-settings-3-line" label="系统设置" to="/settings" /> */}
<ShortcutItem icon="ri-chat-1-line" label="提示词管理" to="/prompts" />
</div>
</Card>
@@ -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 (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
+3 -1
View File
@@ -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
>
<option value="common"></option>
{/* <option value="developer">开发者</option> */}
<option value="developer"></option>
</select>
</div>
+22 -16
View File
@@ -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<typeof loader>();
const rootData = useRouteLoaderData("root") as { userRole: string };
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [expandedGroups, setExpandedGroups] = useState<string[]>([]);
@@ -43,6 +44,7 @@ export default function RuleGroupsIndex() {
const [loading, setLoading] = useState<Record<string, boolean>>({});
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
const [initialLoading, setInitialLoading] = useState<boolean>(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"
>
<i className="ri-edit-line"></i>
</button>
<button
type="button"
className="operation-btn !text-[--color-error]"
onClick={() => handleDeleteGroup(record.id)}
>
<i className="ri-delete-bin-line"></i>
<i className="ri-edit-line"></i> {userRole === 'common' ? '查看' : '编辑'}
</button>
{userRole !== 'common' && (
<button
type="button"
className="operation-btn !text-[--color-error]"
onClick={() => handleDeleteGroup(record.id)}
>
<i className="ri-delete-bin-line"></i>
</button>
)}
</div>
)
}
@@ -566,13 +570,15 @@ export default function RuleGroupsIndex() {
>
</Button>
<Button
type="primary"
icon="ri-add-line"
onClick={() => navigate("/rule-groups/new")}
>
</Button>
{userRole !== 'common' && (
<Button
type="primary"
icon="ri-add-line"
onClick={() => navigate("/rule-groups/new")}
>
</Button>
)}
</div>
</div>
+31 -10
View File
@@ -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<typeof action>();
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() {
{/* 页面头部 */}
<div className="page-header">
<div>
<h1 className="page-title">{isEdit ? "编辑评查点分组" : "新增评查点分组"}</h1>
<h1 className="page-title">{isEdit ? (isReadOnly ? "查看评查点分组" : "编辑评查点分组") : "新增评查点分组"}</h1>
<p className="page-subtitle"></p>
</div>
<div className="header-actions">
@@ -420,13 +432,15 @@ export default function RuleGroupNew() {
>
<i className="ri-arrow-left-line"></i>
</Button>
<Button
type="primary"
form="group-form"
disabled={isSubmitting}
>
<i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'}
</Button>
{!isReadOnly && (
<Button
type="primary"
form="group-form"
disabled={isSubmitting}
>
<i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'}
</Button>
)}
</div>
</div>
@@ -472,6 +486,7 @@ export default function RuleGroupNew() {
value="primary"
checked={formValues.groupType === "primary"}
onChange={handleGroupTypeChange}
disabled={isReadOnly}
/>
<span></span>
</label>
@@ -484,6 +499,7 @@ export default function RuleGroupNew() {
value="secondary"
checked={formValues.groupType === "secondary"}
onChange={handleGroupTypeChange}
disabled={isReadOnly}
/>
<span></span>
</label>
@@ -503,6 +519,7 @@ export default function RuleGroupNew() {
className={`form-select ${touchedFields.parentId && formErrors.parentId ? 'error' : ''}`}
value={formValues.parentId}
onChange={handleChange}
disabled={isReadOnly}
>
<option value=""></option>
{parentGroups
@@ -535,6 +552,7 @@ export default function RuleGroupNew() {
value={formValues.code}
onChange={handleChange}
placeholder="请输入分组编码,如contract-base"
readOnly={isReadOnly}
/>
{touchedFields.code && formErrors.code && (
<div className="form-error">{formErrors.code}</div>
@@ -555,6 +573,7 @@ export default function RuleGroupNew() {
value={formValues.name}
onChange={handleChange}
placeholder="请输入分组名称,如合同基本要素检查"
readOnly={isReadOnly}
/>
{touchedFields.name && formErrors.name && (
<div className="form-error">{formErrors.name}</div>
@@ -583,6 +602,7 @@ export default function RuleGroupNew() {
value={formValues.description}
onChange={handleChange}
placeholder="请输入分组描述,包括适用场景、分组目的等"
readOnly={isReadOnly}
></textarea>
<div className="form-tip"></div>
</div>
@@ -596,6 +616,7 @@ export default function RuleGroupNew() {
className="form-select"
value={formValues.status}
onChange={handleChange}
disabled={isReadOnly}
>
<option value="active"></option>
<option value="inactive"></option>
+120 -57
View File
@@ -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<string, string> = {
// 加载评查文件列表
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<typeof loader>();
const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, result, message } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || '';
// 添加状态管理
const [files, setFiles] = useState<ReviewFileUI[]>(initialFiles);
const [documentTypes, setDocumentTypes] = useState(allDocumentTypes);
const [totalCount, setTotalCount] = useState(initialTotal);
const [isLoading, setIsLoading] = useState(true);
const [reviewType, setReviewType] = useState<string | null>(null);
// 处理初始加载数据loader的错误
useEffect(() => {
@@ -122,6 +102,87 @@ export default function RulesFiles() {
}
}, [result, message]);
// 客户端数据请求
const fetchData = useCallback(async (params: Record<string, string>) => {
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<HTMLSelectElement | HTMLInputElement>) => {
const { name, value } = e.target;
@@ -512,28 +573,30 @@ export default function RulesFiles() {
</FilterPanel>
{/* 文件列表 */}
<Card >
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table table-auto-height"
/>
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
<Card>
<div className={isLoading ? "opacity-70 pointer-events-none transition-opacity" : ""}>
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table table-auto-height"
/>
)}
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</div>
</Card>
</div>
);
+1
View File
@@ -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);
}
+8 -13
View File
@@ -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<typeof loader>();
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<ActionResponse>();
// 检查用户是否为开发者角色
const isDeveloper = userRole === 'developer';
// 状态管理
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
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) {
+16 -2
View File
@@ -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;
}
/* === 面包屑导航 === */