Merge remote-tracking branch 'origin/shiy-login' into PingChuan

This commit is contained in:
PingChuan
2025-12-02 15:40:32 +08:00
23 changed files with 606 additions and 176 deletions
+59 -10
View File
@@ -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
};
}
}
+80 -2
View File
@@ -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字符串
+9 -7
View File
@@ -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;
}
+3
View File
@@ -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,
+1 -1
View File
@@ -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>
+40 -2
View File
@@ -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 -1
View File
@@ -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'}`}
/>
+1
View File
@@ -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}
+12 -6
View File
@@ -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服务器地址
+5 -2
View File
@@ -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');
}
+5
View File
@@ -31,6 +31,11 @@ export const meta: MetaFunction = () => {
];
};
// 配置路由 handle,隐藏主布局的 sidebar(因为聊天页面有自己的 ChatSidebar
export const handle = {
hideSidebar: true
};
/**
* 聊天主页面
* 实现单页面应用模式,所有会话切换都在同一页面内完成
+37 -6
View File
@@ -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: "评查进度",
+41 -15
View File
@@ -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,
+28 -47
View File
@@ -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">
+1 -1
View File
@@ -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>
)}
+23
View File
@@ -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">
+16 -4
View File
@@ -857,6 +857,9 @@ export default function RolePermissions() {
// 存储每个路由的 permissionsrouteId -> 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>
+5
View File
@@ -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
View File
@@ -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