添加合同和卷宗数据隔离
This commit is contained in:
@@ -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,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 响应中提取数据
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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`}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/* === 面包屑导航 === */
|
||||
|
||||
Reference in New Issue
Block a user