Merge remote-tracking branch 'origin/shiy-login' into PingChuan
This commit is contained in:
@@ -102,10 +102,10 @@ export async function findIsProposer(taskId: string | number, userId: number | u
|
||||
return false;
|
||||
}
|
||||
const data = extractApiData<{assigner_id: number}[]>(response.data);
|
||||
// console.log('data', data);
|
||||
// console.log('data', data, userId);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
return data[0].assigner_id === userId;
|
||||
return data[0].assigner_id.toString() === userId?.toString();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -124,6 +124,7 @@ export async function submitCrossCheckingOpinion(
|
||||
): Promise<ApiResponse<SubmitOpinionResponse>> {
|
||||
try {
|
||||
// 获取JWT token
|
||||
console.log('jwtToken', jwtToken)
|
||||
const token = await safeGetJWT(jwtToken);
|
||||
|
||||
const requestData = {
|
||||
@@ -151,9 +152,21 @@ export async function submitCrossCheckingOpinion(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('提交交叉评查意见失败:', error);
|
||||
|
||||
// 正确处理 axios 错误响应
|
||||
let errorMessage = '提交意见失败';
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// 从 axios 错误响应中提取 msg 字段
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
// 处理普通 Error 对象
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '提交意见失败',
|
||||
status: 500
|
||||
error: errorMessage,
|
||||
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -245,9 +258,21 @@ export async function getCrossCheckingOpinions(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取交叉评查意见失败:', error);
|
||||
|
||||
// 正确处理 axios 错误响应
|
||||
let errorMessage = '获取意见列表失败';
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// 从 axios 错误响应中提取 msg 字段
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
// 处理普通 Error 对象
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取意见列表失败',
|
||||
status: 500
|
||||
error: errorMessage,
|
||||
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -343,9 +368,21 @@ export async function performOpinionAction(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('执行意见操作失败:', error);
|
||||
|
||||
// 正确处理 axios 错误响应
|
||||
let errorMessage = '操作失败';
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// 从 axios 错误响应中提取 msg 字段
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
// 处理普通 Error 对象
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '操作失败',
|
||||
status: 500
|
||||
error: errorMessage,
|
||||
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -427,9 +464,21 @@ export async function checkProposalVotes(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('检查失败:', error);
|
||||
|
||||
// 正确处理 axios 错误响应
|
||||
let errorMessage = '检查失败';
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// 从 axios 错误响应中提取 msg 字段
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
// 处理普通 Error 对象
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '检查失败',
|
||||
status: 500
|
||||
error: errorMessage,
|
||||
status: axios.isAxiosError(error) ? error.response?.status || 500 : 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UPLOAD_URL } from '../../config/api-config';
|
||||
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
@@ -208,6 +208,7 @@ export async function uploadCrossCheckingDocument(
|
||||
* @param assignUserIds 需要分配的用户ID数组
|
||||
* @param taskName 任务名称
|
||||
* @param docType 文档类型(如 XZCF、XZXK)
|
||||
* @param taskType 任务类型(如 市局间交叉评查、区局间交叉评查)
|
||||
* @param token JWT Token
|
||||
*/
|
||||
export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
@@ -220,6 +221,7 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
assignUserIds: number[],
|
||||
taskName: string,
|
||||
docType: string,
|
||||
taskType: string = '市局间交叉评查',
|
||||
token: string | null = null
|
||||
): Promise<{
|
||||
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
|
||||
@@ -229,6 +231,10 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = [];
|
||||
const uploadEndpoint = '/cross_review/documents/upload_and_assign';
|
||||
const uploadUrl = UPLOAD_URL + uploadEndpoint;
|
||||
|
||||
// console.log('[批量上传] 任务类型:', taskType, '文档类型:', docType);
|
||||
|
||||
|
||||
for (const fileInfo of files) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
@@ -240,15 +246,22 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
remark: remark || null,
|
||||
is_test_document: isTestDocument,
|
||||
task_name: taskName,
|
||||
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType
|
||||
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType,
|
||||
task_type: taskType
|
||||
};
|
||||
// console.log('fileInfo', fileInfo)
|
||||
|
||||
formData.append('upload_info', JSON.stringify(uploadInfo));
|
||||
formData.append('assign_user_ids', JSON.stringify(assignUserIds));
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
const result = response.data;
|
||||
if (result && result.success) {
|
||||
successes.push({ file: fileInfo, result });
|
||||
@@ -262,6 +275,71 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
return { successes, failures };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建交叉评查任务
|
||||
* @param taskData 任务数据
|
||||
* @param token JWT Token
|
||||
* @returns 创建结果
|
||||
*/
|
||||
export async function createCrossReviewTask(taskData: {
|
||||
documentIds: number[];
|
||||
userIds: number[];
|
||||
assignerId: number;
|
||||
taskName: string;
|
||||
docType: string;
|
||||
taskType?: string;
|
||||
}, token: string | null = null): Promise<{
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const requestBody = {
|
||||
document_ids: taskData.documentIds,
|
||||
user_ids: taskData.userIds,
|
||||
assigner_id: taskData.assignerId,
|
||||
task_name: taskData.taskName,
|
||||
doc_type: taskData.docType,
|
||||
task_type: taskData.taskType || '市局间交叉评查'
|
||||
};
|
||||
|
||||
// console.log('[创建任务] 请求数据:', requestBody);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_BASE_URL}/admin/cross_review/tasks/assign`,
|
||||
requestBody,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
console.log('[创建任务] 成功:', response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[创建任务] 失败:', error);
|
||||
|
||||
let errorMessage = '创建任务失败';
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件ID
|
||||
* @returns 唯一ID字符串
|
||||
|
||||
@@ -11,8 +11,8 @@ export enum CrossCheckingTaskStatus {
|
||||
|
||||
// 交叉评查任务类型枚举
|
||||
export enum CrossCheckingTaskType {
|
||||
CITY = 'city',
|
||||
COUNTY = 'county'
|
||||
CITY = 'CITY',
|
||||
DISTRICT = 'DISTRICT'
|
||||
}
|
||||
|
||||
// 案卷类型枚举
|
||||
@@ -37,7 +37,7 @@ export interface CrossCheckingTask {
|
||||
startDate: string;
|
||||
taskType: CrossCheckingTaskType;
|
||||
docType: string; // 改为直接使用返回的 doc_type 字符串
|
||||
evaluationRegion: string;
|
||||
evaluationRegion: string[];
|
||||
progress: number;
|
||||
status: string; // 改为直接使用返回的 task_status 字符串
|
||||
score: number;
|
||||
@@ -61,6 +61,8 @@ export interface UserTaskInfo {
|
||||
task_status: string;
|
||||
doc_type?: string;
|
||||
task_created_at?: string;
|
||||
evaluation_region?: string[];
|
||||
task_type?: string;
|
||||
progress?: number;
|
||||
total_documents?: number; // 新增:任务包含的文档总数
|
||||
}
|
||||
@@ -150,7 +152,7 @@ export interface TaskListResponse {
|
||||
*/
|
||||
export async function getCrossCheckingTasks(params: TaskListParams = {}, userInfo?: { user_id?: number; [key: string]: unknown }, jwtToken?: string): Promise<ApiResponse<TaskListResponse>> {
|
||||
try {
|
||||
console.log('开始调用getCrossCheckingTasks,参数:', params);
|
||||
// console.log('开始调用getCrossCheckingTasks,参数:', params);
|
||||
|
||||
// 调用用户任务API,获取当前用户参与的任务
|
||||
const userTasksResponse = await getUserTaskDocuments(params.page || 1, params.pageSize || 10, jwtToken);
|
||||
@@ -174,9 +176,9 @@ export async function getCrossCheckingTasks(params: TaskListParams = {}, userInf
|
||||
sequence: index + 1,
|
||||
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, // 保持默认任务类型
|
||||
taskType: userTask.task_type, // 保持默认任务类型
|
||||
docType: userTask.doc_type || '未知类型', // 使用API返回的文档类型
|
||||
evaluationRegion: '待定', // 保持默认评查地区
|
||||
evaluationRegion: userTask.evaluation_region || [], // 保持默认评查地区
|
||||
progress: userTask.progress || 0, // 使用API返回的进度
|
||||
status: userTask.task_status || 'pending', // 使用API返回的任务状态
|
||||
score: userTask.task_status === 'completed' ? 85 : 0, // 默认分数
|
||||
@@ -302,7 +304,7 @@ export async function getCrossCheckingTaskDetail(
|
||||
pageSize: number;
|
||||
}>> {
|
||||
try {
|
||||
console.log('开始调用getCrossCheckingTaskDetail,参数:', { taskId, page, pageSize });
|
||||
// console.log('开始调用getCrossCheckingTaskDetail,参数:', { taskId, page, pageSize });
|
||||
|
||||
// 获取任务的文档列表
|
||||
const taskDocumentsResponse = await getTaskDocuments(taskId, page, pageSize, jwtToken);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 验证用户是否有权访问指定文档
|
||||
*
|
||||
* 权限验证逻辑:
|
||||
* 1. 检查文档是否属于指定的任务
|
||||
* 2. 检查用户是否是该任务的参与者(评审员或发起人)
|
||||
* 3. 防止用户通过修改 URL 参数访问未授权的文档
|
||||
*/
|
||||
|
||||
import { postgrestGet } from "../postgrest-client";
|
||||
|
||||
interface DocumentAccessCheckParams {
|
||||
documentId: string | number;
|
||||
taskId: string | number;
|
||||
userId: number;
|
||||
jwtToken?: string;
|
||||
}
|
||||
|
||||
interface DocumentAccessCheckResult {
|
||||
hasAccess: boolean;
|
||||
reason?: string;
|
||||
userRole?: 'assigner' | 'assignee' | 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文档访问权限
|
||||
* @param params 验证参数
|
||||
* @returns 验证结果
|
||||
*/
|
||||
export async function verifyDocumentAccess(
|
||||
params: DocumentAccessCheckParams
|
||||
): Promise<DocumentAccessCheckResult> {
|
||||
const { documentId, taskId, userId, jwtToken } = params;
|
||||
|
||||
try {
|
||||
// 1. 检查文档是否属于该任务(通过 cross_task_document_mapping 表)
|
||||
const documentMappingResponse = await postgrestGet('cross_task_document_mapping', {
|
||||
select: 'task_id,document_id',
|
||||
filter: {
|
||||
task_id: `eq.${taskId}`,
|
||||
document_id: `eq.${documentId}`
|
||||
},
|
||||
token: jwtToken
|
||||
});
|
||||
|
||||
if (documentMappingResponse.error) {
|
||||
console.error('❌ [verifyDocumentAccess] 查询文档-任务映射失败:', documentMappingResponse.error);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: '查询文档-任务映射失败'
|
||||
};
|
||||
}
|
||||
|
||||
// 提取数据
|
||||
const mappingData = Array.isArray(documentMappingResponse.data)
|
||||
? documentMappingResponse.data
|
||||
: [];
|
||||
|
||||
// 文档不属于该任务
|
||||
if (mappingData.length === 0) {
|
||||
console.warn(`⚠️ [verifyDocumentAccess] 文档 ${documentId} 不属于任务 ${taskId}`);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: '文档不属于该任务'
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查用户是否是该任务的参与者
|
||||
const taskResponse = await postgrestGet('cross_examination_tasks', {
|
||||
select: 'assigner_id,assignee_ids',
|
||||
filter: {
|
||||
id: `eq.${taskId}`
|
||||
},
|
||||
token: jwtToken
|
||||
});
|
||||
|
||||
if (taskResponse.error) {
|
||||
console.error('❌ [verifyDocumentAccess] 查询任务信息失败:', taskResponse.error);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: '查询任务信息失败'
|
||||
};
|
||||
}
|
||||
|
||||
const taskData = Array.isArray(taskResponse.data) ? taskResponse.data[0] : null;
|
||||
|
||||
if (!taskData) {
|
||||
console.warn(`⚠️ [verifyDocumentAccess] 任务 ${taskId} 不存在`);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: '任务不存在'
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 验证用户身份
|
||||
const isAssigner = taskData.assigner_id === userId;
|
||||
const assigneeIds = Array.isArray(taskData.assignee_ids) ? taskData.assignee_ids : [];
|
||||
const isAssignee = assigneeIds.includes(userId);
|
||||
|
||||
if (isAssigner) {
|
||||
console.log(`✅ [verifyDocumentAccess] 用户 ${userId} 是任务 ${taskId} 的发起人,允许访问文档 ${documentId}`);
|
||||
return {
|
||||
hasAccess: true,
|
||||
userRole: 'assigner'
|
||||
};
|
||||
}
|
||||
|
||||
if (isAssignee) {
|
||||
console.log(`✅ [verifyDocumentAccess] 用户 ${userId} 是任务 ${taskId} 的评审员,允许访问文档 ${documentId}`);
|
||||
return {
|
||||
hasAccess: true,
|
||||
userRole: 'assignee'
|
||||
};
|
||||
}
|
||||
|
||||
// 用户既不是发起人也不是评审员
|
||||
console.warn(`⚠️ [verifyDocumentAccess] 用户 ${userId} 无权访问任务 ${taskId} 的文档 ${documentId}`);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: '您没有权限访问该文档',
|
||||
userRole: 'none'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [verifyDocumentAccess] 验证失败:', error);
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: error instanceof Error ? error.message : '权限验证失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速验证文档访问权限(仅返回布尔值)
|
||||
* @param params 验证参数
|
||||
* @returns 是否有权限访问
|
||||
*/
|
||||
export async function canAccessDocument(
|
||||
params: DocumentAccessCheckParams
|
||||
): Promise<boolean> {
|
||||
const result = await verifyDocumentAccess(params);
|
||||
return result.hasAccess;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface DocumentTypeCreateDTO {
|
||||
name: string;
|
||||
description?: string;
|
||||
group_ids: number[]; // 改为 number[] 数组
|
||||
code: string | null;
|
||||
entry_module_id?: number | null;
|
||||
llm_extraction_template_id?: number | null;
|
||||
vlm_extraction_template_id?: number | null;
|
||||
@@ -236,6 +237,7 @@ export async function createDocumentType(
|
||||
name: documentType.name.trim(),
|
||||
description: documentType.description || '',
|
||||
entry_module_id: documentType.entry_module_id || null,
|
||||
code: documentType.code || null,
|
||||
group_ids: documentType.group_ids,
|
||||
llm_extraction_template_id: documentType.llm_extraction_template_id || null,
|
||||
vlm_extraction_template_id: documentType.vlm_extraction_template_id || null,
|
||||
@@ -305,6 +307,7 @@ export async function updateDocumentType(
|
||||
name: documentType.name.trim(),
|
||||
description: documentType.description || '',
|
||||
entry_module_id: documentType.entry_module_id || null,
|
||||
code: documentType.code || null,
|
||||
group_ids: documentType.group_ids,
|
||||
llm_extraction_template_id: documentType.llm_extraction_template_id || null,
|
||||
vlm_extraction_template_id: documentType.vlm_extraction_template_id || null,
|
||||
|
||||
@@ -1023,7 +1023,7 @@ export async function getReviewPoints_fromApi(fileId: string, request: Request)
|
||||
// 成功响应
|
||||
if (response.data) {
|
||||
// console.log('✅ [getReviewPoints_fromApi] API调用成功,返回数据结构:', JSON.stringify({
|
||||
// // 评查点数量: response.data.data?.length || 0,
|
||||
// 评查点数量: response.data.data?.length || 0,
|
||||
// // 统计信息: response.data.stats,
|
||||
// // 评查信息: response.data.reviewInfo,
|
||||
// 有文档数据: response.data.document,
|
||||
|
||||
@@ -62,7 +62,7 @@ export function DocumentListModal({
|
||||
try {
|
||||
// 更新文档状态,传递JWT
|
||||
const updatedFile = await updateDocumentAuditStatus(fileId, 2, frontendJWT);
|
||||
console.log('更新后的文档状态:', updatedFile);
|
||||
// console.log('更新后的文档状态:', updatedFile);
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态时出错:', error);
|
||||
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
|
||||
@@ -182,7 +182,7 @@ interface ReviewPointsListProps {
|
||||
reviewPoints: ReviewPoint[];
|
||||
statistics: Statistics;
|
||||
activeReviewPointResultId: string | null;
|
||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[]) => void;
|
||||
onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void;
|
||||
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
scoringProposals?: ScoringProposal[];
|
||||
jwtToken?: string; // 添加JWT token参数
|
||||
@@ -779,16 +779,17 @@ export function ReviewPointsList({
|
||||
}
|
||||
// 打印最终请求体
|
||||
// console.log('最终请求体:', data);
|
||||
// console.log('jwtToken:', jwtToken);
|
||||
// 用 axios + application/json 提交
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userInfo.frontend_jwt}`,
|
||||
'Authorization': `Bearer ${jwtToken}`,
|
||||
}
|
||||
});
|
||||
const result = response.data;
|
||||
if (response.status === 200) {
|
||||
if (result.code === 200 || result.code === 0) {
|
||||
toastService.success('意见提交成功');
|
||||
|
||||
// 创建新的提案对象
|
||||
@@ -814,11 +815,24 @@ export function ReviewPointsList({
|
||||
|
||||
handleCloseOpinionModal();
|
||||
} else {
|
||||
toastService.error(result.detail || '提交意见失败');
|
||||
throw new Error(result.msg || '提交意见失败')
|
||||
// toastService.error(result.msg || '提交意见失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交意见失败:', error);
|
||||
toastService.error('提交意见失败,请稍后重试');
|
||||
|
||||
// 正确处理 axios 错误响应
|
||||
let errorMessage = '提交意见失败,请稍后重试';
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// 从 axios 错误响应中提取 msg 字段
|
||||
errorMessage = error.response.data.msg || errorMessage;
|
||||
} else if (error instanceof Error) {
|
||||
// 处理普通 Error 对象
|
||||
errorMessage = error.message || errorMessage;
|
||||
}
|
||||
|
||||
toastService.error(errorMessage);
|
||||
}
|
||||
setIsSubmittingOpinion(false);
|
||||
};
|
||||
@@ -1407,7 +1421,7 @@ export function ReviewPointsList({
|
||||
for (const item of chain) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
hasPage = true;
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1421,7 +1435,7 @@ export function ReviewPointsList({
|
||||
// 遍历chain找到第一个有效的page
|
||||
for (const item of chain) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1461,11 +1475,11 @@ export function ReviewPointsList({
|
||||
// 假设onReviewPointSelect在作用域内可用
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions);
|
||||
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||||
@@ -1544,11 +1558,11 @@ export function ReviewPointsList({
|
||||
if (chain[0].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions);
|
||||
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions, chain[0].data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||
@@ -1570,11 +1584,11 @@ export function ReviewPointsList({
|
||||
if (chain[1].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions);
|
||||
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||
@@ -1710,9 +1724,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1721,7 +1735,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1832,9 +1848,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -1844,9 +1860,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]));
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -2404,29 +2420,26 @@ export function ReviewPointsList({
|
||||
<>
|
||||
<div className="relative">
|
||||
{/* 悬浮的意见数量显示 - 固定在左侧 */}
|
||||
<button
|
||||
className="absolute left-[-35px] top-16 z-10 group cursor-pointer"
|
||||
<button
|
||||
className="absolute left-[-35px] top-16 z-10 cursor-pointer"
|
||||
onClick={() => handleOpenOpinionListModal(reviewPoints[0])}
|
||||
type="button"
|
||||
aria-label="查看意见列表"
|
||||
>
|
||||
{/* 默认状态:竖向排列,窄宽度 */}
|
||||
<div className="flex flex-col items-center bg-blue-50 px-2 py-2 rounded-lg border border-blue-200 shadow-md transition-all duration-300 group-hover:scale-0 group-hover:opacity-0 origin-top-right">
|
||||
<i className="ri-chat-1-line text-blue-600 text-base"></i>
|
||||
<span className="text-base text-blue-600 font-bold leading-tight whitespace-nowrap">{scoringProposals.length}</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">条</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">意</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">见</span>
|
||||
</div>
|
||||
|
||||
{/* 悬浮状态:横向排列,显示图标,数字放大 */}
|
||||
<div className="absolute top-0 right-0 opacity-0 scale-0 group-hover:opacity-100 group-hover:scale-100 flex items-center bg-blue-50 px-3 py-2 rounded-lg border border-blue-200 shadow-lg transition-all duration-300 origin-top-right">
|
||||
<div className="flex flex-col">
|
||||
<i className="ri-chat-1-line text-blue-600 text-base"></i>
|
||||
<span className="text-xl text-blue-600 font-bold">{scoringProposals.length}</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">条</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">意</span>
|
||||
<span className="text-xs text-blue-500 leading-tight whitespace-wrap">见</span>
|
||||
<div className={`relative flex flex-col items-center bg-gradient-to-br from-blue-50 to-blue-100 px-2 py-2 rounded-lg border border-blue-300 shadow-md transition-all duration-200 ease-out hover:scale-110 hover:shadow-xl active:scale-95 ${scoringProposals.length === 0 ? 'opacity-50' : 'opacity-100'}`}>
|
||||
{/* 脉动提示点 - 仅当有意见时显示 */}
|
||||
{scoringProposals.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
|
||||
</span>
|
||||
)}
|
||||
<i className="ri-chat-1-line text-blue-600 text-lg mb-0.5"></i>
|
||||
<span className="text-lg text-blue-700 font-bold leading-tight">{scoringProposals.length}</span>
|
||||
<div className="flex flex-col items-center text-[10px] text-blue-600 leading-tight mt-0.5">
|
||||
<span>条</span>
|
||||
<span>意</span>
|
||||
<span>见</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -2656,14 +2669,16 @@ export function ReviewPointsList({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table
|
||||
columns={[
|
||||
<div style={{ minHeight: '500px', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, minHeight: '450px' }}>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: "评查点名称",
|
||||
key: "evaluation_point_name",
|
||||
width: "15%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="text-sm">{record.evaluation_point_name}</div>
|
||||
<div className="text-sm text-left py-1">{record.evaluation_point_name}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -2671,7 +2686,7 @@ export function ReviewPointsList({
|
||||
key: "problem_message",
|
||||
width: "18%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="text-sm text-left">{record.problem_message}</div>
|
||||
<div className="text-sm text-left py-1">{record.problem_message}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -2682,14 +2697,14 @@ export function ReviewPointsList({
|
||||
const reason = record.reason || '';
|
||||
const display = reason.length > 20 ? reason.slice(0, 20) + '...' : reason;
|
||||
return (
|
||||
<span title={reason}>{display}</span>
|
||||
<div className="text-sm text-left py-1" title={reason}>{display}</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "调整分数",
|
||||
key: "proposed_score",
|
||||
width: "5%",
|
||||
width: "8%",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<span className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
@@ -2700,8 +2715,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "投票人",
|
||||
key: "votes",
|
||||
width: "22%",
|
||||
align: "center" as const,
|
||||
width: "24%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
// 投票类型配置
|
||||
const voterGroups = [
|
||||
@@ -2728,19 +2742,20 @@ export function ReviewPointsList({
|
||||
}
|
||||
];
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 py-1 min-w-[120px]">
|
||||
<div className="flex flex-col items-start gap-1.5 py-1 w-full">
|
||||
{voterGroups.map((group) => (
|
||||
Array.isArray(group.voters) && group.voters.length > 0 && (
|
||||
<div key={group.type} className="flex flex-wrap gap-1">
|
||||
<div key={group.type} className="flex items-start gap-1 w-full">
|
||||
{group.voters.map((name, idx) => (
|
||||
<span
|
||||
key={`${group.type}-${name}-${idx}`}
|
||||
className={`
|
||||
px-1.5 py-0.5 rounded text-xs font-medium
|
||||
${group.color} ${group.bg} ${group.border}
|
||||
whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px]
|
||||
whitespace-nowrap
|
||||
transition-all hover:scale-[1.03] hover:shadow-sm
|
||||
`}
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
@@ -2757,9 +2772,10 @@ export function ReviewPointsList({
|
||||
key: "proposer",
|
||||
width: "8%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="flex items-center justify-center text-left">
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px] transition-all hover:scale-[1.03] hover:shadow-sm"
|
||||
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap transition-all hover:scale-[1.03] hover:shadow-sm"
|
||||
title={record.proposer}
|
||||
>
|
||||
{record.proposer}
|
||||
</span>
|
||||
@@ -2769,15 +2785,16 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "发起时间",
|
||||
key: "created_at",
|
||||
width: "12%",
|
||||
width: "8%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="text-sm text-left">{record.created_at}</div>
|
||||
<div className="text-sm text-left py-1">{record.created_at}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "投票状态",
|
||||
key: "opinion_status",
|
||||
width: "12%",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
let label = '';
|
||||
let color = '';
|
||||
@@ -2796,7 +2813,7 @@ export function ReviewPointsList({
|
||||
color = 'text-yellow-600';
|
||||
break;
|
||||
}
|
||||
return <span className={`font-bold ${color}`}>{label}</span>;
|
||||
return <span className={`text-sm font-bold ${color}`}>{label}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2812,25 +2829,27 @@ export function ReviewPointsList({
|
||||
}
|
||||
}
|
||||
]}
|
||||
dataSource={opinionListData}
|
||||
rowKey="proposal_id"
|
||||
emptyText="暂无意见数据"
|
||||
className="opinion-list-table"
|
||||
/>
|
||||
dataSource={opinionListData}
|
||||
rowKey="proposal_id"
|
||||
emptyText="暂无意见数据"
|
||||
className="opinion-list-table"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
{opinionListTotal > 0 && (
|
||||
<Pagination
|
||||
{/* 分页组件 */}
|
||||
{opinionListTotal > 0 && (
|
||||
<Pagination
|
||||
currentPage={opinionListCurrentPage}
|
||||
total={opinionListTotal}
|
||||
pageSize={opinionListPageSize}
|
||||
onChange={handleOpinionListPageChange}
|
||||
onPageSizeChange={handleOpinionListPageSizeChange}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[5,10, 20, 30, 50]}
|
||||
/>
|
||||
)}
|
||||
onChange={handleOpinionListPageChange}
|
||||
onPageSizeChange={handleOpinionListPageSizeChange}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[5,10, 20, 30, 50]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,13 @@ import '../../styles/components/chat-with-llm/index.css';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
// 扩展 Window 接口以支持自定义属性
|
||||
declare global {
|
||||
interface Window {
|
||||
hasSetInitialSidebarState?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主聊天组件
|
||||
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
|
||||
@@ -348,6 +355,11 @@ export default function Chat() {
|
||||
if (conversationId !== currConversationId) {
|
||||
setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID);
|
||||
}
|
||||
|
||||
// 移动端选中对话后自动隐藏侧边栏
|
||||
if (isMobile && !sidebarCollapsed) {
|
||||
setSidebarCollapsed(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -485,7 +497,15 @@ export default function Chat() {
|
||||
// 检查屏幕尺寸
|
||||
useEffect(() => {
|
||||
const checkScreenSize = () => {
|
||||
setIsMobile(window.innerWidth < 992);
|
||||
const isMobileDevice = window.innerWidth < 992;
|
||||
setIsMobile(isMobileDevice);
|
||||
|
||||
// 移动端默认隐藏侧边栏,桌面端默认显示
|
||||
// 只在初次加载时设置,避免影响用户的手动切换
|
||||
if (!window.hasSetInitialSidebarState) {
|
||||
setSidebarCollapsed(isMobileDevice);
|
||||
window.hasSetInitialSidebarState = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始检查
|
||||
@@ -537,7 +557,7 @@ export default function Chat() {
|
||||
const conversationIntroduction = currConversationInfo?.introduction || '';
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row' }}>
|
||||
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
||||
{/* 移动端遮罩层 */}
|
||||
{!sidebarCollapsed && isMobile && (
|
||||
<div
|
||||
@@ -546,6 +566,24 @@ export default function Chat() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ChatSidebar 隐藏时显示的展开按钮 */}
|
||||
{sidebarCollapsed && (
|
||||
<button
|
||||
onClick={handleSidebarToggle}
|
||||
className="fixed left-0 top-1/2 -translate-y-1/2 z-[998] bg-white hover:bg-gray-100 shadow-lg rounded-r-lg px-2 py-4 transition-all duration-200 border border-l-0 border-gray-200"
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
aria-label="展开对话列表"
|
||||
>
|
||||
<i className="ri-menu-unfold-line text-lg text-gray-600"></i>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<ChatSidebar
|
||||
ref={sidebarRef}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from '@remix-run/react';
|
||||
import type { UserRole } from '~/root';
|
||||
import { getUserRoutesByRole, mapUserRoleToRoleKey, type MenuItem } from '~/api/auth/user-routes';
|
||||
import { DOCUMENT_URL } from '~/config/api-config';
|
||||
|
||||
interface SidebarProps {
|
||||
onToggle: () => void;
|
||||
@@ -320,7 +321,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||||
{selectedModulePicPath && (
|
||||
<img
|
||||
src={selectedModulePicPath}
|
||||
src={selectedModuleName === '智慧法务大模型' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
|
||||
alt={selectedModuleName}
|
||||
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
||||
/>
|
||||
|
||||
@@ -211,6 +211,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
// console.log('[FilePreview] 渲染PDF预览', { real_path, targetPage, charPositions });
|
||||
// console.log('[FilePreview] 渲染PDF预览', { fileContent });
|
||||
const pageOffset = fileContent.ocrResult?.__meta?.page_offset || fileContent.ocr_result?.__meta?.page_offset || 0;
|
||||
// console.log('pageOffset', pageOffset)
|
||||
return (
|
||||
<PdfPreview
|
||||
filePath={real_path}
|
||||
|
||||
@@ -37,12 +37,17 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
|
||||
// 主要
|
||||
// 梅州
|
||||
'51703': {
|
||||
baseUrl: 'http://172.16.0.55:8073',
|
||||
documentUrl: 'http://172.16.0.55:8073/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8073/admin/documents',
|
||||
// baseUrl: 'http://172.16.0.55:8073',
|
||||
// documentUrl: 'http://172.16.0.55:8073/docauditai/',
|
||||
// uploadUrl: 'http://172.16.0.55:8073/admin/documents',
|
||||
// collaboraUrl: 'http://172.16.0.81:9980',
|
||||
// appUrl: 'http://172.16.0.34:51703',
|
||||
|
||||
collaboraUrl: 'http://172.16.0.81:9980',
|
||||
appUrl: 'http://172.16.0.34:51703',
|
||||
baseUrl: 'http://10.79.97.17:8000',
|
||||
documentUrl: 'http://10.79.97.17:8000/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8000/admin/documents',
|
||||
collaboraUrl: 'http://10.79.97.17:9980',
|
||||
appUrl: 'http://10.79.97.17:51703',
|
||||
|
||||
oauth: {
|
||||
redirectUri: 'http://10.79.97.17:51703/callback'
|
||||
@@ -119,7 +124,8 @@ const configs: Record<string, ApiConfig> = {
|
||||
uploadUrl: 'http://172.16.0.55:8073/admin/documents',
|
||||
|
||||
collaboraUrl: 'http://172.16.0.81:9980',
|
||||
appUrl: 'http://172.16.0.34:51703',
|
||||
// appUrl: 'http://172.16.0.34:51709',
|
||||
appUrl: 'http://172.16.0.34:5173',
|
||||
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
|
||||
@@ -210,6 +210,7 @@ export default function Index() {
|
||||
} else if (module.name === '智慧法务大模型') {
|
||||
// 智慧法务大模型 → 跳转到 AI 对话
|
||||
targetPath = '/chat-with-llm/chat';
|
||||
sessionStorage.setItem('selectedModulePicPath', '/images/icon_assistant.png')
|
||||
// console.log('📌 [Index] 智慧法务大模型,跳转到:', targetPath);
|
||||
} else {
|
||||
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
|
||||
@@ -291,10 +292,12 @@ export default function Index() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 🔑 设置标志:表示用户通过交叉评查入口进入
|
||||
sessionStorage.setItem('crossCheckingMode', 'true');
|
||||
sessionStorage.setItem('selectedModuleName', '交叉评查')
|
||||
sessionStorage.setItem('selectedModulePicPath', '/images/icon_cross@2x.png')
|
||||
// 清除模块相关的标志(因为不是从入口模块进入)
|
||||
sessionStorage.removeItem('selectedModuleId');
|
||||
sessionStorage.removeItem('selectedModuleName');
|
||||
sessionStorage.removeItem('selectedModulePicPath');
|
||||
// sessionStorage.removeItem('selectedModuleName');
|
||||
// sessionStorage.removeItem('selectedModulePicPath');
|
||||
// 清除系统设置模式标志
|
||||
sessionStorage.removeItem('settingsMode');
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
// 配置路由 handle,隐藏主布局的 sidebar(因为聊天页面有自己的 ChatSidebar)
|
||||
export const handle = {
|
||||
hideSidebar: true
|
||||
};
|
||||
|
||||
/**
|
||||
* 聊天主页面
|
||||
* 实现单页面应用模式,所有会话切换都在同一页面内完成
|
||||
|
||||
@@ -206,9 +206,9 @@ const statusConfig = {
|
||||
};
|
||||
|
||||
// 任务类型标签配置
|
||||
const taskTypeConfig = {
|
||||
[CrossCheckingTaskType.CITY]: { label: '市级交叉评查', color: 'green' as const },
|
||||
[CrossCheckingTaskType.COUNTY]: { label: '下级交叉评查', color: 'orange' as const }
|
||||
const taskTypeConfig: Record<string, { label: string; color: 'green' | 'orange' }> = {
|
||||
[CrossCheckingTaskType.CITY]: { label: '市局间交叉评查', color: 'green' as const },
|
||||
[CrossCheckingTaskType.DISTRICT]: { label: '区局间交叉评查', color: 'orange' as const }
|
||||
};
|
||||
|
||||
// 案卷类型标签配置
|
||||
@@ -248,6 +248,11 @@ export default function CrossCheckingIndex() {
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 客户端调式日志
|
||||
// useEffect(()=>{
|
||||
// console.log('[CrossCheckingIndex] loaderData.tasks', loaderData.tasks)
|
||||
// },[loaderData])
|
||||
|
||||
// 获取进度条样式类
|
||||
const getProgressClass = (progress: number) => {
|
||||
if (progress === 0) return 'low';
|
||||
@@ -603,7 +608,7 @@ export default function CrossCheckingIndex() {
|
||||
align: "center" as const,
|
||||
width: "10%",
|
||||
render: (_: unknown, record: CrossCheckingTask) => {
|
||||
const config = taskTypeConfig[record.taskType];
|
||||
const config = taskTypeConfig[record.taskType] || { label: record.taskType, color: 'gray' as const };
|
||||
return (
|
||||
<Tag color={config.color}>
|
||||
{config.label}
|
||||
@@ -613,10 +618,36 @@ export default function CrossCheckingIndex() {
|
||||
},
|
||||
{
|
||||
title: "评查地区",
|
||||
dataIndex: "evaluationRegion" as keyof CrossCheckingTask,
|
||||
key: "evaluationRegion",
|
||||
align: "left" as const,
|
||||
width: "16%"
|
||||
width: "16%",
|
||||
render: (_: unknown, record: CrossCheckingTask) => {
|
||||
const regions = record.evaluationRegion;
|
||||
|
||||
// 如果不是数组,直接显示字符串
|
||||
if (!Array.isArray(regions)) {
|
||||
return <span className="text-sm">{regions || '-'}</span>;
|
||||
}
|
||||
|
||||
// 如果是空数组
|
||||
if (regions.length === 0) {
|
||||
return <span className="text-sm text-gray-400">-</span>;
|
||||
}
|
||||
|
||||
// 渲染为标签列表
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 items-start">
|
||||
{regions.map((region, index) => (
|
||||
<Tag
|
||||
key={`${region}-${index}`}
|
||||
color="cyan"
|
||||
>
|
||||
{region}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "评查进度",
|
||||
|
||||
@@ -26,7 +26,7 @@ import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } f
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||||
import { getReviewPoints, updateReviewResult} from "~/api/evaluation_points/reviews";
|
||||
import { getReviewPoints, updateReviewResult, getReviewPoints_fromApi} from "~/api/evaluation_points/reviews";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
|
||||
|
||||
@@ -199,8 +199,37 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取评查点数据,传递request对象
|
||||
const reviewData = await getReviewPoints(id, request);
|
||||
// 🔒 安全验证:检查用户是否有权限访问该文档
|
||||
if (!userInfo?.user_id) {
|
||||
return Response.json({
|
||||
result: false,
|
||||
message: '用户身份验证失败,请重新登录'
|
||||
}, { status: 401 });
|
||||
}
|
||||
|
||||
// const { verifyDocumentAccess } = await import("~/api/cross-checking/verify-document-access");
|
||||
// const accessCheck = await verifyDocumentAccess({
|
||||
// documentId: id,
|
||||
// taskId: taskId,
|
||||
// userId: userInfo.user_id,
|
||||
// jwtToken: frontendJWT
|
||||
// });
|
||||
|
||||
// if (!accessCheck.hasAccess) {
|
||||
// console.warn(`⚠️ [Loader] 用户 ${userInfo.user_id} 尝试访问未授权文档 ${id},原因: ${accessCheck.reason}`);
|
||||
// return Response.json({
|
||||
// result: false,
|
||||
// message: accessCheck.reason || '您没有权限访问该文档'
|
||||
// }, { status: 403 });
|
||||
// }
|
||||
|
||||
// console.log(`✅ [Loader] 用户 ${userInfo.user_id} (${accessCheck.userRole}) 访问文档 ${id} - 权限验证通过`);
|
||||
|
||||
// 对接接口,新的获取评查点结果的方法
|
||||
const reviewData = await getReviewPoints_fromApi(id, request)
|
||||
|
||||
// 获取评查点数据,传递request对象 旧获取评查点结果的方法
|
||||
// const reviewData = await getReviewPoints(id, request);
|
||||
|
||||
// 获取当前登录用户是否是发起人
|
||||
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
|
||||
@@ -313,12 +342,9 @@ export default function CrossCheckingResult() {
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
// 添加组件挂载/卸载日志
|
||||
// useEffect(() => {
|
||||
// console.log('[组件] CrossCheckingResult 挂载');
|
||||
// return () => {
|
||||
// console.log('[组件] CrossCheckingResult 卸载');
|
||||
// };
|
||||
// }, []);
|
||||
useEffect(() => {
|
||||
console.log('[组件] CrossCheckingResult', isProposer);
|
||||
}, [isProposer]);
|
||||
|
||||
// 同步外部scoring_proposals到本地状态
|
||||
useEffect(() => {
|
||||
@@ -536,18 +562,18 @@ export default function CrossCheckingResult() {
|
||||
|
||||
// 使用ref防止重复点击,避免触发状态更新
|
||||
if (isProcessingRef.current) {
|
||||
console.log('[完成评查] 正在处理中,跳过');
|
||||
// console.log('[完成评查] 正在处理中,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[完成评查] 标记为处理中');
|
||||
// console.log('[完成评查] 标记为处理中');
|
||||
isProcessingRef.current = true;
|
||||
|
||||
// 1. 先检查未投票(不触发loading状态更新,避免重新渲染)
|
||||
console.log('[完成评查] 开始检查未投票提案');
|
||||
// console.log('[完成评查] 开始检查未投票提案');
|
||||
const checkRes = await checkProposalVotes(document.id, jwtToken);
|
||||
console.log("[完成评查] 检查结果:", checkRes);
|
||||
// console.log("[完成评查] 检查结果:", checkRes);
|
||||
|
||||
if (checkRes.error) {
|
||||
toastService.error(checkRes.error);
|
||||
@@ -582,11 +608,11 @@ export default function CrossCheckingResult() {
|
||||
}
|
||||
|
||||
// 4. 重置处理状态标记,准备显示模态框(不触发状态更新)
|
||||
console.log('[完成评查] 重置处理标记,准备显示模态框');
|
||||
// console.log('[完成评查] 重置处理标记,准备显示模态框');
|
||||
isProcessingRef.current = false;
|
||||
|
||||
// 5. 弹出模态框
|
||||
console.log('[完成评查] 显示确认模态框');
|
||||
// console.log('[完成评查] 显示确认模态框');
|
||||
messageService.show({
|
||||
title: '提示',
|
||||
message: modalMessage,
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
type CrossCheckingUploadedFile,
|
||||
generateFileId,
|
||||
formatFileSize,
|
||||
batchUploadAndAssignCrossCheckingFiles
|
||||
batchUploadAndAssignCrossCheckingFiles,
|
||||
createCrossReviewTask
|
||||
} from "~/api/cross-checking/cross-files-upload";
|
||||
import {
|
||||
getCrossCheckingDocumentTypes,
|
||||
@@ -147,48 +148,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建交叉评查任务
|
||||
* @param taskData 任务数据
|
||||
* @param token JWT Token
|
||||
* @returns 创建结果
|
||||
*/
|
||||
export async function createCrossReviewTask(taskData: {
|
||||
documentIds: number[];
|
||||
userIds: number[];
|
||||
assignerId: number;
|
||||
taskName: string;
|
||||
docType: string;
|
||||
}, token: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/tasks/assign`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
document_ids: taskData.documentIds,
|
||||
user_ids: taskData.userIds,
|
||||
assigner_id: taskData.assignerId,
|
||||
task_name: taskData.taskName,
|
||||
doc_type: taskData.docType
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('任务创建成功:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('创建任务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const caseType = formData.get("caseType") as string;
|
||||
@@ -217,7 +176,7 @@ export default function CrossCheckingUpload() {
|
||||
const [taskInfo, setTaskInfo] = useState({
|
||||
name: '',
|
||||
date: '',
|
||||
type: '市局间交叉评查',
|
||||
type: 'CITY', // 使用枚举值,默认为市局间交叉评查
|
||||
});
|
||||
// 步骤2状态
|
||||
const [groupChecked, setGroupChecked] = useState<string[]>(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []);
|
||||
@@ -441,6 +400,7 @@ export default function CrossCheckingUpload() {
|
||||
try {
|
||||
// 获取选中的文档类型信息
|
||||
const selectedDocType = documentTypes?.find((dt: DocumentType) => dt.id === selectedDocTypeId);
|
||||
|
||||
if (!selectedDocType) {
|
||||
toastService.error("无效的案卷类型");
|
||||
return;
|
||||
@@ -460,6 +420,23 @@ export default function CrossCheckingUpload() {
|
||||
return;
|
||||
}
|
||||
|
||||
// const requireParam = {
|
||||
// filesToUpload: filesToUpload,
|
||||
// selectedDocTypeId: selectedDocTypeId,
|
||||
// priority: priority,
|
||||
// documentNumber: documentNumber,
|
||||
// remark: remark,
|
||||
// isTestDocument: isTestDocument,
|
||||
// userIds: userIds,
|
||||
// taskInfo_name: taskInfo.name,
|
||||
// selectedDocType_name: selectedDocType.code,
|
||||
// taskInfo_type: taskInfo.type,
|
||||
// frontendJWT
|
||||
// }
|
||||
|
||||
// console.log("requireParam", requireParam)
|
||||
// return;
|
||||
|
||||
// 使用文档类型名称作为 doc_type
|
||||
const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
|
||||
filesToUpload,
|
||||
@@ -470,10 +447,14 @@ export default function CrossCheckingUpload() {
|
||||
isTestDocument,
|
||||
userIds,
|
||||
taskInfo.name,
|
||||
selectedDocType.name, // 使用文档类型名称
|
||||
selectedDocType.code, // 使用文档类型code
|
||||
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
|
||||
// return;
|
||||
|
||||
const { successes, failures } = uploadResult;
|
||||
|
||||
if (failures.length > 0) {
|
||||
@@ -710,8 +691,8 @@ export default function CrossCheckingUpload() {
|
||||
value={taskInfo.type}
|
||||
onChange={e => setTaskInfo({ ...taskInfo, type: e.target.value })}
|
||||
>
|
||||
<option value="市局间交叉评查">市局间交叉评查</option>
|
||||
<option value="区局间交叉评查">区局间交叉评查</option>
|
||||
<option value="CITY">市局间交叉评查</option>
|
||||
<option value="DISTRICT">区局间交叉评查</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
|
||||
@@ -324,7 +324,7 @@ export default function DocumentTypesList() {
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
{record.entry_module ? (
|
||||
<span className="type-badge">{record.entry_module.name}</span>
|
||||
<span className="entry-module-badge">{record.entry_module.name}</span>
|
||||
) : (
|
||||
<span className="text-gray-400">暂无关联入口</span>
|
||||
)}
|
||||
|
||||
@@ -134,6 +134,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
const code = formData.get("code") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const entryModuleId = formData.get("entry_module_id") as string;
|
||||
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
|
||||
@@ -171,6 +172,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 构建文档类型数据 - group_ids 转换为 number[]
|
||||
const documentTypeData = {
|
||||
name,
|
||||
code: code || null,
|
||||
description,
|
||||
group_ids: selectedGroups.map(id => parseInt(id, 10)),
|
||||
entry_module_id: entryModuleId ? parseInt(entryModuleId) : null,
|
||||
@@ -239,6 +241,7 @@ export default function DocumentTypeNew() {
|
||||
const [formData, setFormData] = useState({
|
||||
id: documentType?.id || "",
|
||||
name: documentType?.name || "",
|
||||
code: documentType?.code || "",
|
||||
description: documentType?.description || "",
|
||||
entryModuleId: documentType?.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType?.llm_extraction_template_id?.toString() || "",
|
||||
@@ -287,9 +290,11 @@ export default function DocumentTypeNew() {
|
||||
// 当文档类型数据加载完成时更新表单
|
||||
useEffect(() => {
|
||||
if (documentType) {
|
||||
console.log('documentType', documentType)
|
||||
setFormData({
|
||||
id: documentType.id,
|
||||
name: documentType.name,
|
||||
code: documentType.code || "",
|
||||
description: documentType.description,
|
||||
entryModuleId: documentType.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType.llm_extraction_template_id?.toString() || "",
|
||||
@@ -592,6 +597,24 @@ export default function DocumentTypeNew() {
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
</div>
|
||||
|
||||
{/* 文档类型编码 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-code" className="form-label">
|
||||
文档类型编码
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="type-code"
|
||||
name="code"
|
||||
className="form-input"
|
||||
placeholder="请输入文档类型编码"
|
||||
value={formData.code}
|
||||
onChange={handleInputChange}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
<div className="form-tip">用于系统内部识别的唯一编码(可选)</div>
|
||||
</div>
|
||||
|
||||
{/* 入口模块 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="entry-module" className="form-label">
|
||||
|
||||
@@ -857,6 +857,9 @@ export default function RolePermissions() {
|
||||
// 存储每个路由的 permissions(routeId -> permissions[])
|
||||
const [routePermissionsMap, setRoutePermissionsMap] = useState<Map<number, ApiPermission[]>>(new Map());
|
||||
|
||||
// 保存权限的 loading 状态
|
||||
const [savingPermissions, setSavingPermissions] = useState(false);
|
||||
|
||||
// 加载初始数据
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -1130,7 +1133,7 @@ export default function RolePermissions() {
|
||||
};
|
||||
|
||||
// v3.0: 获取HTTP方法对应的标签样式
|
||||
const getMethodTagStyle = (method: string): React.CSSProperties => {
|
||||
const getMethodTagStyle = (method: string | null | undefined): React.CSSProperties => {
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
'GET': { backgroundColor: '#e6f7ed', color: '#52c41a', border: '1px solid #b7eb8f' },
|
||||
'POST': { backgroundColor: '#e6f0ff', color: '#1890ff', border: '1px solid #91caff' },
|
||||
@@ -1138,6 +1141,12 @@ export default function RolePermissions() {
|
||||
'DELETE': { backgroundColor: '#fff1f0', color: '#f5222d', border: '1px solid #ffa39e' },
|
||||
'PATCH': { backgroundColor: '#f0f5ff', color: '#722ed1', border: '1px solid #d3adf7' }
|
||||
};
|
||||
|
||||
// 空值检查:如果 method 为 null 或 undefined,返回默认样式
|
||||
if (!method) {
|
||||
return { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
|
||||
}
|
||||
|
||||
return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
|
||||
};
|
||||
|
||||
@@ -1251,6 +1260,7 @@ export default function RolePermissions() {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingPermissions(true);
|
||||
try {
|
||||
// 1. 保存路由权限
|
||||
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||||
@@ -1284,6 +1294,8 @@ export default function RolePermissions() {
|
||||
} catch (error) {
|
||||
console.error("保存权限失败:", error);
|
||||
toastService.error("保存权限失败");
|
||||
} finally {
|
||||
setSavingPermissions(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1593,11 +1605,11 @@ export default function RolePermissions() {
|
||||
<h3>为角色 "{selectedRole.role_name}" 分配路由权限</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
icon={savingPermissions ? "ri-loader-4-line spin" : "ri-save-line"}
|
||||
onClick={handleSavePermissions}
|
||||
disabled={!isProvincialAdmin}
|
||||
disabled={!isProvincialAdmin || savingPermissions}
|
||||
>
|
||||
保存权限
|
||||
{savingPermissions ? '保存中...' : '保存权限'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -336,3 +336,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 意见列表表格样式 */
|
||||
.opinion-list-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-primary-50 text-primary-600 mr-1 mb-1;
|
||||
}
|
||||
|
||||
.document-types-page .entry-module-badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-50 text-blue-600 mr-1 mb-1;
|
||||
}
|
||||
|
||||
.document-types-page .groups-container {
|
||||
@apply flex flex-wrap gap-1 max-w-md;
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export default defineConfig({
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
// port: 5173,
|
||||
port: Number(process.env.PORT) || 51709,
|
||||
port: Number(process.env.PORT) || 5173,
|
||||
open: true,
|
||||
// open: false,
|
||||
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1
|
||||
|
||||
Reference in New Issue
Block a user