<template>
|
<div class="step3">
|
<a-alert
|
v-if="currentContract"
|
type="info"
|
show-icon
|
class="mb-4"
|
:message="`当前合同:${currentContract.contractName || '未命名合同'}`"
|
:description="currentContract.customerName ? `客户:${currentContract.customerName}` : ''"
|
/>
|
|
<!-- 操作切换 -->
|
<div class="operation-tabs mb-4">
|
<a-tabs v-model:active-key="activeTab">
|
<a-tab-pane key="manual" tab="手动添加">
|
<ACard :bordered="false">
|
<div class="card-header" style="display: flex; align-items: center; margin-bottom: 16px;">
|
<h3 style="margin: 0; font-size: 16px; font-weight: 600;">语义词添加</h3>
|
<!-- <a-button type="primary" style="margin-left: 16px;" @click="switchToBatchImport">批量导入</a-button> -->
|
</div>
|
<PersonTable ref="tableRef" />
|
</ACard>
|
<ADivider />
|
<BasicForm @register="register" />
|
</a-tab-pane>
|
<a-tab-pane key="batch" tab="批量导入">
|
<ACard :bordered="false">
|
<div class="card-header" style="display: flex; align-items: center; margin-bottom: 16px;">
|
<h3 style="margin: 0; font-size: 16px; font-weight: 600;">批量导入</h3>
|
<a-button style="margin-left: 16px;" @click="switchToManual">返回手动添加</a-button>
|
</div>
|
|
<!-- 批量导入表单 -->
|
<div class="form-container">
|
<!-- 合同信息 -->
|
<div class="form-item">
|
<span class="required-mark">*</span>
|
<span class="form-label">合同信息</span>
|
<a-select
|
v-model:value="batchForm.contractId"
|
placeholder="请选择合同"
|
style="width: 360px;"
|
show-search
|
:filter-option="filterOption"
|
@change="handleContractChange"
|
>
|
<a-select-option v-for="contract in contractList" :key="contract.value" :value="contract.value">
|
{{ contract.label }}
|
</a-select-option>
|
</a-select>
|
</div>
|
|
<!-- 语义词文件选择 -->
|
<div class="form-item">
|
<span class="required-mark">*</span>
|
<span class="form-label">语义词文件</span>
|
<div style="display: flex; align-items: center; width: 360px;">
|
<input type="text" readonly placeholder="" style="flex: 1; height: 32px; padding: 0 11px; border: 1px solid #d9d9d9; border-radius: 4px; margin-right: 0; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0;" :value="batchForm.semanticFile || ''" />
|
<input type="file" style="display: none;" ref="fileInput" accept=".xlsx,.csv,.txt" @change="handleFileInputChange" />
|
<a-button type="default" style="border-top-left-radius: 0; border-bottom-left-radius: 0; height: 32px;" @click="triggerFileInput">
|
选择
|
</a-button>
|
</div>
|
</div>
|
|
<!-- 文案编辑 -->
|
<div class="form-item">
|
<span class="required-mark">*</span>
|
<span class="form-label">文案编辑</span>
|
<a-select v-model:value="batchForm.editor" placeholder="请选择文案编辑" style="width: 360px;" show-search :filter-option="filterOption">
|
<a-select-option v-for="person in personList" :key="person.value" :value="person.value">
|
{{ person.label }}
|
</a-select-option>
|
</a-select>
|
</div>
|
|
<!-- 操作按钮 -->
|
<div class="form-actions">
|
<a-button type="primary" @click="handleImport" :loading="importLoading" style="width: 120px; height: 40px; font-size: 16px;">
|
批量导入
|
</a-button>
|
</div>
|
</div>
|
|
<!-- 导入结果 -->
|
<div class="result-container" v-show="showResult" style="margin-top: 20px;">
|
<a-divider orientation="left">导入结果</a-divider>
|
|
<div class="import-summary">
|
<a-statistic title="总记录数" :value="importSummary.total" style="margin-right: 32px" />
|
<a-statistic title="成功数" :value="importSummary.success" prefix=""><template #prefix><span style="color: green">✓</span></template></a-statistic>
|
<a-statistic title="失败数" :value="importSummary.failed" prefix=""><template #prefix><span style="color: red">✗</span></template></a-statistic>
|
<a-statistic title="处理时间" :value="importSummary.duration" suffix="秒" style="margin-left: 32px" />
|
</div>
|
|
<div v-if="importErrors.length > 0" class="import-errors" style="margin-top: 16px;">
|
<a-divider orientation="left">错误信息</a-divider>
|
<a-list :data-source="importErrors" :render-item="renderErrorItem" />
|
</div>
|
</div>
|
</ACard>
|
</a-tab-pane>
|
</a-tabs>
|
</div>
|
|
<!-- 调试按钮 -->
|
<!-- <div style="margin-top: 20px; text-align: center;">
|
<a-button type="primary" @click="debugTableData">调试表格数据</a-button>
|
</div> -->
|
</div>
|
</template>
|
<script lang="ts">
|
import { defineComponent, onMounted, ref, reactive } from 'vue';
|
import { BasicForm, useForm } from '/@/components/Form';
|
import { step3Schemas } from './data';
|
import PersonTable from './PersonTable.vue';
|
import { Divider, Card, message, Alert, Tabs, Upload, Button, Select, Statistic, List } from 'ant-design-vue';
|
import { addSemanticWordsToContractApi } from '/@/api/demo/semanticWordApi';
|
import { defHttp } from '/@/utils/http/axios';
|
import { router } from '/@/router';
|
import { getToken } from '/@/utils/auth';
|
|
export default defineComponent({
|
name: 'Step3Form',
|
components: {
|
BasicForm,
|
PersonTable,
|
ADivider: Divider,
|
ACard: Card,
|
AAlert: Alert,
|
ATabs: Tabs,
|
ATabPane: Tabs.TabPane,
|
AUpload: Upload,
|
AButton: Button,
|
ASelect: Select,
|
ASelectOption: Select.Option,
|
AStatistic: Statistic,
|
AList: List,
|
},
|
props: {
|
// 移除targetFileId属性,因为父组件没有传递
|
// 我们将从其他地方获取合同文件ID
|
},
|
emits: ['next', 'prev'],
|
setup(_, { emit }) {
|
const tableRef = ref<{ getTableData: () => any } | null>(null);
|
const currentContract = ref<Record<string, any> | null>(null);
|
|
// 批量导入相关状态
|
const activeTab = ref('manual');
|
const contractList = ref([]);
|
const contractListCache = ref([]);
|
const fileList = ref([]);
|
const importLoading = ref(false);
|
const showResult = ref(false);
|
const fileInput = ref(null);
|
|
// 批量导入表单数据
|
const batchForm = reactive({
|
contractId: '',
|
semanticFile: '',
|
editor: ''
|
});
|
|
// 文案编辑角色ID
|
const ROLE_ID_COPY_EDITOR = "1972228739099103234";
|
|
// 人员列表 - 只存储文案编辑
|
const personList = ref([]);
|
|
// 导入结果
|
const importSummary = reactive({
|
total: 0,
|
success: 0,
|
failed: 0,
|
duration: 0
|
});
|
|
// 错误信息
|
const importErrors = ref([]);
|
|
const readSelectedContract = () => {
|
try {
|
const contractStr = localStorage.getItem('selectedContractInfo');
|
if (!contractStr) {
|
currentContract.value = null;
|
return null;
|
}
|
const parsed = JSON.parse(contractStr);
|
currentContract.value = parsed;
|
return parsed;
|
} catch (error) {
|
console.error('解析合同信息失败:', error);
|
currentContract.value = null;
|
return null;
|
}
|
};
|
|
onMounted(() => {
|
// 先获取合同列表,然后再读取选中的合同信息
|
Promise.all([getContractOptions(), getPersonList()]).then(() => {
|
// 读取选中的合同信息
|
const selectedContract = readSelectedContract();
|
if (selectedContract && selectedContract.id) {
|
// 如果有选中的合同,设置为当前选中的合同
|
batchForm.contractId = selectedContract.id;
|
}
|
});
|
});
|
|
const [register, { validate, setProps }] = useForm({
|
labelWidth: 120,
|
schemas: step3Schemas,
|
actionColOptions: {
|
span: 14,
|
},
|
resetButtonOptions: {
|
text: '上一步',
|
},
|
submitButtonOptions: {
|
text: '提交',
|
},
|
resetFunc: customResetFunc,
|
submitFunc: customSubmitFunc,
|
});
|
|
async function customResetFunc() {
|
emit('prev');
|
}
|
|
const normalizeAcceptIndicator = (value: unknown): number | undefined => {
|
if (value === null || value === undefined) {
|
return undefined;
|
}
|
const raw = typeof value === 'string' ? value.replace(/%/g, '').trim() : value;
|
const num = Number(raw);
|
return Number.isFinite(num) ? num : undefined;
|
};
|
|
async function customSubmitFunc() {
|
try {
|
const values = await validate();
|
const selectedContract = currentContract.value || readSelectedContract();
|
if (!selectedContract) {
|
message.error('未找到合同信息,请返回上一步重新选择');
|
emit('prev');
|
return;
|
}
|
|
setProps({
|
submitButtonOptions: {
|
loading: true,
|
},
|
});
|
|
// 获取表格中的语义词数据
|
let semanticWordList: Array<{
|
word: string;
|
startDate: string;
|
endDate: string;
|
ranking: number;
|
category: string;
|
status: string;
|
remark: string;
|
outWord: string;
|
price: string;
|
month?: number;
|
acceptindicator?: number;
|
}> = [];
|
|
if (tableRef.value) {
|
const tableData = tableRef.value.getTableData();
|
console.log('1. 表格原始数据:', tableData);
|
console.log('2. 表格数据长度:', tableData.length);
|
|
// 提前校验表格数据是否为空
|
if (!tableData || tableData.length === 0) {
|
message.warning('请先添加至少一个语义词');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
|
const invalidRowIndex = tableData.findIndex((item) => {
|
const hasCategory = !!item.category;
|
const hasWord = typeof item.word === 'string' ? item.word.trim().length > 0 : !!item.word;
|
const hasOutWord =
|
typeof item.outWord === 'string' ? item.outWord.trim().length > 0 : !!item.outWord;
|
return !hasCategory || !hasWord || !hasOutWord;
|
});
|
if (invalidRowIndex !== -1) {
|
message.warning(`第${invalidRowIndex + 1}行的「类型」「语义词」「露出词」均为必填,请补全。`);
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
|
const filteredData = tableData.map((item) => ({
|
...item,
|
word: typeof item.word === 'string' ? item.word.trim() : item.word,
|
outWord: typeof item.outWord === 'string' ? item.outWord.trim() : item.outWord,
|
}));
|
console.log('7. 过滤后的数据长度:', filteredData.length);
|
|
// 数据转换逻辑优化:改为费用与合作周期
|
semanticWordList = filteredData.map((item) => {
|
// 修复排名转换逻辑 - 确保ranking是有效的数字
|
let ranking = 1;
|
if (item.ranking === '1' || item.ranking === '2' || item.ranking === '3') {
|
ranking = parseInt(item.ranking);
|
} else if (item.ranking === '4') {
|
ranking = 4; // 保展现不保排名对应数字4
|
}
|
// 价格:保留两位小数的正数
|
let price = '';
|
if (item.price !== undefined && item.price !== null && String(item.price).trim() !== '') {
|
const num = Number(item.price);
|
if (!isNaN(num) && num >= 0) {
|
price = num.toFixed(2);
|
}
|
}
|
// 合作周期:大于0的整数
|
let month: number | undefined = undefined;
|
if (item.month !== undefined && item.month !== null && String(item.month).trim() !== '') {
|
const m = Number(String(item.month).replace(/\D/g, ''));
|
if (m > 0) month = m;
|
}
|
// 验收指标:转成整数
|
const acceptindicatorValue = normalizeAcceptIndicator(item.acceptindicator);
|
|
// 确保返回符合接口定义的数据结构
|
return {
|
word: item.word.trim(),
|
startDate: '',
|
endDate: '',
|
ranking: ranking,
|
category: item.category || '1', // 使用表格中的category字段
|
status: '1',
|
remark: item.remark || '', // 使用表格中的remark字段
|
outWord: item.outWord || '', // 使用表格中的outWord字段
|
price,
|
month,
|
acceptindicator: acceptindicatorValue,
|
};
|
});
|
}
|
|
console.log('8. 最终语义词列表:', semanticWordList);
|
|
// 校验:至少一条语义词,且每条的价格与周期有效
|
if (semanticWordList.length === 0) {
|
message.warning('请确保所有语义词都已正确填写');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
const invalidPrice = semanticWordList.find((i) => i.price === '' || Number(i.price) < 0);
|
if (invalidPrice) {
|
message.warning('请为每条语义词填写有效的费用(≥0,保留两位小数)');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
const invalidMonth = semanticWordList.find((i) => !i.month || Number(i.month) <= 0);
|
if (invalidMonth) {
|
message.warning('请为每条语义词填写有效的合作周期(>0 的整数)');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
const invalidAcceptIndicator = semanticWordList.find(
|
(i) => i.acceptindicator === undefined || Number(i.acceptindicator) <= 0
|
);
|
if (invalidAcceptIndicator) {
|
message.warning('请为每条语义词填写有效的验收指标(>0 的整数)');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
|
try {
|
const contractId = selectedContract.id || selectedContract.contractId;
|
if (!contractId) {
|
message.error('合同缺少ID,请重新选择');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
|
const mappedList = semanticWordList.map((item) => ({
|
...item,
|
contractId,
|
contractName: selectedContract.contractName,
|
customerName: selectedContract.customerName,
|
}));
|
|
const requestParams = {
|
contractId,
|
contractName: selectedContract.contractName,
|
customerId: selectedContract.customerId,
|
customerName: selectedContract.customerName,
|
semanticWordList: mappedList,
|
};
|
|
await addSemanticWordsToContractApi(requestParams as any);
|
message.success('语义词添加成功');
|
localStorage.removeItem('selectedContractInfo');
|
} catch (error) {
|
console.error('API调用失败:', error);
|
message.error('提交失败,请稍后再试');
|
setProps({ submitButtonOptions: { loading: false } });
|
return;
|
}
|
|
// 完成后切换步骤
|
setTimeout(() => {
|
setProps({ submitButtonOptions: { loading: false } });
|
emit('next', values);
|
}, 1500);
|
} catch (error) {
|
console.error('提交失败:', error);
|
message.error('提交失败');
|
setProps({
|
submitButtonOptions: {
|
loading: false,
|
},
|
});
|
}
|
}
|
|
// 调试表格数据的方法 - 增强版
|
function debugTableData() {
|
if (tableRef.value) {
|
const tableData = tableRef.value.getTableData();
|
console.log('=== 调试表格数据 ===');
|
console.log('表格数据:', tableData);
|
console.log('表格数据长度:', tableData.length);
|
|
if (tableData.length > 0) {
|
console.log('第一个数据的完整结构:', tableData[0]);
|
console.log('所有字段名:', Object.keys(tableData[0]));
|
|
// 检查所有可能的语义词字段
|
const possibleFields = ['name', 'word', 'semanticWord', 'keyword', 'text', 'content'];
|
possibleFields.forEach((field) => {
|
console.log(`字段 ${field}:`, tableData[0][field]);
|
});
|
}
|
|
message.info('调试信息已输出到控制台');
|
} else {
|
message.warning('表格引用为空');
|
}
|
}
|
|
// 切换到批量导入
|
function switchToBatchImport() {
|
// 跳转到批量导入页面
|
router.push('/contract/batchImport');
|
}
|
|
// 切换到手动添加
|
function switchToManual() {
|
activeTab.value = 'manual';
|
}
|
|
// 归一化合同记录
|
const normalizeContractRecords = (response: any) => {
|
if (Array.isArray(response)) {
|
return response;
|
}
|
if (Array.isArray(response?.records)) {
|
return response.records;
|
}
|
if (Array.isArray(response?.result?.records)) {
|
return response.result.records;
|
}
|
if (Array.isArray(response?.data?.records)) {
|
return response.data.records;
|
}
|
return [];
|
};
|
|
// 解析合同标签
|
const resolveContractLabel = (item: any) => {
|
if (item.contractName) return item.contractName;
|
if (item.contractNo) return `合同编号:${item.contractNo}`;
|
return '未命名合同';
|
};
|
|
// 解析合同值
|
const resolveContractValue = (item: any) => {
|
return item.id || item.contractId || item.contractName || '';
|
};
|
|
// 获取合同选项
|
const getContractOptions = async () => {
|
try {
|
const response = await defHttp.get({
|
url: '/contract/contract/list',
|
params: {
|
column: 'createTime',
|
order: 'desc',
|
pageNo: 1,
|
pageSize: 200,
|
},
|
});
|
|
const records = normalizeContractRecords(response);
|
contractListCache.value = records;
|
|
const options = records.map((item: any) => ({
|
label: resolveContractLabel(item),
|
value: resolveContractValue(item),
|
}));
|
|
contractList.value = options;
|
} catch (error) {
|
console.error('获取合同列表失败:', error);
|
message.error('获取合同列表失败');
|
}
|
};
|
|
// 获取文案编辑列表
|
const getPersonList = async () => {
|
try {
|
// 调用后端API获取具有文案编辑角色的用户列表
|
const response = await defHttp.get({
|
url: '/sys/user/userRoleList',
|
params: {
|
roleId: ROLE_ID_COPY_EDITOR,
|
},
|
});
|
|
// 处理响应数据
|
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);
|
|
personList.value = persons;
|
console.log('获取文案编辑列表成功:', persons);
|
} catch (error) {
|
console.error('获取文案编辑列表失败:', error);
|
message.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;
|
}
|
};
|
|
// 过滤选项
|
const filterOption = (input: string, option: any) => {
|
return (option?.label || '').toLowerCase().includes(input.toLowerCase());
|
};
|
|
// 合同选择变化
|
const handleContractChange = (value: string) => {
|
if (!value) return;
|
const contract = getContractDetailById(value);
|
if (contract) {
|
// 将合同信息存储到localStorage,与语义词录入页面保持一致
|
localStorage.setItem('selectedContractInfo', JSON.stringify(contract));
|
// 更新当前合同信息
|
currentContract.value = contract;
|
}
|
};
|
|
// 根据ID获取合同详情
|
const getContractDetailById = (contractId: string) => {
|
return contractListCache.value.find(
|
(item: any) =>
|
item.id === contractId ||
|
item.contractId === contractId ||
|
resolveContractValue(item) === contractId
|
);
|
};
|
|
// 文件上传前校验
|
const beforeUpload = (file) => {
|
const allowedTypes = ['.xlsx', '.csv', '.txt'];
|
const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
|
|
if (!allowedTypes.includes(fileExtension)) {
|
message.error('只支持上传 Excel、CSV、文本文件');
|
return false;
|
}
|
|
if (file.size > 10 * 1024 * 1024) {
|
message.error('文件大小不能超过 10MB');
|
return false;
|
}
|
|
// 阻止自动上传,只需要文件信息
|
return false;
|
};
|
|
// 触发文件选择
|
const triggerFileInput = () => {
|
if (fileInput.value) {
|
fileInput.value.click();
|
}
|
};
|
|
// 处理文件选择
|
const handleFileInputChange = (event) => {
|
const files = event.target.files;
|
if (files && files.length > 0) {
|
const file = files[0];
|
fileList.value = [{
|
name: file.name,
|
originFileObj: file,
|
status: 'selected'
|
}];
|
batchForm.semanticFile = file.name;
|
} else {
|
fileList.value = [];
|
batchForm.semanticFile = '';
|
}
|
};
|
|
// 文件上传变化
|
const handleFileChange = (info) => {
|
// 过滤掉已上传或已删除的文件,只保留正在上传的文件
|
const newFileList = info.fileList.filter(file => file.status !== 'removed');
|
fileList.value = newFileList;
|
|
if (newFileList.length > 0) {
|
batchForm.semanticFile = newFileList[0].name;
|
} else {
|
batchForm.semanticFile = '';
|
}
|
};
|
|
// 下载模板
|
const downloadTemplate = () => {
|
message.info('模板下载功能开发中');
|
};
|
|
// 开始导入
|
const handleImport = async () => {
|
try {
|
// 表单验证
|
if (!batchForm.contractId) {
|
message.error('请选择合同');
|
return;
|
}
|
|
if (fileList.value.length === 0 || !fileList.value[0]?.originFileObj) {
|
message.error('请选择有效的语义词文件(Excel/CSV/TXT)');
|
return;
|
}
|
|
if (!batchForm.editor) {
|
message.error('请选择文案编辑');
|
return;
|
}
|
|
importLoading.value = true;
|
importSummary.total = 0;
|
importSummary.success = 0;
|
importSummary.failed = 0;
|
importSummary.duration = 0;
|
importErrors.value = [];
|
showResult.value = false;
|
|
// 强制获取 AntD Upload 原始 File 对象
|
const file = fileList.value[0];
|
const fileObj = file.originFileObj; // 使用 originFileObj,这是 AntD Upload 组件的原始文件对象
|
|
// 双重校验:确保 fileObj 是 File 实例
|
if (!(fileObj instanceof File)) {
|
message.error('获取文件失败,请重新选择文件');
|
importLoading.value = false;
|
return;
|
}
|
console.log('有效File对象:', fileObj);
|
console.log('文件名:', fileObj.name);
|
console.log('文件大小:', fileObj.size);
|
|
// 构建业务参数
|
const importParam = {
|
contractId: batchForm.contractId,
|
ranking: "3", // 签约排名写死为3
|
status: "7", // 状态写死为7
|
remark: "批量订单", // 备注信息写死为批量订单
|
month: "12", // 合作月份写死为12个月
|
price: "0", // 价格写死为0
|
acceptindicator: "80" // 验收指标写死为80
|
};
|
console.log('业务参数:', importParam);
|
|
// 获取登录 Token
|
const token = getToken();
|
if (!token) {
|
message.error('未登录,请先登录系统');
|
importLoading.value = false;
|
return;
|
}
|
|
// 发送请求 - 使用 XMLHttpRequest 手动构建 multipart/form-data 请求
|
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('===== 手动构建 multipart/form-data 请求 =====');
|
console.log('请求 URL:', url);
|
console.log('文件对象:', fileObj);
|
console.log('业务参数:', importParam);
|
|
// 手动构建 multipart/form-data 请求
|
const responseData = await new Promise((resolve, reject) => {
|
// 生成随机边界
|
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substr(2, 16);
|
|
// 构建请求体的各个部分
|
const parts = [];
|
|
// 1. 添加业务参数部分
|
parts.push(
|
`--${boundary}\r\n`,
|
'Content-Disposition: form-data; name="importParam"\r\n',
|
'Content-Type: application/json\r\n',
|
'\r\n',
|
JSON.stringify(importParam),
|
'\r\n'
|
);
|
|
// 2. 添加文件部分
|
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. 读取文件内容
|
const fileReader = new FileReader();
|
fileReader.onload = function(e) {
|
if (!e.target?.result) {
|
reject(new Error('读取文件失败'));
|
return;
|
}
|
|
const fileContent = e.target.result;
|
|
// 4. 添加文件内容和结束边界
|
const allParts = [...parts];
|
|
// 添加文件内容(使用 ArrayBuffer)
|
allParts.push(fileContent);
|
|
// 添加结束边界
|
allParts.push(
|
'\r\n',
|
`--${boundary}--\r\n`
|
);
|
|
// 5. 创建最终的请求体 Blob
|
const requestBody = new Blob(allParts, { type: `multipart/form-data; boundary=${boundary}` });
|
|
// 6. 创建 XMLHttpRequest
|
const xhr = new XMLHttpRequest();
|
|
// 监听请求完成
|
xhr.onload = function() {
|
if (xhr.status >= 200 && xhr.status < 300) {
|
try {
|
const data = JSON.parse(xhr.responseText);
|
console.log('响应数据:', data);
|
resolve(data);
|
} catch (error) {
|
console.error('解析响应失败:', error);
|
reject(new Error('解析响应失败'));
|
}
|
} else {
|
console.error('请求失败:', xhr.status, xhr.statusText);
|
reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
|
}
|
};
|
|
// 监听网络错误
|
xhr.onerror = function() {
|
console.error('网络错误');
|
reject(new Error('网络错误'));
|
};
|
|
// 打开请求
|
xhr.open('POST', url, true);
|
|
// 设置请求头
|
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());
|
|
// 发送请求
|
xhr.send(requestBody);
|
};
|
|
// 读取文件为 ArrayBuffer
|
fileReader.readAsArrayBuffer(fileObj);
|
});
|
|
console.log('后端响应数据:', responseData);
|
|
// 解析后端响应结果
|
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.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: '部分记录导入失败,具体原因请查看后端日志或联系管理员' }];
|
}
|
|
message.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 || '导入失败,后端返回异常信息'
|
}];
|
|
message.error(responseData?.message || responseData?.msg || '批量导入失败');
|
|
// 失败时不显示结果
|
showResult.value = false;
|
}
|
} catch (error) {
|
console.error('导入失败:', error);
|
importSummary.total = 0;
|
importSummary.success = 0;
|
importSummary.failed = 0;
|
importSummary.duration = 0;
|
importErrors.value = [{
|
key: 1,
|
message: error.message || '网络异常、文件无效或接口调用失败,请稍后重试'
|
}];
|
|
showResult.value = false;
|
message.error(error.message || '导入失败,请检查网络连接或登录状态');
|
} finally {
|
// 确保无论成功失败,都关闭加载状态
|
importLoading.value = false;
|
}
|
};
|
|
// 渲染错误项
|
const renderErrorItem = (error) => {
|
return {
|
description: error.message
|
};
|
};
|
|
return {
|
register,
|
tableRef,
|
debugTableData,
|
currentContract,
|
activeTab,
|
switchToBatchImport,
|
switchToManual,
|
batchForm,
|
contractList,
|
fileList,
|
importLoading,
|
showResult,
|
importSummary,
|
importErrors,
|
personList,
|
filterOption,
|
handleContractChange,
|
beforeUpload,
|
handleFileChange,
|
downloadTemplate,
|
handleImport,
|
renderErrorItem,
|
fileInput,
|
triggerFileInput,
|
handleFileInputChange
|
};
|
},
|
});
|
</script>
|
<style lang="less" scoped>
|
.step3 {
|
// width: 550px;
|
margin: 0 auto;
|
}
|
|
.form-container {
|
max-width: 800px;
|
margin: 0 auto;
|
padding: 20px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.form-item {
|
margin-bottom: 24px;
|
display: flex;
|
align-items: center;
|
width: 100%;
|
max-width: 600px;
|
}
|
|
.form-label {
|
width: 100px;
|
font-size: 14px;
|
}
|
|
.form-item .form-label {
|
margin-right: 16px;
|
}
|
|
.required-mark {
|
color: #ff4d4f;
|
margin-right: 4px;
|
}
|
|
.form-actions {
|
margin-top: 24px;
|
text-align: center;
|
width: 100%;
|
display: flex;
|
justify-content: center;
|
}
|
|
.result-container {
|
max-width: 800px;
|
margin: 20px auto;
|
padding: 20px;
|
}
|
|
.import-summary {
|
display: flex;
|
align-items: center;
|
margin-bottom: 16px;
|
flex-wrap: wrap;
|
}
|
|
.import-errors {
|
margin-top: 16px;
|
}
|
</style>
|