| | |
| | | } |
| | | }; |
| | | |
| | | // 终极优化:handleImport 方法(强制确保有效File对象,去掉有问题的签名) |
| | | // 完全重写:handleImport 方法,只使用 defHttp 发送请求 |
| | | const handleImport = async () => { |
| | | try { |
| | | // 1. 前置表单验证(严格校验) |
| | | // 1. 前置表单验证 |
| | | if (!formState.contractId) { |
| | | createMessage.error('请选择合同'); |
| | | return; |
| | |
| | | importErrors.value = []; |
| | | showResult.value = false; |
| | | |
| | | // 3. 强制获取 AntD Upload 原始 File 对象(核心!确保是有效File实例) |
| | | // 3. 获取文件对象 |
| | | const file = fileList.value[0]; |
| | | const fileObj = file.originFileObj; // 使用 originFileObj,这是 AntD Upload 组件的原始文件对象 |
| | | const fileObj = file.originFileObj; |
| | | |
| | | // 双重校验:确保 fileObj 是 File 实例 |
| | | if (!(fileObj instanceof File)) { |
| | | createMessage.error('获取文件失败,请重新选择文件'); |
| | | importLoading.value = false; |
| | | return; |
| | | } |
| | | console.log('有效File对象:', fileObj); |
| | | |
| | | console.log('===== 批量导入请求信息 ====='); |
| | | console.log('文件对象:', fileObj); |
| | | console.log('文件名:', fileObj.name); |
| | | console.log('文件大小:', fileObj.size); |
| | | |
| | | // 4. 构建业务参数 |
| | | const importParam = { |
| | | contractId: formState.contractId, |
| | | ranking: "3", // 签约排名写死为3 |
| | | status: "7", // 状态写死为7 |
| | | remark: "批量订单", // 备注信息写死为批量订单 |
| | | month: "12", // 合作月份写死为12个月 |
| | | price: "0", // 价格写死为0 |
| | | acceptindicator: "80" // 验收指标写死为80 |
| | | ranking: "3", |
| | | status: "1", |
| | | remark: "批量订单", |
| | | month: "3", |
| | | price: "3000", |
| | | acceptindicator: "80", |
| | | // 尝试在importParam中添加文案编辑参数 |
| | | changer: formState.editor, |
| | | editorId: formState.editor, |
| | | userId: formState.editor |
| | | }; |
| | | console.log('业务参数:', importParam); |
| | | |
| | | // 5. 构建 FormData - 确保正确创建 FormData 对象 |
| | | // 5. 构建 FormData |
| | | const formData = new FormData(); |
| | | |
| | | // 验证 fileObj 是真正的 File 对象 |
| | | if (!(fileObj instanceof File) && !(fileObj instanceof Blob)) { |
| | | createMessage.error('文件对象无效,请重新选择文件'); |
| | | importLoading.value = false; |
| | | return; |
| | | } |
| | | |
| | | // 添加文件到 FormData(关键:必须使用 File 对象,不能是其他格式) |
| | | formData.append('file', fileObj, fileObj.name); |
| | | |
| | | // 添加业务参数(必须序列化为 JSON 字符串) |
| | | formData.append('importParam', JSON.stringify(importParam)); |
| | | |
| | | // 调试信息:验证 FormData 内容 |
| | | console.log('FormData 内容验证:'); |
| | | console.log('- file 字段:', formData.get('file')); |
| | | console.log('- importParam 字段:', formData.get('importParam')); |
| | | console.log('- file 类型:', fileObj instanceof File ? 'File' : fileObj instanceof Blob ? 'Blob' : typeof fileObj); |
| | | // 添加changer参数 - API文档要求的必填参数 |
| | | formData.append('changer', formState.editor); |
| | | console.log('添加changer参数:', formState.editor); |
| | | |
| | | // 6. 获取登录 Token |
| | | // 尝试添加其他可能的参数名 |
| | | formData.append('editorId', formState.editor); |
| | | formData.append('userId', formState.editor); |
| | | |
| | | console.log('FormData 内容:'); |
| | | for (const [key, value] of formData.entries()) { |
| | | console.log(`- ${key}: ${typeof value === 'string' ? value : value.name}`); |
| | | } |
| | | |
| | | // 6. 使用 XMLHttpRequest 手动构建请求,绕过拦截器问题 |
| | | console.log('使用 XMLHttpRequest 发送请求...'); |
| | | |
| | | // 构建请求 URL |
| | | const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot'; |
| | | const url = `${domainUrl}/api/excel/importContract`; |
| | | console.log('请求 URL:', url); |
| | | |
| | | // 获取登录 Token |
| | | const token = getToken(); |
| | | if (!token) { |
| | | createMessage.error('未登录,请先登录系统'); |
| | | importLoading.value = false; |
| | | return; |
| | | } |
| | | |
| | | // 7. 发送请求 - 使用 XMLHttpRequest 手动构建 multipart/form-data 请求 |
| | | // 这样可以完全控制请求体和请求头,确保 Content-Type 被正确设置 |
| | | const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL || 'http://192.168.31.222:8080/jeecg-boot'; |
| | | const url = `${domainUrl}/api/excel/importContract`; |
| | | |
| | | console.log('===== [v9] 手动构建 multipart/form-data 请求(完全控制) ====='); |
| | | console.log('请求 URL:', url); |
| | | console.log('文件对象:', fileObj); |
| | | console.log('业务参数:', importParam); |
| | | |
| | | // 手动构建 multipart/form-data 请求 |
| | | const responseData = await new Promise((resolve, reject) => { |
| | |
| | | JSON.stringify(importParam), |
| | | '\r\n' |
| | | ); |
| | | |
| | | // 2. 添加文件部分 |
| | | |
| | | // 2. 添加changer参数部分 - API文档要求的必填参数 |
| | | console.log('手动构建请求体 - 添加changer参数:', formState.editor); |
| | | parts.push( |
| | | `--${boundary}\r\n`, |
| | | `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}"\r\n`, |
| | | 'Content-Disposition: form-data; name="changer"\r\n', |
| | | 'Content-Type: text/plain\r\n', |
| | | '\r\n', |
| | | formState.editor, |
| | | '\r\n' |
| | | ); |
| | | |
| | | // 尝试添加其他可能的参数名 |
| | | console.log('手动构建请求体 - 添加editorId参数:', formState.editor); |
| | | parts.push( |
| | | `--${boundary}\r\n`, |
| | | 'Content-Disposition: form-data; name="editorId"\r\n', |
| | | 'Content-Type: text/plain\r\n', |
| | | '\r\n', |
| | | formState.editor, |
| | | '\r\n' |
| | | ); |
| | | |
| | | console.log('手动构建请求体 - 添加userId参数:', formState.editor); |
| | | parts.push( |
| | | `--${boundary}\r\n`, |
| | | 'Content-Disposition: form-data; name="userId"\r\n', |
| | | 'Content-Type: text/plain\r\n', |
| | | '\r\n', |
| | | formState.editor, |
| | | '\r\n' |
| | | ); |
| | | |
| | | // 3. 添加文件部分 |
| | | parts.push( |
| | | `--${boundary}\r\n`, |
| | | `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}\r\n`, |
| | | 'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n', |
| | | 'Content-Transfer-Encoding: binary\r\n', |
| | | '\r\n' |
| | | ); |
| | | |
| | | // 3. 读取文件内容 |
| | | // 4. 读取文件内容 |
| | | const fileReader = new FileReader(); |
| | | fileReader.onload = function(e) { |
| | | if (!e.target?.result) { |
| | |
| | | |
| | | const fileContent = e.target.result; |
| | | |
| | | // 4. 添加文件内容和结束边界 |
| | | // 5. 添加文件内容和结束边界 |
| | | const allParts = [...parts]; |
| | | |
| | | // 添加文件内容(使用 ArrayBuffer) |
| | |
| | | `--${boundary}--\r\n` |
| | | ); |
| | | |
| | | // 5. 创建最终的请求体 Blob |
| | | // 6. 创建最终的请求体 Blob |
| | | const requestBody = new Blob(allParts, { type: `multipart/form-data; boundary=${boundary}` }); |
| | | |
| | | // 6. 创建 XMLHttpRequest |
| | | // 7. 创建 XMLHttpRequest |
| | | const xhr = new XMLHttpRequest(); |
| | | |
| | | // 监听请求完成 |
| | |
| | | } |
| | | } else { |
| | | console.error('请求失败:', xhr.status, xhr.statusText); |
| | | reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`)); |
| | | try { |
| | | const errorData = JSON.parse(xhr.responseText); |
| | | reject(new Error(errorData.message || errorData.msg || `请求失败: ${xhr.status} ${xhr.statusText}`)); |
| | | } catch (e) { |
| | | reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`)); |
| | | } |
| | | } |
| | | }; |
| | | |
| | |
| | | reject(new Error('网络错误')); |
| | | }; |
| | | |
| | | // 监听请求进度(可选) |
| | | xhr.upload.onprogress = function(event) { |
| | | if (event.lengthComputable) { |
| | | const percentComplete = (event.loaded / event.total) * 100; |
| | | console.log('上传进度:', percentComplete.toFixed(2) + '%'); |
| | | } |
| | | }; |
| | | |
| | | // 打开请求 |
| | | xhr.open('POST', url, true); |
| | | |
| | | // 7. 设置请求头 |
| | | // 设置请求头 |
| | | xhr.setRequestHeader('X-Access-Token', token); |
| | | xhr.setRequestHeader('X-Tenant-Id', '0'); |
| | | xhr.setRequestHeader('X-Version', 'v3'); |
| | | xhr.setRequestHeader('Accept', 'application/json, text/plain, */*'); |
| | | // 手动设置正确的 Content-Type,包含边界 |
| | | xhr.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`); |
| | | // 设置 Content-Length |
| | | xhr.setRequestHeader('Content-Length', requestBody.size.toString()); |
| | | |
| | | // 8. 发送请求 |
| | | // 发送请求 |
| | | xhr.send(requestBody); |
| | | }; |
| | | |
| | |
| | | fileReader.readAsArrayBuffer(fileObj); |
| | | }); |
| | | |
| | | console.log('后端响应数据:', responseData); |
| | | console.log('后端响应:', responseData); |
| | | |
| | | const response = { data: responseData }; |
| | | |
| | | // 9. 解析后端响应结果 |
| | | |
| | | // 7. 处理响应 |
| | | if (responseData && responseData.success) { |
| | | // 适配 JeecgBoot 后端返回格式 |
| | | importSummary.total = responseData.result?.total || responseData.data?.total || responseData.total || 0; |
| | | importSummary.success = responseData.result?.success || responseData.data?.success || responseData.success || 0; |
| | | importSummary.success = responseData.result?.success || responseData.data?.success || 0; |
| | | importSummary.failed = responseData.result?.failed || responseData.data?.failed || responseData.failed || 0; |
| | | importSummary.duration = responseData.result?.duration || responseData.data?.duration || 0; |
| | | importErrors.value = responseData.result?.errors || responseData.data?.errors || responseData.errors || responseData.result?.errorList || responseData.data?.errorList || []; |
| | | |
| | | // 无错误但失败数大于0的兜底提示 |
| | | if (importErrors.value.length === 0 && importSummary.failed > 0) { |
| | | importErrors.value = [{ key: 1, message: '部分记录导入失败,具体原因请查看后端日志或联系管理员' }]; |
| | | importErrors.value = [{ key: 1, message: '部分记录导入失败,请查看后端日志' }]; |
| | | } |
| | | |
| | | createMessage.success('批量导入处理完成'); |
| | | // 成功后返回上一级页面 |
| | | setTimeout(() => { |
| | | router.back(); |
| | | }, 1000); |
| | | |
| | | // 成功时不显示结果,直接返回 |
| | | showResult.value = false; |
| | | } else { |
| | | // 导入失败处理 |
| | | importSummary.total = 0; |
| | | importSummary.success = 0; |
| | | importSummary.failed = 0; |
| | | importSummary.duration = 0; |
| | | importErrors.value = [{ |
| | | key: 1, |
| | | message: responseData?.message || responseData?.msg || '导入失败,后端返回异常信息' |
| | | }]; |
| | | |
| | | createMessage.error(responseData?.message || responseData?.msg || '批量导入失败'); |
| | | |
| | | // 失败时不显示结果 |
| | | showResult.value = false; |
| | | const errorMsg = responseData?.message || responseData?.msg || '导入失败'; |
| | | importErrors.value = [{ key: 1, message: errorMsg }]; |
| | | createMessage.error(errorMsg); |
| | | } |
| | | } catch (error) { |
| | | // 全局异常捕获 |
| | | console.error('导入过程中捕获异常:', error); |
| | | |
| | | // axios 错误响应格式:error.response.data 包含后端返回的错误信息 |
| | | console.error('导入异常:', error); |
| | | const errorData = error.response?.data || error; |
| | | const errorMessage = errorData?.message || errorData?.msg || error.message || '网络异常、文件无效或接口调用失败,请稍后重试'; |
| | | |
| | | importSummary.total = 0; |
| | | importSummary.success = 0; |
| | | importSummary.failed = 0; |
| | | importSummary.duration = 0; |
| | | importErrors.value = [{ |
| | | key: 1, |
| | | message: errorMessage |
| | | }]; |
| | | |
| | | showResult.value = false; |
| | | createMessage.error(errorMessage); |
| | | const errorMsg = errorData?.message || errorData?.msg || error.message || '网络异常'; |
| | | importErrors.value = [{ key: 1, message: errorMsg }]; |
| | | createMessage.error(errorMsg); |
| | | } finally { |
| | | // 确保无论成功失败,都关闭加载状态 |
| | | importLoading.value = false; |
| | | } |
| | | }; |
| | |
| | | // 获取文案编辑列表 |
| | | const getPersonList = async () => { |
| | | try { |
| | | const response = await defHttp.get({ |
| | | url: '/sys/user/userRoleList', |
| | | params: { |
| | | roleId: ROLE_ID_COPY_EDITOR, |
| | | }, |
| | | }); |
| | | // 尝试多个可能的API端点获取文案编辑列表 |
| | | const apiEndpoints = [ |
| | | '/sys/user/userRoleList', |
| | | '/sys/user/list', |
| | | '/user/list' |
| | | ]; |
| | | |
| | | const records = normalizeContractRecords(response); |
| | | const persons = records.map((item: any) => ({ |
| | | value: item.id || item.userId || '', |
| | | label: item.realname || item.username || '未知用户' |
| | | })).filter(item => item.value && item.label); |
| | | let records = []; |
| | | let apiSuccess = false; |
| | | |
| | | for (const endpoint of apiEndpoints) { |
| | | try { |
| | | console.log(`尝试API端点: ${endpoint}`); |
| | | const response = await defHttp.get({ |
| | | url: endpoint, |
| | | params: { |
| | | roleId: ROLE_ID_COPY_EDITOR, |
| | | pageNo: 1, |
| | | pageSize: 100 |
| | | }, |
| | | timeout: 5000 |
| | | }); |
| | | |
| | | records = normalizeContractRecords(response); |
| | | if (records.length > 0) { |
| | | apiSuccess = true; |
| | | console.log(`API端点 ${endpoint} 成功返回 ${records.length} 条记录`); |
| | | break; |
| | | } |
| | | } catch (error) { |
| | | console.log(`API端点 ${endpoint} 调用失败:`, error); |
| | | } |
| | | } |
| | | |
| | | if (!apiSuccess) { |
| | | createMessage.error('获取文案编辑列表失败'); |
| | | personList.value = []; |
| | | return; |
| | | } |
| | | |
| | | // 构建人员列表,使用ID作为value |
| | | const persons = records.map((item: any) => { |
| | | // 尝试获取ID |
| | | const id = item.id || item.userId || item.userid || ''; |
| | | // 尝试获取姓名 |
| | | const name = item.realname || item.username || item.name || '未知用户'; |
| | | |
| | | return { |
| | | value: id, // 使用ID作为值 |
| | | label: name, // 只显示名字 |
| | | id: id |
| | | }; |
| | | }).filter(item => item.value && item.label); |
| | | |
| | | personList.value = persons; |
| | | console.log('获取文案编辑列表成功:', persons); |
| | | console.log('原始记录数据:', records); // 输出原始记录数据,查看字段结构 |
| | | |
| | | // 验证是否有手机号 |
| | | const hasPhone = persons.some(person => person.phone); |
| | | console.log('是否包含手机号:', hasPhone); |
| | | if (!hasPhone) { |
| | | console.warn('警告: 文案编辑列表中没有手机号信息'); |
| | | // 尝试直接从原始记录中查找可能的手机号字段 |
| | | if (records.length > 0) { |
| | | const firstRecord = records[0]; |
| | | console.log('第一条记录的所有字段:', Object.keys(firstRecord)); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error('获取文案编辑列表失败:', error); |
| | | createMessage.error('获取文案编辑列表失败,使用模拟数据'); |
| | | |
| | | const mockPersons = [ |
| | | { value: '1', label: '测试用户' }, |
| | | { value: '2', label: '陈贵福' }, |
| | | { value: '3', label: '马春红' }, |
| | | { value: '4', label: '熊康利' }, |
| | | { value: '5', label: '小刘' }, |
| | | { value: '6', label: '小宋' }, |
| | | { value: '7', label: '郭永亮' }, |
| | | { value: '8', label: '张晓东' } |
| | | ]; |
| | | personList.value = mockPersons; |
| | | createMessage.error('获取文案编辑列表失败'); |
| | | personList.value = []; |
| | | } |
| | | }; |
| | | |