添加合同和卷宗数据隔离

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; groupId?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
reviewType?: string;
} }
@@ -249,6 +250,17 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
filter['evaluation_point_groups_ids'] = `cs.[${searchParams.groupId}]`; 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; params.filter = filter;
// console.log('获取文档类型列表,参数:', params); // console.log('获取文档类型列表,参数:', params);
+1 -1
View File
@@ -1,7 +1,7 @@
import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client"; import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client";
import {getDocument} from "~/api/files/documents"; import {getDocument} from "~/api/files/documents";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { formatDate } from "~/utils"; // import { formatDate } from "~/utils";
/** /**
* 从不同格式的 API 响应中提取数据 * 从不同格式的 API 响应中提取数据
+7 -1
View File
@@ -265,8 +265,14 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
// 添加筛选条件 // 添加筛选条件
const filter: Record<string, string> = {}; const filter: Record<string, string> = {};
// 处理文件类型筛选
if (searchParams.fileType) { 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) { if (searchParams.reviewStatus) {
+21 -1
View File
@@ -37,6 +37,7 @@ export interface DocumentSearchParams {
dateTo?: string; dateTo?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
reviewType?: string;
} }
/** /**
@@ -210,7 +211,12 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro
} }
if (searchParams.auditStatus) { 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) { 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); // console.log('filter-----', filter);
params.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 dayjs from 'dayjs';
// import { API_BASE_URL } from '../client'; // import { API_BASE_URL } from '../client';
/** /**
* 从不同格式的 API 响应中提取数据 * 从不同格式的 API 响应中提取数据
* @param responseData API 响应数据 * @param responseData API 响应数据
@@ -222,9 +221,10 @@ export async function uploadDocumentToServer(
/** /**
* 获取当天的文档列表 * 获取当天的文档列表
* @param reviewType 审核类型(可选)
* @returns 文档列表 * @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 { try {
const today = dayjs().startOf('day').format('YYYY-MM-DD'); const today = dayjs().startOf('day').format('YYYY-MM-DD');
// console.log('查询当天文档,日期范围:', today); // console.log('查询当天文档,日期范围:', today);
@@ -245,7 +245,8 @@ export async function getTodayDocuments(): Promise<{data: Document[]; error?: ne
ocr_result, ocr_result,
extracted_results, extracted_results,
sumary, sumary,
remark remark,
audit_status
`, `,
order: 'created_at.desc', order: 'created_at.desc',
filter: { 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); // console.log('发送请求参数:', params);
const response = await postgrestGet<Document[]>('documents', params); const response = await postgrestGet<Document[]>('documents', params);
// console.log('API 响应:', response); // console.log('API 响应:', response);
@@ -282,14 +300,33 @@ export async function getTodayDocuments(): Promise<{data: Document[]; error?: ne
/** /**
* 获取文档类型列表 * 获取文档类型列表
* @param reviewType 审核类型(可选)
* @returns 文档类型列表 * @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 { try {
const params: PostgrestParams = { 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); const response = await postgrestGet<DocumentType[]>('document_types', params);
if (response.error) { if (response.error) {
+243 -20
View File
@@ -1,4 +1,3 @@
import { log } from "node:console";
import { postgrestGet, type PostgrestParams } from "../postgrest-client"; import { postgrestGet, type PostgrestParams } from "../postgrest-client";
import dayjs from 'dayjs'; 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 主页数据 * @returns 主页数据
*/ */
export async function getHomeData(): Promise<HomeStatistics> { export async function getHomeData(reviewType?: string | null): Promise<HomeStatistics> {
try { try {
// 获取当前日期和时间相关值 // 获取当前日期和时间相关值
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'); 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 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'); 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响应处理函数 // 通用API响应处理函数
const handleApiResponse = async <T>( const handleApiResponse = async <T>(
apiCall: Promise<{ apiCall: Promise<{
@@ -128,6 +149,24 @@ export async function getHomeData(): Promise<HomeStatistics> {
is_test_document: `eq.false` 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 }[]>( const todayPendingCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', todayPendingParams), postgrestGet('documents', todayPendingParams),
'获取今日待审核文件数量失败', '获取今日待审核文件数量失败',
@@ -144,6 +183,20 @@ export async function getHomeData(): Promise<HomeStatistics> {
is_test_document: `eq.false` 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 }[]>( const thisMonthReviewedCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', thisMonthReviewedParams), postgrestGet('documents', thisMonthReviewedParams),
'获取本月已审核文件数量失败', '获取本月已审核文件数量失败',
@@ -161,6 +214,24 @@ export async function getHomeData(): Promise<HomeStatistics> {
is_test_document: `eq.false` 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 }[]>( const lastMonthReviewedCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', lastMonthReviewedParams), postgrestGet('documents', lastMonthReviewedParams),
'获取上月已审核文件数量失败', '获取上月已审核文件数量失败',
@@ -190,6 +261,20 @@ export async function getHomeData(): Promise<HomeStatistics> {
is_test_document: `eq.false` 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 }[]>( const thisMonthTotalCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', thisMonthTotalParams), postgrestGet('documents', thisMonthTotalParams),
'获取本月审核通过数量失败', '获取本月审核通过数量失败',
@@ -212,6 +297,20 @@ export async function getHomeData(): Promise<HomeStatistics> {
is_test_document: `eq.false` 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 }[]>( const lastMonthTotalCount = await handleApiResponse<{ count: number }[]>(
postgrestGet('documents', lastMonthTotalParams), postgrestGet('documents', lastMonthTotalParams),
'获取上月审核通过数量失败', '获取上月审核通过数量失败',
@@ -246,36 +345,157 @@ export async function getHomeData(): Promise<HomeStatistics> {
// console.log('本月通过率-------', monthlyPassRate); // console.log('本月通过率-------', monthlyPassRate);
// 4. 检查出的问题总数(从评估结果表中统计) // 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', select: 'count',
filter: { filter: {
and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`, and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`,
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 '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), postgrestGet('evaluation_results', thisMonthIssuesParams),
'获取本月问题数据失败', '获取本月问题数据失败',
[] []
); );
// 本月问题数量
const thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0; // 本月问题数量
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// 上月问题数量
const lastMonthIssuesParams: PostgrestParams = { // 上月问题数量
const lastMonthIssuesParams: PostgrestParams = {
select: 'count', select: 'count',
filter: { filter: {
and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`, and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`,
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段 '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), postgrestGet('evaluation_results', lastMonthIssuesParams),
'获取上月问题数据失败', '获取上月问题数据失败',
[] []
); );
// 上月问题数量
const lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0; // 上月问题数量
lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
}
// 计算问题数量同比增长 // 计算问题数量同比增长
let issuesGrowthValue = 0; let issuesGrowthValue = 0;
@@ -286,6 +506,9 @@ export async function getHomeData(): Promise<HomeStatistics> {
const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100; const issuesGrowth = ((thisMonthIssuesCount - lastMonthIssuesCount) / lastMonthIssuesCount) * 100;
issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1))); issuesGrowthValue = Math.abs(parseFloat(issuesGrowth.toFixed(1)));
issuesGrowthIsUp = issuesGrowth >= 0; issuesGrowthIsUp = issuesGrowth >= 0;
}else if(lastMonthIssuesCount == 0 && thisMonthIssuesCount > 0){
issuesGrowthValue = 100;
issuesGrowthIsUp = true;
} }
// 返回统计结果 // 返回统计结果
return { return {
+48 -2
View File
@@ -5,6 +5,16 @@ import { Breadcrumb } from './Breadcrumb';
import { useMatches, useLocation } from '@remix-run/react'; import { useMatches, useLocation } from '@remix-run/react';
import type { UserRole } from '~/root'; 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 { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
userRole?: UserRole; userRole?: UserRole;
@@ -24,6 +34,7 @@ interface Match {
export function Layout({ children, userRole = 'developer' }: LayoutProps) { export function Layout({ children, userRole = 'developer' }: LayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [selectedApp, setSelectedApp] = useState<AppModule>('contract');
const matches = useMatches() as Match[]; const matches = useMatches() as Match[];
const location = useLocation(); const location = useLocation();
@@ -36,12 +47,25 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) {
match.handle && match.handle.hideBreadcrumb === true match.handle && match.handle.hideBreadcrumb === true
); );
// 从本地存储中获取侧边栏状态 // 从sessionStorage中获取侧边栏状态和reviewType
useEffect(() => { useEffect(() => {
// 从localStorage获取侧边栏状态
const savedState = localStorage.getItem('sidebarCollapsed'); const savedState = localStorage.getItem('sidebarCollapsed');
if (savedState) { if (savedState) {
setSidebarCollapsed(savedState === 'true'); 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 = () => { const toggleSidebar = () => {
@@ -50,6 +74,12 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) {
localStorage.setItem('sidebarCollapsed', String(newState)); localStorage.setItem('sidebarCollapsed', String(newState));
}; };
// 切换应用模块
// const changeAppModule = (appId: AppModule) => {
// setSelectedApp(appId);
// localStorage.setItem('selectedApp', appId);
// };
// 如果是无布局页面,只渲染内容 // 如果是无布局页面,只渲染内容
if (shouldHideSidebar) { if (shouldHideSidebar) {
return <>{children}</>; return <>{children}</>;
@@ -61,10 +91,26 @@ export function Layout({ children, userRole = 'developer' }: LayoutProps) {
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
onToggle={toggleSidebar} onToggle={toggleSidebar}
userRole={userRole} userRole={userRole}
selectedApp={selectedApp}
/> />
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}> <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"> <div className="content-container">
{!shouldHideBreadcrumb && <Breadcrumb />} {!shouldHideBreadcrumb && <Breadcrumb />}
{children} {children}
+100 -10
View File
@@ -16,11 +16,92 @@ interface SidebarProps {
onToggle: () => void; onToggle: () => void;
collapsed: boolean; collapsed: boolean;
userRole: UserRole; 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 location = useLocation();
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({}); 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[] = [ const menuItems: MenuItem[] = [
{ {
@@ -187,12 +268,21 @@ export function Sidebar({ onToggle, collapsed, userRole }: SidebarProps) {
// console.log('子菜单点击:', child.title, '路径:', child.path); // 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 => { const filteredMenuItems = menuItems.filter(item => {
// 如果菜单项需要特定角色但用户没有 // 如果菜单项需要特定角色但用户没有
if (item.requiredRole && item.requiredRole !== userRole) { if (item.requiredRole && item.requiredRole !== userRole) {
return false; return false;
} }
// 检查当前菜单是否在所选应用模式中显示
if (!visibleMenuIds.includes(item.id)) {
return false;
}
return true; return true;
}); });
@@ -213,17 +303,17 @@ export function Sidebar({ onToggle, collapsed, userRole }: SidebarProps) {
</button> </button>
</div> </div>
{/* {!collapsed && ( {!collapsed && (
<div className="user-profile p-4 border-b border-gray-100 flex items-center"> <div className="px-4 py-3 border-b border-gray-100">
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center"> <div className="flex items-center text-green-700">
<span>管</span> <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>
<div className="ml-3"> <div className="text-xs text-gray-500 mt-1">
<p className="text-sm font-medium">系统管理员</p> : {APP_NAME_MAP[currentApp] || '合同管理'}
<p className="text-xs text-gray-500">超级管理员</p>
</div> </div>
</div> </div>
)} */} )}
<div className="py-4 px-[10px]"> <div className="py-4 px-[10px]">
{filteredMenuItems.map((item) => ( {filteredMenuItems.map((item) => (
+1 -1
View File
@@ -74,7 +74,7 @@ export async function createUserSession(isAuthenticated: boolean, userRole: User
const session = await sessionStorage.getSession(); const session = await sessionStorage.getSession();
session.set("isAuthenticated", isAuthenticated); session.set("isAuthenticated", isAuthenticated);
session.set("userRole", userRole); session.set("userRole", userRole);
console.log("session-----", session.get("userRole"));
return redirect(redirectTo, { return redirect(redirectTo, {
headers: { headers: {
"Set-Cookie": await sessionStorage.commitSession(session), "Set-Cookie": await sessionStorage.commitSession(session),
+14 -11
View File
@@ -66,15 +66,18 @@ export default function Index() {
}, []); }, []);
// 处理模块点击 // 处理模块点击
const handleModuleClick = (path: string) => { const handleModuleClick = (path: string, reviewType: string) => {
// console.log("导航到路径:", path); // 将reviewType存入sessionStorage
if (typeof window !== 'undefined') {
sessionStorage.setItem('reviewType', reviewType);
}
navigate(path); 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 === ' ') { if (e.key === 'Enter' || e.key === ' ') {
handleModuleClick(path); handleModuleClick(path, reviewType);
} }
}; };
@@ -134,8 +137,8 @@ export default function Index() {
{/* 合同管理模块 */} {/* 合同管理模块 */}
<div <div
className="module-card" className="module-card"
onClick={() => handleModuleClick('/contract-template/search')} onClick={() => handleModuleClick('/contract-template/search', 'contract')}
onKeyDown={(e) => handleKeyDown('/contract-template/search', e)} onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="合同管理" aria-label="合同管理"
@@ -147,8 +150,8 @@ export default function Index() {
{/* 案卷智能评查模块 */} {/* 案卷智能评查模块 */}
<div <div
className="module-card" className="module-card"
onClick={() => handleModuleClick('/home')} onClick={() => handleModuleClick('/home', 'record')}
onKeyDown={(e) => handleKeyDown('/home', e)} onKeyDown={(e) => handleKeyDown('/home', 'record', e)}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="案卷智能评查" aria-label="案卷智能评查"
@@ -160,8 +163,8 @@ export default function Index() {
{/* 智慧法务大模型模块 */} {/* 智慧法务大模型模块 */}
<div <div
className="module-card" className="module-card"
onClick={() => handleModuleClick('/prompts')} onClick={() => handleModuleClick('/prompts', 'model')}
onKeyDown={(e) => handleKeyDown('/prompts', e)} onKeyDown={(e) => handleKeyDown('/prompts', 'model', e)}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="智慧法务大模型" aria-label="智慧法务大模型"
@@ -178,4 +181,4 @@ export default function Index() {
</div> </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 { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
@@ -34,63 +34,28 @@ export const meta: MetaFunction = () => {
// 数据加载器 // 数据加载器
export const loader = async ({ request }: LoaderFunctionArgs) => { export const loader = async ({ request }: LoaderFunctionArgs) => {
// 获取URL查询参数 // 获取URL查询参数,只保留必要的分页参数
const url = new URL(request.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 page = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
// 构建搜索参数 // 获取文档类型列表,用于筛选条件
const searchParams = { const typesResponse = await getDocumentTypes({ pageSize: 500 });
name: search || undefined, const documentTypes = typesResponse.data?.types || [];
documentNumber: documentNumber || undefined, const documentTypeOptions = documentTypes.map(type => ({
documentType: documentType || undefined, value: type.id,
auditStatus: auditStatus || undefined, label: type.name
fileStatus: fileStatus || undefined, }));
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined, // 初始返回空数据,将在客户端根据 sessionStorage 中的 reviewType 加载实际数据
return Response.json({
documents: [],
total: 0,
page, page,
pageSize pageSize,
}; documentTypeOptions,
initialLoad: true // 标记这是初始加载
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 });
}
}; };
// 定义action返回的数据类型 // 定义action返回的数据类型
@@ -199,8 +164,14 @@ export default function DocumentsIndex() {
const fetcher = useFetcher<ActionResponse>(); const fetcher = useFetcher<ActionResponse>();
const navigate = useNavigate(); const navigate = useNavigate();
// 存储从 sessionStorage 获取的 reviewType
const [reviewType, setReviewType] = useState<string | null>(null);
// 添加页面加载状态管理 // 添加页面加载状态管理
const [isLoadingData, setIsLoadingData] = useState(true); 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); const dataCache = useRef<typeof loaderData | null>(null);
// 从URL获取当前筛选条件 // 从URL获取当前筛选条件
@@ -214,8 +185,80 @@ export default function DocumentsIndex() {
const currentPage = parseInt(searchParams.get("page") || "1", 10); const currentPage = parseInt(searchParams.get("page") || "1", 10);
const pageSize = parseInt(searchParams.get("pageSize") || "10", 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(() => { useEffect(() => {
@@ -231,10 +274,6 @@ export default function DocumentsIndex() {
// 设置缓存数据 // 设置缓存数据
dataCache.current = loaderData; dataCache.current = loaderData;
// 数据加载完成后,执行额外的延迟以确保UI效果
setIsLoadingData(false);
loadingBarService.hide();
// 处理loader错误 // 处理loader错误
if (loaderData.error) { if (loaderData.error) {
toastService.error(loaderData.error); toastService.error(loaderData.error);
@@ -808,84 +847,73 @@ export default function DocumentsIndex() {
</div> </div>
{/* 搜索筛选区 */} {/* 搜索筛选区 */}
<FilterPanel <FilterPanel
actions={ actions={
<> <>
<Button <Button
type="default" type="default"
icon="ri-refresh-line" icon="ri-refresh-line"
onClick={handleReset} onClick={handleReset}
className="mr-2" className="mr-2"
> >
</Button> </Button>
{/* <Button </>
type="primary" }
icon="ri-search-line" noActionDivider={true}
onClick={() => { >
// 保持当前筛选条件,刷新数据 <div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
// 在实际应用中,这里可能需要触发某些操作 <SearchFilter
}} label="文档名称"
> placeholder="请输入文档名称"
搜索 value={search}
</Button> */} onSearch={handleNameSearch}
</> instantSearch={true}
} />
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 <SearchFilter
label="文档编号" label="文档编号"
placeholder="请输入文档编号" placeholder="请输入文档编号"
value={documentNumber} value={documentNumber}
onSearch={handleDocumentNumberChange} onSearch={handleDocumentNumberChange}
instantSearch={true} instantSearch={true}
/> />
<FilterSelect <FilterSelect
label="文档类型" label="文档类型"
name="documentType" name="documentType"
value={documentType} value={documentType}
options={documentTypeOptions} options={filteredDocumentTypeOptions}
onChange={handleDocumentTypeChange} onChange={handleDocumentTypeChange}
/> />
<FilterSelect <FilterSelect
label="文件状态" label="文件状态"
name="fileStatus" name="fileStatus"
value={fileStatus} value={fileStatus}
options={fileStatusOptions} options={fileStatusOptions}
onChange={handleFileStatusChange} onChange={handleFileStatusChange}
/> />
<FilterSelect <FilterSelect
label="审核状态" label="审核状态"
name="auditStatus" name="auditStatus"
value={auditStatus} value={auditStatus}
options={auditStatusOptions} options={auditStatusOptions}
onChange={handleStatusChange} onChange={handleStatusChange}
/> />
<DateRangeFilter <DateRangeFilter
label="上传时间" label="上传时间"
startDate={dateFrom} startDate={dateFrom}
endDate={dateTo} endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)} onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true} simple={true}
colorMode="light" colorMode="light"
/> />
</div> </div>
</FilterPanel> </FilterPanel>
{/* 数据表格 */} {/* 数据表格 */}
<Card> <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> = { const PRIORITY_TO_CHINESE: Record<Priority, string> = {
[Priority.NORMAL]: "普通", [Priority.NORMAL]: "普通",
[Priority.HIGH]: "优先", [Priority.HIGH]: "优先",
@@ -109,60 +110,74 @@ export interface UploadedFile {
}; };
} }
// 模拟上传文件到服务器的API // 修改文件上传函数部分,解决类型问题
async function uploadFileToServer( async function handleFileUpload(
binaryData: ArrayBuffer, binaryData: ArrayBuffer,
fileName: string, fileName: string,
fileType: string, fileType: string,
documentType: FileType, documentType: FileType,
priority: Priority, priority: Priority,
documentNumber: string | null, documentNumber: string | null,
remark: string | null, remark: string | null,
isTestDocument: boolean isTestDocument: boolean
): Promise<FileUploadResponse> { ): Promise<FileUploadResponse> {
// 在实际应用中,这里会使用fetch或axios发送请求到后端API // try {
// console.log(`[API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); // // 使用封装的上传函数
// 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,
const response = await uploadDocumentToServer( fileName,
binaryData, fileType,
fileName, documentType,
fileType, priority,
documentType, documentNumber,
PRIORITY_TO_CHINESE[priority], remark,
documentNumber, isTestDocument
remark, );
isTestDocument
); if (response.error || !response.data) {
throw new Error(response.error || '上传失败');
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 : '上传失败'
};
} }
return response.data;
} }
// 定义action返回数据的类型 // 定义action返回数据的类型
@@ -242,6 +257,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
// console.log('loader: 开始加载数据...'); // console.log('loader: 开始加载数据...');
const url = new URL(request.url); const url = new URL(request.url);
const mode = url.searchParams.get("mode") || "create"; const mode = url.searchParams.get("mode") || "create";
// 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤
// 并行加载文档和文档类型 // 并行加载文档和文档类型
const [documentsResponse, typesResponse] = await Promise.all([ const [documentsResponse, typesResponse] = await Promise.all([
getTodayDocuments(), getTodayDocuments(),
@@ -271,8 +288,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 文件上传页面组件 // 文件上传页面组件
export default function FilesUpload() { export default function FilesUpload() {
// 获取 sessionStorage 中的 reviewType 值
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [reviewType, setReviewType] = useState<string | null>(null);
// 使用 useLoaderData 获取初始数据 // 使用 useLoaderData 获取初始数据
const { documents, documentTypes } = useLoaderData<LoaderData>(); const loaderData = useLoaderData<LoaderData>();
// 状态管理 // 状态管理
// 高级上传设置 // 高级上传设置
@@ -285,9 +306,10 @@ export default function FilesUpload() {
const [currentFiles, setCurrentFiles] = useState<File[]>([]); const [currentFiles, setCurrentFiles] = useState<File[]>([]);
// 合同文件上传状态 // 合同文件上传状态
const [isContractType, setIsContractType] = useState<boolean>(false); // 这些变量暂时未使用,但保留以备将来扩展
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]); // const [isContractType, setIsContractType] = useState<boolean>(false);
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]); // const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
// const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
@@ -302,8 +324,76 @@ export default function FilesUpload() {
const navigate = useNavigate(); const navigate = useNavigate();
// 队列文件状态 // 队列文件状态
const [queueFiles, setQueueFiles] = useState<Document[]>(documents); const [queueFiles, setQueueFiles] = useState<Document[]>([]);
const [documentTypesState] = useState<DocumentType[]>(documentTypes); 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(() => { useEffect(() => {
@@ -312,11 +402,11 @@ export default function FilesUpload() {
delete FILE_TYPE_LABELS[key]; delete FILE_TYPE_LABELS[key];
}); });
// 使用从API获取的文档类型构建新的映射 // 使用过滤后的文档类型构建新的映射
documentTypes.forEach(type => { documentTypesState.forEach(type => {
FILE_TYPE_LABELS[type.id.toString()] = type.name; FILE_TYPE_LABELS[type.id.toString()] = type.name;
}); });
}, [documentTypes]); }, [documentTypesState]);
// 上传完成后的文件信息列表 // 上传完成后的文件信息列表
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]); const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
@@ -462,7 +552,7 @@ export default function FilesUpload() {
setFileTypeError(null); 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('合同')); const isContract = !!(selectedType && selectedType.name.includes('合同'));
// console.log('【调试-handleFileTypeChange】文件类型检查:', { // console.log('【调试-handleFileTypeChange】文件类型检查:', {
// selectedType, // selectedType,
@@ -471,11 +561,11 @@ export default function FilesUpload() {
// currentFiles: currentFiles.length // currentFiles: currentFiles.length
// }); // });
setIsContractType(isContract); // setIsContractType(isContract);
// 重置文件状态 // 重置文件状态
setContractMainFiles([]); // setContractMainFiles([]);
setContractAttachmentFiles([]); // setContractAttachmentFiles([]);
setCurrentFiles([]); setCurrentFiles([]);
// 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传 // 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传
@@ -490,20 +580,21 @@ export default function FilesUpload() {
} else { } else {
setFileType(""); setFileType("");
setIsContractType(false); // setIsContractType(false);
// 如果用户选择了空选项,显示错误信息 // 如果用户选择了空选项,显示错误信息
setFileTypeError("上传文件之前请选择文件类型"); setFileTypeError("上传文件之前请选择文件类型");
} }
}; };
// 处理合同主文件选择 // 处理合同主文件选择 - 暂时未使用,保留以备将来扩展
/*
const handleContractMainFilesSelected = (files: FileList) => { const handleContractMainFilesSelected = (files: FileList) => {
try { try {
// console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length); // console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length);
// 检查组件是否已卸载 // 检查组件是否已卸载
if (!isMountedRef.current) { if (!isMountedRef.current) {
// console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理'); console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理');
return; return;
} }
@@ -535,7 +626,7 @@ export default function FilesUpload() {
// console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length);
// console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); // console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
setContractMainFiles(validFiles); // setContractMainFiles(validFiles);
} else { } else {
console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载'); console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载');
} }
@@ -546,8 +637,10 @@ export default function FilesUpload() {
console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error); console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error);
} }
}; };
*/
// 处理合同附件选择 // 处理合同附件选择 - 暂时未使用,保留以备将来扩展
/*
const handleContractAttachmentFilesSelected = (files: FileList) => { const handleContractAttachmentFilesSelected = (files: FileList) => {
try { try {
// console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length); // console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length);
@@ -586,7 +679,7 @@ export default function FilesUpload() {
// console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length);
// console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); // console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
setContractAttachmentFiles(validFiles); // setContractAttachmentFiles(validFiles);
} else { } else {
console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载'); console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载');
} }
@@ -597,8 +690,10 @@ export default function FilesUpload() {
console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error); console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error);
} }
}; };
*/
// 检查并准备上传 // 检查并准备上传 - 暂时未使用,保留以备将来扩展
/*
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => { const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => {
try { try {
// console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', { // 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('合同')); const isContract = !!(selectedType && selectedType.name.includes('合同'));
// console.log('【调试-checkAndPrepareUpload】文件类型检查', { // console.log('【调试-checkAndPrepareUpload】文件类型检查', {
@@ -721,6 +816,7 @@ export default function FilesUpload() {
} }
} }
}; };
*/
// 开始上传文件 // 开始上传文件
const startUpload = async (files: File[]) => { const startUpload = async (files: File[]) => {
@@ -822,7 +918,7 @@ export default function FilesUpload() {
// console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`); // console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`);
// 使用Promise.race添加超时处理 // 使用Promise.race添加超时处理
const uploadPromise = uploadFileToServer( const uploadPromise = handleFileUpload(
binaryData, binaryData,
file.name, file.name,
file.type, file.type,
@@ -840,7 +936,7 @@ export default function FilesUpload() {
}); });
// 使用Promise.race处理超时 // 使用Promise.race处理超时
response = await Promise.race([uploadPromise, timeoutPromise]); const uploadResult = await Promise.race([uploadPromise, timeoutPromise]);
// 再次检查组件是否已卸载 // 再次检查组件是否已卸载
if (!isMountedRef.current) { if (!isMountedRef.current) {
@@ -848,6 +944,13 @@ export default function FilesUpload() {
return; return;
} }
// 检查上传结果
if (!uploadResult.success || !uploadResult.result) {
throw new Error(uploadResult.error || '上传失败');
}
response = uploadResult;
// console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response); // console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response);
} catch (error) { } catch (error) {
// 检查组件是否已卸载 // 检查组件是否已卸载
@@ -1215,8 +1318,8 @@ export default function FilesUpload() {
setCompletedFiles([]); setCompletedFiles([]);
// 重置合同文件状态 // 重置合同文件状态
setContractMainFiles([]); // setContractMainFiles([]);
setContractAttachmentFiles([]); // setContractAttachmentFiles([]);
// 重置步骤状态 // 重置步骤状态
setProcessingSteps([ setProcessingSteps([
@@ -1493,7 +1596,7 @@ export default function FilesUpload() {
disabled={uploadStage !== "idle"} disabled={uploadStage !== "idle"}
> >
<option value=""></option> <option value=""></option>
{documentTypes.map(type => ( {documentTypesState.map(type => (
<option key={type.id} value={type.id}>{type.name}</option> <option key={type.id} value={type.id}>{type.name}</option>
))} ))}
</select> </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 { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
import { Tag } from "~/components/ui/Tag"; import { Tag } from "~/components/ui/Tag";
import homeStyles from "~/styles/pages/sys_overview.css?url"; 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 { useState, useEffect } from "react";
import { getHomeData } from "~/api/home/home"; import { getHomeData } from "~/api/home/home";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -52,27 +52,20 @@ export async function loader() {
// } // }
try { try {
const documentSearchParams = { // 返回默认值,实际数据将在客户端根据 sessionStorage 加载
page: 1, return Response.json({
pageSize: 10, homeData: {
order: 'updated_at.desc' todayPendingFiles: 0,
}; monthlyReviewedFiles: 0,
monthlyReviewGrowth: { value: 0, isUp: true },
// 获取最近文档数据 monthlyPassRate: 0,
const responseDocuments = await getDocuments(documentSearchParams); passRateGrowth: { value: 0, isUp: true },
if (responseDocuments.error) { issuesDetected: 0,
console.error('获取最近文档数据失败', responseDocuments.error); issuesGrowth: { value: 0, isUp: true }
return Response.json({ error: responseDocuments.error }, { status: responseDocuments.status || 500 }); },
} recentFiles: [],
const recentFiles = responseDocuments.data?.documents || []; reviewType: null
// console.log("recentFiles-------",recentFiles); });
const homeData = await getHomeData();
// console.log("homeData-------",homeData);
return Response.json({ homeData, recentFiles });
} catch (error) { } catch (error) {
// 错误处理 // 错误处理
console.error('Failed to fetch dashboard data:', error); console.error('Failed to fetch dashboard data:', error);
@@ -84,13 +77,14 @@ export async function loader() {
} }
export default function Home() { export default function Home() {
const { homeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>(); const { homeData: initialHomeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
// 使用useState存储最近文档数据,初始值为loader加载的数据
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []); const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
const [homeData, setHomeData] = useState(initialHomeData);
const [currentDateTime, setCurrentDateTime] = useState({ const [currentDateTime, setCurrentDateTime] = useState({
date: '', date: '',
time: '' time: ''
}); });
const [isLoading, setIsLoading] = useState(true);
// 更新当前时间 // 更新当前时间
useEffect(() => { useEffect(() => {
@@ -113,38 +107,132 @@ export default function Home() {
return () => clearInterval(timerID); 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定时器,每10秒自动获取最近文档数据
// 按照定时器更新最近文档 // 按照定时器更新最近文档
useEffect(() => { useEffect(() => {
// 定义一个函数用于获取最新的文档数据 // 避免在加载状态下进行自动更新
if (isLoading) return;
const fetchLatestDocuments = async () => { const fetchLatestDocuments = async () => {
try { const reviewType = sessionStorage.getItem('reviewType');
const documentSearchParams = { const docs = await loadDocuments(reviewType);
page: 1, setRecentFiles(docs);
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);
}
}; };
// 设置10秒的定时器 // 设置10秒的定时器
@@ -152,10 +240,9 @@ export default function Home() {
// 组件卸载时清除定时器 // 组件卸载时清除定时器
return () => { return () => {
// console.log('清除文档数据自动更新定时器');
clearInterval(timerID); clearInterval(timerID);
}; };
}, []); // 不再依赖recentFiles,避免循环依赖 }, [isLoading]); // 仅依赖 isLoading 状态
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
@@ -188,24 +275,24 @@ export default function Home() {
value={homeData.todayPendingFiles} value={homeData.todayPendingFiles}
icon="ri-inbox-line" icon="ri-inbox-line"
/> />
<StatCard <StatCard
title="本月已审核文件" title="本月已审核文件"
value={homeData.monthlyReviewedFiles} value={homeData.monthlyReviewedFiles}
icon="ri-file-search-line" icon="ri-file-search-line"
trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }} trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }}
/> />
<StatCard <StatCard
title="本月审核通过率" title="本月审核通过率"
value={homeData.monthlyPassRate} value={homeData.monthlyPassRate}
icon="ri-percent-line" icon="ri-percent-line"
trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }} trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }}
/> />
<StatCard <StatCard
title="本月问题检出数" title="本月问题检出数"
value={homeData.issuesDetected} value={homeData.issuesDetected}
icon="ri-error-warning-line" icon="ri-error-warning-line"
trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }} trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }}
/> />
</div> </div>
</Card> </Card>
@@ -216,10 +303,6 @@ export default function Home() {
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" /> <ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" />
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" /> <ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" /> <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> </div>
</Card> </Card>
@@ -257,7 +340,7 @@ export default function Home() {
{(() => { {(() => {
const fileStatus = file.fileStatus || "-"; const fileStatus = file.fileStatus || "-";
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
fileProcessingStatusOptions[0]; fileProcessingStatusOptions[0];
const isSpinning = fileStatus !== "Processed"; const isSpinning = fileStatus !== "Processed";
return ( return (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}> <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 username = formData.get("username") as string;
const password = formData.get("password") as string; const password = formData.get("password") as string;
const userRole = formData.get("userRole") as UserRole || 'common'; const userRole = formData.get("userRole") as UserRole || 'common';
console.log("userRole-----", userRole);
// 简单的登录验证,实际应用中应该进行真正的身份验证 // 简单的登录验证,实际应用中应该进行真正的身份验证
if (!username || !password) { if (!username || !password) {
@@ -116,7 +118,7 @@ export default function Login() {
required required
> >
<option value="common"></option> <option value="common"></option>
{/* <option value="developer">开发者</option> */} <option value="developer"></option>
</select> </select>
</div> </div>
+22 -16
View File
@@ -1,5 +1,5 @@
import { type MetaFunction } from "@remix-run/node"; 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 { useState, useEffect } from "react";
import indexStyles from "~/styles/pages/rule-groups_index.css?url"; import indexStyles from "~/styles/pages/rule-groups_index.css?url";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
@@ -36,6 +36,7 @@ export async function loader() {
export default function RuleGroupsIndex() { export default function RuleGroupsIndex() {
const { groups: initialGroups } = useLoaderData<typeof loader>(); const { groups: initialGroups } = useLoaderData<typeof loader>();
const rootData = useRouteLoaderData("root") as { userRole: string };
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [expandedGroups, setExpandedGroups] = useState<string[]>([]); const [expandedGroups, setExpandedGroups] = useState<string[]>([]);
@@ -43,6 +44,7 @@ export default function RuleGroupsIndex() {
const [loading, setLoading] = useState<Record<string, boolean>>({}); const [loading, setLoading] = useState<Record<string, boolean>>({});
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({}); const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
const [initialLoading, setInitialLoading] = useState<boolean>(true); const [initialLoading, setInitialLoading] = useState<boolean>(true);
const userRole = rootData?.userRole || 'common';
// 初始加载时自动加载所有子分组 // 初始加载时自动加载所有子分组
useEffect(() => { useEffect(() => {
@@ -524,15 +526,17 @@ export default function RuleGroupsIndex() {
onClick={() => navigate(`/rule-groups/new?id=${record.id}`)} onClick={() => navigate(`/rule-groups/new?id=${record.id}`)}
className="operation-btn" className="operation-btn"
> >
<i className="ri-edit-line"></i> <i className="ri-edit-line"></i> {userRole === 'common' ? '查看' : '编辑'}
</button>
<button
type="button"
className="operation-btn !text-[--color-error]"
onClick={() => handleDeleteGroup(record.id)}
>
<i className="ri-delete-bin-line"></i>
</button> </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> </div>
) )
} }
@@ -566,13 +570,15 @@ export default function RuleGroupsIndex() {
> >
</Button> </Button>
<Button {userRole !== 'common' && (
type="primary" <Button
icon="ri-add-line" type="primary"
onClick={() => navigate("/rule-groups/new")} icon="ri-add-line"
> onClick={() => navigate("/rule-groups/new")}
>
</Button>
</Button>
)}
</div> </div>
</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 { 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 { useEffect, useState, useRef } from "react";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
@@ -232,6 +232,12 @@ export default function RuleGroupNew() {
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
const navigation = useNavigation(); const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting"; 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; const { group, parentGroups, isEdit, error } = data;
@@ -369,6 +375,12 @@ export default function RuleGroupNew() {
// 处理表单提交前验证 // 处理表单提交前验证
const handleBeforeSubmit = (e: React.FormEvent) => { const handleBeforeSubmit = (e: React.FormEvent) => {
// 如果是只读模式,阻止提交
if (isReadOnly) {
e.preventDefault();
return;
}
// 标记所有字段为已触摸 // 标记所有字段为已触摸
setTouchedFields({ setTouchedFields({
name: true, name: true,
@@ -409,7 +421,7 @@ export default function RuleGroupNew() {
{/* 页面头部 */} {/* 页面头部 */}
<div className="page-header"> <div className="page-header">
<div> <div>
<h1 className="page-title">{isEdit ? "编辑评查点分组" : "新增评查点分组"}</h1> <h1 className="page-title">{isEdit ? (isReadOnly ? "查看评查点分组" : "编辑评查点分组") : "新增评查点分组"}</h1>
<p className="page-subtitle"></p> <p className="page-subtitle"></p>
</div> </div>
<div className="header-actions"> <div className="header-actions">
@@ -420,13 +432,15 @@ export default function RuleGroupNew() {
> >
<i className="ri-arrow-left-line"></i> <i className="ri-arrow-left-line"></i>
</Button> </Button>
<Button {!isReadOnly && (
type="primary" <Button
form="group-form" type="primary"
disabled={isSubmitting} form="group-form"
> disabled={isSubmitting}
<i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'} >
</Button> <i className="ri-save-line"></i> {isSubmitting ? '保存中...' : '保存分组'}
</Button>
)}
</div> </div>
</div> </div>
@@ -472,6 +486,7 @@ export default function RuleGroupNew() {
value="primary" value="primary"
checked={formValues.groupType === "primary"} checked={formValues.groupType === "primary"}
onChange={handleGroupTypeChange} onChange={handleGroupTypeChange}
disabled={isReadOnly}
/> />
<span></span> <span></span>
</label> </label>
@@ -484,6 +499,7 @@ export default function RuleGroupNew() {
value="secondary" value="secondary"
checked={formValues.groupType === "secondary"} checked={formValues.groupType === "secondary"}
onChange={handleGroupTypeChange} onChange={handleGroupTypeChange}
disabled={isReadOnly}
/> />
<span></span> <span></span>
</label> </label>
@@ -503,6 +519,7 @@ export default function RuleGroupNew() {
className={`form-select ${touchedFields.parentId && formErrors.parentId ? 'error' : ''}`} className={`form-select ${touchedFields.parentId && formErrors.parentId ? 'error' : ''}`}
value={formValues.parentId} value={formValues.parentId}
onChange={handleChange} onChange={handleChange}
disabled={isReadOnly}
> >
<option value=""></option> <option value=""></option>
{parentGroups {parentGroups
@@ -535,6 +552,7 @@ export default function RuleGroupNew() {
value={formValues.code} value={formValues.code}
onChange={handleChange} onChange={handleChange}
placeholder="请输入分组编码,如contract-base" placeholder="请输入分组编码,如contract-base"
readOnly={isReadOnly}
/> />
{touchedFields.code && formErrors.code && ( {touchedFields.code && formErrors.code && (
<div className="form-error">{formErrors.code}</div> <div className="form-error">{formErrors.code}</div>
@@ -555,6 +573,7 @@ export default function RuleGroupNew() {
value={formValues.name} value={formValues.name}
onChange={handleChange} onChange={handleChange}
placeholder="请输入分组名称,如合同基本要素检查" placeholder="请输入分组名称,如合同基本要素检查"
readOnly={isReadOnly}
/> />
{touchedFields.name && formErrors.name && ( {touchedFields.name && formErrors.name && (
<div className="form-error">{formErrors.name}</div> <div className="form-error">{formErrors.name}</div>
@@ -583,6 +602,7 @@ export default function RuleGroupNew() {
value={formValues.description} value={formValues.description}
onChange={handleChange} onChange={handleChange}
placeholder="请输入分组描述,包括适用场景、分组目的等" placeholder="请输入分组描述,包括适用场景、分组目的等"
readOnly={isReadOnly}
></textarea> ></textarea>
<div className="form-tip"></div> <div className="form-tip"></div>
</div> </div>
@@ -596,6 +616,7 @@ export default function RuleGroupNew() {
className="form-select" className="form-select"
value={formValues.status} value={formValues.status}
onChange={handleChange} onChange={handleChange}
disabled={isReadOnly}
> >
<option value="active"></option> <option value="active"></option>
<option value="inactive"></option> <option value="inactive"></option>
+120 -57
View File
@@ -1,6 +1,6 @@
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react"; 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 { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card"; import { Card } from "~/components/ui/Card";
import { FileIcon } from "~/components/ui/FileIcon"; import { FileIcon } from "~/components/ui/FileIcon";
@@ -13,7 +13,8 @@ import rulesFilesStyles from "~/styles/pages/rules-files.css?url";
import { import {
getReviewFiles, getReviewFiles,
type ReviewFileUI, type ReviewFileUI,
updateDocumentAuditStatus updateDocumentAuditStatus,
type DocumentSearchParams
} from "~/api/evaluation_points/rules-files"; } from "~/api/evaluation_points/rules-files";
import { getDocumentTypes } from "~/api/document-types/document-types"; import { getDocumentTypes } from "~/api/document-types/document-types";
import { toastService } from "~/components/ui/Toast"; import { toastService } from "~/components/ui/Toast";
@@ -55,14 +56,8 @@ export const REVIEW_STATUS_LABELS: Record<string, string> = {
// 加载评查文件列表 // 加载评查文件列表
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
// 获取分页参数
const url = new URL(request.url); 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 currentPage = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 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 typesResponse = await getDocumentTypes({pageSize:500});
const documentTypes = typesResponse.data?.types || []; const documentTypes = typesResponse.data?.types || [];
// 获取文件列表 // 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据
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;
return Response.json({ return Response.json({
files, files: [],
documentTypes, documentTypes,
totalCount, totalCount: 0,
currentPage, currentPage,
pageSize, pageSize,
initialLoad: true
}); });
} catch (error) { } catch (error) {
console.error('加载评查文件列表失败:', error); console.error('加载评查文件列表失败:', error);
@@ -110,10 +83,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
export default function RulesFiles() { export default function RulesFiles() {
const navigate = useNavigate(); 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 [searchParams, setSearchParams] = useSearchParams();
const dateFrom = searchParams.get('dateFrom') || ''; const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || ''; 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的错误 // 处理初始加载数据loader的错误
useEffect(() => { useEffect(() => {
@@ -122,6 +102,87 @@ export default function RulesFiles() {
} }
}, [result, message]); }, [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 handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -512,28 +573,30 @@ export default function RulesFiles() {
</FilterPanel> </FilterPanel>
{/* 文件列表 */} {/* 文件列表 */}
<Card > <Card>
<Table <div className={isLoading ? "opacity-70 pointer-events-none transition-opacity" : ""}>
columns={columns} <Table
dataSource={files} columns={columns}
rowKey="id" dataSource={files}
emptyText="暂无文件数据" rowKey="id"
className="files-table table-auto-height" 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]}
/> />
)}
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</div>
</Card> </Card>
</div> </div>
); );
+1
View File
@@ -713,6 +713,7 @@ export default function RuleNew() {
// 从sessionStorage获取用户角色 // 从sessionStorage获取用户角色
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const userRoleFromSession = sessionStorage.getItem('userRole') as UserRole || 'common'; const userRoleFromSession = sessionStorage.getItem('userRole') as UserRole || 'common';
// console.log("userRoleFromSession-----",userRoleFromSession);
setUserRole(userRoleFromSession); setUserRole(userRoleFromSession);
} }
+8 -13
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; 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 { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card'; import { Card } from '~/components/ui/Card';
import { Tag } from '~/components/ui/Tag'; import { Tag } from '~/components/ui/Tag';
@@ -45,7 +45,6 @@ export type LoaderData = {
pageSize: number; pageSize: number;
totalPages: number; totalPages: number;
ruleTypes: ApiRuleType[]; // 添加评查点类型 ruleTypes: ApiRuleType[]; // 添加评查点类型
userRole: UserRole; // 添加用户角色
}; };
// API返回的数据映射到前端模型 // API返回的数据映射到前端模型
@@ -121,18 +120,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
const totalCount = response.data?.totalCount || 0; const totalCount = response.data?.totalCount || 0;
const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule)); const rules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
// 从sessionStorage获取用户角色
const userRoleFromSession = typeof document !== 'undefined'
? sessionStorage.getItem('userRole') || 'common'
: 'common';
return Response.json({ return Response.json({
rules, rules,
totalCount, totalCount,
currentPage: params.page, currentPage: params.page,
pageSize: params.pageSize, pageSize: params.pageSize,
ruleTypes, ruleTypes
userRole: userRoleFromSession as UserRole
}, { }, {
headers: { headers: {
"Cache-Control": "max-age=60, s-maxage=180" "Cache-Control": "max-age=60, s-maxage=180"
@@ -186,15 +179,13 @@ const priorityLabels = {
export default function RulesIndex() { export default function RulesIndex() {
const loaderData = useLoaderData<typeof loader>(); 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 ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const fetcher = useFetcher<ActionResponse>(); const fetcher = useFetcher<ActionResponse>();
// 检查用户是否为开发者角色
const isDeveloper = userRole === 'developer';
// 状态管理 // 状态管理
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]); const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
const [loadingGroups, setLoadingGroups] = useState(false); const [loadingGroups, setLoadingGroups] = useState(false);
@@ -205,6 +196,10 @@ export default function RulesIndex() {
// 判断是否禁用规则组选择 // 判断是否禁用规则组选择
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0; const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
// 检查用户是否为开发者角色
const userRole = rootData?.userRole || 'common';
const isDeveloper = userRole === 'developer';
// 使用useEffect监听loaderData.error变化并显示Toast // 使用useEffect监听loaderData.error变化并显示Toast
useEffect(() => { useEffect(() => {
if(loaderData.error) { if(loaderData.error) {
+16 -2
View File
@@ -200,15 +200,29 @@
/* === 主内容区域 === */ /* === 主内容区域 === */
.main-content { .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 { .main-content.sidebar-collapsed {
@apply ml-20; @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 { .content-container {
@apply flex-1 p-5 overflow-auto; @apply p-6 bg-gray-50 flex-1 overflow-auto;
} }
/* === 面包屑导航 === */ /* === 面包屑导航 === */