Merge branch 'Wren' into shiy-login

This commit is contained in:
2025-07-24 09:46:41 +08:00
6 changed files with 236 additions and 151 deletions
+10 -11
View File
@@ -26,15 +26,14 @@ function extractApiData<T>(responseData: unknown): T | null {
export interface SubmitOpinionRequest {
reviewPointResultId: string | number;
documentId: string | number;
auditPoint: string;
foundIssue: string;
evaluationPointId: number; // 必须是数字ID
auditOpinion: string;
deductionScore: number;
}
/**
* 提出意见的响应接口
*/
*/
export interface SubmitOpinionResponse {
success: boolean;
message: string;
@@ -117,20 +116,20 @@ export async function findIsProposer(taskId: string | number, userId: number | u
*/
export async function submitCrossCheckingOpinion(
opinionData: SubmitOpinionRequest,
jwtToken?: string
jwtToken?: string,
userInfo?: { user_id: number }
): Promise<ApiResponse<SubmitOpinionResponse>> {
try {
// 获取JWT token
const token = await safeGetJWT(jwtToken);
const requestData = {
proposer_user_id: 1,
evaluation_result_id: opinionData.reviewPointResultId,
// document_id: opinionData.documentId,
// audit_point: opinionData.auditPoint,
// found_issue: opinionData.foundIssue,
proposed_score: opinionData.deductionScore,
reason: opinionData.auditOpinion
document_id: opinionData.documentId,
evaluation_point_id: Number(opinionData.evaluationPointId), // 强制转数字
proposed_score: opinionData.deductionScore,
reason: opinionData.auditOpinion,
proposer_id: userInfo?.user_id || 1,
evaluation_result_id: opinionData.reviewPointResultId
};
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
+49 -49
View File
@@ -65,8 +65,14 @@ export interface CrossCheckingUploadedFile {
/**
* 将文件转换为二进制数据
*/
export async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
export async function uploadFileToBinary(file: File | Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
// 只保留简单类型检查和调试
if (!(file instanceof File) && !(file instanceof Blob)) {
reject(new Error(`参数必须是File或Blob对象,当前类型: ${typeof file}`));
return;
}
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
@@ -206,75 +212,69 @@ export async function uploadCrossCheckingDocument(
}
/**
* 批量上传交叉评查文件
* 批量上传并自动分配交叉评查任务(新接口适配)
* @param files 文件列表
* @param typeId 文档类型ID
* @param priority 优先级
* @param documentNumber 文档编号
* @param remark 备注信息
* @param remark 备注
* @param isTestDocument 是否为测试文档
* @returns 上传结果列表
* @param assignUserIds 需要分配的用户ID数组
* @param taskName 任务名称
* @param docType 文档类型(如 XZCF、XZXK
* @param token JWT Token
*/
export async function batchUploadCrossCheckingFiles(
export async function batchUploadAndAssignCrossCheckingFiles(
files: CrossCheckingUploadedFile[],
typeId: number,
priority: string = 'normal',
documentNumber: string = '',
remark: string = '',
isTestDocument: boolean = false
isTestDocument: boolean = false,
assignUserIds: number[],
taskName: string,
docType: string,
token: string | null = null
): Promise<{
successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}>;
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
}> {
const successes: Array<{file: CrossCheckingUploadedFile; result: CrossCheckingFileUploadResponse}> = [];
const successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}> = [];
const failures: Array<{file: CrossCheckingUploadedFile; error: string}> = [];
console.log('【交叉评查批量上传】开始批量上传文件,文件数量:', files.length);
const uploadEndpoint = '/cross_review/documents/upload_and_assign';
const uploadUrl = UPLOAD_URL + uploadEndpoint;
for (const fileInfo of files) {
try {
console.log('【交叉评查批量上传】上传文件:', fileInfo.name);
// 转换文件为二进制格式
const binaryData = await uploadFileToBinary(fileInfo.file);
// 上传文件
const result = await uploadCrossCheckingDocument(
binaryData,
fileInfo.file.name,
fileInfo.file.type,
typeId,
priority,
documentNumber,
remark,
isTestDocument,
null, // 交叉评查文件通常没有关联的文档ID
false
);
if (result.error) {
console.error('【交叉评查批量上传】文件上传失败:', fileInfo.name, result.error);
failures.push({
file: fileInfo,
error: result.error
});
} else if (result.data) {
console.log('【交叉评查批量上传】文件上传成功:', fileInfo.name);
successes.push({
file: fileInfo,
result: result.data
});
const formData = new FormData();
formData.append('file', fileInfo.file, fileInfo.name);
const uploadInfo = {
type_id: typeof typeId === 'string' ? parseInt(typeId, 10) : typeId,
evaluation_level: priority,
document_number: documentNumber || null,
remark: remark || null,
is_test_document: isTestDocument,
task_name: taskName,
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType
};
formData.append('upload_info', JSON.stringify(uploadInfo));
formData.append('assign_user_ids', JSON.stringify(assignUserIds));
const headers: HeadersInit = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const response = await fetch(uploadUrl, {
method: 'POST',
headers,
body: formData
});
const result = await response.json();
if (result && result.success) {
successes.push({ file: fileInfo, result });
} else {
failures.push({ file: fileInfo, error: result.error || '未知错误' });
}
} catch (error) {
console.error('【交叉评查批量上传】处理文件时发生错误:', fileInfo.name, error);
failures.push({
file: fileInfo,
error: error instanceof Error ? error.message : '上传失败'
});
failures.push({ file: fileInfo, error: error instanceof Error ? error.message : '上传失败' });
}
}
console.log('【交叉评查批量上传】批量上传完成,成功:', successes.length, '失败:', failures.length);
return { successes, failures };
}
+12 -22
View File
@@ -172,29 +172,19 @@ export function convertToTreeData(organizations: OrganizationNode[]): Array<{
}>;
}> {
return organizations.map(org => {
const children: Array<{
label: string;
value: string;
isUser?: boolean;
userInfo?: UserInfo;
}> = [];
// 递归处理子组织
const subOrganizations = org.children && org.children.length > 0 ? convertToTreeData(org.children) : [];
// 添加该组织下的用户
if (org.users && org.users.length > 0) {
children.push(...org.users.map(user => ({
label: user.nick_name,
value: `user_${user.id}`,
isUser: true,
userInfo: user
})));
}
// 递归处理子组织,保持原有的层级结构
if (org.children && org.children.length > 0) {
const subOrganizations = convertToTreeData(org.children);
children.push(...subOrganizations);
}
const userChildren = (org.users && org.users.length > 0)
? org.users.map(user => ({
label: user.nick_name,
value: `user_${user.id}`,
isUser: true,
userInfo: user
}))
: [];
// 合并子组织和用户
const children = [...subOrganizations, ...userChildren];
return {
label: org.ou_name,
value: org.ou_id,
@@ -31,6 +31,7 @@ import {
type OpinionActionType
} from '../../api/cross-checking/cross-file-result';
import { useFetcher, useNavigate } from '@remix-run/react';
import { API_BASE_URL } from '~/config/api-config';
// import '../../styles/components/TooltipStyles.css';
/**
@@ -86,6 +87,7 @@ export interface ReviewPoint {
id: string;
documentId?: string;
pointId?: string;
evaluationPointId?: string | number; // 新增,允许兜底
editAuditStatusId?: string | number;
editAuditStatus: number;
editAuditStatusMessage?: string; // 添加审核意见字段
@@ -439,6 +441,17 @@ export function ReviewPointsList({
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
const fetcher = useFetcher();
// 归一化 reviewPoints,确保每个点都有 id 字段
const [normalizedReviewPoints, setNormalizedReviewPoints] = useState<ReviewPoint[]>([]);
console.log('normalizedReviewPoints', normalizedReviewPoints);
useEffect(() => {
const norm = reviewPoints.map(point => ({
...point,
id: String(point.id || point.evaluationPointId || point.pointId || '') // 保证 id 为字符串且不为 undefined
}));
setNormalizedReviewPoints(norm);
}, [reviewPoints]);
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
// 将来可以用于显示相关的评分提案信息
useEffect(() => {
@@ -690,11 +703,6 @@ export function ReviewPointsList({
return;
}
if (opinionForm.deductionScore <= 0) {
toastService.error('扣分必须大于0');
return;
}
if (opinionForm.deductionScore > 100) {
toastService.error('扣分不能大于100分');
return;
@@ -705,26 +713,75 @@ export function ReviewPointsList({
return;
}
setIsSubmittingOpinion(true);
// 新增:详细打印每个校验条件
console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
console.log('校验前 opinionForm:', opinionForm);
console.log('校验前 userInfo:', userInfo);
console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
// 更严谨的校验逻辑
if (
selectedReviewPoint.documentId === undefined ||
selectedReviewPoint.pointId === undefined ||
opinionForm.deductionScore === undefined ||
opinionForm.auditOpinion === undefined ||
userInfo?.user_id === undefined ||
isNaN(Number(selectedReviewPoint.documentId)) ||
isNaN(Number(selectedReviewPoint.pointId)) ||
isNaN(Number(opinionForm.deductionScore)) ||
!String(opinionForm.auditOpinion).trim()
) {
toastService.error('请完整填写所有必填项');
setIsSubmittingOpinion(false);
return;
}
// 打印所有关键数据
console.log('selectedReviewPoint:', selectedReviewPoint);
console.log('opinionForm:', opinionForm);
console.log('userInfo:', userInfo);
// 组装后端要求的字段名和内容
const data: Record<string, any> = {
document_id: Number(selectedReviewPoint.documentId),
evaluation_point_id: Number(selectedReviewPoint.pointId),
proposed_score: Number(opinionForm.deductionScore),
reason: opinionForm.auditOpinion,
proposer_id: userInfo.user_id,
problem_message: opinionForm.foundIssue,
evaluation_result_id: Number(selectedReviewPoint.id),
};
if (selectedReviewPoint.evaluationPointId) {
data.evaluation_result_id = Number(selectedReviewPoint.evaluationPointId);
}
// 打印最终请求体
console.log('最终请求体:', data);
// 用原生 fetch + application/json 提交
try {
// 使用 fetcher 调用路由的 action
const formData = new FormData();
formData.append("intent", "submitCrossCheckingOpinion");
formData.append("reviewPointResultId", selectedReviewPoint.id);
formData.append("documentId", selectedReviewPoint.documentId || '');
formData.append("auditPoint", opinionForm.auditPoint);
formData.append("foundIssue", opinionForm.foundIssue);
formData.append("auditOpinion", opinionForm.auditOpinion);
formData.append("deductionScore", opinionForm.deductionScore.toString());
fetcher.submit(formData, { method: "POST" });
const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userInfo.frontend_jwt}`,
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
toastService.success('意见提交成功');
handleCloseOpinionModal();
} else {
toastService.error(result.detail || '提交意见失败');
}
} catch (error) {
console.error('提交意见失败:', error);
toastService.error('提交意见失败,请稍后重试');
setIsSubmittingOpinion(false);
}
setIsSubmittingOpinion(false);
};
/**
@@ -2572,10 +2629,14 @@ export function ReviewPointsList({
{
title: "调整理由",
key: "reason",
width: "25%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.reason}</div>
)
width: "15%",
render: (_: unknown, record: CrossCheckingOpinion) => {
const reason = record.reason || '';
const display = reason.length > 20 ? reason.slice(0, 20) + '...' : reason;
return (
<span title={reason}>{display}</span>
);
}
},
{
title: "调整分数",
@@ -2665,6 +2726,31 @@ export function ReviewPointsList({
<div className="text-sm text-left">{record.created_at}</div>
)
},
{
title: "投票状态",
key: "opinion_status",
width: "10%",
render: (_: unknown, record: CrossCheckingOpinion) => {
let label = '';
let color = '';
switch (record.status) {
case 'approved':
label = '通过';
color = 'text-green-600';
break;
case 'rejected':
label = '不通过';
color = 'text-red-600';
break;
case 'pending':
default:
label = '投票中';
color = 'text-yellow-600';
break;
}
return <span className={`font-bold ${color}`}>{label}</span>;
}
},
{
title: "操作",
key: "operation",
+2
View File
@@ -71,6 +71,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 获取用户会话信息
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
console.log('frontendJWT', frontendJWT);
// 获取任务列表和统计数据,传递用户信息和JWT
const [tasksResponse, statsResponse] = await Promise.all([
+54 -46
View File
@@ -14,13 +14,14 @@ import {
type CrossCheckingUploadedFile,
generateFileId,
formatFileSize,
batchUploadCrossCheckingFiles
batchUploadAndAssignCrossCheckingFiles
} from "~/api/cross-checking/cross-files-upload";
import {
getOrganizationTree,
convertToTreeData
} from "~/api/user";
import React from "react"; // Added for React.useState
import { API_BASE_URL } from '~/config/api-config';
export const meta: MetaFunction = () => {
return [
@@ -144,14 +145,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
* @param token JWT Token
* @returns 创建结果
*/
async function createCrossReviewTask(taskData: {
export async function createCrossReviewTask(taskData: {
documentIds: number[];
userIds: number[];
assignerId: number;
taskName: string;
docType: string;
}, token: string) {
try {
const response = await fetch('/admin/crossreview/tasks/assign', {
const response = await fetch(`${API_BASE_URL}/admin/cross_review/tasks/assign`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -161,7 +163,8 @@ async function createCrossReviewTask(taskData: {
document_ids: taskData.documentIds,
user_ids: taskData.userIds,
assigner_id: taskData.assignerId,
task_name: taskData.taskName
task_name: taskData.taskName,
doc_type: taskData.docType
})
});
@@ -207,7 +210,7 @@ export default function CrossCheckingUpload() {
type: '市局交叉评查',
});
// 步骤2状态
const [groupChecked, setGroupChecked] = useState<string[]>([]);
const [groupChecked, setGroupChecked] = useState<string[]>(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []);
const [userSelectionState, setUserSelectionState] = useState<UserSelectionState>({
treeData: DEFAULT_TREE,
loading: false,
@@ -419,30 +422,9 @@ export default function CrossCheckingUpload() {
setIsUploading(true);
try {
// 第一步:上传文件
console.log("开始批量上传文件:", filesToUpload.length, "个,案卷类型:", caseType);
const uploadResult = await batchUploadCrossCheckingFiles(
filesToUpload.map(f => f.file),
caseType,
priority,
isTestDocument,
frontendJWT
);
// 第一步:上传文件并自动分配任务(新接口)
console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", caseType);
const { successes, failures } = uploadResult;
if (failures.length > 0) {
toastService.error(`文件上传失败:${failures[0].error}`);
return;
}
// 第二步:创建交叉评查任务
console.log("文件上传成功,开始创建任务");
// 提取文档ID
const documentIds = successes.map(success => success.result.result?.id).filter(id => id !== undefined) as number[];
// 提取用户ID(从选中的组织架构中获取用户)
const userIds = groupChecked.filter(id => {
// 检查是否为用户ID(通常用户ID以特定前缀开头或有特定格式)
@@ -455,18 +437,31 @@ export default function CrossCheckingUpload() {
}
// 创建任务数据
const taskData = {
documentIds,
userIds,
assignerId: userInfo?.user_id || 1, // 使用当前用户ID作为分配者
taskName: taskInfo.name
const docTypeMap = {
[CaseType.ADMINISTRATIVE_PENALTY]: 'XZCF',
[CaseType.ADMINISTRATIVE_PERMIT]: 'XZXK'
};
console.log("创建任务数据:", taskData);
// 调用创建任务接口
await createCrossReviewTask(taskData, frontendJWT);
const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
filesToUpload,
CASE_TYPE_TO_TYPE_ID[caseType],
priority,
documentNumber,
remark,
isTestDocument,
userIds,
taskInfo.name,
docTypeMap[caseType] || 'XZCF',
frontendJWT
);
const { successes, failures } = uploadResult;
if (failures.length > 0) {
toastService.error(`文件上传或任务分配失败:${failures[0].error}`);
return;
}
// 任务创建成功
toastService.success("交叉评查任务创建成功!");
messageService.success(
@@ -630,14 +625,28 @@ export default function CrossCheckingUpload() {
}
}
};
loadOrganizationData();
}, [currentStep]); // 只依赖 currentStep,避免无限循环
// 在 CrossCheckingUpload 组件内添加工具函数
function findUserNameById(tree: TreeNode[], userId: string): string | null {
for (const node of tree) {
if (node.value === userId && (node as { isUser?: boolean }).isUser) {
return node.label;
}
if (node.children) {
const found = findUserNameById(node.children, userId);
if (found) return found;
}
}
return null;
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* 步骤指示器 */
{/* 步骤指示器 */}
<div className="steps-indicator">
{STEPS.map((step) => (
<div
@@ -649,7 +658,6 @@ export default function CrossCheckingUpload() {
</div>
))}
</div>
}
{/* 步骤1:创建任务 */}
{currentStep === 1 && (
@@ -741,16 +749,16 @@ export default function CrossCheckingUpload() {
{groupChecked.length > 0 ? (
<div className="space-y-2 max-h-64 overflow-y-auto">
{groupChecked.map((member, index) => {
// 处理用户选择值,支持新的API格式
let displayName = member;
let displayName: string = member;
let displayOrg = '';
if (member.startsWith('user_')) {
// 用户选择,格式为 user_123
displayName = `用户ID: ${member.replace('user_', '')}`;
// 查找真实用户名
const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', '');
displayName = userName;
displayOrg = '用户';
} else {
// 组织选择,格式为 ou_id 或 ou_id-ou_id
// 组织
const parts = member.split('-');
displayName = parts[parts.length - 1];
displayOrg = parts.slice(0, -1).join(' - ') || '组织';