1、新增应付明细
2、新增应收明细 3、新增异常处置 4、新增风险处置 5、新增在途追踪 6、修复业务模块bug
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -76,8 +76,8 @@
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoods" :key="item.sourceIndex" :label="item.label" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column label="货物类型" min-width="130"><template #default="{ row }"><el-input :model-value="row.cargoType" readonly /></template></el-table-column>
|
||||
<el-table-column label="剩余数量" width="120"><template #default="{ row }">{{ formatQuantity(row.remainingQuantity) }}</template></el-table-column>
|
||||
<el-table-column label="本次数量" width="160"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(row, value)" /></template></el-table-column>
|
||||
<el-table-column label="剩余数量" width="120"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||
<el-table-column label="本次数量" width="160"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||
@@ -204,14 +204,76 @@ export default {
|
||||
};
|
||||
},
|
||||
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
||||
remaining(route) { return Math.max(0, this.totalQuantity - Number(route.dispatchedQuantity || 0)); },
|
||||
remaining(route) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.totalQuantity -
|
||||
Number(route.dispatchedQuantity || 0) -
|
||||
this.pendingSegmentQuantity(route)
|
||||
);
|
||||
},
|
||||
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
||||
freightAmount(route) { return Number(route.unitPrice || 0) * this.dispatchWeight(route); },
|
||||
handleDispatchQuantityInput(row, value) {
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
goods.cargoName || '',
|
||||
goods.cargoType || '',
|
||||
includeUnit ? goods.quantityUnit || '' : '',
|
||||
].join('\u0000');
|
||||
},
|
||||
isSameGoods(left = {}, right = {}) {
|
||||
if (left.sourceIndex !== undefined && right.sourceIndex !== undefined) return Number(left.sourceIndex) === Number(right.sourceIndex);
|
||||
return left.cargoName === right.cargoName && left.cargoType === right.cargoType && left.quantityUnit === right.quantityUnit;
|
||||
},
|
||||
dispatchedGoodsQuantity(route, row) {
|
||||
const dispatchedGoods = route.dispatchedGoods || {};
|
||||
return this.quantityNumber(
|
||||
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
|
||||
);
|
||||
},
|
||||
baseGoodsRemainingQuantity(route, row) {
|
||||
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
|
||||
},
|
||||
pendingSegmentQuantity(route) {
|
||||
return this.pending.reduce((sum, item) => {
|
||||
if (item.segmentNo !== route.segmentNo) return sum;
|
||||
return sum + this.quantityNumber(item.quantity);
|
||||
}, 0);
|
||||
},
|
||||
pendingGoodsQuantity(route, row) {
|
||||
return this.pending.reduce((sum, item) => {
|
||||
if (item.segmentNo !== route.segmentNo || !this.isSameGoods(item, row)) return sum;
|
||||
return sum + this.quantityNumber(item.quantity);
|
||||
}, 0);
|
||||
},
|
||||
editingGoodsQuantity(route, row) {
|
||||
return (route.goods || []).reduce((sum, item) => {
|
||||
if (item === row || !this.isSameGoods(item, row)) return sum;
|
||||
return sum + this.quantityNumber(item.dispatchQuantity);
|
||||
}, 0);
|
||||
},
|
||||
goodsRemainingQuantity(route, row) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.baseGoodsRemainingQuantity(route, row) -
|
||||
this.pendingGoodsQuantity(route, row) -
|
||||
this.editingGoodsQuantity(route, row)
|
||||
);
|
||||
},
|
||||
availableDispatchQuantity(route, row) { return this.goodsRemainingQuantity(route, row); },
|
||||
handleDispatchQuantityInput(route, row, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
row.dispatchQuantity = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
const nextValue = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||
const availableQuantity = this.availableDispatchQuantity(route, row);
|
||||
if (Number(nextValue || 0) > availableQuantity) {
|
||||
row.dispatchQuantity = this.formatQuantity(availableQuantity);
|
||||
this.$message.warning('本次数量不能超过剩余数量');
|
||||
return;
|
||||
}
|
||||
row.dispatchQuantity = nextValue;
|
||||
},
|
||||
handleUnitPriceInput(route, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
@@ -225,11 +287,10 @@ export default {
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
createGoodsRow(goods = {}, sourceIndex, dispatchedGoods = {}) {
|
||||
createGoodsRow(goods = {}, sourceIndex) {
|
||||
return {
|
||||
...goods,
|
||||
sourceIndex,
|
||||
remainingQuantity: Math.max(0, Number(goods.quantity || 0) - Number(dispatchedGoods[`${goods.cargoName || ''}\u0000${goods.cargoType || ''}`] || 0)),
|
||||
dispatchQuantity: undefined,
|
||||
};
|
||||
},
|
||||
@@ -237,7 +298,7 @@ export default {
|
||||
selectGoods(route, row) {
|
||||
const source = (this.master.goods || [])[Number(row.sourceIndex)];
|
||||
if (!source) return;
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex), route.dispatchedGoods || {}));
|
||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)));
|
||||
},
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); },
|
||||
handleCarrierTypeChange(route, value) {
|
||||
@@ -294,7 +355,7 @@ export default {
|
||||
addToPending(route, continueDispatch) {
|
||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||
if (goods.some(item => Number(item.dispatchQuantity) > Number(item.remainingQuantity))) return this.$message.warning('本次数量不能超过该货物的可调度数量');
|
||||
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
||||
if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
|
||||
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
||||
if (route.documentType === '运单') {
|
||||
@@ -310,7 +371,7 @@ export default {
|
||||
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
||||
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
||||
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
||||
cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
||||
sourceIndex: item.sourceIndex, cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
||||
}));
|
||||
this.editingId = null;
|
||||
this.pendingExpanded = true;
|
||||
@@ -327,7 +388,7 @@ export default {
|
||||
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
|
||||
if (!goods && sourceIndex >= 0) {
|
||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex, route.dispatchedGoods || {});
|
||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
||||
route.goods.push(goods);
|
||||
}
|
||||
if (goods) goods.dispatchQuantity = item.quantity;
|
||||
|
||||
@@ -49,13 +49,16 @@
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<h3>收发货信息</h3>
|
||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||
<div class="section-head__actions">
|
||||
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="form" label-position="right" label-width="auto"
|
||||
><div class="route-steps">
|
||||
<el-steps direction="vertical" :active="form.routes.length + 1">
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon">起</span></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--start">起</span></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
||||
@@ -80,7 +83,7 @@
|
||||
</template>
|
||||
</el-step>
|
||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
||||
<template #icon><span class="route-step-icon">经</span></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><span>运输类型</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
@@ -89,12 +92,12 @@
|
||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip v-if="form.routes.length > 1" content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon">终</span></template>
|
||||
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><span>运输类型</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
@@ -155,12 +158,13 @@
|
||||
><el-input
|
||||
v-model="row.cargoType"
|
||||
placeholder="从货物类型获取" /></template></el-table-column
|
||||
><el-table-column label="数量" width="104" class-name="master-editor__quantity-cell"
|
||||
><el-table-column label="数量" width="132" class-name="master-editor__quantity-cell"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
:min="0"
|
||||
controls-position="right" /></template></el-table-column
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||
><el-table-column label="数量单位" width="120"
|
||||
><template #default="{ row }"
|
||||
><el-select v-model="row.quantityUnit"
|
||||
@@ -200,13 +204,49 @@
|
||||
</section>
|
||||
<section>
|
||||
<h3>附件</h3>
|
||||
<el-upload
|
||||
multiple
|
||||
action="/api/blade-resource/oss/endpoint/put-file"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="attach"
|
||||
><el-button type="primary" plain>上传附件</el-button></el-upload
|
||||
>
|
||||
<div class="master-editor__attachment">
|
||||
<div class="master-editor__attachment-head">
|
||||
<vehicle-attachment-upload
|
||||
v-model="attachmentRows"
|
||||
:file-types="attachmentFileTypes"
|
||||
:max-size="500"
|
||||
:show-tip="false"
|
||||
:show-file-list="false"
|
||||
button-text="上传附件"
|
||||
@change="handleAttachmentChange"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!attachmentRows.length"
|
||||
@click="handleAttachmentBatchDownload"
|
||||
>
|
||||
批量下载
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="attachmentRows"
|
||||
border
|
||||
class="master-editor__attachment-table"
|
||||
@selection-change="handleAttachmentSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.originalName || row.name }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件大小" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
<el-dialog
|
||||
v-model="addressDialogVisible"
|
||||
@@ -402,9 +442,9 @@ import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { List, Location, Search } from '@element-plus/icons-vue';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { List, Search } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
@@ -455,7 +495,24 @@ export default {
|
||||
cargoOptions: [],
|
||||
cargoImportVisible: false,
|
||||
cargoImportLoading: false,
|
||||
uploadHeaders: getUploadHeaders(),
|
||||
attachmentRows: [],
|
||||
selectedAttachmentRows: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
'jpeg',
|
||||
'png',
|
||||
'jpg',
|
||||
'doc',
|
||||
'docx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'eml',
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
form: {
|
||||
@@ -471,6 +528,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
lastRouteName() {
|
||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||
},
|
||||
@@ -513,10 +571,23 @@ export default {
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||||
this.selectedAttachmentRows = [];
|
||||
if (this.form.projectId) await this.loadContracts(this.form.projectId, false);
|
||||
},
|
||||
parseJsonArray(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) return value;
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
getRouteTitle(index) {
|
||||
const departureName =
|
||||
index === 0
|
||||
@@ -980,7 +1051,7 @@ export default {
|
||||
this.form.routes.push(this.createRoute());
|
||||
},
|
||||
removeRoute(index) {
|
||||
if (this.form.routes.length > 1) this.form.routes.splice(index, 1);
|
||||
this.form.routes.splice(index, 1);
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
||||
@@ -988,10 +1059,56 @@ export default {
|
||||
removeGoods(index) {
|
||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||
},
|
||||
attach(response) {
|
||||
const list = JSON.parse(this.form.attachmentsJson || '[]');
|
||||
list.push(response.data || response);
|
||||
this.form.attachmentsJson = JSON.stringify(list);
|
||||
handleQuantityInput(row, value) {
|
||||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||
const [integer = '', ...decimals] = text.split('.');
|
||||
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||||
},
|
||||
syncAttachmentsJson() {
|
||||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||
},
|
||||
handleAttachmentChange(list) {
|
||||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
this.attachmentRows = (list || []).map(item => ({
|
||||
...item,
|
||||
description: item.description || '',
|
||||
uploadUserName: item.uploadUserName || userName,
|
||||
uploadTime: item.uploadTime || uploadTime,
|
||||
}));
|
||||
this.selectedAttachmentRows = [];
|
||||
this.syncAttachmentsJson();
|
||||
},
|
||||
handleAttachmentSelectionChange(rows) {
|
||||
this.selectedAttachmentRows = rows || [];
|
||||
},
|
||||
removeAttachment(index) {
|
||||
this.attachmentRows.splice(index, 1);
|
||||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(item =>
|
||||
this.attachmentRows.includes(item)
|
||||
);
|
||||
this.syncAttachmentsJson();
|
||||
},
|
||||
formatFileSize(size) {
|
||||
const number = Number(size);
|
||||
if (!number) return '';
|
||||
if (number < 1024) return `${number}B`;
|
||||
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||||
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||||
},
|
||||
downloadAttachment(row) {
|
||||
const url = row.url || row.link;
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空');
|
||||
return;
|
||||
}
|
||||
downloadFileByUrl(url, row.originalName || row.name || '附件');
|
||||
},
|
||||
handleAttachmentBatchDownload() {
|
||||
const rows = this.selectedAttachmentRows.length
|
||||
? this.selectedAttachmentRows
|
||||
: this.attachmentRows;
|
||||
rows.forEach(row => this.downloadAttachment(row));
|
||||
},
|
||||
async save(draft = false) {
|
||||
if (!draft) {
|
||||
@@ -1001,7 +1118,6 @@ export default {
|
||||
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
|
||||
return this.$message.warning('请至少填写一条有效货物');
|
||||
if (
|
||||
!this.form.routes.length ||
|
||||
this.form.routes.some(item => !item.departureName || !item.transportType) ||
|
||||
!this.form.finalTransportType
|
||||
)
|
||||
@@ -1009,6 +1125,7 @@ export default {
|
||||
}
|
||||
const payload = {
|
||||
...this.form,
|
||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||
routes: [
|
||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
||||
{
|
||||
@@ -1024,7 +1141,8 @@ export default {
|
||||
delete payload.finalTransportType;
|
||||
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
|
||||
this.$message.success('保存成功');
|
||||
return res.data || res;
|
||||
// 兼容 Axios 响应及 BladeX 常见的 data.data 两层响应结构。
|
||||
return res?.data?.data || res?.data || res;
|
||||
},
|
||||
async saveDraft() {
|
||||
await this.save(true);
|
||||
@@ -1036,7 +1154,12 @@ export default {
|
||||
},
|
||||
async createAndDispatch() {
|
||||
const data = await this.save();
|
||||
this.$emit('dispatch', data.id);
|
||||
const id = data?.id || data?.masterOrderId || data?.masterId;
|
||||
if (!id) {
|
||||
this.$message.error('保存成功但未获取到总单ID,无法进入调度');
|
||||
return;
|
||||
}
|
||||
this.$emit('dispatch', id);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1060,15 +1183,15 @@ export default {
|
||||
.goods-heading {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
h3 {
|
||||
margin-right: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.goods-toolbar {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.goods-actions {
|
||||
@@ -1076,8 +1199,31 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
:deep(.master-editor__quantity-cell .el-input-number) {
|
||||
width: 80px;
|
||||
.master-editor__attachment {
|
||||
width: 100%;
|
||||
}
|
||||
.master-editor__attachment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.master-editor__attachment-table {
|
||||
width: 100%;
|
||||
:deep(.el-table__header th) {
|
||||
background: #f5f7fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(.el-table__cell) {
|
||||
border-color: #eff1f7;
|
||||
}
|
||||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
:deep(.master-editor__quantity-cell .el-input) {
|
||||
width: 108px;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
@@ -1088,6 +1234,10 @@ export default {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.section-head__actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.route-dialog__search {
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 12px 4px;
|
||||
@@ -1138,6 +1288,12 @@ export default {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
&--middle {
|
||||
background: #67c23a;
|
||||
}
|
||||
&--end {
|
||||
background: #e6a23c;
|
||||
}
|
||||
}
|
||||
}
|
||||
.route-node-form {
|
||||
|
||||
@@ -34,19 +34,24 @@
|
||||
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create">
|
||||
<section class="waybill-import-create__form-panel">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="archive-form">
|
||||
<el-row :gutter="20"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="发货计划"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
||||
<el-row :gutter="20"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出" @change="contractChange"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="计划名称"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
||||
<el-form-item label="运单明细表" prop="file"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="files" :on-change="fileChange" :on-remove="fileRemove"><el-button type="primary">添加附件</el-button></el-upload><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式</span><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item>
|
||||
</el-form>
|
||||
<div class="waybill-import-create__form-actions"><el-button @click="createVisible = false">关闭</el-button><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="confirmImport">确认导入</el-button></div>
|
||||
</section>
|
||||
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" min-width="140" show-overflow-tooltip /><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-link type="primary" @click="cancelRow(row)">取消</el-link><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></template></el-table-column></el-table></section>
|
||||
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.minWidth || 150" show-overflow-tooltip><template #default="{ row }"><template v-if="row._editing"><el-select v-if="column.editor === 'transportType'" v-model="row.transportType" clearable filterable placeholder="请选择"><el-option v-for="item in transportTypeOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-else-if="column.editor === 'driver'" v-model="row.driverName" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in driverOptions" :key="item.id || item.driverName || item.name" :label="item.driverName || item.name" :value="item.driverName || item.name" /></el-select><el-cascader v-else-if="column.editor === 'cargoType'" :model-value="resolveCargoTypePath(row)" :options="cargoTypeOptions" :props="{ label: 'cargoName', value: 'id', children: 'children', emitPath: true }" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)" /><el-select v-else-if="column.editor === 'cargoName'" v-model="row.cargoName" clearable filterable placeholder="请选择" @visible-change="visible => visible && loadCommonCargoOptions(row)" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in getCommonCargoOptions(row)" :key="item.id || item.cargoName || item.name" :label="formatCargoTypeLabel(item)" :value="formatCargoTypeLabel(item)" /></el-select><el-date-picker v-else-if="column.editor === 'datetime'" v-model="row[column.prop]" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择" /><el-input v-else-if="column.editor === 'textarea'" v-model="row[column.prop]" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" :placeholder="`请输入${column.label}`" /><el-input v-else-if="column.editor === 'number'" :model-value="row[column.prop]" inputmode="decimal" :placeholder="`请输入${column.label}`" @input="value => updateEditorValue(column, row, value)" /><el-input v-else v-model="row[column.prop]" :placeholder="`请输入${column.label}`" /></template><span v-else>{{ formatCell(row, column) }}</span></template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><template v-if="row._editing"><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="cancelRow(row)">取消</el-link></template><template v-else><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></template></template></el-table-column></el-table></section>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
|
||||
@@ -56,21 +61,162 @@ const visible = computed({ get: () => props.modelValue, set: value => emit('upda
|
||||
const createVisible = ref(false), detailVisible = ref(false), formRef = ref();
|
||||
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
||||
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
||||
const form = reactive({ projectId: '', customerName: '', contractId: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
||||
const form = reactive({ projectId: '', customerName: '', contractId: '', contractName: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
||||
const rules = { projectId: [{ required: true, message: '请选择项目' }], customerName: [{ required: true, message: '客户不能为空' }], contractId: [{ required: true, message: '请选择客户合同' }], carrierType: [{ required: true, message: '请选择承运类型' }], status: [{ required: true, message: '请选择导入状态' }], importType: [{ required: true, message: '请选择导入类型' }], file: [{ required: true, message: '请上传明细表' }] };
|
||||
const batches = ref([]), details = ref([]), rows = ref([]), selected = ref([]), files = ref([]), projects = ref([]), contracts = ref([]), carriers = ref([]), creators = ref([]), plans = ref([]);
|
||||
const transportTypeOptions = ref([]), cargoTypeOptions = ref([]), cargoTypeFlatOptions = ref([]), commonCargoOptionsMap = ref({}), commonCargoLoadingMap = ref({}), driverOptions = ref([]);
|
||||
const rowSelection = ref([]), previewTab = ref('all');
|
||||
const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
|
||||
const previewRows = computed(() => previewTab.value === 'duplicate' ? duplicateRows.value : rows.value);
|
||||
const page = reactive({ current: 1, size: 10, total: 0 }), detailPage = reactive({ current: 1, size: 10, total: 0 });
|
||||
const detailColumns = [{ prop: 'batchNo', label: '运单批次号' }, { prop: 'originalNo', label: '原始单号' }, { prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'quantity', label: '重量' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '收货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }, { prop: 'otherFeeTotal', label: '其他费用合计' }, { prop: 'freightTotal', label: '运费合计' }, { prop: 'remark', label: '备注' }];
|
||||
const detailColumns = [
|
||||
{ prop: 'batchNo', label: '运单批次号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
||||
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
|
||||
{ prop: 'transportType', label: '运输类型', editor: 'transportType', minWidth: 150 },
|
||||
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
|
||||
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
|
||||
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'departureAddress', label: '发货地址', editor: 'textarea', minWidth: 260 },
|
||||
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 140 },
|
||||
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 160 },
|
||||
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
||||
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
||||
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
||||
{ prop: 'startDate', label: '开始时间', editor: 'datetime', minWidth: 190 },
|
||||
{ prop: 'endDate', label: '结束时间', editor: 'datetime', minWidth: 190 },
|
||||
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
||||
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 130 },
|
||||
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 180 },
|
||||
];
|
||||
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
|
||||
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
|
||||
const extractRecords = res => {
|
||||
const data = res?.data || res;
|
||||
if (Array.isArray(data)) return data;
|
||||
if (Array.isArray(data?.records)) return data.records;
|
||||
if (Array.isArray(data?.data)) return data.data;
|
||||
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||
return [];
|
||||
};
|
||||
const formatCargoTypeLabel = item => item?.cargoName || item?.name || item?.typeName || item?.label || '';
|
||||
const buildCargoTypeTree = cargoTypes => {
|
||||
const flatCargoTypes = [];
|
||||
const collectCargoTypes = list => {
|
||||
(list || []).forEach(item => {
|
||||
flatCargoTypes.push({ ...item, children: undefined });
|
||||
if (item.children?.length) collectCargoTypes(item.children);
|
||||
});
|
||||
};
|
||||
collectCargoTypes(cargoTypes);
|
||||
const nodeMap = new Map();
|
||||
const codeMap = new Map();
|
||||
flatCargoTypes.forEach(item => {
|
||||
const id = item.id ?? item.cargoCode ?? item.code;
|
||||
if (id === undefined || id === null) return;
|
||||
const node = {
|
||||
...item,
|
||||
id: String(id),
|
||||
cargoName: formatCargoTypeLabel(item),
|
||||
cargoCode: item.cargoCode || item.code || '',
|
||||
parentId: item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
|
||||
parentCargoCode: item.parentCargoCode || item.parentCode || '',
|
||||
children: [],
|
||||
};
|
||||
nodeMap.set(node.id, node);
|
||||
if (node.cargoCode) codeMap.set(String(node.cargoCode), node);
|
||||
});
|
||||
const rootNodes = [];
|
||||
nodeMap.forEach(node => {
|
||||
const parent = nodeMap.get(node.parentId) || codeMap.get(String(node.parentCargoCode || '')) || codeMap.get(String(node.parentId || ''));
|
||||
if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node);
|
||||
else rootNodes.push(node);
|
||||
});
|
||||
const normalize = (cargoType, level = 1, parentPath = []) => {
|
||||
if (level > 2) return null;
|
||||
const id = String(cargoType.id);
|
||||
const path = [...parentPath, id];
|
||||
const children = level === 1 ? (cargoType.children || []).map(item => normalize(item, level + 1, path)).filter(Boolean) : [];
|
||||
return { ...cargoType, id, cargoName: formatCargoTypeLabel(cargoType), path, leaf: level === 2, children: children.length ? children : undefined };
|
||||
};
|
||||
return rootNodes.map(item => normalize(item, 1)).filter(Boolean);
|
||||
};
|
||||
const flattenCargoTypeOptions = options => {
|
||||
const result = [];
|
||||
const collect = list => (list || []).forEach(item => {
|
||||
result.push(item);
|
||||
if (item.children?.length) collect(item.children);
|
||||
});
|
||||
collect(options);
|
||||
return result;
|
||||
};
|
||||
const getCargoTypePathLabels = path => {
|
||||
let options = cargoTypeOptions.value;
|
||||
const labels = [];
|
||||
(path || []).forEach(value => {
|
||||
const option = (options || []).find(item => String(item.id) === String(value));
|
||||
if (!option) return;
|
||||
labels.push(option.cargoName || '');
|
||||
options = option.children || [];
|
||||
});
|
||||
return labels.filter(Boolean);
|
||||
};
|
||||
const findCargoTypeByPath = path => {
|
||||
const id = Array.isArray(path) ? path[path.length - 1] : path;
|
||||
return cargoTypeFlatOptions.value.find(item => String(item.id) === String(id));
|
||||
};
|
||||
const resolveCargoTypePath = row => {
|
||||
if (Array.isArray(row.cargoTypePath) && row.cargoTypePath.length) return row.cargoTypePath;
|
||||
const cargoType = cargoTypeFlatOptions.value.find(item => [item.cargoName, item.name, item.typeName, item.cargoCode].some(value => String(value || '') === String(row.cargoType || row.cargoTypeCode || '')));
|
||||
return cargoType?.path || [];
|
||||
};
|
||||
const commonCargoKey = row => {
|
||||
const path = resolveCargoTypePath(row);
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
return String(cargoType?.cargoCode || cargoType?.code || path.join('/'));
|
||||
};
|
||||
const getCommonCargoOptions = row => commonCargoOptionsMap.value[commonCargoKey(row)] || [];
|
||||
const loadCommonCargoOptions = async row => {
|
||||
const path = resolveCargoTypePath(row);
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
const key = commonCargoKey(row);
|
||||
if (!key || commonCargoLoadingMap.value[key]) return;
|
||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: true };
|
||||
try {
|
||||
const labels = getCargoTypePathLabels(path);
|
||||
const res = await getCommonCargoList(1, 500, {
|
||||
secondCargoTypeName: labels[1] || row.cargoType || '',
|
||||
secondCargoTypeCode: cargoType?.cargoCode || cargoType?.code || '',
|
||||
allDept: 0,
|
||||
});
|
||||
commonCargoOptionsMap.value = { ...commonCargoOptionsMap.value, [key]: extractRecords(res) };
|
||||
} finally {
|
||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
||||
}
|
||||
};
|
||||
const loadEditorOptions = async () => {
|
||||
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
|
||||
getDictionary({ code: 'transport_type' }),
|
||||
getCargoTypeList(1, 9999),
|
||||
getDriverList(1, 9999, {}),
|
||||
]);
|
||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
||||
label: item.dictValue || item.label,
|
||||
value: item.dictKey || item.value,
|
||||
}));
|
||||
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
||||
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
||||
driverOptions.value = extractRecords(driverRes);
|
||||
};
|
||||
const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; page.total = res.data?.data?.total || 0; };
|
||||
const resetQuery = () => { Object.assign(query, { batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); page.current = 1; loadBatches(); };
|
||||
const loadDetails = async () => { const res = await api.getImportDetails({ batchId: detailQuery.batchId, ...detailQuery, current: detailPage.current, size: detailPage.size }); details.value = res.data?.data?.records || []; detailPage.total = res.data?.data?.total || 0; };
|
||||
const openCreate = async () => {
|
||||
createVisible.value = true;
|
||||
const projectRes = await getProjectList(1, 9999, {});
|
||||
const [projectRes] = await Promise.all([getProjectList(1, 9999, projectQueryParams), loadEditorOptions()]);
|
||||
projects.value = projectRes.data?.data?.records || [];
|
||||
api.getImportOptions?.().then(res => {
|
||||
const data = res.data?.data || {};
|
||||
@@ -84,7 +230,27 @@ const openDetail = row => { detailQuery.batchId = row.id; detailVisible.value =
|
||||
const editBatch = row => { openCreate(); Object.assign(form, row); };
|
||||
const removeBatches = () => api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
||||
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
|
||||
const projectChange = id => { const item = projects.value.find(row => row.id === id); form.customerName = item?.customerName || ''; api.getImportContracts?.(id).then(res => { contracts.value = res.data?.data || []; }); };
|
||||
const projectChange = async id => {
|
||||
const item = projects.value.find(row => row.id === id);
|
||||
form.customerName = item?.customerName || item?.customerNames || item?.customer || '';
|
||||
form.contractId = '';
|
||||
form.contractName = '';
|
||||
contracts.value = [];
|
||||
if (!id) return;
|
||||
const res = await getContractList(1, 9999, {
|
||||
projectId: id,
|
||||
projectName: item?.projectName,
|
||||
});
|
||||
contracts.value = res.data?.data?.records || [];
|
||||
if (contracts.value.length) {
|
||||
form.contractId = contracts.value[0].id;
|
||||
form.contractName = contracts.value[0].contractName || '';
|
||||
}
|
||||
};
|
||||
const contractChange = id => {
|
||||
const item = contracts.value.find(row => row.id === id);
|
||||
form.contractName = item?.contractName || '';
|
||||
};
|
||||
const fileChange = async (file, list) => {
|
||||
files.value = list.slice(-1); form.file = file.raw;
|
||||
const XLSX = await import('xlsx');
|
||||
@@ -96,6 +262,44 @@ const fileChange = async (file, list) => {
|
||||
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
||||
};
|
||||
const fileRemove = () => { files.value = []; form.file = null; };
|
||||
const formatCell = (row, column) => row[column.prop] || '';
|
||||
const updateEditorValue = (column, row, value) => {
|
||||
if (column.editor === 'number') {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
row[column.prop] = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
return;
|
||||
}
|
||||
row[column.prop] = value;
|
||||
};
|
||||
const handleEditorChange = (column, row, value) => {
|
||||
if (column.editor === 'driver') {
|
||||
const driver = driverOptions.value.find(item => [item.driverName, item.name].some(name => String(name || '') === String(value || '')));
|
||||
row.driverName = value || '';
|
||||
if (driver) row.driverPhone = driver.mobile || driver.phone || driver.driverPhone || row.driverPhone || '';
|
||||
}
|
||||
if (column.editor === 'cargoType') {
|
||||
const path = Array.isArray(value) ? value : [];
|
||||
const cargoType = findCargoTypeByPath(path);
|
||||
const labels = getCargoTypePathLabels(path);
|
||||
row.cargoTypePath = path;
|
||||
row.cargoType = labels.at(-1) || '';
|
||||
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||||
row.cargoName = '';
|
||||
row.specification = '';
|
||||
row.model = '';
|
||||
loadCommonCargoOptions(row);
|
||||
}
|
||||
if (column.editor === 'cargoName') {
|
||||
const cargo = getCommonCargoOptions(row).find(item => formatCargoTypeLabel(item) === value);
|
||||
row.cargoName = value || '';
|
||||
if (cargo) {
|
||||
row.specification = cargo.specification || cargo.spec || row.specification || '';
|
||||
row.model = cargo.model || cargo.modelName || row.model || '';
|
||||
row.packageType = cargo.packageType || row.packageType || '';
|
||||
row.brand = cargo.brand || row.brand || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
const res = await api.exportImportTemplate();
|
||||
@@ -119,9 +323,9 @@ const validateImportRows = () => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const editRow = row => { row._editing = true; };
|
||||
const saveRow = row => { row._editing = false; };
|
||||
const cancelRow = row => { row._cancelled = true; };
|
||||
const editRow = row => { row._editSnapshot = JSON.parse(JSON.stringify(row)); row._editing = true; };
|
||||
const saveRow = row => { row._editing = false; delete row._editSnapshot; };
|
||||
const cancelRow = row => { if (row._editSnapshot) Object.assign(row, row._editSnapshot); row._editing = false; delete row._editSnapshot; };
|
||||
const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); };
|
||||
const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; };
|
||||
const saveDraft = () => api.saveImportDraft({ ...form, rows: rows.value });
|
||||
@@ -163,5 +367,12 @@ const confirmImport = async () => { await formRef.value?.validate(); if (!valida
|
||||
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
|
||||
&__detail-head { display: flex; align-items: center; gap: 24px; margin-bottom: 8px; }
|
||||
&__file-tip { margin: 0 24px; color: #606266; }
|
||||
|
||||
:deep(.el-table .el-input),
|
||||
:deep(.el-table .el-select),
|
||||
:deep(.el-table .el-cascader),
|
||||
:deep(.el-table .el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -262,22 +262,20 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="loading-manage-page__multi-line">
|
||||
<el-link
|
||||
v-for="item in splitText(row.loadingSubNos)"
|
||||
:key="item"
|
||||
type="primary"
|
||||
class="loading-manage-page__sub-link"
|
||||
>{{ item }}</el-link>
|
||||
</div>
|
||||
<span class="loading-manage-page__sub-nos">
|
||||
{{ splitText(row.loadingSubNos).join(',') || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="车牌号/航班号/船号/班列号"
|
||||
prop="vehicleNo"
|
||||
min-width="210"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #header>
|
||||
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||
@@ -285,10 +283,21 @@
|
||||
label="发货地"
|
||||
prop="departureAddress"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="途经地" prop="transitAddress" min-width="190" show-overflow-tooltip />
|
||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180" show-overflow-tooltip />
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'departure') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'transit') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'arrival') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="运输类型"
|
||||
@@ -369,7 +378,7 @@
|
||||
</section-card>
|
||||
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
||||
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
||||
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable remote clearable placeholder="切换项目" :remote-method="loadProjectOptions" :loading="projectLoading" @visible-change="visible => visible && loadProjectOptions()" @change="handleProcessProjectChange"><el-option v-for="item in projectOptions" :key="item.id" :label="item.projectName || item.name" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
||||
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable placeholder="切换项目" @change="handleProcessProjectChange"><el-option v-for="item in processProjectOptions" :key="item.id" :label="item.projectName" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
||||
</div>
|
||||
<el-form
|
||||
v-else
|
||||
@@ -384,7 +393,7 @@
|
||||
<template #extra>
|
||||
<el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch">筛选</el-link>
|
||||
</template>
|
||||
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
|
||||
<div class="loading-manage-dialog__candidate-search">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="运单号">
|
||||
@@ -424,6 +433,7 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<template v-if="candidateSearchVisible">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="货物名称">
|
||||
<el-input v-model="candidateQuery.cargoName" clearable placeholder="请输入" />
|
||||
@@ -562,14 +572,24 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="loading-manage-dialog__candidate-actions">
|
||||
</template>
|
||||
<el-col
|
||||
:span="candidateSearchVisible ? 24 : 6"
|
||||
class="loading-manage-dialog__candidate-actions"
|
||||
>
|
||||
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
|
||||
<el-button @click="handleCandidateReset">重置</el-button>
|
||||
<el-link type="primary" @click="candidateSearchVisible = false"
|
||||
>收起</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
@click="candidateSearchVisible = !candidateSearchVisible"
|
||||
>{{ candidateSearchVisible ? '收起' : '展开' }}</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</section-card>
|
||||
|
||||
<section-card title="配载信息" class="loading-manage-dialog__load-info-card">
|
||||
<section-card title="待配载运单">
|
||||
<el-table
|
||||
ref="candidateTableRef"
|
||||
v-loading="candidateLoading"
|
||||
@@ -736,9 +756,8 @@
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="weight" label="重量(吨)" min-width="120" />
|
||||
<el-table-column prop="volume" label="体积(方)" min-width="120" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -759,7 +778,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="loading-manage-dialog__grid">
|
||||
<el-form-item v-if="dialogForm.carrierType !== '自运'" label="承运商" required>
|
||||
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
|
||||
<el-select
|
||||
v-model="dialogForm.carrierName"
|
||||
clearable
|
||||
@@ -779,7 +798,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="司机" :required="dialogForm.carrierType === '自运'">
|
||||
<el-form-item label="司机" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
|
||||
<el-select
|
||||
v-model="dialogForm.driverName"
|
||||
clearable
|
||||
@@ -802,7 +821,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" :required="dialogForm.carrierType === '自运'">
|
||||
<el-form-item label="手机号" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
|
||||
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" required>
|
||||
@@ -855,6 +874,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section-card>
|
||||
</section-card>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
@@ -1111,6 +1131,7 @@ export default {
|
||||
config: loadingManageConfig,
|
||||
processNodes: [],
|
||||
processProjectId: '',
|
||||
processProjectOptions: [],
|
||||
routeChangeVisible: false,
|
||||
routeChangeTab: 'change',
|
||||
routeChangeRows: [],
|
||||
@@ -1229,6 +1250,30 @@ export default {
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
formatRouteAddress(row, type) {
|
||||
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||||
const values = [
|
||||
row[`${prefix}Name`],
|
||||
row[`${prefix}RegionName`],
|
||||
row[`${prefix}DistrictName`],
|
||||
row[`${prefix}Address`],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.flatMap(value => this.splitText(value));
|
||||
const addressType = [
|
||||
row[`${prefix}AddressType`],
|
||||
row[`${prefix}Type`],
|
||||
row.addressType,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
const isStation = /港口|码头|机场|铁路|车站|空港/.test(`${addressType}${values[0] || ''}`);
|
||||
const displayValues = values.filter(
|
||||
(value, index) => index === 0 || isStation || !row[`${prefix}Name`] || value !== row[`${prefix}Address`]
|
||||
);
|
||||
if (type === 'transit') return displayValues.join(',') || '-';
|
||||
return (isStation ? displayValues[0] : row[`${prefix}Name`] || displayValues[0]) || '-';
|
||||
},
|
||||
rowActions(row) {
|
||||
const status = row.businessStatus;
|
||||
if (status === 'draft') {
|
||||
@@ -1286,12 +1331,11 @@ export default {
|
||||
...createDialogForm(),
|
||||
...(res.data?.data || res.data || {}),
|
||||
};
|
||||
this.processProjectId = this.dialogForm.projectId || '';
|
||||
await this.loadProjectOptions(this.dialogForm.projectName || '');
|
||||
await this.restoreWaybillRows(
|
||||
this.dialogForm.waybillIdsJson,
|
||||
this.dialogForm.loadingSubNos
|
||||
);
|
||||
this.syncProcessProjectOptions();
|
||||
this.restoreRouteAndCargo();
|
||||
this.restoreRouteChangeRecords();
|
||||
await this.loadProcessNodes();
|
||||
@@ -1326,6 +1370,7 @@ export default {
|
||||
this.candidateSearchVisible = false;
|
||||
this.processNodes = [];
|
||||
this.processProjectId = '';
|
||||
this.processProjectOptions = [];
|
||||
},
|
||||
clearLoadingDialog() {
|
||||
this.resetDialogData();
|
||||
@@ -1355,13 +1400,62 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.processProjectId, status: 1 }));
|
||||
const config = rows.find(item => item.nodeConfigJson) || {};
|
||||
this.processNodes = parseJsonArray(config.nodeConfigJson);
|
||||
const projectId = String(this.processProjectId);
|
||||
const rows = unwrapRecords(
|
||||
await getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
|
||||
);
|
||||
const config =
|
||||
rows.find(item => {
|
||||
const projectIds = String(item.projectIds || '')
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean);
|
||||
return projectIds.includes(projectId) && item.nodeConfigJson;
|
||||
}) || {};
|
||||
this.processNodes = this.parseProcessConfigNodes(config);
|
||||
} catch (error) {
|
||||
this.processNodes = [];
|
||||
}
|
||||
},
|
||||
syncProcessProjectOptions() {
|
||||
const projectMap = new Map();
|
||||
this.selectedWaybillRows.forEach(row => {
|
||||
const id = row.projectId;
|
||||
if (!id || projectMap.has(String(id))) return;
|
||||
projectMap.set(String(id), {
|
||||
id,
|
||||
projectName: row.projectName || row.projectNames || String(id),
|
||||
});
|
||||
});
|
||||
if (!projectMap.size && this.dialogForm.projectId) {
|
||||
projectMap.set(String(this.dialogForm.projectId), {
|
||||
id: this.dialogForm.projectId,
|
||||
projectName: this.dialogForm.projectName || String(this.dialogForm.projectId),
|
||||
});
|
||||
}
|
||||
this.processProjectOptions = [...projectMap.values()];
|
||||
this.processProjectId = this.processProjectOptions[0]?.id || '';
|
||||
},
|
||||
parseProcessConfigNodes(config = {}) {
|
||||
let nodes = [];
|
||||
try {
|
||||
const parsed =
|
||||
typeof config.nodeConfigJson === 'string'
|
||||
? JSON.parse(config.nodeConfigJson)
|
||||
: config.nodeConfigJson;
|
||||
nodes = Array.isArray(parsed) ? parsed : parsed?.nodes || [];
|
||||
} catch (error) {
|
||||
nodes = [];
|
||||
}
|
||||
const includedNodes = String(config.includedNodes || '')
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean);
|
||||
return nodes.filter(
|
||||
node =>
|
||||
node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
|
||||
);
|
||||
},
|
||||
async handleProcessProjectChange() {
|
||||
await this.loadProcessNodes();
|
||||
},
|
||||
@@ -1605,7 +1699,7 @@ export default {
|
||||
this.dialogForm.loadingSubNos = rows
|
||||
.map(row => row.waybillNo)
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
.join(',');
|
||||
this.dialogForm.waybillIdsJson = JSON.stringify(rows.map(row => row.id).filter(Boolean));
|
||||
this.dialogForm.projectName = uniqueText(rows, 'projectName');
|
||||
this.dialogForm.customerName = uniqueText(rows, 'customerName');
|
||||
@@ -1697,6 +1791,16 @@ export default {
|
||||
weight: item.weight || item.cargoWeight || '',
|
||||
volume: item.volume || item.cargoVolume || '',
|
||||
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
||||
quantityUnit:
|
||||
item.quantityUnit ||
|
||||
item.goodsQuantityUnit ||
|
||||
item.cargoUnit ||
|
||||
item.unit ||
|
||||
waybill.quantityUnit ||
|
||||
waybill.goodsQuantityUnit ||
|
||||
waybill.cargoUnit ||
|
||||
waybill.unit ||
|
||||
'',
|
||||
planDate: item.planDate || waybill.startDate || '',
|
||||
unitPrice: item.unitPrice || waybill.unitPrice || '',
|
||||
materialCode: item.materialCode || '',
|
||||
@@ -1754,7 +1858,7 @@ export default {
|
||||
ElMessage.warning('请输入车牌号');
|
||||
return false;
|
||||
}
|
||||
if (this.dialogForm.carrierType === '自运') {
|
||||
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
|
||||
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
|
||||
ElMessage.warning('请输入司机和司机手机号');
|
||||
return false;
|
||||
@@ -1855,6 +1959,7 @@ export default {
|
||||
{
|
||||
...this.buildQueryParams(this.searchForm),
|
||||
ids,
|
||||
exportColumns: JSON.stringify(loadingManageConfig.exportColumns),
|
||||
},
|
||||
{
|
||||
feedback: true,
|
||||
@@ -1864,7 +1969,7 @@ export default {
|
||||
});
|
||||
},
|
||||
handleCarrierTypeChange() {
|
||||
if (this.dialogForm.carrierType === '自运') {
|
||||
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
|
||||
this.dialogForm.carrierName = '';
|
||||
}
|
||||
},
|
||||
@@ -1991,17 +2096,27 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-manage-page__link,
|
||||
.loading-manage-page__sub-link {
|
||||
.loading-manage-page__link {
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.loading-manage-page__multi-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
line-height: 1.6;
|
||||
.loading-manage-page__sub-nos {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-manage-page__address-cell {
|
||||
line-height: 20px;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.loading-manage-page__vehicle-header {
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-manage-page__status {
|
||||
@@ -2069,12 +2184,12 @@ export default {
|
||||
|
||||
.loading-manage-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0 24px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__span-2 {
|
||||
grid-column: span 2;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__title-action {
|
||||
@@ -2118,6 +2233,10 @@ export default {
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.loading-manage-dialog__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.loading-manage-dialog__waybill-route-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -2233,5 +2352,14 @@ export default {
|
||||
}
|
||||
.loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; }.loading-route-change-dialog__location { cursor: pointer; color: #909399; }.loading-route-change-dialog__hint { color: #909399; }.loading-route-change-dialog__route-list { display: flex; flex-direction: column; gap: 8px; }.loading-route-change-dialog__route-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto 24px; align-items: center; gap: 12px; padding: 10px 12px; background: #f5f6f7; cursor: move; }.loading-route-change-dialog__route-type { width: 30px; height: 30px; border-radius: 50%; background: #dcdfe6; text-align: center; line-height: 30px; }.loading-route-change-dialog__tags { display: flex; gap: 4px; }.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; } }
|
||||
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
|
||||
@media (max-width: 900px) { .loading-detail__task-row, .loading-detail__bottom-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 900px) {
|
||||
.loading-manage-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.loading-detail__task-row,
|
||||
.loading-detail__bottom-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -421,12 +421,13 @@ export default {
|
||||
}
|
||||
.master-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 150px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
.card-info {
|
||||
flex: 0 0 380px;
|
||||
flex: 0 0 266px;
|
||||
padding: 18px 20px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
@@ -446,13 +447,16 @@ export default {
|
||||
}
|
||||
.status-tag {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.route-progress {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
min-height: 170px;
|
||||
padding: 22px 24px 18px;
|
||||
border-right: 1px solid #eff1f7;
|
||||
@@ -529,14 +533,16 @@ export default {
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
width: 110px;
|
||||
padding: 16px;
|
||||
width: 88px;
|
||||
padding: 16px 12px;
|
||||
header {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-link {
|
||||
margin: 0 8px 8px 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,960 @@
|
||||
<template>
|
||||
<basic-container class="voucher-manage-page">
|
||||
<section class="voucher-manage-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px">
|
||||
<div class="voucher-manage-page__search-grid">
|
||||
<el-form-item label="凭证批次号"
|
||||
><el-input v-model="query.voucherBatchNo" clearable placeholder="请输入"
|
||||
/></el-form-item>
|
||||
<el-form-item label="审核状态"
|
||||
><el-select v-model="query.auditStatus" clearable placeholder="全部"
|
||||
><el-option label="待审核" value="待审核" /><el-option
|
||||
label="审核通过"
|
||||
value="审核通过" /><el-option label="审核驳回" value="审核驳回" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="处理状态"
|
||||
><el-select v-model="query.processStatus" clearable placeholder="全部"
|
||||
><el-option label="上传中" value="上传中" /><el-option
|
||||
label="处理中"
|
||||
value="处理中" /><el-option label="处理完成" value="处理完成" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="运单批次号"
|
||||
><el-input v-model="query.waybillBatchNo" clearable placeholder="请输入"
|
||||
/></el-form-item>
|
||||
<template v-if="searchExpanded">
|
||||
<el-form-item label="上传来源"
|
||||
><el-select v-model="query.uploadSource" clearable placeholder="全部"
|
||||
><el-option label="内部" value="内部" /><el-option
|
||||
label="承运商"
|
||||
value="承运商" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="承运商"
|
||||
><el-input v-model="query.carrierName" clearable placeholder="请输入"
|
||||
/></el-form-item>
|
||||
<el-form-item label="创建日期"
|
||||
><el-date-picker
|
||||
v-model="createRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="-"
|
||||
start-placeholder="请选择"
|
||||
end-placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</template>
|
||||
<div class="voucher-manage-page__search-actions">
|
||||
<el-button type="primary" @click="search">查询</el-button
|
||||
><el-button @click="reset">重置</el-button
|
||||
><el-link type="primary" @click="searchExpanded = !searchExpanded">{{
|
||||
searchExpanded ? '收起' : '展开'
|
||||
}}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
<section>
|
||||
<div class="voucher-manage-page__toolbar">
|
||||
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
|
||||
><el-button plain @click="openProgress">上传进度</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="58"
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
|
||||
><template #default="{ row }"
|
||||
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
|
||||
row.voucherBatchNo
|
||||
}}</el-link></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
|
||||
prop="fileName"
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="uploadSource" label="上传来源" width="100" /><el-table-column
|
||||
prop="carrierName"
|
||||
label="承运商名称"
|
||||
min-width="160"
|
||||
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="processStatus" label="处理状态" width="120" /><el-table-column
|
||||
prop="voucherCount"
|
||||
label="凭证数量"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
prop="relatedWaybillCount"
|
||||
label="已关联运单"
|
||||
width="115"
|
||||
/><el-table-column prop="unRelatedWaybillCount" label="未关联运单" width="115" />
|
||||
<el-table-column prop="status" label="状态" width="100"
|
||||
><template #default="{ row }">{{
|
||||
Number(row.status) === 1 ? '启用' : '停用'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
/><el-table-column label="操作" width="320" fixed="right" align="left"
|
||||
><template #default="{ row }"
|
||||
><div class="voucher-manage-page__actions">
|
||||
<el-link
|
||||
v-if="row.processStatus === '处理完成' && row.auditStatus === '待审核'"
|
||||
type="primary"
|
||||
>流程</el-link
|
||||
><el-link v-if="row.processStatus === '处理完成'" type="primary" @click="view(row)"
|
||||
>查看</el-link
|
||||
><el-link
|
||||
v-if="row.processStatus === '处理完成'"
|
||||
type="primary"
|
||||
@click="download(row)"
|
||||
>下载</el-link
|
||||
><el-link
|
||||
v-if="row.processStatus !== '处理完成' || row.auditStatus === '待审核'"
|
||||
type="primary"
|
||||
@click="openUpload(row)"
|
||||
>更换运单批次</el-link
|
||||
><el-link
|
||||
v-if="row.processStatus === '上传中' || row.auditStatus === '审核驳回'"
|
||||
type="danger"
|
||||
@click="removeRow(row)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<div class="voucher-manage-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</basic-container>
|
||||
|
||||
<el-dialog
|
||||
v-model="uploadVisible"
|
||||
:title="editing.id ? '更换运单批次' : '批量导入凭证'"
|
||||
width="1200px"
|
||||
destroy-on-close
|
||||
:before-close="beforeUploadDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="uploadFormRef"
|
||||
:model="editing"
|
||||
:rules="rules"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
class="voucher-manage-page__upload-form"
|
||||
>
|
||||
<el-form-item label="项目名称" prop="projectId"
|
||||
><el-select
|
||||
v-model="editing.projectId"
|
||||
class="voucher-manage-page__project-select"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
@change="changeProject"
|
||||
><el-option
|
||||
v-for="item in projectOptions"
|
||||
:key="item.id"
|
||||
:label="item.projectName"
|
||||
:value="item.id" /></el-select
|
||||
></el-form-item>
|
||||
<el-form-item label="执行凭证" prop="fileUrl"
|
||||
><el-upload
|
||||
action="#"
|
||||
accept=".zip,.7z"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="uploadVoucher"
|
||||
:on-remove="clearFile"
|
||||
><el-button
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="!editing.projectId || uploading"
|
||||
>上传</el-button
|
||||
><template #tip
|
||||
><span class="voucher-manage-page__upload-tip"
|
||||
>支持 zip、7z 格式,采用分片上传,上传中断后可在“上传进度”继续上传。</span
|
||||
></template
|
||||
></el-upload
|
||||
><el-progress
|
||||
v-if="uploading || editing.fileTaskId"
|
||||
:percentage="uploadPercent"
|
||||
:status="uploadPercent === 100 ? 'success' : undefined"
|
||||
class="voucher-manage-page__upload-progress"
|
||||
/></el-form-item>
|
||||
<el-form-item label="关联运输批次" prop="waybillImportBatchIds"
|
||||
><el-button type="primary" @click="openBatchDialog">选择</el-button></el-form-item
|
||||
>
|
||||
</el-form>
|
||||
<el-table :data="selectedBatches" border
|
||||
><el-table-column type="index" label="序号" width="80" /><el-table-column
|
||||
prop="batchNo"
|
||||
label="运输批次号"
|
||||
min-width="220"
|
||||
/><el-table-column prop="createTime" label="创建时间" min-width="170" /><el-table-column
|
||||
prop="waybillCount"
|
||||
label="运单数"
|
||||
width="120"
|
||||
/><el-table-column prop="createUserName" label="创建人" min-width="140" /><el-table-column
|
||||
label="操作"
|
||||
width="100"
|
||||
><template #default="{ row }"
|
||||
><el-link type="danger" @click="removeBatch(row)">删除</el-link></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
>
|
||||
<template #footer
|
||||
><el-button @click="closeUploadDialog">取消</el-button
|
||||
><el-button type="primary" :loading="saving" @click="submitUpload"
|
||||
>确认上传</el-button
|
||||
></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="batchVisible" title="关联运单批次" width="1200px" destroy-on-close>
|
||||
<el-form
|
||||
:model="batchQuery"
|
||||
label-position="right"
|
||||
label-width="160px"
|
||||
class="voucher-manage-page__batch-search"
|
||||
><el-row :gutter="20"
|
||||
><el-col :span="12"
|
||||
><el-form-item label="运单批次号"
|
||||
><el-input
|
||||
v-model="batchQuery.batchNo"
|
||||
clearable
|
||||
placeholder="请输入" /></el-form-item></el-col
|
||||
><el-col :span="12"
|
||||
><el-form-item label="创建日期"
|
||||
><el-date-picker
|
||||
v-model="batchCreateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="-" /></el-form-item></el-col
|
||||
><el-col :span="12"
|
||||
><el-form-item label="创建人"
|
||||
><el-input
|
||||
v-model="batchQuery.createUser"
|
||||
clearable
|
||||
placeholder="请输入" /></el-form-item></el-col
|
||||
><el-col :span="12"
|
||||
><el-form-item label="运单数"
|
||||
><el-input-number v-model="batchQuery.waybillCount" :min="0" /></el-form-item></el-col
|
||||
></el-row>
|
||||
<div class="voucher-manage-page__search-actions">
|
||||
<el-button type="primary" @click="loadBatches">查询</el-button
|
||||
><el-button @click="resetBatchQuery">重置</el-button>
|
||||
</div></el-form
|
||||
>
|
||||
<el-table
|
||||
ref="batchTableRef"
|
||||
:data="batchRows"
|
||||
border
|
||||
@selection-change="batchSelection = $event"
|
||||
><el-table-column type="selection" width="56" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="80" /><el-table-column
|
||||
prop="batchNo"
|
||||
label="运单批次号"
|
||||
min-width="240" /><el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180" /><el-table-column label="运单数" width="120"
|
||||
><template #default="{ row }">{{ row.waybillCount ?? '-' }}</template></el-table-column
|
||||
><el-table-column prop="createUserName" label="创建人" min-width="140"
|
||||
/></el-table>
|
||||
<div class="voucher-manage-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="batchPage.current"
|
||||
v-model:page-size="batchPage.size"
|
||||
:total="batchPage.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@current-change="loadBatches"
|
||||
@size-change="loadBatches"
|
||||
/>
|
||||
</div>
|
||||
<template #footer
|
||||
><el-button @click="batchVisible = false">取消</el-button
|
||||
><el-button type="primary" @click="confirmBatches">确认</el-button></template
|
||||
>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="progressVisible"
|
||||
title="上传进度"
|
||||
width="1200px"
|
||||
destroy-on-close
|
||||
:before-close="beforeProgressDialogClose"
|
||||
@opened="loadProgress"
|
||||
>
|
||||
<section class="voucher-manage-page__progress-search">
|
||||
<el-form :model="progressQuery" label-position="right" label-width="160px"
|
||||
><div class="voucher-manage-page__search-grid">
|
||||
<el-form-item label="凭证批次号"
|
||||
><el-input
|
||||
v-model="progressQuery.businessId"
|
||||
clearable
|
||||
placeholder="请输入" /></el-form-item
|
||||
><el-form-item label="文件名称"
|
||||
><el-input
|
||||
v-model="progressQuery.attachmentName"
|
||||
clearable
|
||||
placeholder="请输入" /></el-form-item
|
||||
><el-form-item label="上传状态"
|
||||
><el-select v-model="progressQuery.status" clearable placeholder="全部"
|
||||
><el-option label="正在上传" value="uploading" /><el-option
|
||||
label="已暂停"
|
||||
value="paused" /><el-option label="上传完成" value="completed" /><el-option
|
||||
label="上传失败"
|
||||
value="failed" /></el-select
|
||||
></el-form-item>
|
||||
<div class="voucher-manage-page__search-actions">
|
||||
<el-button type="primary" @click="searchProgress">查询</el-button
|
||||
><el-button @click="resetProgress">重置</el-button>
|
||||
</div>
|
||||
</div></el-form
|
||||
>
|
||||
</section>
|
||||
<el-table :data="progressRows" border class="voucher-manage-page__table"
|
||||
><el-table-column type="selection" width="52" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="58"
|
||||
/><el-table-column prop="businessId" label="凭证批次号" min-width="160" /><el-table-column
|
||||
prop="attachmentName"
|
||||
label="文件名称"
|
||||
min-width="250"
|
||||
show-overflow-tooltip
|
||||
/><el-table-column prop="suffix" label="文件类型" width="100" /><el-table-column
|
||||
label="文件大小"
|
||||
width="120"
|
||||
><template #default="{ row }">{{ formatSize(row.size) }}</template></el-table-column
|
||||
><el-table-column prop="createUserName" label="上传人" width="120" /><el-table-column
|
||||
prop="createTime"
|
||||
label="上传时间"
|
||||
min-width="170"
|
||||
/><el-table-column label="上传进度" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><div class="voucher-manage-page__progress-cell">
|
||||
<el-progress
|
||||
:percentage="taskPercent(row)"
|
||||
:stroke-width="10"
|
||||
:show-text="false"
|
||||
color="#409eff"
|
||||
/><span class="voucher-manage-page__progress-value">{{ taskPercent(row) }}%</span>
|
||||
</div></template
|
||||
></el-table-column
|
||||
><el-table-column label="上传状态" width="120"
|
||||
><template #default="{ row }">{{ taskStatusName(row.status) }}</template></el-table-column
|
||||
><el-table-column label="操作" width="160" fixed="right"
|
||||
><template #default="{ row }"
|
||||
><el-link
|
||||
v-if="row.status === 'paused' || row.status === 'failed'"
|
||||
type="primary"
|
||||
@click="resumeTask(row)"
|
||||
>继续上传</el-link
|
||||
><el-link v-if="row.status === 'uploading'" type="danger" @click="pauseTask(row)"
|
||||
>取消</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
>
|
||||
<div class="voucher-manage-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="progressPage.current"
|
||||
v-model:page-size="progressPage.size"
|
||||
:total="progressPage.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@current-change="loadProgress"
|
||||
@size-change="loadProgress"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<input
|
||||
ref="resumeFileInput"
|
||||
class="voucher-manage-page__hidden-file"
|
||||
type="file"
|
||||
accept=".zip,.7z"
|
||||
@change="resumeFileSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import md5 from 'js-md5';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import * as api from '@/api/business/voucher-manage';
|
||||
|
||||
const loading = ref(false),
|
||||
rows = ref([]),
|
||||
createRange = ref([]),
|
||||
searchExpanded = ref(false),
|
||||
uploadVisible = ref(false),
|
||||
batchVisible = ref(false),
|
||||
progressVisible = ref(false),
|
||||
uploadFormRef = ref(),
|
||||
uploading = ref(false),
|
||||
saving = ref(false),
|
||||
uploadPercent = ref(0),
|
||||
resumeFileInput = ref(),
|
||||
resumeTarget = ref(),
|
||||
uploadAbortController = ref(),
|
||||
uploadSession = ref(0),
|
||||
closingUploadDialog = ref(false),
|
||||
closingProgressDialog = ref(false),
|
||||
cancelledTaskIds = ref(new Set()),
|
||||
uploadCancelled = ref(false),
|
||||
activeUploadTaskId = ref();
|
||||
const query = reactive({
|
||||
voucherBatchNo: '',
|
||||
auditStatus: '',
|
||||
processStatus: '',
|
||||
waybillBatchNo: '',
|
||||
uploadSource: '',
|
||||
carrierName: '',
|
||||
});
|
||||
const page = reactive({ current: 1, size: 10, total: 0 });
|
||||
const editing = reactive({
|
||||
id: '',
|
||||
voucherBatchNo: '',
|
||||
projectId: '',
|
||||
projectName: '',
|
||||
fileName: '',
|
||||
fileUrl: '',
|
||||
fileTaskId: '',
|
||||
waybillImportBatchIds: [],
|
||||
});
|
||||
const projectOptions = ref([]);
|
||||
const selectedBatches = ref([]),
|
||||
batchRows = ref([]),
|
||||
batchSelection = ref([]),
|
||||
batchCreateRange = ref([]);
|
||||
const batchQuery = reactive({ batchNo: '', createUser: '', waybillCount: undefined });
|
||||
const batchPage = reactive({ current: 1, size: 10, total: 0 });
|
||||
const progressQuery = reactive({ businessId: '', attachmentName: '', status: '' });
|
||||
const progressPage = reactive({ current: 1, size: 10, total: 0 });
|
||||
const progressRows = ref([]);
|
||||
const rules = {
|
||||
projectId: [{ required: true, message: '请选择项目名称' }],
|
||||
waybillImportBatchIds: [{ required: true, message: '请关联运输批次' }],
|
||||
};
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.getList(page.current, page.size, {
|
||||
...query,
|
||||
createTimeStart: createRange.value?.[0] ? `${createRange.value[0]} 00:00:00` : '',
|
||||
createTimeEnd: createRange.value?.[1] ? `${createRange.value[1]} 23:59:59` : '',
|
||||
});
|
||||
const data = res.data?.data || {};
|
||||
rows.value = data.records || [];
|
||||
page.total = data.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const search = () => {
|
||||
page.current = 1;
|
||||
load();
|
||||
};
|
||||
const reset = () => {
|
||||
Object.assign(query, {
|
||||
voucherBatchNo: '',
|
||||
auditStatus: '',
|
||||
processStatus: '',
|
||||
waybillBatchNo: '',
|
||||
uploadSource: '',
|
||||
carrierName: '',
|
||||
});
|
||||
createRange.value = [];
|
||||
search();
|
||||
};
|
||||
const loadProjects = async () => {
|
||||
const res = await getProjectList(1, 9999, { temporaryCreditLimitSelectable: true });
|
||||
projectOptions.value = res.data?.data?.records || [];
|
||||
};
|
||||
const changeProject = projectId => {
|
||||
editing.projectName = projectOptions.value.find(item => item.id === projectId)?.projectName || '';
|
||||
};
|
||||
const openUpload = async row => {
|
||||
await loadProjects();
|
||||
Object.assign(editing, {
|
||||
id: row?.id || '',
|
||||
voucherBatchNo: row?.voucherBatchNo || '',
|
||||
projectId: row?.projectId || '',
|
||||
projectName: row?.projectName || '',
|
||||
fileName: row?.fileName || '',
|
||||
fileUrl: row?.fileUrl || '',
|
||||
fileTaskId: row?.fileTaskId || '',
|
||||
waybillImportBatchIds: [],
|
||||
});
|
||||
uploadPercent.value = 0;
|
||||
selectedBatches.value = [];
|
||||
uploadVisible.value = true;
|
||||
if (row?.waybillBatchNo) {
|
||||
await loadBatches();
|
||||
selectedBatches.value = batchRows.value.filter(item =>
|
||||
row.waybillBatchNo.split(',').includes(item.batchNo)
|
||||
);
|
||||
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||||
}
|
||||
};
|
||||
const clearFile = () => Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
||||
const stopCurrentUpload = async taskId => {
|
||||
// 先使整个上传会话失效,阻止 MD5 计算、创建任务等异步流程返回后继续上传。
|
||||
uploadCancelled.value = true;
|
||||
uploadSession.value += 1;
|
||||
uploadAbortController.value?.abort();
|
||||
const currentTaskId = taskId || activeUploadTaskId.value || editing.fileTaskId;
|
||||
if (!currentTaskId) return;
|
||||
cancelledTaskIds.value.add(String(currentTaskId));
|
||||
try {
|
||||
await api.pauseFileTask([currentTaskId]);
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '暂停上传失败');
|
||||
}
|
||||
};
|
||||
const closeUploadDialog = async () => {
|
||||
if (closingUploadDialog.value) return;
|
||||
closingUploadDialog.value = true;
|
||||
try {
|
||||
await stopCurrentUpload(editing.fileTaskId);
|
||||
uploadVisible.value = false;
|
||||
} finally {
|
||||
closingUploadDialog.value = false;
|
||||
}
|
||||
};
|
||||
const beforeUploadDialogClose = async done => {
|
||||
if (closingUploadDialog.value) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
closingUploadDialog.value = true;
|
||||
try {
|
||||
await stopCurrentUpload(editing.fileTaskId);
|
||||
done();
|
||||
} finally {
|
||||
closingUploadDialog.value = false;
|
||||
}
|
||||
};
|
||||
const beforeProgressDialogClose = async done => {
|
||||
if (closingProgressDialog.value) return;
|
||||
closingProgressDialog.value = true;
|
||||
try {
|
||||
if (uploading.value || activeUploadTaskId.value) {
|
||||
await stopCurrentUpload(activeUploadTaskId.value);
|
||||
}
|
||||
done();
|
||||
} finally {
|
||||
closingProgressDialog.value = false;
|
||||
}
|
||||
};
|
||||
const ensureUploadSession = (sessionId, taskId) => {
|
||||
if (
|
||||
uploadCancelled.value ||
|
||||
sessionId !== uploadSession.value ||
|
||||
(taskId && cancelledTaskIds.value.has(String(taskId)))
|
||||
) {
|
||||
throw new DOMException('上传已暂停', 'AbortError');
|
||||
}
|
||||
};
|
||||
const calculateMd5 = async file => {
|
||||
const buffer = await file.arrayBuffer();
|
||||
return md5.base64(buffer);
|
||||
};
|
||||
const uploadParts = async (file, task, sessionId) => {
|
||||
uploading.value = true;
|
||||
activeUploadTaskId.value = task.id;
|
||||
const abortController = new AbortController();
|
||||
uploadAbortController.value = abortController;
|
||||
const chunkSize = task.chunkSize;
|
||||
try {
|
||||
for (
|
||||
let partNumber = Math.max(1, (task.currentIndex || 0) + 1);
|
||||
partNumber <= task.chunkTotal;
|
||||
partNumber += 1
|
||||
) {
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const url = urlRes.data?.data;
|
||||
const part = file.slice(
|
||||
(partNumber - 1) * chunkSize,
|
||||
Math.min(partNumber * chunkSize, file.size)
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: part,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
|
||||
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
uploadPercent.value = Math.round((partNumber / task.chunkTotal) * 100);
|
||||
if (progressVisible.value) await loadProgress();
|
||||
}
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
const fileUrlRes = await api.getFileTaskUrl(task.id);
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
editing.fileName = file.name;
|
||||
editing.fileTaskId = task.id;
|
||||
editing.fileUrl = fileUrlRes.data?.data || '';
|
||||
await api.completeUploadFile({
|
||||
voucherBatchNo: task.businessId,
|
||||
fileTaskId: task.id,
|
||||
fileName: file.name,
|
||||
fileUrl: editing.fileUrl,
|
||||
});
|
||||
ElMessage.success('执行凭证上传完成');
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError')
|
||||
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||||
throw error;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
if (uploadAbortController.value === abortController) uploadAbortController.value = undefined;
|
||||
if (activeUploadTaskId.value === task.id) activeUploadTaskId.value = undefined;
|
||||
if (progressVisible.value) await loadProgress();
|
||||
}
|
||||
};
|
||||
const ensureUploadDraft = async fileName => {
|
||||
if (editing.id) return;
|
||||
if (!editing.projectId) throw new Error('请先选择项目名称');
|
||||
const res = await api.createUploadDraft({ projectId: editing.projectId, fileName });
|
||||
const draft = res.data?.data;
|
||||
if (!draft?.id || !draft?.voucherBatchNo) throw new Error('创建凭证上传记录失败');
|
||||
Object.assign(editing, {
|
||||
id: draft.id,
|
||||
voucherBatchNo: draft.voucherBatchNo,
|
||||
projectName: draft.projectName,
|
||||
});
|
||||
};
|
||||
const uploadVoucher = async file => {
|
||||
const raw = file.raw;
|
||||
if (!raw) return;
|
||||
uploadCancelled.value = false;
|
||||
const sessionId = uploadSession.value + 1;
|
||||
uploadSession.value = sessionId;
|
||||
try {
|
||||
if (!resumeTarget.value) await ensureUploadDraft(raw.name);
|
||||
ensureUploadSession(sessionId);
|
||||
const md5 = await calculateMd5(raw);
|
||||
ensureUploadSession(sessionId);
|
||||
const resumeTask = resumeTarget.value;
|
||||
let task;
|
||||
if (resumeTask) {
|
||||
if (
|
||||
md5 !== resumeTask.md5 ||
|
||||
raw.name !== resumeTask.attachmentName ||
|
||||
raw.size !== Number(resumeTask.size)
|
||||
) {
|
||||
ElMessage.error('请选择与上传任务相同的文件');
|
||||
return;
|
||||
}
|
||||
const taskRes = await api.getFileTask(resumeTask.id);
|
||||
ensureUploadSession(sessionId);
|
||||
task = taskRes.data?.data;
|
||||
if (!task || String(task.id) !== String(resumeTask.id)) {
|
||||
ElMessage.error('上传任务不存在或已失效');
|
||||
return;
|
||||
}
|
||||
await api.resumeFileTask(task.id);
|
||||
ensureUploadSession(sessionId);
|
||||
task.status = 'uploading';
|
||||
if (progressVisible.value) await loadProgress();
|
||||
} else {
|
||||
const existingRes = await api.queryFileTask(md5);
|
||||
ensureUploadSession(sessionId);
|
||||
task = existingRes.data?.data;
|
||||
}
|
||||
const chunkSize = 5 * 1024 * 1024;
|
||||
if (
|
||||
!task ||
|
||||
task.status === 'completed' ||
|
||||
(!resumeTask && task.businessId !== editing.voucherBatchNo)
|
||||
) {
|
||||
const createRes = await api.createFileTask({
|
||||
md5,
|
||||
attachmentName: raw.name,
|
||||
size: raw.size,
|
||||
chunkSize,
|
||||
chunkTotal: Math.ceil(raw.size / chunkSize),
|
||||
force: !!task,
|
||||
businessType: 'voucher',
|
||||
businessId: editing.voucherBatchNo,
|
||||
});
|
||||
ensureUploadSession(sessionId);
|
||||
task = createRes.data?.data;
|
||||
}
|
||||
editing.fileName = raw.name;
|
||||
editing.fileTaskId = task.id;
|
||||
activeUploadTaskId.value = task.id;
|
||||
cancelledTaskIds.value.delete(String(task.id));
|
||||
resumeTarget.value = undefined;
|
||||
await uploadParts(raw, task, sessionId);
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError')
|
||||
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||||
}
|
||||
};
|
||||
watch(uploadVisible, visible => {
|
||||
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
|
||||
if (!visible) void stopCurrentUpload(editing.fileTaskId);
|
||||
});
|
||||
const openBatchDialog = async () => {
|
||||
batchVisible.value = true;
|
||||
await loadBatches();
|
||||
};
|
||||
const loadBatches = async () => {
|
||||
const res = await api.getWaybillBatches(batchPage.current, batchPage.size, {
|
||||
batchNo: batchQuery.batchNo,
|
||||
createUser: batchQuery.createUser,
|
||||
waybillCount: batchQuery.waybillCount,
|
||||
createTimeStart: batchCreateRange.value?.[0] ? `${batchCreateRange.value[0]} 00:00:00` : '',
|
||||
createTimeEnd: batchCreateRange.value?.[1] ? `${batchCreateRange.value[1]} 23:59:59` : '',
|
||||
});
|
||||
const data = res.data?.data || {};
|
||||
batchRows.value = (data.records || []).map(item => ({ ...item, id: item.waybillIds }));
|
||||
batchPage.total = data.total || 0;
|
||||
};
|
||||
const resetBatchQuery = () => {
|
||||
Object.assign(batchQuery, { batchNo: '', createUser: '', waybillCount: undefined });
|
||||
batchCreateRange.value = [];
|
||||
batchPage.current = 1;
|
||||
loadBatches();
|
||||
};
|
||||
const confirmBatches = () => {
|
||||
const batches = new Map();
|
||||
batchSelection.value.forEach(item => {
|
||||
if (!batches.has(item.batchNo)) batches.set(item.batchNo, item);
|
||||
});
|
||||
selectedBatches.value = [...batches.values()];
|
||||
editing.waybillImportBatchIds = selectedBatches.value.flatMap(item =>
|
||||
String(item.waybillIds || item.id)
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
);
|
||||
batchVisible.value = false;
|
||||
};
|
||||
const removeBatch = row => {
|
||||
selectedBatches.value = selectedBatches.value.filter(item => item.id !== row.id);
|
||||
editing.waybillImportBatchIds = selectedBatches.value.flatMap(item =>
|
||||
String(item.waybillIds || item.id)
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
);
|
||||
};
|
||||
const submitUpload = async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
if (uploading.value && editing.fileTaskId) {
|
||||
await api.pauseFileTask([editing.fileTaskId]);
|
||||
uploadAbortController.value?.abort();
|
||||
}
|
||||
const valid = await uploadFormRef.value.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
if (!editing.fileUrl && !editing.fileTaskId) {
|
||||
ElMessage.warning('请先选择执行凭证');
|
||||
return;
|
||||
}
|
||||
await api.submit({
|
||||
...editing,
|
||||
waybillImportBatchIds: selectedBatches.value.flatMap(item =>
|
||||
String(item.waybillIds || item.id)
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
),
|
||||
});
|
||||
ElMessage.success('提交成功');
|
||||
await closeUploadDialog();
|
||||
load();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
const openProgress = () => {
|
||||
progressVisible.value = true;
|
||||
};
|
||||
const loadProgress = async () => {
|
||||
const res = await api.getFileTaskList(progressPage.current, progressPage.size, {
|
||||
...progressQuery,
|
||||
businessType: 'voucher',
|
||||
});
|
||||
const data = res.data?.data || {};
|
||||
progressRows.value = data.records || [];
|
||||
progressPage.total = data.total || 0;
|
||||
};
|
||||
const searchProgress = () => {
|
||||
progressPage.current = 1;
|
||||
loadProgress();
|
||||
};
|
||||
const resetProgress = () => {
|
||||
Object.assign(progressQuery, { businessId: '', attachmentName: '', status: '' });
|
||||
searchProgress();
|
||||
};
|
||||
const taskPercent = row =>
|
||||
row.chunkTotal ? Math.round(((row.currentIndex || 0) / row.chunkTotal) * 100) : 0;
|
||||
const taskStatusName = status =>
|
||||
({ uploading: '正在上传', paused: '已暂停', completed: '上传完成', failed: '上传失败' }[status] ||
|
||||
status);
|
||||
const formatSize = size =>
|
||||
size >= 1024 ** 3 ? `${(size / 1024 ** 3).toFixed(1)}G` : `${(size / 1024 ** 2).toFixed(1)}M`;
|
||||
const pauseTask = async row => {
|
||||
await api.pauseFileTask([row.id]);
|
||||
ElMessage.success('已取消上传');
|
||||
loadProgress();
|
||||
};
|
||||
const resumeTask = row => {
|
||||
resumeTarget.value = row;
|
||||
activeUploadTaskId.value = row.id;
|
||||
resumeFileInput.value?.click();
|
||||
};
|
||||
const resumeFileSelected = event => {
|
||||
const [raw] = event.target.files || [];
|
||||
if (raw) uploadVoucher({ raw });
|
||||
event.target.value = '';
|
||||
};
|
||||
const removeRow = row =>
|
||||
ElMessageBox.confirm(`确认删除凭证批次“${row.voucherBatchNo}”吗?`, '提示', {
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
await api.remove(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
});
|
||||
const view = row => ElMessage.info(`凭证批次:${row.voucherBatchNo}`);
|
||||
const download = row => window.open(row.fileUrl, '_blank');
|
||||
load();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.voucher-manage-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px 24px;
|
||||
}
|
||||
&__search-actions {
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
&__table {
|
||||
width: 100%;
|
||||
}
|
||||
&__link {
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 12px;
|
||||
}
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px 0 0;
|
||||
background: #fff;
|
||||
}
|
||||
&__project-select {
|
||||
width: 280px;
|
||||
}
|
||||
&__hidden-file {
|
||||
display: none;
|
||||
}
|
||||
&__upload-form :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&__upload-tip {
|
||||
margin-left: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
&__progress-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
:deep(.el-progress) {
|
||||
flex: 1;
|
||||
}
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background: #eff1f7;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
&__progress-value {
|
||||
min-width: 38px;
|
||||
color: #303133;
|
||||
text-align: right;
|
||||
}
|
||||
&__batch-search {
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper),
|
||||
:deep(.el-date-editor--daterange) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.voucher-manage-page__project-select) {
|
||||
width: 280px;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
}
|
||||
:deep(.el-table__body tr:nth-child(even) > td) {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user