添加jwt验证,添加交叉评查首页加载对接接口,评查任务文档列表对接接口,意见列表对接接口
This commit is contained in:
@@ -65,15 +65,29 @@ export interface ApiResponse<T> {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取JWT token
|
||||
* @param jwtToken JWT token字符串
|
||||
* @returns JWT token字符串
|
||||
*/
|
||||
async function safeGetJWT(jwtToken?: string): Promise<string> {
|
||||
return jwtToken || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交交叉评查意见
|
||||
* @param opinionData 意见数据
|
||||
* @param jwtToken JWT token
|
||||
* @returns 提交结果
|
||||
*/
|
||||
export async function submitCrossCheckingOpinion(
|
||||
opinionData: SubmitOpinionRequest
|
||||
opinionData: SubmitOpinionRequest,
|
||||
jwtToken?: string
|
||||
): Promise<ApiResponse<SubmitOpinionResponse>> {
|
||||
try {
|
||||
// 获取JWT token
|
||||
const token = await safeGetJWT(jwtToken);
|
||||
|
||||
const requestData = {
|
||||
proposer_user_id: 1,
|
||||
evaluation_result_id: opinionData.reviewPointResultId,
|
||||
@@ -87,7 +101,8 @@ export async function submitCrossCheckingOpinion(
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
@@ -119,6 +134,8 @@ export async function submitCrossCheckingOpinion(
|
||||
* @param documentId 文档ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页大小
|
||||
* @param userId 用户ID,可选,便于后端接口对接
|
||||
* @param jwtToken JWT token
|
||||
* @returns 意见列表和总数
|
||||
*/
|
||||
import { API_BASE_URL } from '../../config/api-config';
|
||||
@@ -127,9 +144,13 @@ export async function getCrossCheckingOpinions(
|
||||
documentId: string | number,
|
||||
page: number = 1,
|
||||
pageSize: number = 10,
|
||||
userId?: number // 可选,便于后端接口对接
|
||||
userId?: number, // 可选,便于后端接口对接
|
||||
jwtToken?: string // 改为jwtToken参数
|
||||
): Promise<ApiResponse<{ opinions: CrossCheckingOpinion[], total: number }>> {
|
||||
try {
|
||||
// 获取JWT token
|
||||
const token = await safeGetJWT(jwtToken);
|
||||
|
||||
// 如果没传userId,默认用1
|
||||
const realUserId = userId ?? 1;
|
||||
// 实际后端API调用,拼接API_BASE_URL
|
||||
@@ -137,6 +158,7 @@ export async function getCrossCheckingOpinions(
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: realUserId,
|
||||
@@ -226,6 +248,13 @@ export async function getCrossCheckingOpinions(
|
||||
*/
|
||||
export type OpinionActionType = 'agree' | 'disagree' | 'withdraw_vote' | 'withdraw_opinion';
|
||||
|
||||
/**
|
||||
* 投票请求参数接口
|
||||
*/
|
||||
export interface OpinionVoteCreate {
|
||||
vote_type: 'agree' | 'disagree';
|
||||
}
|
||||
|
||||
/**
|
||||
* 意见操作请求参数
|
||||
*/
|
||||
@@ -237,33 +266,62 @@ export interface OpinionActionRequest {
|
||||
/**
|
||||
* 执行意见操作(赞同、反对、撤销投票、撤销意见)
|
||||
* @param actionData 操作数据
|
||||
* @param jwtToken JWT token
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export async function performOpinionAction(
|
||||
actionData: OpinionActionRequest
|
||||
actionData: OpinionActionRequest,
|
||||
jwtToken?: string
|
||||
): Promise<ApiResponse<{ success: boolean; message: string }>> {
|
||||
try {
|
||||
// 模拟API调用延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const token = await safeGetJWT(jwtToken);
|
||||
|
||||
let message = '';
|
||||
let endpoint = '';
|
||||
let requestBody: Record<string, unknown> = {};
|
||||
|
||||
switch (actionData.action) {
|
||||
case 'agree':
|
||||
message = '已赞同该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'agree' };
|
||||
break;
|
||||
case 'disagree':
|
||||
message = '已反对该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'disagree' };
|
||||
break;
|
||||
case 'withdraw_vote':
|
||||
message = '已撤销投票';
|
||||
// 撤销投票的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes/withdraw`;
|
||||
requestBody = {};
|
||||
break;
|
||||
case 'withdraw_opinion':
|
||||
message = '已撤销意见';
|
||||
// 撤销意见的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/withdraw`;
|
||||
requestBody = {};
|
||||
break;
|
||||
default:
|
||||
throw new Error('无效的操作类型');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || '操作失败');
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { API_BASE_URL } from '../../config/api-config';
|
||||
import { postgrestPut } from '../postgrest-client';
|
||||
|
||||
// 交叉评查任务状态枚举
|
||||
export enum CrossCheckingTaskStatus {
|
||||
@@ -26,13 +27,14 @@ export interface CrossCheckingTask {
|
||||
taskName: string;
|
||||
startDate: string;
|
||||
taskType: CrossCheckingTaskType;
|
||||
docType: CrossCheckingDocType; // 案卷类型
|
||||
docType: string; // 改为直接使用返回的 doc_type 字符串
|
||||
evaluationRegion: string;
|
||||
progress: number;
|
||||
status: CrossCheckingTaskStatus;
|
||||
status: string; // 改为直接使用返回的 task_status 字符串
|
||||
score: number;
|
||||
operation: string;
|
||||
documentIds: number[];
|
||||
documents: UserTaskDocument[]; // 改为 documents 数组,包含完整的文档信息
|
||||
totalDocuments?: number; // 新增:任务包含的文档总数
|
||||
}
|
||||
|
||||
// 用户任务文档接口类型定义
|
||||
@@ -43,22 +45,57 @@ export interface UserTaskDocument {
|
||||
document_type_name: string;
|
||||
}
|
||||
|
||||
// 用户任务信息接口
|
||||
// 新的用户任务信息接口(根据新的API格式)
|
||||
export interface UserTaskInfo {
|
||||
task_id: number;
|
||||
task_name?: string;
|
||||
task_status: string;
|
||||
documents: UserTaskDocument[];
|
||||
doc_type?: string;
|
||||
task_created_at?: string;
|
||||
progress?: number;
|
||||
total_documents?: number; // 新增:任务包含的文档总数
|
||||
}
|
||||
|
||||
// 用户任务API响应格式
|
||||
// 用户任务API响应格式(新格式)
|
||||
export interface UserTaskApiResponse {
|
||||
data: UserTaskInfo[];
|
||||
pagination: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
total_pages: number;
|
||||
};
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: UserTaskInfo[];
|
||||
}
|
||||
|
||||
// 任务文档接口类型定义(新增)
|
||||
export interface TaskDocument {
|
||||
document_id: number;
|
||||
file_name: string;
|
||||
status: string;
|
||||
path: string;
|
||||
file_code: string;
|
||||
file_type_name: string;
|
||||
file_type_id: number;
|
||||
file_size: number;
|
||||
upload_time: string;
|
||||
created_at: string;
|
||||
evaluations_status: number;
|
||||
audit_status: number;
|
||||
created_by_user_id: number;
|
||||
issues: Array<{
|
||||
severity: string;
|
||||
message: string;
|
||||
}>;
|
||||
final_score: number;
|
||||
pass_count: number;
|
||||
warning_count: number;
|
||||
fail_count: number;
|
||||
manual_count: number;
|
||||
}
|
||||
|
||||
// 任务文档API响应格式(新增)
|
||||
export interface TaskDocumentApiResponse {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: TaskDocument[];
|
||||
}
|
||||
|
||||
// API响应格式
|
||||
@@ -91,95 +128,23 @@ export interface TaskListResponse {
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟数据 - 临时使用
|
||||
*/
|
||||
const mockTasks: CrossCheckingTask[] = [
|
||||
{
|
||||
id: 1,
|
||||
sequence: 1,
|
||||
taskName: '2024年度交叉评查',
|
||||
startDate: '2024-12-23',
|
||||
taskType: CrossCheckingTaskType.CITY,
|
||||
docType: CrossCheckingDocType.PENALTY,
|
||||
evaluationRegion: '梅州市、揭阳市、潮州市、云浮市',
|
||||
progress: 0,
|
||||
status: CrossCheckingTaskStatus.PENDING,
|
||||
score: 0,
|
||||
operation: '去评查',
|
||||
documentIds: [1, 2, 3, 4, 5]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sequence: 2,
|
||||
taskName: '2024年第4季度交叉评查',
|
||||
startDate: '2024-12-05',
|
||||
taskType: CrossCheckingTaskType.COUNTY,
|
||||
docType: CrossCheckingDocType.PERMIT,
|
||||
evaluationRegion: '梅江区、梅县区、平远县、蕉岭县、大埔县、丰顺县、五华县',
|
||||
progress: 72,
|
||||
status: CrossCheckingTaskStatus.IN_PROGRESS,
|
||||
score: 0,
|
||||
operation: '进行中',
|
||||
documentIds: [1, 2, 3, 4, 5]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sequence: 3,
|
||||
taskName: '2024年第3季度交叉评查',
|
||||
startDate: '2024-9-23',
|
||||
taskType: CrossCheckingTaskType.COUNTY,
|
||||
docType: CrossCheckingDocType.PERMIT,
|
||||
evaluationRegion: '梅江区、梅县区、平远县、蕉岭县、大埔县、丰顺县、五华县',
|
||||
progress: 100,
|
||||
status: CrossCheckingTaskStatus.COMPLETED,
|
||||
score: 95,
|
||||
operation: '查看结果',
|
||||
documentIds: [1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, 1370,1371,1372,1373,1374]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
sequence: 4,
|
||||
taskName: '2024年中交叉评查',
|
||||
startDate: '2024-6-23',
|
||||
taskType: CrossCheckingTaskType.CITY,
|
||||
docType: CrossCheckingDocType.PENALTY,
|
||||
evaluationRegion: '梅州市、揭阳市、潮州市、云浮市',
|
||||
progress: 100,
|
||||
status: CrossCheckingTaskStatus.COMPLETED,
|
||||
score: 85,
|
||||
operation: '查看结果',
|
||||
documentIds: [1, 2, 3, 4, 5]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
sequence: 5,
|
||||
taskName: '2024年第2季度交叉评查',
|
||||
startDate: '2024-3-23',
|
||||
taskType: CrossCheckingTaskType.COUNTY,
|
||||
docType: CrossCheckingDocType.PENALTY,
|
||||
evaluationRegion: '梅江区、梅县区、平远县、蕉岭县、大埔县、丰顺县、五华县',
|
||||
progress: 100,
|
||||
status: CrossCheckingTaskStatus.COMPLETED,
|
||||
score: 92,
|
||||
operation: '查看结果',
|
||||
documentIds: [1, 2, 3, 4, 5]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 获取交叉评查任务列表
|
||||
* @param params 查询参数
|
||||
* @param userInfo 用户信息
|
||||
* @param jwtToken JWT token
|
||||
* @returns 任务列表响应
|
||||
*/
|
||||
export async function getCrossCheckingTasks(params: TaskListParams = {}): Promise<ApiResponse<TaskListResponse>> {
|
||||
export async function getCrossCheckingTasks(params: TaskListParams = {}, userInfo?: { user_id?: number; [key: string]: unknown }, jwtToken?: string): Promise<ApiResponse<TaskListResponse>> {
|
||||
try {
|
||||
console.log('开始调用getCrossCheckingTasks,参数:', params);
|
||||
|
||||
// 调用用户任务API,获取当前用户参与的任务
|
||||
const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1
|
||||
const userTasksResponse = await getUserTaskDocuments(params.page || 1, params.pageSize || 10, jwtToken);
|
||||
|
||||
console.log('getUserTaskDocuments响应:', userTasksResponse);
|
||||
// console.log('getUserTaskDocuments响应:', JSON.stringify(userTasksResponse,null,2));
|
||||
|
||||
if (!userTasksResponse.success || !userTasksResponse.data) {
|
||||
console.error('获取用户任务失败:', userTasksResponse.error);
|
||||
@@ -190,33 +155,29 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}): Promis
|
||||
}
|
||||
|
||||
// 将用户任务数据转换为CrossCheckingTask格式
|
||||
const userTasks = userTasksResponse.data;
|
||||
const userTasks = userTasksResponse.data.items;
|
||||
const convertedTasks: CrossCheckingTask[] = userTasks.map((userTask: UserTaskInfo, index: number) => {
|
||||
// 从用户任务中提取任务信息,如果没有对应信息则使用默认值
|
||||
// 从用户任务中提取任务信息,使用API返回的实际数据
|
||||
const task: CrossCheckingTask = {
|
||||
id: userTask.task_id,
|
||||
sequence: index + 1,
|
||||
taskName: `任务 ${userTask.task_id}`, // 用户任务API中没有任务名称,使用默认值
|
||||
startDate: new Date().toISOString().split('T')[0], // 使用当前日期作为默认值
|
||||
taskType: CrossCheckingTaskType.CITY, // 默认任务类型
|
||||
docType: CrossCheckingDocType.PENALTY, // 默认案卷类型
|
||||
evaluationRegion: '待定', // 默认评查地区
|
||||
progress: userTask.task_status === 'completed' ? 100 :
|
||||
userTask.task_status === 'in_progress' ? 50 : 0,
|
||||
status: userTask.task_status === 'completed' ? CrossCheckingTaskStatus.COMPLETED :
|
||||
userTask.task_status === 'in_progress' ? CrossCheckingTaskStatus.IN_PROGRESS :
|
||||
CrossCheckingTaskStatus.PENDING,
|
||||
taskName: userTask.task_name || `任务 ${userTask.task_id}`, // 使用API返回的任务名称
|
||||
startDate: userTask.task_created_at ? new Date(userTask.task_created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||
taskType: CrossCheckingTaskType.CITY, // 保持默认任务类型
|
||||
docType: userTask.doc_type || '未知类型', // 使用API返回的文档类型
|
||||
evaluationRegion: '待定', // 保持默认评查地区
|
||||
progress: userTask.progress || 0, // 使用API返回的进度
|
||||
status: userTask.task_status || 'pending', // 使用API返回的任务状态
|
||||
score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数
|
||||
operation: userTask.task_status === 'completed' ? '查看结果' :
|
||||
userTask.task_status === 'in_progress' ? '进行中' : '去评查',
|
||||
documentIds: userTask.documents.map((doc: UserTaskDocument) => doc.document_id)
|
||||
documents: [], // 暂时为空数组,因为新API格式中任务列表不包含具体文档信息
|
||||
totalDocuments: userTask.total_documents || 0 // 使用API返回的文档总数
|
||||
};
|
||||
return task;
|
||||
});
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
taskType,
|
||||
docType,
|
||||
status,
|
||||
@@ -262,21 +223,14 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}): Promis
|
||||
});
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const totalCount = filteredTasks.length;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedTasks = filteredTasks.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: paginatedTasks,
|
||||
totalCount,
|
||||
currentPage: page,
|
||||
pageSize,
|
||||
totalPages
|
||||
tasks: filteredTasks,
|
||||
totalCount: userTasksResponse.data.total,
|
||||
currentPage: userTasksResponse.data.page,
|
||||
pageSize: userTasksResponse.data.page_size,
|
||||
totalPages: Math.ceil(userTasksResponse.data.total / userTasksResponse.data.page_size)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -288,41 +242,6 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}): Promis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的交叉评查任务
|
||||
* @param taskData 任务数据
|
||||
* @returns 创建结果
|
||||
*/
|
||||
export async function createCrossCheckingTask(taskData: Omit<CrossCheckingTask, 'id' | 'sequence' | 'progress' | 'score'>): Promise<ApiResponse<CrossCheckingTask>> {
|
||||
try {
|
||||
// 模拟API延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const newTask: CrossCheckingTask = {
|
||||
...taskData,
|
||||
id: Math.max(...mockTasks.map(t => t.id)) + 1,
|
||||
sequence: mockTasks.length + 1,
|
||||
progress: 0,
|
||||
score: 0
|
||||
};
|
||||
|
||||
// 添加到模拟数据
|
||||
mockTasks.unshift(newTask);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newTask,
|
||||
message: '创建任务成功'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建交叉评查任务失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '创建任务失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除交叉评查任务
|
||||
* @param taskId 任务ID
|
||||
@@ -333,16 +252,9 @@ export async function deleteCrossCheckingTask(taskId: number): Promise<ApiRespon
|
||||
// 模拟API延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const taskIndex = mockTasks.findIndex(task => task.id === taskId);
|
||||
if (taskIndex === -1) {
|
||||
return {
|
||||
success: false,
|
||||
error: '任务不存在'
|
||||
};
|
||||
}
|
||||
|
||||
// 从模拟数据中删除
|
||||
mockTasks.splice(taskIndex, 1);
|
||||
// 这里应该调用实际的API来删除任务
|
||||
// 目前暂时返回成功,因为没有实际的删除API
|
||||
console.log(`尝试删除任务ID: ${taskId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -361,96 +273,51 @@ export async function deleteCrossCheckingTask(taskId: number): Promise<ApiRespon
|
||||
/**
|
||||
* 获取任务详情及相关文档
|
||||
* @param taskId 任务ID
|
||||
* @param documentIds 指定的文档ID数组,用于筛选任务包含的文档
|
||||
* @param page 页码,默认为1
|
||||
* @param pageSize 每页大小,默认为10
|
||||
* @param jwtToken JWT token
|
||||
* @returns 任务详情和文档列表
|
||||
*/
|
||||
export async function getCrossCheckingTaskDetail(
|
||||
taskId: number,
|
||||
documentIds: number[],
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
pageSize: number = 10,
|
||||
jwtToken?: string
|
||||
): Promise<ApiResponse<{
|
||||
task: CrossCheckingTask;
|
||||
files: import('../evaluation_points/rules-files').ReviewFileUI[];
|
||||
task: CrossCheckingTask | null;
|
||||
files: TaskDocument[];
|
||||
total: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
}>> {
|
||||
try {
|
||||
// 从用户任务API中获取任务信息
|
||||
const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1
|
||||
console.log('开始调用getCrossCheckingTaskDetail,参数:', { taskId, page, pageSize });
|
||||
|
||||
if (!userTasksResponse.success || !userTasksResponse.data) {
|
||||
console.error('获取用户任务失败:', userTasksResponse.error);
|
||||
// 获取任务的文档列表
|
||||
const taskDocumentsResponse = await getTaskDocuments(taskId, page, pageSize, jwtToken);
|
||||
|
||||
if (!taskDocumentsResponse.success || !taskDocumentsResponse.data) {
|
||||
console.error('获取任务文档失败:', taskDocumentsResponse.error);
|
||||
return {
|
||||
success: false,
|
||||
error: userTasksResponse.error || '获取用户任务失败'
|
||||
error: taskDocumentsResponse.error || '获取任务文档失败'
|
||||
};
|
||||
}
|
||||
|
||||
// 查找指定的任务
|
||||
const userTask = userTasksResponse.data.find(t => t.task_id === taskId);
|
||||
if (!userTask) {
|
||||
return {
|
||||
success: false,
|
||||
error: '任务不存在'
|
||||
};
|
||||
}
|
||||
const documentsData = taskDocumentsResponse.data;
|
||||
|
||||
// 将用户任务转换为CrossCheckingTask格式
|
||||
const task: CrossCheckingTask = {
|
||||
id: userTask.task_id,
|
||||
sequence: 1, // 暂时使用默认值
|
||||
taskName: `任务 ${userTask.task_id}`, // 用户任务API中没有任务名称,使用默认值
|
||||
startDate: new Date().toISOString().split('T')[0], // 使用当前日期作为默认值
|
||||
taskType: CrossCheckingTaskType.CITY, // 默认任务类型
|
||||
docType: CrossCheckingDocType.PENALTY, // 默认案卷类型
|
||||
evaluationRegion: '待定', // 默认评查地区
|
||||
progress: userTask.task_status === 'completed' ? 100 :
|
||||
userTask.task_status === 'in_progress' ? 50 : 0,
|
||||
status: userTask.task_status === 'completed' ? CrossCheckingTaskStatus.COMPLETED :
|
||||
userTask.task_status === 'in_progress' ? CrossCheckingTaskStatus.IN_PROGRESS :
|
||||
CrossCheckingTaskStatus.PENDING,
|
||||
score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数
|
||||
operation: userTask.task_status === 'completed' ? '查看结果' :
|
||||
userTask.task_status === 'in_progress' ? '进行中' : '去评查',
|
||||
documentIds: userTask.documents.map(doc => doc.document_id)
|
||||
};
|
||||
|
||||
let files: import('../evaluation_points/rules-files').ReviewFileUI[] = [];
|
||||
let total = 0;
|
||||
|
||||
// 如果提供了documentIds,则调用getReviewFiles获取相关文档
|
||||
if (documentIds && documentIds.length > 0) {
|
||||
const { getReviewFiles } = await import('../evaluation_points/rules-files');
|
||||
|
||||
const reviewFilesResponse = await getReviewFiles({
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
sortOrder: 'upload_time_desc'
|
||||
}, documentIds);
|
||||
|
||||
if (reviewFilesResponse.error) {
|
||||
return {
|
||||
success: false,
|
||||
error: reviewFilesResponse.error
|
||||
};
|
||||
}
|
||||
|
||||
files = reviewFilesResponse.data?.files || [];
|
||||
total = reviewFilesResponse.data?.total || 0;
|
||||
}
|
||||
|
||||
console.log('files', files);
|
||||
|
||||
return {
|
||||
const result = {
|
||||
success: true,
|
||||
data: {
|
||||
task,
|
||||
files,
|
||||
total
|
||||
task: null, // 暂时不返回任务详情,因为新接口主要关注文档列表
|
||||
files: documentsData.items,
|
||||
total: documentsData.total,
|
||||
currentPage: documentsData.page,
|
||||
pageSize: documentsData.page_size
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return {
|
||||
@@ -462,9 +329,11 @@ export async function getCrossCheckingTaskDetail(
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param userInfo 用户信息
|
||||
* @param jwtToken JWT token
|
||||
* @returns 统计数据
|
||||
*/
|
||||
export async function getCrossCheckingStats(): Promise<ApiResponse<{
|
||||
export async function getCrossCheckingStats(userInfo?: { user_id?: number; [key: string]: unknown }, jwtToken?: string): Promise<ApiResponse<{
|
||||
totalTasks: number;
|
||||
pendingTasks: number;
|
||||
inProgressTasks: number;
|
||||
@@ -473,8 +342,8 @@ export async function getCrossCheckingStats(): Promise<ApiResponse<{
|
||||
try {
|
||||
console.log('开始调用getCrossCheckingStats');
|
||||
|
||||
// 获取用户任务数据来计算统计
|
||||
const userTasksResponse = await getUserTaskDocuments(1); // 暂时使用固定用户ID 1
|
||||
// 获取用户任务数据来计算统计(默认获取第一页数据进行统计)
|
||||
const userTasksResponse = await getUserTaskDocuments(1, 100, jwtToken); // 获取前100个任务用于统计
|
||||
|
||||
if (!userTasksResponse.success || !userTasksResponse.data) {
|
||||
console.error('获取用户任务失败:', userTasksResponse.error);
|
||||
@@ -484,8 +353,8 @@ export async function getCrossCheckingStats(): Promise<ApiResponse<{
|
||||
};
|
||||
}
|
||||
|
||||
const userTasks = userTasksResponse.data;
|
||||
const totalTasks = userTasks.length;
|
||||
const userTasks = userTasksResponse.data.items;
|
||||
const totalTasks = userTasksResponse.data.total;
|
||||
const pendingTasks = userTasks.filter(t => t.task_status === 'pending').length;
|
||||
const inProgressTasks = userTasks.filter(t => t.task_status === 'in_progress').length;
|
||||
const completedTasks = userTasks.filter(t => t.task_status === 'completed').length;
|
||||
@@ -508,47 +377,137 @@ export async function getCrossCheckingStats(): Promise<ApiResponse<{
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 新增:用户任务文档相关接口 ====================
|
||||
// ==================== 更新:用户任务文档相关接口 ====================
|
||||
|
||||
/**
|
||||
* 获取用户参与的所有任务及文档
|
||||
* @param userId 用户ID
|
||||
* @returns 用户任务及文档列表
|
||||
* 获取用户参与的所有任务列表(更新为新的API格式)
|
||||
* @param page 页码
|
||||
* @param pageSize 每页大小
|
||||
* @param jwtToken JWT token
|
||||
* @returns 用户任务列表
|
||||
*/
|
||||
export async function getUserTaskDocuments(userId: number): Promise<ApiResponse<UserTaskInfo[]>> {
|
||||
export async function getUserTaskDocuments(page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<UserTaskApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const url = `${base}/admin/cross_review/tasks/user_documents?user_id=${userId}`;
|
||||
console.log('最终请求URL:', url);
|
||||
const url = `${base}/admin/cross_review/tasks/user_tasks`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
let userTasks: UserTaskInfo[] = [];
|
||||
if (Array.isArray(result.data)) {
|
||||
userTasks = result.data;
|
||||
} else if (Array.isArray(result)) {
|
||||
userTasks = result;
|
||||
} else {
|
||||
userTasks = [];
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: userTasks
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取用户任务及文档失败'
|
||||
error: error instanceof Error ? error.message : '获取用户任务列表失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定任务的文档列表(新增接口)
|
||||
* @param taskId 任务ID
|
||||
* @param page 页码
|
||||
* @param pageSize 每页大小
|
||||
* @param jwtToken JWT token
|
||||
* @returns 任务文档列表
|
||||
*/
|
||||
export async function getTaskDocuments(taskId: number, page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<TaskDocumentApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const url = `${base}/admin/cross_review/tasks/${taskId}/documents`;
|
||||
// console.log('最终请求URL:', url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取任务文档列表失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新文件的审核状态
|
||||
* @param id 文件ID
|
||||
* @param auditStatus 审核状态
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function updateDocumentAuditStatus(id: string, auditStatus: number): Promise<{
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!id) {
|
||||
return { error: '文件ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
const response = await postgrestPut<TaskDocument, Partial<TaskDocument>>(
|
||||
'documents',
|
||||
{ audit_status: auditStatus },
|
||||
{
|
||||
id: parseInt(id)
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '更新文件审核状态失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { postgrestGet, type PostgrestParams, postgrestPut, postgrestPost } from "../postgrest-client";
|
||||
import {getDocument} from "~/api/files/documents";
|
||||
import {getDocumentWithNoUserId} from "~/api/files/documents";
|
||||
import dayjs from "dayjs";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
// import { formatDate } from "~/utils";
|
||||
|
||||
/**
|
||||
@@ -125,11 +126,22 @@ interface ScoringProposal {
|
||||
/**
|
||||
* 获取当前评查文件的所有评查点结果
|
||||
* @param fileId 评查文件ID
|
||||
* @param request Remix请求对象,用于获取用户会话
|
||||
* @returns 评查点结果列表和统计数据
|
||||
*/
|
||||
export async function getReviewPoints(fileId: string, userId: string) {
|
||||
export async function getReviewPoints(fileId: string, request: Request) {
|
||||
// 获取用户会话信息
|
||||
const { userInfo } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
console.error("用户身份验证失败");
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
// const userId = userInfo.user_id.toString();
|
||||
|
||||
// 首先先获取这个文档的数据
|
||||
const documentData = await getDocument(fileId);
|
||||
const documentData = await getDocumentWithNoUserId(fileId);
|
||||
if (documentData.error) {
|
||||
console.error("获取文档数据错误:", documentData.error);
|
||||
return Response.json({ error: documentData.error }, { status: documentData.status || 500 });
|
||||
@@ -722,14 +734,31 @@ export async function getReviewPoints(fileId: string, userId: string) {
|
||||
* @param editAuditStatusId 审核状态ID
|
||||
* @param result 评查结果 (true/false)
|
||||
* @param message 评查意见
|
||||
* @param request Remix请求对象,用于获取用户会话
|
||||
* @returns 更新后的评查结果
|
||||
*/
|
||||
export async function updateReviewResult(resultId: string, editAuditStatusId: string | number, result: string, message: string): Promise<{
|
||||
export async function updateReviewResult(
|
||||
resultId: string,
|
||||
editAuditStatusId: string | number,
|
||||
result: string,
|
||||
message: string,
|
||||
request: Request
|
||||
): Promise<{
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { userInfo } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
console.error("用户身份验证失败");
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
const userId = userInfo.user_id;
|
||||
|
||||
if (!resultId) {
|
||||
return { error: '评查结果ID不能为空', status: 400 };
|
||||
}
|
||||
@@ -794,7 +823,10 @@ export async function updateReviewResult(resultId: string, editAuditStatusId: st
|
||||
// 重新审核时不更新message
|
||||
...(isReview ? {} : { message })
|
||||
},
|
||||
{ id: editAuditStatusId }
|
||||
{
|
||||
id: editAuditStatusId,
|
||||
user_id: userId // 添加用户ID条件,确保只能更新自己的记录
|
||||
}
|
||||
);
|
||||
|
||||
if (auditStatusResponse.error) {
|
||||
@@ -812,7 +844,8 @@ export async function updateReviewResult(resultId: string, editAuditStatusId: st
|
||||
evaluation_point_id: evaluationPointId,
|
||||
evaluation_result_id: resultId,
|
||||
edit_audit_status: editAuditStatusValue,
|
||||
message: isReview ? '' : message
|
||||
message: isReview ? '' : message,
|
||||
user_id: userId // 添加用户ID
|
||||
};
|
||||
|
||||
// 使用postgrestPost创建新记录
|
||||
@@ -842,14 +875,25 @@ export async function updateReviewResult(resultId: string, editAuditStatusId: st
|
||||
/**
|
||||
* 确认评查结果并更新文档审核状态 只更新文档的审核状态为通过
|
||||
* @param documentId 文档ID
|
||||
* @param request Remix请求对象,用于获取用户会话
|
||||
* @returns 更新结果
|
||||
*/
|
||||
export async function confirmReviewResults(documentId: string): Promise<{
|
||||
export async function confirmReviewResults(documentId: string, request: Request): Promise<{
|
||||
data?: { auditStatus: number; };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { userInfo } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
console.error("用户身份验证失败");
|
||||
return { error: '用户身份验证失败', status: 401 };
|
||||
}
|
||||
|
||||
const userId = userInfo.user_id;
|
||||
|
||||
if (!documentId) {
|
||||
return { error: '文档ID不能为空', status: 400 };
|
||||
}
|
||||
@@ -881,7 +925,10 @@ export async function confirmReviewResults(documentId: string): Promise<{
|
||||
const response = await postgrestPut<{ id: string }, typeof updateDocumentParams>(
|
||||
'documents',
|
||||
updateDocumentParams,
|
||||
{ id: documentId }
|
||||
{
|
||||
id: documentId,
|
||||
user_id: userId // 添加用户ID条件,确保只能更新自己的文档
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
|
||||
@@ -396,6 +396,56 @@ export async function getDocument(id: string, userId: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取单个文档详情
|
||||
* @param id 文档ID
|
||||
* @returns 文档详情
|
||||
*/
|
||||
export async function getDocumentWithNoUserId(id: string): Promise<{
|
||||
data?: DocumentUI;
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
if (!id) {
|
||||
return { error: '文档ID不能为空', status: 400 };
|
||||
}
|
||||
|
||||
const response = await postgrestGet<Document[]>(
|
||||
'documents',
|
||||
{
|
||||
filter: {
|
||||
'id': `eq.${id}`,
|
||||
},
|
||||
limit: 1
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
const extractedData = extractApiData<Document[]>(response.data);
|
||||
if (!extractedData || extractedData.length === 0) {
|
||||
return { error: '文档不存在', status: 404 };
|
||||
}
|
||||
|
||||
// console.log('extractedData', extractedData);
|
||||
const documentUI = await convertToUIDocument(extractedData[0]);
|
||||
|
||||
return { data: documentUI };
|
||||
} catch (error) {
|
||||
console.error('获取文档详情失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档详情失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件下载链接
|
||||
* @param filePath 文件路径
|
||||
|
||||
@@ -133,6 +133,7 @@ export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
||||
* @param isTestDocument 是否为测试文档
|
||||
* @param documentId 关联的文档ID(用于合同附件上传)
|
||||
* @param isReupload 是否为重新上传
|
||||
* @param jwtToken JWT token
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export async function uploadDocumentToServer(
|
||||
@@ -145,7 +146,8 @@ export async function uploadDocumentToServer(
|
||||
remark?: string | null,
|
||||
isTestDocument: boolean = false,
|
||||
documentId?: number | null,
|
||||
isReupload: boolean = false
|
||||
isReupload: boolean = false,
|
||||
jwtToken?: string
|
||||
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength });
|
||||
@@ -185,7 +187,8 @@ export async function uploadDocumentToServer(
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-File-Name': encodeURIComponent(fileName)
|
||||
'X-File-Name': encodeURIComponent(fileName),
|
||||
'Authorization': `Bearer ${jwtToken || ''}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
@@ -242,11 +245,20 @@ export async function uploadDocumentToServer(
|
||||
|
||||
/**
|
||||
* 获取当天的文档列表
|
||||
* @param userInfo 用户信息(必需)
|
||||
* @param reviewType 审核类型(可选)
|
||||
* @returns 文档列表
|
||||
*/
|
||||
export async function getTodayDocuments(reviewType?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
export async function getTodayDocuments(userInfo?: { user_id?: number; [key: string]: unknown }, reviewType?: string): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// 检查用户信息是否存在
|
||||
if (!userInfo?.user_id) {
|
||||
return {
|
||||
error: '没有找到用户信息,请刷新重试',
|
||||
status: 401
|
||||
};
|
||||
}
|
||||
|
||||
const today = dayjs().startOf('day').format('YYYY-MM-DD');
|
||||
// console.log('查询当天文档,日期范围:', today);
|
||||
|
||||
@@ -276,10 +288,12 @@ export async function getTodayDocuments(reviewType?: string): Promise<{data: Doc
|
||||
order: 'created_at.desc',
|
||||
filter: {
|
||||
'created_at': `gte.${today}`,
|
||||
'type_id': 'eq.1'
|
||||
'type_id': 'eq.1',
|
||||
'user_id': `eq.${userInfo.user_id}`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 查询contract_structure_comparison表中的数据
|
||||
// const comparisonParams: PostgrestParams = {
|
||||
// select: `
|
||||
@@ -391,7 +405,8 @@ export async function getTodayDocuments(reviewType?: string): Promise<{data: Doc
|
||||
`,
|
||||
order: 'created_at.desc',
|
||||
filter: {
|
||||
'created_at': `gte.${today}`
|
||||
'created_at': `gte.${today}`,
|
||||
'user_id': `eq.${userInfo.user_id}`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { tokenManager } from "./token-manager.server";
|
||||
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
|
||||
/**
|
||||
* 用户角色类型定义
|
||||
@@ -132,6 +133,56 @@ export async function getSession(request: Request) {
|
||||
* - isTokenExpired: Token 是否已过期
|
||||
* - refreshedSession: 如果刷新了 Token,返回更新后的会话对象
|
||||
*/
|
||||
/**
|
||||
* 生成前端JWT
|
||||
* @param userInfo 用户信息
|
||||
* @param expiresIn OAuth token过期时间(秒)
|
||||
* @returns JWT字符串
|
||||
*/
|
||||
async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise<string> {
|
||||
const jwtUserInfo: UserInfoForJWT = {
|
||||
sub: userInfo.sub,
|
||||
user_id: savedUserData.id!,
|
||||
username: savedUserData.username,
|
||||
nick_name: savedUserData.nick_name,
|
||||
email: savedUserData.email,
|
||||
phone_number: savedUserData.phone_number,
|
||||
ou_id: savedUserData.ou_id,
|
||||
ou_name: savedUserData.ou_name,
|
||||
is_leader: savedUserData.is_leader,
|
||||
user_role: userRole
|
||||
};
|
||||
|
||||
return JWTUtils.generateJWT(jwtUserInfo, expiresIn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含JWT的用户信息对象
|
||||
* @param userInfo OAuth用户信息
|
||||
* @param savedUserData 数据库中保存的用户数据
|
||||
* @param userRole 用户角色
|
||||
* @param frontendJWT 前端JWT
|
||||
* @returns 完整的用户信息对象
|
||||
*/
|
||||
function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) {
|
||||
return {
|
||||
// 保持与callback.tsx中enhancedUserInfo相同的数据结构
|
||||
sub: userInfo.sub,
|
||||
username: savedUserData.username,
|
||||
nick_name: savedUserData.nick_name,
|
||||
phone_number: savedUserData.phone_number,
|
||||
email: savedUserData.email,
|
||||
ou_id: savedUserData.ou_id,
|
||||
ou_name: savedUserData.ou_name,
|
||||
status: savedUserData.status,
|
||||
is_leader: savedUserData.is_leader,
|
||||
// 增强字段,与OAuth登录保持一致
|
||||
user_id: savedUserData.id,
|
||||
user_role: userRole,
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserSession(request: Request) {
|
||||
const session = await getSession(request);
|
||||
const isAuthenticated = session.get("isAuthenticated") === true;
|
||||
@@ -141,9 +192,11 @@ export async function getUserSession(request: Request) {
|
||||
let tokenIssuedAt = session.get("tokenIssuedAt");
|
||||
let tokenExpiresIn = session.get("tokenExpiresIn");
|
||||
const userInfo = session.get("userInfo");
|
||||
let frontendJWT = session.get("frontendJWT");
|
||||
|
||||
let isTokenExpired = false;
|
||||
let refreshedSession = null;
|
||||
let shouldRegenerateJWT = false;
|
||||
|
||||
// 如果有token信息,检查是否需要刷新
|
||||
if (accessToken && refreshToken && tokenIssuedAt && tokenExpiresIn) {
|
||||
@@ -175,6 +228,9 @@ export async function getUserSession(request: Request) {
|
||||
tokenIssuedAt = newToken.tokenIssuedAt;
|
||||
tokenExpiresIn = newToken.tokenExpiresIn;
|
||||
|
||||
// 标记需要重新生成JWT
|
||||
shouldRegenerateJWT = true;
|
||||
|
||||
refreshedSession = session;
|
||||
}
|
||||
|
||||
@@ -189,6 +245,77 @@ export async function getUserSession(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查前端JWT状态
|
||||
if (isAuthenticated && !isTokenExpired && userInfo) {
|
||||
let needsJWTRefresh = false;
|
||||
|
||||
// 检查是否有前端JWT
|
||||
if (!frontendJWT) {
|
||||
needsJWTRefresh = true;
|
||||
console.log("缺少前端JWT,需要生成");
|
||||
} else {
|
||||
// 检查JWT是否即将过期
|
||||
if (JWTUtils.isJWTExpiringSoon(frontendJWT)) {
|
||||
needsJWTRefresh = true;
|
||||
console.log("前端JWT即将过期,需要重新生成");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果OAuth token被刷新了,也需要重新生成JWT
|
||||
if (shouldRegenerateJWT) {
|
||||
needsJWTRefresh = true;
|
||||
console.log("OAuth token已刷新,需要重新生成JWT");
|
||||
}
|
||||
|
||||
// 重新生成JWT
|
||||
if (needsJWTRefresh && tokenExpiresIn) {
|
||||
try {
|
||||
// 从userInfo中获取用户数据
|
||||
if (userInfo.user_id && userInfo.sub) {
|
||||
const mockSavedUserData: SsoUser = {
|
||||
id: userInfo.user_id,
|
||||
sub: userInfo.sub,
|
||||
username: userInfo.username || userInfo.sub,
|
||||
nick_name: userInfo.nick_name || "未知用户",
|
||||
phone_number: userInfo.phone_number,
|
||||
email: userInfo.email,
|
||||
ou_id: userInfo.ou_id || "default",
|
||||
ou_name: userInfo.ou_name || "未知部门",
|
||||
status: 0,
|
||||
is_leader: userInfo.is_leader || false
|
||||
};
|
||||
|
||||
const newJWT = await generateFrontendJWT(userInfo, mockSavedUserData, userRole, tokenExpiresIn);
|
||||
|
||||
// 打印JWT重新生成信息
|
||||
console.log("=== Token刷新时重新生成JWT ===");
|
||||
console.log("原始userInfo:", userInfo);
|
||||
console.log("重构的用户数据:", mockSavedUserData);
|
||||
console.log("用户角色:", userRole);
|
||||
console.log("新生成的JWT:", newJWT);
|
||||
console.log("JWT过期时间:", JWTUtils.getJWTExpiration(newJWT));
|
||||
|
||||
// 更新session中的JWT
|
||||
if (!refreshedSession) {
|
||||
refreshedSession = session;
|
||||
}
|
||||
refreshedSession.set("frontendJWT", newJWT);
|
||||
|
||||
// 更新userInfo以包含新的JWT
|
||||
const updatedUserInfo = createUserInfoWithJWT(userInfo, mockSavedUserData, userRole, newJWT);
|
||||
refreshedSession.set("userInfo", updatedUserInfo);
|
||||
|
||||
console.log("更新后的userInfo:", updatedUserInfo);
|
||||
console.log("=== JWT重新生成完成 ===");
|
||||
|
||||
frontendJWT = newJWT;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成前端JWT失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated: isAuthenticated && !isTokenExpired,
|
||||
userRole,
|
||||
@@ -196,7 +323,8 @@ export async function getUserSession(request: Request) {
|
||||
refreshToken,
|
||||
userInfo,
|
||||
isTokenExpired,
|
||||
refreshedSession // 如果刷新了token,返回更新后的session
|
||||
refreshedSession, // 如果刷新了token,返回更新后的session
|
||||
frontendJWT // 返回前端JWT
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { FileTypeTag } from '../ui/FileTypeTag';
|
||||
import { StatusBadge } from '../ui/StatusBadge';
|
||||
import { Pagination } from '../ui/Pagination';
|
||||
import { LoadingIndicator } from '../ui/SkeletonScreen';
|
||||
import type { ReviewFileUI } from '~/api/evaluation_points/rules-files';
|
||||
// import { updateDocumentAuditStatus } from '~/api/evaluation_points/rules-files';
|
||||
import { updateDocumentAuditStatus, type TaskDocument } from '~/api/cross-checking/cross-files'; // 更新导入
|
||||
import { toastService } from '../ui/Toast';
|
||||
import { formatDate } from '~/utils';
|
||||
|
||||
// 导出样式链接
|
||||
export const links = () => [];
|
||||
@@ -18,7 +18,7 @@ interface DocumentListModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
files: ReviewFileUI[];
|
||||
files: TaskDocument[]; // 更新类型
|
||||
onViewFile?: (fileId: string) => void;
|
||||
loading?: boolean;
|
||||
// 分页相关属性
|
||||
@@ -49,13 +49,10 @@ export function DocumentListModal({
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (auditStatus === 0 || auditStatus === null) {
|
||||
try {
|
||||
// TODO: 这里需要从父组件传递 userId,或者重新设计这个函数的调用方式
|
||||
// 暂时跳过状态更新,直接进入查看
|
||||
// const response = await updateDocumentAuditStatus(fileId, 2, userId);
|
||||
// if (response.error) {
|
||||
// throw new Error(response.error);
|
||||
// }
|
||||
console.warn('DocumentListModal: 跳过审核状态更新,需要传递 userId 参数');
|
||||
// TODO: 不需要传递userId,直接使用fileId找到对应文档,然后更新文档状态
|
||||
// 更新文档状态
|
||||
const updatedFile = await updateDocumentAuditStatus(fileId, 2);
|
||||
console.log('更新后的文档状态:', updatedFile);
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态时出错:', error);
|
||||
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
@@ -70,60 +67,74 @@ export function DocumentListModal({
|
||||
};
|
||||
|
||||
// 渲染问题摘要
|
||||
const renderIssues = (file: ReviewFileUI) => {
|
||||
// 如果文件状态为完成
|
||||
if (file.status === 'Processed') {
|
||||
// 如果没有问题,显示"所有评查点均通过"
|
||||
if (file.warningCount <= 0 && file.failCount <= 0) {
|
||||
return (
|
||||
<div className="text-sm text-success">
|
||||
<i className="ri-check-double-line mr-1"></i>所有评查点均通过
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const renderIssues = (file: TaskDocument) => {
|
||||
// 如果文件有问题信息
|
||||
if (file.issues && file.issues.length > 0) {
|
||||
// 最多显示2个问题
|
||||
const displayIssues = file.issues.slice(0, 2);
|
||||
|
||||
// 显示问题列表
|
||||
if (file.issues && file.issues.length > 0) {
|
||||
// 最多显示2个问题
|
||||
const displayIssues = file.issues.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{displayIssues.map((issue, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
<i className="ri-circle-fill mr-1 text-yellow-400"></i>
|
||||
{issue.message}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{file.issues.length > 2 && (
|
||||
<div className="text-secondary mt-1">
|
||||
还有 {file.issues.length - 2} 个问题...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{displayIssues.map((issue, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
<i className={`ri-circle-fill mr-1 ${
|
||||
issue.severity === 'error' ? 'text-red-400' :
|
||||
issue.severity === 'warning' ? 'text-yellow-400' :
|
||||
'text-blue-400'
|
||||
}`}></i>
|
||||
{issue.message}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{file.issues.length > 2 && (
|
||||
<div className="text-secondary mt-1">
|
||||
还有 {file.issues.length - 2} 个问题...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有问题信息,根据状态显示
|
||||
if (file.evaluations_status === 1) {
|
||||
return (
|
||||
<div className="text-sm text-success">
|
||||
<i className="ri-check-double-line mr-1"></i>所有评查点均通过
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 其他状态显示占位符
|
||||
return <div className="text-sm text-secondary">-</div>;
|
||||
};
|
||||
|
||||
// 获取文件大小的友好显示
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名称",
|
||||
key: "fileName",
|
||||
width: "30%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
render: (_: unknown, file: TaskDocument) => (
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center self-center">
|
||||
<FileIcon fileName={file.fileName} className="text-lg w-10 h-10" />
|
||||
<FileIcon fileName={file.file_name} className="text-lg w-10 h-10" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 flex flex-col py-2 ml-2">
|
||||
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.fileName}>{file.fileName}</div>
|
||||
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.file_name}>{file.file_name}</div>
|
||||
<div className="text-xs text-secondary mt-2">
|
||||
文件编号:{file.fileCode}
|
||||
文件编号:{file.file_code}
|
||||
</div>
|
||||
<div className="text-xs text-secondary mt-1">
|
||||
大小:{formatFileSize(file.file_size)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,12 +143,12 @@ export function DocumentListModal({
|
||||
{
|
||||
title: "文件类型",
|
||||
key: "fileType",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
width: "10%",
|
||||
render: (_: unknown, file: TaskDocument) => (
|
||||
<FileTypeTag
|
||||
type="other"
|
||||
typeName={file.fileType}
|
||||
text={file.fileType}
|
||||
typeName={file.file_type_name}
|
||||
text={file.file_type_name}
|
||||
size="sm"
|
||||
showIcon={false}
|
||||
colorMode="light"
|
||||
@@ -148,13 +159,15 @@ export function DocumentListModal({
|
||||
title: "上传时间",
|
||||
key: "uploadTime",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) => {
|
||||
const [date, time] = file.uploadTime.split(' ');
|
||||
render: (_: unknown, file: TaskDocument) => {
|
||||
const uploadTime = formatDate(file.upload_time).split(' ');
|
||||
const date = uploadTime[0];
|
||||
const time = uploadTime[1];
|
||||
return (
|
||||
<div>
|
||||
<span className="text-base">{date}</span>
|
||||
<span className="text-base">{date}</span> {/* 2025-07-22 */}
|
||||
<br />
|
||||
<span className="text-xs text-secondary">{time}</span>
|
||||
<span className="text-xs text-secondary">{time}</span> {/* 10:00:00 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -163,34 +176,42 @@ export function DocumentListModal({
|
||||
title: "评查统计",
|
||||
key: "reviewStatus",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) =>
|
||||
render: (_: unknown, file: TaskDocument) =>
|
||||
// 要文件切分处理完之后,再显示评查统计
|
||||
file.status === 'Processed' ? (
|
||||
<div>
|
||||
{file.passCount > 0 && (
|
||||
{file.pass_count > 0 && (
|
||||
<StatusBadge
|
||||
status="pass"
|
||||
text={`通过(${file.passCount})`}
|
||||
text={`通过(${file.pass_count})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{file.warningCount > 0 && (
|
||||
{file.warning_count > 0 && (
|
||||
<StatusBadge
|
||||
status="warning"
|
||||
text={`警告(${file.warningCount})`}
|
||||
text={`警告(${file.warning_count})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{file.failCount > 0 && (
|
||||
{file.fail_count > 0 && (
|
||||
<StatusBadge
|
||||
status="fail"
|
||||
text={`不通过(${file.failCount})`}
|
||||
text={`不通过(${file.fail_count})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{/* {file.manual_count > 0 && (
|
||||
<StatusBadge
|
||||
status="pending"
|
||||
text={`需人工(${file.manual_count})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
) : (
|
||||
@@ -199,23 +220,43 @@ export function DocumentListModal({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "评查分数",
|
||||
key: "score",
|
||||
width: "8%",
|
||||
render: (_: unknown, file: TaskDocument) => (
|
||||
<div className="text-center">
|
||||
{file.final_score ? (
|
||||
<span className={`font-medium ${
|
||||
file.final_score >= 90 ? 'text-green-600' :
|
||||
file.final_score >= 70 ? 'text-yellow-600' :
|
||||
'text-red-600'
|
||||
}`}>
|
||||
{file.final_score}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "问题摘要",
|
||||
key: "issues",
|
||||
width: "20%",
|
||||
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
|
||||
render: (_: unknown, file: TaskDocument) => renderIssues(file)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "14%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
width: "auto",
|
||||
render: (_: unknown, file: TaskDocument) => (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-eye-line"
|
||||
onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
|
||||
onClick={() => handleReviewFileClick(file.document_id.toString(), file.audit_status)}
|
||||
disabled={file.status !== 'Processed'}
|
||||
className="mr-2"
|
||||
>
|
||||
@@ -258,13 +299,13 @@ export function DocumentListModal({
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
rowKey="document_id"
|
||||
emptyText="暂无文件数据"
|
||||
className="files-table table-auto-height"
|
||||
/>
|
||||
|
||||
{/* 分页组件 - 只有在提供了分页回调函数且总数大于每页大小时才显示 */}
|
||||
{(onPageChange || onPageSizeChange) && total > pageSize ? (
|
||||
{onPageChange && total > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
total={total}
|
||||
@@ -275,11 +316,6 @@ export function DocumentListModal({
|
||||
showPageSizeChanger={!!onPageSizeChange}
|
||||
pageSizeOptions={[10, 20, 30, 50]}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 mt-4 text-center">
|
||||
共 {total} 条记录,每页 {pageSize} 条
|
||||
{total <= pageSize && ""}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -26,13 +26,11 @@ import { Pagination } from '../ui/Pagination';
|
||||
import { Button } from '../ui/Button';
|
||||
import { LoadingIndicator } from '../ui/SkeletonScreen';
|
||||
import {
|
||||
submitCrossCheckingOpinion,
|
||||
getCrossCheckingOpinions,
|
||||
performOpinionAction,
|
||||
type SubmitOpinionRequest,
|
||||
type CrossCheckingOpinion,
|
||||
type OpinionActionType
|
||||
} from '../../api/cross-checking/cross-file-result';
|
||||
import { useFetcher } from '@remix-run/react';
|
||||
// import '../../styles/components/TooltipStyles.css';
|
||||
|
||||
/**
|
||||
@@ -168,6 +166,7 @@ interface ReviewPointsListProps {
|
||||
onReviewPointSelect: (id: string, page?: number) => void;
|
||||
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
scoringProposals?: ScoringProposal[];
|
||||
jwtToken?: string; // 添加JWT token参数
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,12 +423,14 @@ export function ReviewPointsList({
|
||||
statistics,
|
||||
activeReviewPointResultId,
|
||||
onReviewPointSelect,
|
||||
scoringProposals = []
|
||||
scoringProposals = [],
|
||||
jwtToken
|
||||
}: ReviewPointsListProps) {
|
||||
// 状态管理
|
||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
|
||||
// 将来可以用于显示相关的评分提案信息
|
||||
@@ -467,6 +468,54 @@ export function ReviewPointsList({
|
||||
const [opinionListPageSize, setOpinionListPageSize] = useState(10);
|
||||
const [performingAction, setPerformingAction] = useState<string | null>(null);
|
||||
|
||||
// 监听fetcher状态变化 - 获取意见列表数据
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) {
|
||||
const data = fetcher.data as {
|
||||
success?: boolean;
|
||||
data?: {
|
||||
opinions: CrossCheckingOpinion[];
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (data.success && data.data) {
|
||||
console.log('意见列表数据', data.data);
|
||||
setOpinionListData(data.data.opinions || []);
|
||||
setOpinionListTotal(data.data.total || 0);
|
||||
// 使用当前状态值而不是依赖项中的值
|
||||
setOpinionListCurrentPage(prev => prev);
|
||||
setOpinionListPageSize(prev => prev);
|
||||
} else {
|
||||
console.error('加载意见列表失败:', data.error);
|
||||
toastService.error(data.error || '加载意见列表失败');
|
||||
}
|
||||
|
||||
setOpinionListLoading(false);
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, opinionListLoading]);
|
||||
|
||||
// 监听fetcher状态变化 - 提交意见
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle' && isSubmittingOpinion) {
|
||||
const data = fetcher.data as {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (data.success) {
|
||||
toastService.success('意见提交成功');
|
||||
handleCloseOpinionModal();
|
||||
} else {
|
||||
console.error('提交意见失败:', data.error);
|
||||
toastService.error(data.error || '提交意见失败');
|
||||
}
|
||||
|
||||
setIsSubmittingOpinion(false);
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, isSubmittingOpinion]);
|
||||
|
||||
// 存放评查点ID与有效页码的映射
|
||||
const [effectivePages, setEffectivePages] = useState<Record<string, number>>({});
|
||||
|
||||
@@ -525,22 +574,19 @@ export function ReviewPointsList({
|
||||
setOpinionListLoading(true);
|
||||
try {
|
||||
console.log('加载意见列表数据', targetDocumentId, page, pageSize);
|
||||
const response = await getCrossCheckingOpinions(targetDocumentId, page, pageSize);
|
||||
|
||||
console.log('意见列表数据', response);
|
||||
if (response.error) {
|
||||
toastService.error(response.error);
|
||||
return;
|
||||
}
|
||||
// 使用 fetcher 调用路由的 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "getCrossCheckingOpinions");
|
||||
formData.append("documentId", targetDocumentId.toString());
|
||||
formData.append("page", page.toString());
|
||||
formData.append("pageSize", pageSize.toString());
|
||||
|
||||
fetcher.submit(formData, { method: "POST" });
|
||||
|
||||
setOpinionListData(response.data?.opinions || []);
|
||||
setOpinionListTotal(response.data?.total || 0);
|
||||
setOpinionListCurrentPage(page);
|
||||
setOpinionListPageSize(pageSize);
|
||||
} catch (error) {
|
||||
console.error('加载意见列表失败:', error);
|
||||
toastService.error('加载意见列表失败');
|
||||
} finally {
|
||||
setOpinionListLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -580,7 +626,7 @@ export function ReviewPointsList({
|
||||
setPerformingAction(actionKey);
|
||||
|
||||
try {
|
||||
const response = await performOpinionAction({ opinionId, action });
|
||||
const response = await performOpinionAction({ opinionId, action }, jwtToken);
|
||||
|
||||
if (response.error) {
|
||||
toastService.error(response.error);
|
||||
@@ -648,28 +694,21 @@ export function ReviewPointsList({
|
||||
setIsSubmittingOpinion(true);
|
||||
|
||||
try {
|
||||
const opinionData: SubmitOpinionRequest = {
|
||||
reviewPointResultId: selectedReviewPoint.id,
|
||||
documentId: selectedReviewPoint.documentId || '',
|
||||
auditPoint: opinionForm.auditPoint,
|
||||
foundIssue: opinionForm.foundIssue,
|
||||
auditOpinion: opinionForm.auditOpinion,
|
||||
deductionScore: opinionForm.deductionScore
|
||||
};
|
||||
// 使用 fetcher 调用路由的 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "submitCrossCheckingOpinion");
|
||||
formData.append("reviewPointResultId", selectedReviewPoint.id);
|
||||
formData.append("documentId", selectedReviewPoint.documentId || '');
|
||||
formData.append("auditPoint", opinionForm.auditPoint);
|
||||
formData.append("foundIssue", opinionForm.foundIssue);
|
||||
formData.append("auditOpinion", opinionForm.auditOpinion);
|
||||
formData.append("deductionScore", opinionForm.deductionScore.toString());
|
||||
|
||||
const response = await submitCrossCheckingOpinion(opinionData);
|
||||
|
||||
if (response.error) {
|
||||
toastService.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.success('意见提交成功');
|
||||
handleCloseOpinionModal();
|
||||
fetcher.submit(formData, { method: "POST" });
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交意见失败:', error);
|
||||
toastService.error('提交意见失败,请稍后重试');
|
||||
} finally {
|
||||
setIsSubmittingOpinion(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 认证相关的React Hook
|
||||
* 提供JWT认证状态管理和用户信息获取功能
|
||||
*/
|
||||
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
|
||||
// 定义从loader返回的认证数据结构
|
||||
interface AuthLoaderData {
|
||||
userInfo?: {
|
||||
sub: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
email?: string;
|
||||
phone_number?: string;
|
||||
ou_id: string;
|
||||
ou_name: string;
|
||||
is_leader: boolean;
|
||||
user_role: string;
|
||||
frontend_jwt?: string;
|
||||
};
|
||||
isAuthenticated?: boolean;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证状态Hook
|
||||
* @returns 认证状态和用户信息
|
||||
*/
|
||||
export function useAuth() {
|
||||
// 从loader data中获取认证信息
|
||||
const loaderData = useLoaderData() as AuthLoaderData;
|
||||
|
||||
const isAuthenticated = loaderData?.isAuthenticated || false;
|
||||
const userInfo = loaderData?.userInfo;
|
||||
const frontendJWT = userInfo?.frontend_jwt;
|
||||
|
||||
/**
|
||||
* 验证JWT是否有效
|
||||
* @returns JWT验证结果
|
||||
*/
|
||||
const validateJWT = () => {
|
||||
if (!frontendJWT) {
|
||||
return { valid: false, error: "JWT不存在" };
|
||||
}
|
||||
|
||||
return JWTUtils.verifyJWT(frontendJWT);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查JWT是否即将过期
|
||||
* @param bufferMinutes 缓冲时间(分钟)
|
||||
* @returns 是否即将过期
|
||||
*/
|
||||
const isJWTExpiringSoon = (bufferMinutes: number = 5) => {
|
||||
if (!frontendJWT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return JWTUtils.isJWTExpiringSoon(frontendJWT, bufferMinutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从JWT中获取用户信息
|
||||
* @returns 用户信息
|
||||
*/
|
||||
const getUserInfoFromJWT = (): UserInfoForJWT | null => {
|
||||
if (!frontendJWT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JWTUtils.extractUserInfo(frontendJWT);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取JWT过期时间
|
||||
* @returns 过期时间戳
|
||||
*/
|
||||
const getJWTExpiration = () => {
|
||||
if (!frontendJWT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JWTUtils.getJWTExpiration(frontendJWT);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否有特定角色
|
||||
* @param role 角色名称
|
||||
* @returns 是否有该角色
|
||||
*/
|
||||
const hasRole = (role: string) => {
|
||||
return userInfo?.user_role === role;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户是否为管理员
|
||||
* @returns 是否为管理员
|
||||
*/
|
||||
const isAdmin = () => {
|
||||
return userInfo?.user_role === 'developer' || userInfo?.is_leader === true;
|
||||
};
|
||||
|
||||
return {
|
||||
// 基本认证状态
|
||||
isAuthenticated,
|
||||
userInfo,
|
||||
frontendJWT,
|
||||
|
||||
// JWT相关方法
|
||||
validateJWT,
|
||||
isJWTExpiringSoon,
|
||||
getUserInfoFromJWT,
|
||||
getJWTExpiration,
|
||||
|
||||
// 权限检查方法
|
||||
hasRole,
|
||||
isAdmin,
|
||||
|
||||
// 便捷属性
|
||||
userId: userInfo?.user_id,
|
||||
username: userInfo?.username,
|
||||
nickName: userInfo?.nick_name,
|
||||
email: userInfo?.email,
|
||||
phoneNumber: userInfo?.phone_number,
|
||||
ouName: userInfo?.ou_name,
|
||||
userRole: userInfo?.user_role,
|
||||
isLeader: userInfo?.is_leader
|
||||
};
|
||||
}
|
||||
|
||||
export default useAuth;
|
||||
+34
-2
@@ -2,6 +2,7 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { sessionStorage, saveUserInfo } from "~/api/login/auth.server";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import { toastService } from "~/components/ui";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
@@ -73,9 +74,40 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (!saveResult.success) {
|
||||
console.error("保存用户信息到数据库失败:", saveResult.error);
|
||||
// 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证
|
||||
} else {
|
||||
console.log("用户信息已成功保存到数据库");
|
||||
return redirect("/login?error=save_user_error");
|
||||
}
|
||||
|
||||
console.log("用户信息已成功保存到数据库");
|
||||
const savedUserData = saveResult.data!;
|
||||
|
||||
// 生成前端专用JWT
|
||||
const jwtUserInfo: UserInfoForJWT = {
|
||||
sub: userInfo.data.sub,
|
||||
user_id: savedUserData.id!,
|
||||
username: savedUserData.username,
|
||||
nick_name: savedUserData.nick_name,
|
||||
email: savedUserData.email,
|
||||
phone_number: savedUserData.phone_number,
|
||||
ou_id: savedUserData.ou_id,
|
||||
ou_name: savedUserData.ou_name,
|
||||
is_leader: savedUserData.is_leader,
|
||||
user_role: userRole
|
||||
};
|
||||
|
||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||
console.log("前端JWT已生成");
|
||||
|
||||
// 将JWT存储在session中
|
||||
session.set("frontendJWT", frontendJWT);
|
||||
|
||||
// 更新userInfo以包含数据库ID和JWT信息
|
||||
const enhancedUserInfo = {
|
||||
...userInfo.data,
|
||||
user_id: savedUserData.id,
|
||||
user_role: userRole,
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
session.set("userInfo", enhancedUserInfo);
|
||||
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useNavigate, useFetcher } from "@remix-run/react";
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { Card } from '~/components/ui/Card';
|
||||
@@ -17,12 +17,12 @@ import {
|
||||
deleteCrossCheckingTask,
|
||||
getCrossCheckingTaskDetail,
|
||||
type CrossCheckingTask,
|
||||
type TaskDocument,
|
||||
type TaskListParams,
|
||||
CrossCheckingTaskStatus,
|
||||
CrossCheckingTaskType,
|
||||
CrossCheckingDocType
|
||||
} from '~/api/cross-checking/cross-files';
|
||||
import type { ReviewFileUI } from '~/api/evaluation_points/rules-files';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: crossCheckingStyles }
|
||||
@@ -68,10 +68,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
};
|
||||
|
||||
try {
|
||||
// 获取任务列表和统计数据
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取任务列表和统计数据,传递用户信息和JWT
|
||||
const [tasksResponse, statsResponse] = await Promise.all([
|
||||
getCrossCheckingTasks(params),
|
||||
getCrossCheckingStats()
|
||||
getCrossCheckingTasks(params, userInfo, frontendJWT),
|
||||
getCrossCheckingStats(userInfo, frontendJWT)
|
||||
]);
|
||||
|
||||
if (!tasksResponse.success) {
|
||||
@@ -108,17 +112,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function action({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const _action = formData.get('_action');
|
||||
const taskId = formData.get('taskId');
|
||||
|
||||
if (!taskId) {
|
||||
return Response.json({ result: false, message: "缺少任务ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (_action === 'delete') {
|
||||
if (_action === 'delete' && taskId) {
|
||||
try {
|
||||
const deleteResponse = await deleteCrossCheckingTask(Number(taskId));
|
||||
|
||||
if (!deleteResponse.success) {
|
||||
@@ -129,13 +129,57 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
return Response.json({ result: true, message: "任务删除成功" }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('操作任务失败:', error);
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: error instanceof Error ? error.message : "操作失败"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
if (_action === 'getTaskDetail') {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const page = parseInt(formData.get('page') as string || '1', 10);
|
||||
const pageSize = parseInt(formData.get('pageSize') as string || '10', 10);
|
||||
|
||||
if (!taskId) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "缺少必要参数"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await getCrossCheckingTaskDetail(
|
||||
Number(taskId),
|
||||
page,
|
||||
pageSize,
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: response.error
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
// console.log('用户任务详情返回:', response.data);
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: response.data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "获取任务详情失败"
|
||||
}, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作任务失败:', error);
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: error instanceof Error ? error.message : "操作失败"
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
return Response.json({ result: false, message: "无效的操作" }, { status: 400 });
|
||||
@@ -174,7 +218,7 @@ export default function CrossCheckingIndex() {
|
||||
const [modalState, setModalState] = useState<{
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
files: ReviewFileUI[];
|
||||
files: TaskDocument[];
|
||||
loading: boolean;
|
||||
// 分页相关状态
|
||||
currentPage: number;
|
||||
@@ -198,9 +242,9 @@ export default function CrossCheckingIndex() {
|
||||
};
|
||||
|
||||
// 处理查看结果 - 打开文档列表模态框
|
||||
const handleViewResult = async (taskId: number, documentIds: number[]) => {
|
||||
const handleViewResult = async (taskId: number) => {
|
||||
// 存储任务信息用于分页
|
||||
setCurrentTaskInfo({ taskId, documentIds });
|
||||
setCurrentTaskInfo({ taskId });
|
||||
|
||||
// 打开模态框
|
||||
setModalState(prev => ({
|
||||
@@ -211,7 +255,7 @@ export default function CrossCheckingIndex() {
|
||||
}));
|
||||
|
||||
// 加载第一页数据
|
||||
await loadModalData(taskId, documentIds, 1, 10);
|
||||
await loadModalData(taskId, 1, 10);
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
@@ -236,35 +280,24 @@ export default function CrossCheckingIndex() {
|
||||
// 存储当前任务信息用于分页
|
||||
const [currentTaskInfo, setCurrentTaskInfo] = useState<{
|
||||
taskId: number;
|
||||
documentIds: number[];
|
||||
} | null>(null);
|
||||
|
||||
// 加载分页数据
|
||||
const loadModalData = async (taskId: number, documentIds: number[], page: number = 1, pageSize: number = 10) => {
|
||||
const loadModalData = async (taskId: number, page: number = 1, pageSize: number = 10) => {
|
||||
try {
|
||||
setModalState(prev => ({
|
||||
...prev,
|
||||
loading: true
|
||||
}));
|
||||
|
||||
// 调用支持分页的API,传递分页参数
|
||||
const response = await getCrossCheckingTaskDetail(taskId, documentIds, page, pageSize);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
// 使用 fetcher 调用 action 来获取任务详情
|
||||
const formData = new FormData();
|
||||
formData.append('_action', 'getTaskDetail');
|
||||
formData.append('taskId', taskId.toString());
|
||||
formData.append('page', page.toString());
|
||||
formData.append('pageSize', pageSize.toString());
|
||||
|
||||
const { task, files, total } = response.data!;
|
||||
|
||||
setModalState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
title: `${task.taskName} - 文档列表`,
|
||||
files: files,
|
||||
total: total,
|
||||
currentPage: page,
|
||||
pageSize: pageSize
|
||||
}));
|
||||
fetcher.submit(formData, { method: "POST" });
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取任务文档列表失败:', error);
|
||||
@@ -280,14 +313,14 @@ export default function CrossCheckingIndex() {
|
||||
// 处理模态框分页变化
|
||||
const handleModalPageChange = (page: number) => {
|
||||
if (currentTaskInfo) {
|
||||
loadModalData(currentTaskInfo.taskId, currentTaskInfo.documentIds, page, modalState.pageSize);
|
||||
loadModalData(currentTaskInfo.taskId, page, modalState.pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理模态框每页大小变化
|
||||
const handleModalPageSizeChange = (size: number) => {
|
||||
if (currentTaskInfo) {
|
||||
loadModalData(currentTaskInfo.taskId, currentTaskInfo.documentIds, 1, size);
|
||||
loadModalData(currentTaskInfo.taskId, 1, size);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -315,7 +348,7 @@ export default function CrossCheckingIndex() {
|
||||
type="primary"
|
||||
size="small"
|
||||
className="operation-btn primary"
|
||||
onClick={() => handleViewResult(task.id, task.documentIds)}
|
||||
onClick={() => handleViewResult(task.id)}
|
||||
>
|
||||
<i className="ri-play-line"></i>
|
||||
去评查
|
||||
@@ -327,7 +360,7 @@ export default function CrossCheckingIndex() {
|
||||
type="default"
|
||||
size="small"
|
||||
className="operation-btn secondary"
|
||||
onClick={() => handleViewResult(task.id, task.documentIds)}
|
||||
onClick={() => handleViewResult(task.id)}
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
进行中
|
||||
@@ -339,7 +372,7 @@ export default function CrossCheckingIndex() {
|
||||
type="default"
|
||||
size="small"
|
||||
className="operation-btn secondary"
|
||||
onClick={() => handleViewResult(task.id, task.documentIds)}
|
||||
onClick={() => handleViewResult(task.id)}
|
||||
>
|
||||
<i className="ri-file-text-line"></i>
|
||||
查看结果
|
||||
@@ -425,7 +458,7 @@ export default function CrossCheckingIndex() {
|
||||
|
||||
|
||||
|
||||
// 监听fetcher状态变化
|
||||
// 监听fetcher状态变化 - 删除操作
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle' && isDeleting) {
|
||||
setIsDeleting(false);
|
||||
@@ -441,6 +474,44 @@ export default function CrossCheckingIndex() {
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, isDeleting]);
|
||||
|
||||
// 监听fetcher状态变化 - 获取任务详情
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === 'idle' && !isDeleting && modalState.loading) {
|
||||
const data = fetcher.data as {
|
||||
success?: boolean;
|
||||
data?: {
|
||||
files: TaskDocument[];
|
||||
total: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (data.success && data.data) {
|
||||
const { files, total, currentPage, pageSize: returnedPageSize } = data.data;
|
||||
|
||||
setModalState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
title: `任务 ${currentTaskInfo?.taskId || ''} - 文档列表`,
|
||||
files: files || [],
|
||||
total: total || 0,
|
||||
currentPage: currentPage || prev.currentPage,
|
||||
pageSize: returnedPageSize || prev.pageSize
|
||||
}));
|
||||
} else {
|
||||
console.error('获取任务文档列表失败:', data.error);
|
||||
toastService.error(`获取任务文档列表失败: ${data.error || '未知错误'}`);
|
||||
|
||||
setModalState(prev => ({
|
||||
...prev,
|
||||
loading: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, isDeleting, modalState.loading, currentTaskInfo?.taskId]);
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
@@ -479,7 +550,7 @@ export default function CrossCheckingIndex() {
|
||||
align: "center" as const,
|
||||
width: "8%",
|
||||
render: (_: unknown, record: CrossCheckingTask) => {
|
||||
const config = docTypeConfig[record.docType];
|
||||
const config = docTypeConfig[record.docType as keyof typeof docTypeConfig] || { label: record.docType, color: 'gray' as const };
|
||||
return (
|
||||
<Tag color={config.color}>
|
||||
{config.label}
|
||||
@@ -521,7 +592,7 @@ export default function CrossCheckingIndex() {
|
||||
align: "center" as const,
|
||||
width: "auto",
|
||||
render: (_: unknown, record: CrossCheckingTask) => {
|
||||
const config = statusConfig[record.status];
|
||||
const config = statusConfig[record.status as keyof typeof statusConfig] || { label: record.status, color: 'gray' as const };
|
||||
return (
|
||||
<Tag color={config.color}>
|
||||
{config.label}
|
||||
@@ -653,8 +724,8 @@ export default function CrossCheckingIndex() {
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
showTotal={false}
|
||||
showPageSizeChanger={false}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[10, 20, 30, 50]}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
||||
*/
|
||||
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||||
import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||
import { getReviewPoints, updateReviewResult } from "~/api/evaluation_points/reviews";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
// 导入交叉评查详情页面组件
|
||||
@@ -189,11 +189,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||||
}
|
||||
|
||||
// 获取评查点数据
|
||||
const reviewData = await getReviewPoints(id);
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取评查点数据,传递request对象
|
||||
const reviewData = await getReviewPoints(id, request);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
// console.log("reviewData-------",JSON.stringify(reviewData,null,2));
|
||||
if ('error' in reviewData && reviewData.error) {
|
||||
console.error("获取评查点数据错误:", reviewData.error);
|
||||
return Response.json({ result: false, message: reviewData.error });
|
||||
@@ -209,7 +214,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
statistics: reviewData.stats,
|
||||
comparison_document: reviewData.comparison_document,
|
||||
scoring_proposals: reviewData.scoring_proposals || []
|
||||
scoring_proposals: reviewData.scoring_proposals || [],
|
||||
jwtToken: frontendJWT // 传递JWT token
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
@@ -221,10 +227,104 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 action 函数处理需要用户认证的操作
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent") as string;
|
||||
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (intent === "updateReviewResult") {
|
||||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||||
const editAuditStatusId = formData.get("editAuditStatusId") as string;
|
||||
const result = formData.get("result") as string;
|
||||
const message = formData.get("message") as string;
|
||||
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "submitCrossCheckingOpinion") {
|
||||
const { submitCrossCheckingOpinion } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||||
const documentId = formData.get("documentId") as string;
|
||||
const auditPoint = formData.get("auditPoint") as string;
|
||||
const foundIssue = formData.get("foundIssue") as string;
|
||||
const auditOpinion = formData.get("auditOpinion") as string;
|
||||
const deductionScore = parseFloat(formData.get("deductionScore") as string);
|
||||
|
||||
const opinionData = {
|
||||
reviewPointResultId,
|
||||
documentId,
|
||||
auditPoint,
|
||||
foundIssue,
|
||||
auditOpinion,
|
||||
deductionScore
|
||||
};
|
||||
|
||||
const response = await submitCrossCheckingOpinion(opinionData, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "getCrossCheckingOpinions") {
|
||||
const { getCrossCheckingOpinions } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const documentId = formData.get("documentId") as string;
|
||||
const page = parseInt(formData.get("page") as string || "1", 10);
|
||||
const pageSize = parseInt(formData.get("pageSize") as string || "10", 10);
|
||||
const userId = userInfo?.user_id;
|
||||
|
||||
const response = await getCrossCheckingOpinions(documentId, page, pageSize, userId, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "confirmReviewResults") {
|
||||
toastService.error('确认评查结果功能暂未实现');
|
||||
// TODO 应该在cross-file-result.ts中新增一个确认的方法
|
||||
// const documentId = formData.get("documentId") as string;
|
||||
|
||||
// const response = await confirmReviewResults(documentId, request);
|
||||
|
||||
// if (response.error) {
|
||||
// return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
// }
|
||||
|
||||
// return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
return Response.json({ success: false, error: "未知的操作类型" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Action处理失败:', error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '操作失败'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export default function CrossCheckingResult() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals } = loaderData;
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
@@ -314,12 +414,24 @@ export default function CrossCheckingResult() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 API 更新评查结果
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, boolResult, message);
|
||||
// 使用 fetch 调用 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "updateReviewResult");
|
||||
formData.append("reviewPointResultId", reviewPointResultId);
|
||||
formData.append("editAuditStatusId", editAuditStatusId.toString());
|
||||
formData.append("result", boolResult);
|
||||
formData.append("message", message);
|
||||
|
||||
const response = await fetch(window.location.pathname, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.error) {
|
||||
console.error('更新评查结果失败:', response.error);
|
||||
toastService.error(`更新评查结果失败: ${response.error}`);
|
||||
if (!result.success) {
|
||||
console.error('更新评查结果失败:', result.error);
|
||||
toastService.error(`更新评查结果失败: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -409,12 +521,21 @@ export default function CrossCheckingResult() {
|
||||
// 显示加载状态
|
||||
setIsLoading(true);
|
||||
|
||||
// 调用API确认评查结果
|
||||
const response = await confirmReviewResults(document.id.toString());
|
||||
// 使用 fetch 调用 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "confirmReviewResults");
|
||||
formData.append("documentId", document.id.toString());
|
||||
|
||||
const response = await fetch(window.location.pathname, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.error) {
|
||||
console.error('确认评查结果失败:', response.error);
|
||||
toastService.error(`确认评查结果失败: ${response.error}`);
|
||||
if (!result.success) {
|
||||
console.error('确认评查结果失败:', result.error);
|
||||
toastService.error(`确认评查结果失败: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -526,6 +647,7 @@ export default function CrossCheckingResult() {
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
scoringProposals={scoring_proposals as ScoringProposal[]}
|
||||
jwtToken={jwtToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+44
-17
@@ -123,7 +123,8 @@ async function handleFileUpload(
|
||||
remark: string | null,
|
||||
isTestDocument: boolean,
|
||||
documentId?: number | null,
|
||||
isReupload: boolean = false
|
||||
isReupload: boolean = false,
|
||||
jwtToken?: string
|
||||
): Promise<FileUploadResponse> {
|
||||
const response = await uploadDocumentToServer(
|
||||
binaryData,
|
||||
@@ -135,7 +136,8 @@ async function handleFileUpload(
|
||||
remark,
|
||||
isTestDocument,
|
||||
documentId,
|
||||
isReupload
|
||||
isReupload,
|
||||
jwtToken
|
||||
);
|
||||
|
||||
if (response.error || !response.data) {
|
||||
@@ -220,6 +222,8 @@ type LoaderData = {
|
||||
nick_name?: string;
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
frontendJWT?: string | null;
|
||||
userError?: string;
|
||||
};
|
||||
|
||||
// 添加 loader 函数
|
||||
@@ -227,7 +231,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo } = await getUserSession(request);
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
// console.log('loader: 开始加载数据...');
|
||||
const url = new URL(request.url);
|
||||
@@ -236,7 +240,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤
|
||||
// 并行加载文档和文档类型
|
||||
const [documentsResponse, typesResponse] = await Promise.all([
|
||||
getTodayDocuments(),
|
||||
getTodayDocuments(userInfo),
|
||||
getDocumentTypes()
|
||||
]);
|
||||
|
||||
@@ -244,6 +248,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// console.log('loader: 文档类型加载结果:', typesResponse);
|
||||
|
||||
if (documentsResponse.error || typesResponse.error) {
|
||||
// 如果是用户信息错误,返回特殊的错误状态
|
||||
if (documentsResponse.error === '没有找到用户信息,请刷新重试') {
|
||||
return Response.json({
|
||||
documents: [],
|
||||
documentTypes: typesResponse.data || [],
|
||||
userInfo: null,
|
||||
frontendJWT: null,
|
||||
userError: documentsResponse.error
|
||||
});
|
||||
}
|
||||
throw new Error(documentsResponse.error || typesResponse.error);
|
||||
}
|
||||
|
||||
@@ -251,14 +265,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
mode,
|
||||
documents: documentsResponse.data || [],
|
||||
documentTypes: typesResponse.data || [],
|
||||
userInfo // 传递用户信息到客户端
|
||||
userInfo, // 传递用户信息到客户端
|
||||
frontendJWT // 传递JWT到客户端
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('loader: 加载数据失败:', error);
|
||||
return Response.json({
|
||||
documents: [],
|
||||
documentTypes: [],
|
||||
userInfo: null
|
||||
userInfo: null,
|
||||
frontendJWT: null,
|
||||
userError: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -368,16 +385,17 @@ export default function FilesUpload() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 reviewType 获取过滤后的文档列表
|
||||
const response = await getTodayDocuments(reviewType);
|
||||
|
||||
if (response.error) {
|
||||
console.error('过滤文档列表失败:', response.error);
|
||||
// 失败时使用原始数据
|
||||
setQueueFiles(loaderData.documents);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 使用 reviewType 获取过滤后的文档列表
|
||||
const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType);
|
||||
|
||||
if (response.error) {
|
||||
console.error('过滤文档列表失败:', response.error);
|
||||
toastService.error(response.error);
|
||||
// 失败时使用原始数据
|
||||
setQueueFiles(loaderData.documents);
|
||||
return;
|
||||
}
|
||||
const documents = response.data || [];
|
||||
console.log('过滤文档列表成功:', documents);
|
||||
setQueueFiles(documents);
|
||||
@@ -386,6 +404,7 @@ export default function FilesUpload() {
|
||||
startStatusChecker(documents);
|
||||
} catch (error) {
|
||||
console.error('过滤文档列表失败:', error);
|
||||
toastService.error('获取文档列表失败:'+(error instanceof Error ? error.message : '未知错误'));
|
||||
// 出错时使用原始数据
|
||||
const documents = loaderData.documents;
|
||||
setQueueFiles(documents);
|
||||
@@ -439,6 +458,13 @@ export default function FilesUpload() {
|
||||
setFileTypeError(actionData.errors.fileType);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// 检查用户错误并显示 toast 提示
|
||||
useEffect(() => {
|
||||
if (loaderData.userError) {
|
||||
toastService.error(loaderData.userError);
|
||||
}
|
||||
}, [loaderData.userError]);
|
||||
|
||||
// 添加组件挂载状态引用
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
@@ -982,7 +1008,8 @@ export default function FilesUpload() {
|
||||
remark || null,
|
||||
isTestDocument,
|
||||
temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id
|
||||
false
|
||||
false,
|
||||
loaderData.frontendJWT || undefined
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
|
||||
|
||||
+87
-3
@@ -3,7 +3,8 @@ import { useSearchParams, Form } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { getUserSession, getSession, createUserSessionWithInfo, getUserBySub, addDefaultRole } from "~/api/login/auth.server";
|
||||
import { getUserSession, getSession, sessionStorage, getUserBySub, addDefaultRole } from "~/api/login/auth.server";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
import styles from "~/styles/pages/login.css?url";
|
||||
|
||||
export const links = () => [
|
||||
@@ -61,8 +62,91 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
await addDefaultRole(user.id, 2); // 添加common角色
|
||||
}
|
||||
|
||||
// 创建用户会话,默认角色为common,并保存用户信息
|
||||
return createUserSessionWithInfo(true, 'common', redirectTo, user);
|
||||
// 设置模拟的OAuth token信息
|
||||
const mockTokenExpiresIn = 60 * 60 * 2; // 2小时,与真实OAuth token保持一致
|
||||
const userRole = 'common';
|
||||
|
||||
// 生成前端专用JWT
|
||||
const jwtUserInfo: UserInfoForJWT = {
|
||||
sub: user.sub,
|
||||
user_id: user.id!,
|
||||
username: user.username,
|
||||
nick_name: user.nick_name,
|
||||
email: user.email,
|
||||
phone_number: user.phone_number,
|
||||
ou_id: user.ou_id,
|
||||
ou_name: user.ou_name,
|
||||
is_leader: user.is_leader,
|
||||
user_role: userRole
|
||||
};
|
||||
|
||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, mockTokenExpiresIn);
|
||||
|
||||
// 打印JWT生成信息
|
||||
console.log("=== 测试用户登录 - JWT生成信息 ===");
|
||||
console.log("用户信息:", jwtUserInfo);
|
||||
console.log("生成的JWT:", frontendJWT);
|
||||
console.log("JWT过期时间:", JWTUtils.getJWTExpiration(frontendJWT));
|
||||
console.log("JWT解析结果:", JWTUtils.decodeJWT(frontendJWT));
|
||||
console.log("JWT验证结果:", JWTUtils.verifyJWT(frontendJWT));
|
||||
|
||||
// 创建session,保持与OAuth登录相同的数据结构
|
||||
session.set("isAuthenticated", true);
|
||||
session.set("accessToken", "mock_access_token_for_test"); // 模拟的访问令牌
|
||||
session.set("refreshToken", "mock_refresh_token_for_test"); // 模拟的刷新令牌
|
||||
session.set("tokenIssuedAt", Date.now());
|
||||
session.set("tokenExpiresIn", mockTokenExpiresIn);
|
||||
session.set("userRole", userRole);
|
||||
session.set("frontendJWT", frontendJWT);
|
||||
|
||||
// 构建与OAuth登录相同结构的userInfo
|
||||
const enhancedUserInfo = {
|
||||
// 保持与callback.tsx中相同的数据结构
|
||||
sub: user.sub,
|
||||
username: user.username,
|
||||
nick_name: user.nick_name,
|
||||
phone_number: user.phone_number,
|
||||
email: user.email,
|
||||
ou_id: user.ou_id,
|
||||
ou_name: user.ou_name,
|
||||
status: user.status,
|
||||
is_leader: user.is_leader,
|
||||
// 增强字段,与OAuth登录保持一致
|
||||
user_id: user.id,
|
||||
user_role: userRole,
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
|
||||
session.set("userInfo", enhancedUserInfo);
|
||||
|
||||
// 打印session信息
|
||||
console.log("=== 测试用户登录 - Session信息 ===");
|
||||
console.log("保存到session的userInfo:", enhancedUserInfo);
|
||||
console.log("session数据结构:", {
|
||||
isAuthenticated: true,
|
||||
userRole: userRole,
|
||||
accessToken: "mock_access_token_for_test",
|
||||
refreshToken: "mock_refresh_token_for_test",
|
||||
tokenIssuedAt: Date.now(),
|
||||
tokenExpiresIn: mockTokenExpiresIn,
|
||||
frontendJWT: frontendJWT,
|
||||
userInfo: enhancedUserInfo
|
||||
});
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
console.log("=== 测试用户登录完成 ===");
|
||||
console.log("用户:", user.username);
|
||||
console.log("角色:", userRole);
|
||||
console.log("重定向到:", redirectTo);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: redirectTo,
|
||||
"Set-Cookie": cookie,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 如果用户不存在,重定向到登录页面并显示错误
|
||||
return redirect(`/login?error=${encodeURIComponent("测试用户不存在")}`);
|
||||
|
||||
+205
-85
@@ -25,9 +25,9 @@
|
||||
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
||||
*/
|
||||
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||
import reviewsStyles from "~/styles/reviews.css?url";
|
||||
import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
@@ -183,8 +183,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||||
}
|
||||
|
||||
// 获取评查点数据
|
||||
const reviewData = await getReviewPoints(id);
|
||||
// 获取评查点数据,传递request对象
|
||||
const reviewData = await getReviewPoints(id, request);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
@@ -214,9 +214,78 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 action 函数处理需要用户认证的操作
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const intent = formData.get("intent") as string;
|
||||
|
||||
console.log('Action接收到请求, intent:', intent);
|
||||
|
||||
if (intent === "updateReviewResult") {
|
||||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||||
const editAuditStatusId = formData.get("editAuditStatusId") as string;
|
||||
const result = formData.get("result") as string;
|
||||
const message = formData.get("message") as string;
|
||||
|
||||
console.log('更新评查结果参数:', { reviewPointResultId, editAuditStatusId, result, message });
|
||||
|
||||
try {
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request);
|
||||
|
||||
if (response.error) {
|
||||
console.error('updateReviewResult返回错误:', response.error);
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
} catch (updateError) {
|
||||
console.error('调用updateReviewResult时发生异常:', updateError);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: updateError instanceof Error ? updateError.message : '更新评查结果时发生未知错误'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "confirmReviewResults") {
|
||||
const documentId = formData.get("documentId") as string;
|
||||
|
||||
console.log('确认评查结果参数:', { documentId });
|
||||
|
||||
try {
|
||||
const response = await confirmReviewResults(documentId, request);
|
||||
|
||||
if (response.error) {
|
||||
console.error('confirmReviewResults返回错误:', response.error);
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
} catch (confirmError) {
|
||||
console.error('调用confirmReviewResults时发生异常:', confirmError);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: confirmError instanceof Error ? confirmError.message : '确认评查结果时发生未知错误'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
console.error('收到未知的操作类型:', intent);
|
||||
return Response.json({ success: false, error: "未知的操作类型" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Action处理失败:', error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '操作失败'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export default function ReviewDetails() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
const { document, reviewPoints, statistics, reviewInfo, comparison_document } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
||||
@@ -224,6 +293,11 @@ export default function ReviewDetails() {
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
||||
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||
reviewPointResultId: string;
|
||||
newStatus: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
@@ -374,6 +448,80 @@ export default function ReviewDetails() {
|
||||
// }
|
||||
// }
|
||||
|
||||
// 监听fetcher状态变化
|
||||
useEffect(() => {
|
||||
if (fetcher.state === "idle" && fetcher.data && pendingUpdate) {
|
||||
const result = fetcher.data as { success: boolean; error?: string; data?: unknown };
|
||||
console.log('Fetcher返回数据:', result);
|
||||
|
||||
if (result.success) {
|
||||
console.log('评查点状态更新成功');
|
||||
|
||||
// 使用pendingUpdate中的参数更新本地状态
|
||||
if (reviewData && pendingUpdate.reviewPointResultId) {
|
||||
const reviewPointToUpdate = reviewData.reviewPoints.find(point => point.id === pendingUpdate.reviewPointResultId);
|
||||
const oldStatus = reviewPointToUpdate?.status || '';
|
||||
const wasSuccess = reviewPointToUpdate?.result === true;
|
||||
const newIsSuccess = pendingUpdate.newStatus === 'true';
|
||||
|
||||
// 更新评查点
|
||||
const updatedReviewPoints = reviewData.reviewPoints.map(point =>
|
||||
point.id === pendingUpdate.reviewPointResultId ? {
|
||||
...point,
|
||||
result: pendingUpdate.newStatus === 'true' ? true : (pendingUpdate.newStatus === 'false' ? false : point.result),
|
||||
editAuditStatus: pendingUpdate.newStatus === 'review' ? 0 : 1,
|
||||
title: pendingUpdate.newStatus === 'review' ? point.title : pendingUpdate.message,
|
||||
editAuditStatusMessage: pendingUpdate.newStatus === 'review' ? point.editAuditStatusMessage : pendingUpdate.message
|
||||
} : point
|
||||
);
|
||||
|
||||
// 更新统计数据
|
||||
const updatedStatistics = { ...reviewData.statistics };
|
||||
|
||||
// 只处理结果实际变化的情况
|
||||
if (pendingUpdate.newStatus !== 'review' && wasSuccess !== newIsSuccess) {
|
||||
if (newIsSuccess) {
|
||||
// 从不通过变为通过
|
||||
updatedStatistics.success += 1;
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning = Math.max(0, updatedStatistics.warning - 1);
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error = Math.max(0, updatedStatistics.error - 1);
|
||||
}
|
||||
} else {
|
||||
// 从通过变为不通过
|
||||
updatedStatistics.success = Math.max(0, updatedStatistics.success - 1);
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning += 1;
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 UI 状态
|
||||
setReviewData({
|
||||
...reviewData,
|
||||
reviewPoints: updatedReviewPoints,
|
||||
statistics: updatedStatistics
|
||||
});
|
||||
}
|
||||
|
||||
if(document && document.id && pendingUpdate.newStatus !== 'review'){
|
||||
toastService.success('评查点状态已更新');
|
||||
}
|
||||
|
||||
// 清除pendingUpdate
|
||||
setPendingUpdate(null);
|
||||
} else {
|
||||
console.error('更新评查结果失败:', result.error);
|
||||
toastService.error(`更新评查结果失败: ${result.error || '未知错误'}`);
|
||||
// 清除pendingUpdate
|
||||
setPendingUpdate(null);
|
||||
}
|
||||
}
|
||||
}, [fetcher.state, fetcher.data, pendingUpdate, document, reviewData]);
|
||||
|
||||
// 处理评审点状态变更
|
||||
const handleReviewPointStatusChange = async (reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string) => {
|
||||
// 将字符串的布尔值转换为布尔类型
|
||||
@@ -383,85 +531,28 @@ export default function ReviewDetails() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 API 更新评查结果
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, boolResult, message);
|
||||
console.log('开始提交评查结果更新:', { reviewPointResultId, editAuditStatusId, boolResult, message });
|
||||
|
||||
if (response.error) {
|
||||
console.error('更新评查结果失败:', response.error);
|
||||
toastService.error(`更新评查结果失败: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
// 设置待处理的更新信息
|
||||
setPendingUpdate({
|
||||
reviewPointResultId,
|
||||
newStatus: boolResult,
|
||||
message
|
||||
});
|
||||
|
||||
// console.log('评查点状态更新成功:', {
|
||||
// id: reviewPointResultId,
|
||||
// result: boolResult,
|
||||
// message: message
|
||||
// });
|
||||
|
||||
// 更新本地状态
|
||||
if (reviewData) {
|
||||
// 找到要更新的评查点和它的原始状态
|
||||
const reviewPointToUpdate = reviewData.reviewPoints.find(point => point.id === reviewPointResultId);
|
||||
const oldStatus = reviewPointToUpdate?.status || '';
|
||||
const wasSuccess = reviewPointToUpdate?.result === true;
|
||||
const newIsSuccess = newStatus === 'true';
|
||||
|
||||
// 更新评查点
|
||||
const updatedReviewPoints = reviewData.reviewPoints.map(point =>
|
||||
point.id === reviewPointResultId ? {
|
||||
...point,
|
||||
result: newStatus === 'true' ? true : (newStatus === 'false' ? false : point.result),
|
||||
editAuditStatus: boolResult === 'review' ? 0 : 1,
|
||||
title: boolResult === 'review' ? point.title : message,
|
||||
editAuditStatusMessage: boolResult === 'review' ? point.editAuditStatusMessage : message
|
||||
} : point
|
||||
);
|
||||
|
||||
// 更新统计数据
|
||||
const updatedStatistics = { ...reviewData.statistics };
|
||||
|
||||
// 只处理结果实际变化的情况,即从通过变为不通过,或从不通过变为通过
|
||||
if (newStatus !== 'review' && wasSuccess !== newIsSuccess) {
|
||||
if (newIsSuccess) {
|
||||
// 从不通过变为通过
|
||||
updatedStatistics.success += 1;
|
||||
// 减少对应的错误或警告数量
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning = Math.max(0, updatedStatistics.warning - 1);
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error = Math.max(0, updatedStatistics.error - 1);
|
||||
}
|
||||
} else {
|
||||
// 从通过变为不通过
|
||||
updatedStatistics.success = Math.max(0, updatedStatistics.success - 1);
|
||||
// 增加对应的错误或警告数量
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning += 1;
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 UI 状态
|
||||
setReviewData({
|
||||
...reviewData,
|
||||
reviewPoints: updatedReviewPoints,
|
||||
statistics: updatedStatistics
|
||||
});
|
||||
|
||||
// 显示成功消息
|
||||
if(document && document.id && newStatus !== 'review'){
|
||||
toastService.success('评查点状态已更新');
|
||||
}
|
||||
// 使用 Remix 的 useFetcher 调用 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "updateReviewResult");
|
||||
formData.append("reviewPointResultId", reviewPointResultId);
|
||||
formData.append("editAuditStatusId", editAuditStatusId.toString());
|
||||
formData.append("result", boolResult);
|
||||
formData.append("message", message);
|
||||
|
||||
// console.log("newReviewPoints",updatedReviewPoints);
|
||||
|
||||
// 如果是review操作才调用API刷新
|
||||
// if (document && document.id && newStatus === 'review') {
|
||||
// await refreshReviewData(document.id.toString());
|
||||
// }
|
||||
}
|
||||
fetcher.submit(formData, { method: "POST" });
|
||||
|
||||
console.log('请求已提交,等待响应...');
|
||||
|
||||
// 注意:本地状态更新现在在useEffect中处理,当fetcher返回成功响应时触发
|
||||
} catch (error) {
|
||||
console.error('更新评查结果出错:', error);
|
||||
toastService.error('更新评查结果失败,请稍后重试');
|
||||
@@ -478,12 +569,41 @@ export default function ReviewDetails() {
|
||||
// 显示加载状态
|
||||
setIsLoading(true);
|
||||
|
||||
// 调用API确认评查结果
|
||||
const response = await confirmReviewResults(document.id.toString());
|
||||
// 使用 fetch 调用 action
|
||||
const formData = new FormData();
|
||||
formData.append("intent", "confirmReviewResults");
|
||||
formData.append("documentId", document.id.toString());
|
||||
|
||||
const response = await fetch(window.location.pathname, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
// 检查响应是否为JSON格式
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
console.error('服务器返回了非JSON响应,状态码:', response.status);
|
||||
const text = await response.text();
|
||||
console.error('响应内容:', text.substring(0, 500));
|
||||
|
||||
if (response.status === 401) {
|
||||
toastService.error('登录已过期,请重新登录');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
} else if (response.status >= 500) {
|
||||
toastService.error('服务器内部错误,请稍后重试');
|
||||
return;
|
||||
} else {
|
||||
toastService.error('请求失败,请检查网络连接');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.error) {
|
||||
console.error('确认评查结果失败:', response.error);
|
||||
toastService.error(`确认评查结果失败: ${response.error}`);
|
||||
if (!result.success) {
|
||||
console.error('确认评查结果失败:', result.error);
|
||||
toastService.error(`确认评查结果失败: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* JWT工具类
|
||||
* 用于生成和验证前端专用的JWT Token
|
||||
*
|
||||
* 主要功能:
|
||||
* - 生成包含用户信息的JWT
|
||||
* - 验证JWT的有效性
|
||||
* - 解析JWT获取用户信息
|
||||
*/
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
const { sign, verify, decode } = jwt;
|
||||
|
||||
// JWT密钥 - 在生产环境中应该从环境变量读取
|
||||
const JWT_SECRET = 'gdyc-super-secrets-jjwtt-key-change-this-in-production-20250721-from-login-callback';
|
||||
|
||||
// JWT配置
|
||||
const JWT_CONFIG = {
|
||||
algorithm: 'HS256' as const,
|
||||
issuer: 'docreview-system',
|
||||
audience: 'docreview-frontend'
|
||||
};
|
||||
|
||||
// JWT载荷接口
|
||||
export interface JWTPayload {
|
||||
// 标准字段
|
||||
sub: string; // 用户唯一标识(来自IDaaS)
|
||||
iss: string; // 发行者
|
||||
aud: string; // 受众
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
|
||||
// 自定义用户信息字段
|
||||
user_id: string; // 数据库中的用户ID
|
||||
username: string; // 用户名
|
||||
nick_name: string; // 用户昵称
|
||||
email?: string; // 邮箱
|
||||
phone_number?: string; // 手机号
|
||||
ou_id: string; // 组织单位ID
|
||||
ou_name: string; // 组织单位名称
|
||||
is_leader: boolean; // 是否为负责人
|
||||
user_role: string; // 用户角色
|
||||
}
|
||||
|
||||
// 用户信息接口(用于生成JWT)
|
||||
export interface UserInfoForJWT {
|
||||
sub: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
email?: string;
|
||||
phone_number?: string;
|
||||
ou_id: string;
|
||||
ou_name: string;
|
||||
is_leader: boolean;
|
||||
user_role: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*/
|
||||
export class JWTUtils {
|
||||
/**
|
||||
* 生成JWT
|
||||
* @param userInfo 用户信息
|
||||
* @param expiresIn 过期时间(秒),默认为OAuth token过期时间的90%
|
||||
* @returns JWT字符串
|
||||
*/
|
||||
static generateJWT(userInfo: UserInfoForJWT, expiresIn: number): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// 将过期时间设置为OAuth token过期时间的90%,确保JWT在OAuth token之前过期
|
||||
const jwtExpiresIn = Math.floor(expiresIn);
|
||||
|
||||
const payload: JWTPayload = {
|
||||
// 标准字段
|
||||
sub: userInfo.sub,
|
||||
iss: JWT_CONFIG.issuer,
|
||||
aud: JWT_CONFIG.audience,
|
||||
iat: now,
|
||||
exp: now + jwtExpiresIn,
|
||||
|
||||
// 用户信息字段
|
||||
user_id: userInfo.user_id,
|
||||
username: userInfo.username,
|
||||
nick_name: userInfo.nick_name,
|
||||
email: userInfo.email,
|
||||
phone_number: userInfo.phone_number,
|
||||
ou_id: userInfo.ou_id,
|
||||
ou_name: userInfo.ou_name,
|
||||
is_leader: userInfo.is_leader,
|
||||
user_role: userInfo.user_role
|
||||
};
|
||||
|
||||
return sign(payload, JWT_SECRET, {
|
||||
algorithm: JWT_CONFIG.algorithm
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT
|
||||
* @param token JWT字符串
|
||||
* @returns 验证结果和载荷
|
||||
*/
|
||||
static verifyJWT(token: string): { valid: boolean; payload?: JWTPayload; error?: string } {
|
||||
try {
|
||||
const payload = verify(token, JWT_SECRET, {
|
||||
algorithms: [JWT_CONFIG.algorithm],
|
||||
issuer: JWT_CONFIG.issuer,
|
||||
audience: JWT_CONFIG.audience
|
||||
}) as JWTPayload;
|
||||
|
||||
return { valid: true, payload };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { valid: false, error: error.message };
|
||||
}
|
||||
return { valid: false, error: 'JWT验证失败' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JWT(不验证签名)
|
||||
* @param token JWT字符串
|
||||
* @returns 解析结果
|
||||
*/
|
||||
static decodeJWT(token: string): { payload?: JWTPayload; error?: string } {
|
||||
try {
|
||||
const payload = decode(token) as JWTPayload;
|
||||
return { payload };
|
||||
} catch (error) {
|
||||
return { error: 'JWT解析失败' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查JWT是否即将过期
|
||||
* @param token JWT字符串
|
||||
* @param bufferMinutes 缓冲时间(分钟),默认5分钟
|
||||
* @returns 是否即将过期
|
||||
*/
|
||||
static isJWTExpiringSoon(token: string, bufferMinutes: number = 5): boolean {
|
||||
const decoded = this.decodeJWT(token);
|
||||
if (!decoded.payload) {
|
||||
return true; // 解析失败视为过期
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const bufferSeconds = bufferMinutes * 60;
|
||||
|
||||
return decoded.payload.exp <= (now + bufferSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JWT过期时间
|
||||
* @param token JWT字符串
|
||||
* @returns 过期时间戳
|
||||
*/
|
||||
static getJWTExpiration(token: string): number | null {
|
||||
const decoded = this.decodeJWT(token);
|
||||
return decoded.payload?.exp || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT中提取用户信息
|
||||
* @param token JWT字符串
|
||||
* @returns 用户信息
|
||||
*/
|
||||
static extractUserInfo(token: string): UserInfoForJWT | null {
|
||||
const verification = this.verifyJWT(token);
|
||||
if (!verification.valid || !verification.payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = verification.payload;
|
||||
return {
|
||||
sub: payload.sub,
|
||||
user_id: payload.user_id,
|
||||
username: payload.username,
|
||||
nick_name: payload.nick_name,
|
||||
email: payload.email,
|
||||
phone_number: payload.phone_number,
|
||||
ou_id: payload.ou_id,
|
||||
ou_name: payload.ou_name,
|
||||
is_leader: payload.is_leader,
|
||||
user_role: payload.user_role
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default JWTUtils;
|
||||
@@ -0,0 +1,204 @@
|
||||
# JWT 认证系统实现说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目实现了一个基于JWT的前端认证系统,配合OAuth2.0统一认证使用。该系统在用户通过OAuth2.0认证后,生成一个前端专用的JWT Token,包含用户信息和权限数据。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. JWT生成与管理 (`app/utils/jwt.ts`)
|
||||
- 生成包含用户信息的JWT
|
||||
- 验证JWT的有效性
|
||||
- 检查JWT过期状态
|
||||
- 解析JWT获取用户信息
|
||||
|
||||
### 2. 认证服务集成 (`app/api/login/auth.server.ts`)
|
||||
- 在OAuth2.0登录成功后自动生成JWT
|
||||
- 在Token刷新时重新生成JWT
|
||||
- JWT自动存储在session中
|
||||
|
||||
### 3. 前端认证Hook (`app/hooks/useAuth.ts`)
|
||||
- 提供便捷的认证状态获取
|
||||
- JWT验证和过期检查
|
||||
- 用户权限检查功能
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在Loader中返回认证信息
|
||||
|
||||
```typescript
|
||||
// app/routes/your-route.tsx
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const sessionData = await getUserSession(request);
|
||||
|
||||
return Response.json({
|
||||
// 你的其他数据
|
||||
yourData: "...",
|
||||
|
||||
// 认证信息
|
||||
isAuthenticated: sessionData.isAuthenticated,
|
||||
userInfo: sessionData.userInfo,
|
||||
userRole: sessionData.userRole,
|
||||
frontendJWT: sessionData.frontendJWT
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 在组件中使用认证信息
|
||||
|
||||
```typescript
|
||||
// 在你的React组件中
|
||||
import { useAuth } from '~/hooks/useAuth';
|
||||
|
||||
export default function YourComponent() {
|
||||
const auth = useAuth();
|
||||
|
||||
// 检查认证状态
|
||||
if (!auth.isAuthenticated) {
|
||||
return <div>请先登录</div>;
|
||||
}
|
||||
|
||||
// 使用用户信息
|
||||
return (
|
||||
<div>
|
||||
<h1>欢迎,{auth.nickName}</h1>
|
||||
<p>用户ID: {auth.userId}</p>
|
||||
<p>角色: {auth.userRole}</p>
|
||||
|
||||
{/* 权限检查 */}
|
||||
{auth.isAdmin() && (
|
||||
<button>管理员操作</button>
|
||||
)}
|
||||
|
||||
{auth.hasRole('developer') && (
|
||||
<button>开发者操作</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### JWT验证和状态检查
|
||||
|
||||
```typescript
|
||||
const auth = useAuth();
|
||||
|
||||
// 验证JWT
|
||||
const jwtValidation = auth.validateJWT();
|
||||
if (!jwtValidation.valid) {
|
||||
console.error('JWT无效:', jwtValidation.error);
|
||||
}
|
||||
|
||||
// 检查是否即将过期
|
||||
if (auth.isJWTExpiringSoon()) {
|
||||
console.log('JWT即将过期,建议刷新页面');
|
||||
}
|
||||
|
||||
// 获取过期时间
|
||||
const expiration = auth.getJWTExpiration();
|
||||
if (expiration) {
|
||||
console.log('JWT过期时间:', new Date(expiration * 1000));
|
||||
}
|
||||
```
|
||||
|
||||
## JWT载荷结构
|
||||
|
||||
```typescript
|
||||
{
|
||||
// 标准字段
|
||||
sub: string; // 用户唯一标识(来自IDaaS)
|
||||
iss: string; // 发行者: 'docreview-system'
|
||||
aud: string; // 受众: 'docreview-frontend'
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
|
||||
// 自定义用户信息字段
|
||||
user_id: string; // 数据库中的用户ID
|
||||
username: string; // 用户名
|
||||
nick_name: string; // 用户昵称
|
||||
email?: string; // 邮箱
|
||||
phone_number?: string; // 手机号
|
||||
ou_id: string; // 组织单位ID
|
||||
ou_name: string; // 组织单位名称
|
||||
is_leader: boolean; // 是否为负责人
|
||||
user_role: string; // 用户角色
|
||||
}
|
||||
```
|
||||
|
||||
## 安全特性
|
||||
|
||||
1. **过期时间控制**: JWT过期时间设置为OAuth Token过期时间的90%,确保JWT在OAuth Token之前过期
|
||||
2. **自动刷新**: 当OAuth Token刷新时,自动重新生成JWT
|
||||
3. **签名验证**: 使用HS256算法和固定密钥进行签名验证
|
||||
4. **过期检查**: 提供5分钟缓冲时间的过期预警
|
||||
|
||||
## 调试工具
|
||||
|
||||
使用 `AuthDebugInfo` 组件在开发阶段查看JWT状态:
|
||||
|
||||
```typescript
|
||||
import { AuthDebugInfo } from '~/components/auth/AuthDebugInfo';
|
||||
|
||||
export default function DebugPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>认证调试页面</h1>
|
||||
<AuthDebugInfo />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### JWT密钥配置
|
||||
在 `app/utils/jwt.ts` 中修改JWT密钥:
|
||||
|
||||
```typescript
|
||||
// 生产环境中应该从环境变量读取
|
||||
const JWT_SECRET = 'your-super-secret-jwt-key-change-this-in-production-2024';
|
||||
```
|
||||
|
||||
### JWT配置参数
|
||||
```typescript
|
||||
const JWT_CONFIG = {
|
||||
algorithm: 'HS256',
|
||||
issuer: 'docreview-system',
|
||||
audience: 'docreview-frontend'
|
||||
};
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **生产环境安全**:
|
||||
- JWT密钥应该从环境变量读取
|
||||
- 删除或隐藏 `AuthDebugInfo` 组件
|
||||
- 考虑使用更强的签名算法
|
||||
|
||||
2. **性能优化**:
|
||||
- JWT验证在前端进行,减少服务器压力
|
||||
- 使用session存储避免频繁重新生成
|
||||
|
||||
3. **错误处理**:
|
||||
- JWT验证失败时应该引导用户重新登录
|
||||
- 过期检查应该在关键操作前进行
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 用户通过OAuth2.0登录
|
||||
2. `callback.tsx` 接收认证结果并保存用户信息到数据库
|
||||
3. 根据保存的用户信息生成JWT
|
||||
4. JWT存储在session中
|
||||
5. 后续请求通过 `getUserSession` 检查JWT状态
|
||||
6. 前端通过 `useAuth` hook 使用认证信息
|
||||
7. 当OAuth Token刷新时,自动重新生成JWT
|
||||
|
||||
## 扩展功能
|
||||
|
||||
可以考虑的扩展功能:
|
||||
- JWT黑名单机制
|
||||
- 多设备登录管理
|
||||
- 权限细粒度控制
|
||||
- JWT刷新接口
|
||||
- 客户端JWT存储(LocalStorage/SessionStorage)
|
||||
Generated
+112
@@ -15,6 +15,7 @@
|
||||
"@remix-run/node": "^2.16.2",
|
||||
"@remix-run/react": "^2.16.2",
|
||||
"@remix-run/serve": "^2.16.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"ahooks": "^3.8.5",
|
||||
"antd": "^5.25.4",
|
||||
@@ -27,6 +28,7 @@
|
||||
"html-docx-js": "^0.3.1",
|
||||
"immer": "^10.1.1",
|
||||
"isbot": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.22",
|
||||
"mammoth": "^1.9.0",
|
||||
@@ -3308,6 +3310,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/katex": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz",
|
||||
@@ -6075,6 +6087,12 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -7124,6 +7142,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -11383,6 +11410,34 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -11417,6 +11472,27 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.22",
|
||||
"resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.22.tgz",
|
||||
@@ -11658,6 +11734,12 @@
|
||||
"lodash._root": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
@@ -11670,6 +11752,24 @@
|
||||
"integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz",
|
||||
@@ -11681,6 +11781,12 @@
|
||||
"lodash.keysin": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.istypedarray": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz",
|
||||
@@ -11715,6 +11821,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.restparam": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@
|
||||
"@remix-run/node": "^2.16.2",
|
||||
"@remix-run/react": "^2.16.2",
|
||||
"@remix-run/serve": "^2.16.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"ahooks": "^3.8.5",
|
||||
"antd": "^5.25.4",
|
||||
@@ -35,6 +36,7 @@
|
||||
"html-docx-js": "^0.3.1",
|
||||
"immer": "^10.1.1",
|
||||
"isbot": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.22",
|
||||
"mammoth": "^1.9.0",
|
||||
@@ -79,4 +81,4 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user