1、完善配载

2、调整客商
3、调整临时额度
4、新增表头拖拽排序
This commit is contained in:
2026-08-11 15:26:44 +08:00
parent 0a5b3b2cab
commit 9c37fa4c61
11 changed files with 759 additions and 231 deletions
+7
View File
@@ -89,3 +89,10 @@ export const batchComplete = ids =>
ids, ids,
}, },
}); });
export const changeRoute = row =>
request({
url: `${baseUrl}/change-route`,
method: 'post',
data: row,
});
+1 -1
View File
@@ -23,7 +23,7 @@ export default {
//配置首页不可关闭 //配置首页不可关闭
setting: { setting: {
sidebar: 'vertical', sidebar: 'vertical',
tag: false, tag: true,
debug: false, debug: false,
collapse: true, collapse: true,
search: false, search: false,
+2
View File
@@ -15,6 +15,7 @@ import crudCommon from '@/mixins/crud.js';
import { getScreen, findColumn } from './utils/util'; import { getScreen, findColumn } from './utils/util';
import { setupPaginationEnhancer } from './utils/pagination'; import { setupPaginationEnhancer } from './utils/pagination';
import { setupDialogFormLabelAlign } from './utils/dialog-form-label'; import { setupDialogFormLabelAlign } from './utils/dialog-form-label';
import { setupTableColumnDrag } from './utils/table-column-drag';
import './permission'; import './permission';
import error from './error'; import error from './error';
import App from './App.vue'; import App from './App.vue';
@@ -107,6 +108,7 @@ app.use(VueClipboard, {
autoSetContainer: true, autoSetContainer: true,
appendToBody: true, // 这可以帮助解决一些更复杂的使用场景下的问题 appendToBody: true, // 这可以帮助解决一些更复杂的使用场景下的问题
}); });
setupTableColumnDrag(app);
app.mount('#app'); app.mount('#app');
setupPaginationEnhancer(); setupPaginationEnhancer();
setupDialogFormLabelAlign(); setupDialogFormLabelAlign();
@@ -32,6 +32,7 @@ export const config = {
temporaryCreditLimitSelectable: true, temporaryCreditLimitSelectable: true,
}, },
enableAttachmentTable: true, enableAttachmentTable: true,
detailAttachmentUploadTimeWidth: 220,
enableDraftSave: true, enableDraftSave: true,
saveAsDraft: true, saveAsDraft: true,
defaultForm: { defaultForm: {
@@ -271,6 +272,8 @@ export const option = {
}, },
...auditColumns.map(column => ({ ...column, hide: true })), ...auditColumns.map(column => ({ ...column, hide: true })),
]), ]),
dialogWidth: 1280, dialogWidth: 1400,
menuFixed: 'right',
menuWidth: 320,
index: false, index: false,
}; };
+21 -5
View File
@@ -574,12 +574,10 @@
gap: 8px; gap: 8px;
} }
// 全站统一:表格操作列文字链接统一为主色(蓝色) // 全站统一:表格普通操作链接使用主色(蓝色),删除等危险操作保持危险色(红色)。
// 覆盖 Avue #menu 槽位与手写表格操作列中 el-link 的 warning / success / danger 等类型,
// 同时统一 hover / active / disabled 态,保证全站操作列风格一致。
// 选择器同时覆盖 Avue 行菜单(.avue-crud__menu)与任意含 el-link 的表格体单元格(手写操作列)。 // 选择器同时覆盖 Avue 行菜单(.avue-crud__menu)与任意含 el-link 的表格体单元格(手写操作列)。
.avue-crud__menu .el-link, .avue-crud__menu .el-link:not(.el-link--danger),
.el-table .el-table__body .cell:has(.el-link) .el-link { .el-table .el-table__body .cell:has(.el-link) .el-link:not(.el-link--danger) {
// 覆盖 Element Plus el-link 默认的 font-weight: var(--el-font-weight-primary)500 中等粗), // 覆盖 Element Plus el-link 默认的 font-weight: var(--el-font-weight-primary)500 中等粗),
// 操作列文字统一为常规字重,避免「查看/编辑/删除」等链接显得加粗。 // 操作列文字统一为常规字重,避免「查看/编辑/删除」等链接显得加粗。
font-weight: normal; font-weight: normal;
@@ -589,6 +587,14 @@
--el-link-disabled-text-color: var(--el-color-primary-light-5); --el-link-disabled-text-color: var(--el-color-primary-light-5);
} }
.avue-crud__menu .el-link.el-link--danger,
.el-table .el-table__body .cell:has(.el-link) .el-link.el-link--danger {
--el-link-text-color: var(--el-color-danger);
--el-link-hover-text-color: var(--el-color-danger-light-3);
--el-link-active-text-color: var(--el-color-danger-dark-2);
--el-link-disabled-text-color: var(--el-color-danger-light-5);
}
// 全站统一:去除操作列 el-link 默认 margin,避免与 8px gap 叠加导致文字间距过宽 // 全站统一:去除操作列 el-link 默认 margin,避免与 8px gap 叠加导致文字间距过宽
.avue-crud .avue-crud__menu .el-link, .avue-crud .avue-crud__menu .el-link,
.el-table .el-table__body .cell:not(.avue-crud__menu):has(> .el-link) .el-link { .el-table .el-table__body .cell:not(.avue-crud__menu):has(> .el-link) .el-link {
@@ -652,3 +658,13 @@
gap: 6px; gap: 6px;
} }
} }
// 全局表格列拖拽:仅对可移动的普通列显示拖拽反馈。
.el-table th.tms-table-column-draggable {
cursor: grab;
&.is-column-dragging {
cursor: grabbing;
opacity: 0.55;
}
}
+1
View File
@@ -86,6 +86,7 @@ const collectLabelGroups = form => {
}; };
const alignFormLabelsByColumn = form => { const alignFormLabelsByColumn = form => {
if (form.classList.contains('dialog-form-label-fixed')) return;
const groups = collectLabelGroups(form); const groups = collectLabelGroups(form);
groups.forEach(group => { groups.forEach(group => {
+176
View File
@@ -0,0 +1,176 @@
const STORAGE_PREFIX = 'tms:table-column-order:';
const getStore = instance =>
instance?.store ||
instance?.exposed?.store ||
instance?.proxy?.store ||
instance?.$?.exposed?.store ||
instance?.$?.ctx?.store ||
instance?.$?.store;
const getColumnKey = column =>
String(column.property || column.columnKey || column.label || column.id || '');
const getTableKey = (instance, columns) => {
const routePath = instance.$route?.path || instance.proxy?.$route?.path || window.location.pathname;
const signature = columns
.map(getColumnKey)
.filter(Boolean)
.sort()
.join('|');
return `${STORAGE_PREFIX}${routePath}:${signature}`;
};
const canDragColumn = column =>
column && !column.fixed && !['selection', 'index', 'expand'].includes(column.type);
const getColumnFromHeader = (header, columns) =>
columns.find(column => header.classList.contains(column.id));
const updateColumnOrder = (instance, columns) => {
const store = getStore(instance);
if (!store?.states?._columns || columns.length !== store.states._columns.value.length) return;
columns.forEach((column, index) => {
column.no = index;
});
store.states._columns.value = [...columns];
store.updateColumns?.();
store.scheduleLayout?.(true, true);
};
const restoreColumnOrder = instance => {
const store = getStore(instance);
const columns = store?.states?._columns?.value;
if (!Array.isArray(columns) || columns.length < 2) return;
const saved = JSON.parse(localStorage.getItem(getTableKey(instance, columns)) || '[]');
if (!Array.isArray(saved) || !saved.length) return;
const orderMap = new Map(saved.map((key, index) => [key, index]));
const ordered = [...columns].sort((a, b) => {
const aOrder = orderMap.get(getColumnKey(a));
const bOrder = orderMap.get(getColumnKey(b));
if (aOrder === undefined && bOrder === undefined) return a.no - b.no;
if (aOrder === undefined) return 1;
if (bOrder === undefined) return -1;
return aOrder - bOrder;
});
updateColumnOrder(instance, ordered);
};
const bindHeaderDrag = instance => {
const store = getStore(instance);
const columns = store?.states?._columns?.value;
const headerRow = instance.$el?.querySelector('.el-table__header-wrapper thead tr');
if (!headerRow || headerRow.__tmsColumnDragBound || !Array.isArray(columns)) return;
headerRow.__tmsColumnDragBound = true;
let draggingColumn = null;
[...headerRow.children].forEach(header => {
const column = getColumnFromHeader(header, columns);
if (!canDragColumn(column)) return;
header.draggable = true;
header.classList.add('tms-table-column-draggable');
});
headerRow.addEventListener('dragstart', event => {
const header = event.target.closest('th');
const currentColumns = store.states._columns.value;
const column = header && getColumnFromHeader(header, currentColumns);
if (!canDragColumn(column)) {
event.preventDefault();
return;
}
draggingColumn = column;
event.dataTransfer.effectAllowed = 'move';
header.classList.add('is-column-dragging');
});
headerRow.addEventListener('dragover', event => {
const header = event.target.closest('th');
const column = header && getColumnFromHeader(header, store.states._columns.value);
if (draggingColumn && canDragColumn(column)) event.preventDefault();
});
headerRow.addEventListener('drop', event => {
const header = event.target.closest('th');
const targetColumn = header && getColumnFromHeader(header, store.states._columns.value);
if (!draggingColumn || !canDragColumn(targetColumn) || draggingColumn === targetColumn) return;
event.preventDefault();
const ordered = [...store.states._columns.value];
const from = ordered.indexOf(draggingColumn);
if (from < 0) return;
const movingRight = from < ordered.indexOf(targetColumn);
ordered.splice(from, 1);
const targetIndex = ordered.indexOf(targetColumn);
ordered.splice(targetIndex + (movingRight ? 1 : 0), 0, draggingColumn);
updateColumnOrder(instance, ordered);
localStorage.setItem(getTableKey(instance, ordered), JSON.stringify(ordered.map(getColumnKey)));
});
headerRow.addEventListener('dragend', () => {
instance.$el.querySelectorAll('.is-column-dragging').forEach(item => item.classList.remove('is-column-dragging'));
draggingColumn = null;
});
};
export const setupTableColumnDrag = app => {
let dragging = null;
const restoredTables = new WeakSet();
const getTableInstance = element => {
let instance = element.closest('.el-table')?.__vueParentComponent;
while (instance && instance.type?.name !== 'ElTable') {
instance = instance.parent;
}
return instance;
};
const enableHeaderDrag = () => {
document.querySelectorAll('.el-table__header-wrapper th').forEach(header => {
if (header.closest('.el-table__fixed, .el-table__fixed-right')) return;
const instance = getTableInstance(header);
if (instance && !restoredTables.has(instance)) {
restoreColumnOrder(instance);
restoredTables.add(instance);
}
const column = getColumnFromHeader(header, getStore(instance)?.states?._columns?.value || []);
if (!canDragColumn(column)) return;
header.draggable = true;
header.classList.add('tms-table-column-draggable');
});
};
const tableObserver = new MutationObserver(enableHeaderDrag);
tableObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(enableHeaderDrag, 0);
document.addEventListener('dragstart', event => {
const header = event.target.closest?.('.el-table__header-wrapper th.tms-table-column-draggable');
const instance = header && getTableInstance(header);
const store = getStore(instance);
const column = header && getColumnFromHeader(header, store?.states?._columns?.value || []);
if (!header || !canDragColumn(column)) return;
dragging = { instance, column, header };
event.dataTransfer.effectAllowed = 'move';
header.classList.add('is-column-dragging');
});
document.addEventListener('dragover', event => {
const header = event.target.closest?.('.el-table__header-wrapper th.tms-table-column-draggable');
const instance = header && getTableInstance(header);
if (dragging && instance === dragging.instance) event.preventDefault();
});
document.addEventListener('drop', event => {
const header = event.target.closest?.('.el-table__header-wrapper th.tms-table-column-draggable');
const instance = header && getTableInstance(header);
const store = getStore(instance);
const target = header && getColumnFromHeader(header, store?.states?._columns?.value || []);
if (!dragging || instance !== dragging.instance || !canDragColumn(target) || target === dragging.column) return;
event.preventDefault();
const ordered = [...store.states._columns.value];
const from = ordered.indexOf(dragging.column);
if (from < 0) return;
const movingRight = from < ordered.indexOf(target);
ordered.splice(from, 1);
const targetIndex = ordered.indexOf(target);
ordered.splice(targetIndex + (movingRight ? 1 : 0), 0, dragging.column);
updateColumnOrder(instance, ordered);
localStorage.setItem(getTableKey(instance, ordered), JSON.stringify(ordered.map(getColumnKey)));
});
document.addEventListener('dragend', () => {
document.querySelectorAll('.is-column-dragging').forEach(item => item.classList.remove('is-column-dragging'));
dragging = null;
});
};
@@ -2179,7 +2179,12 @@
<template #default="{ row }">{{ formatFileSize(row.size) }}</template> <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" /> <el-table-column prop="uploadUserName" label="上传人" width="140" />
<el-table-column prop="uploadTime" label="上传时间" width="180" sortable/> <el-table-column
prop="uploadTime"
label="上传时间"
:width="config.detailAttachmentUploadTimeWidth || 180"
sortable
/>
</el-table> </el-table>
</section-card> </section-card>
</div> </div>
+362 -141
View File
@@ -138,9 +138,9 @@
> >
<el-option <el-option
v-for="item in carrierOptions" v-for="item in carrierOptions"
:key="item.id || item.customerName || item.carrierName" :key="item.id || item.fullName || item.customerName || item.carrierName"
:label="item.customerName || item.carrierName || item.name" :label="item.fullName || item.customerName || item.carrierName || item.name"
:value="item.customerName || item.carrierName || item.name" :value="item.fullName || item.customerName || item.carrierName || item.name"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -214,7 +214,7 @@
<div class="loading-manage-page__search-actions"> <div class="loading-manage-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button> <el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleResetSearch">重置</el-button> <el-button @click="handleResetSearch">重置</el-button>
<el-link type="primary" @click="searchExpanded = !searchExpanded">{{ searchExpanded ? '收起筛选' : '展开筛选' }}</el-link> <el-link type="primary" @click="searchExpanded = !searchExpanded">{{ searchExpanded ? '收起' : '展开' }}</el-link>
</div> </div>
</div> </div>
</el-form> </el-form>
@@ -350,7 +350,29 @@
@closed="resetLoadingDialog" @closed="resetLoadingDialog"
> >
<div v-loading="dialogLoading" class="loading-manage-dialog__body"> <div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情">
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div><el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag></div>
<div><el-tag type="warning">{{ dialogForm.transportType || '-' }}</el-tag></div>
</div>
<div class="loading-detail__task-row">
<div class="loading-detail__field"><span class="loading-detail__label">任务信息</span><span class="loading-detail__value">{{ taskSummary }}</span></div>
<div class="loading-detail__field"><span class="loading-detail__label">附件</span><span class="loading-detail__value"><el-link v-for="file in detailAttachments" :key="file.url || file.name" type="primary" @click="previewAttachment(file)">{{ file.name }}</el-link><span v-if="!detailAttachments.length">-</span></span></div>
</div>
</section-card>
<section-card>
<template #title><span class="loading-detail__route-title">运输路线<el-link type="primary" @click="openRouteChangeDialog">变更运输路线</el-link></span></template>
<div v-if="routeNodes.length" class="loading-detail__steps"><div v-for="(node, index) in routeNodes" :key="node.key || index" class="loading-detail__step"><div class="loading-detail__step-line"><span :class="['loading-detail__step-dot', index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle']">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class="loading-detail__step-address">{{ node.address }}</div><div class="loading-detail__step-tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></div></div></div><el-empty v-else description="暂无路线" :image-size="60" />
</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="运输节点"><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 <el-form
v-else
ref="loadingFormRef" ref="loadingFormRef"
:model="dialogForm" :model="dialogForm"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@@ -358,102 +380,9 @@
label-width="auto" label-width="auto"
class="loading-manage-dialog__form" class="loading-manage-dialog__form"
> >
<section-card title="配载基本信息">
<div class="loading-manage-dialog__grid">
<el-form-item label="配载单号">
<el-input v-model="dialogForm.loadingNo" disabled placeholder="系统自动生成" />
</el-form-item>
<el-form-item label="运输类型" required>
<el-select
v-model="dialogForm.transportType"
filterable
clearable
placeholder="请选择"
@visible-change="visible => visible && loadTransportTypeOptions()"
>
<el-option
v-for="item in transportTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<el-input v-model="dialogForm.projectName" readonly placeholder="从待配载运单带出" />
</el-form-item>
<el-form-item label="客户名称">
<el-input v-model="dialogForm.customerName" readonly placeholder="从待配载运单带出" />
</el-form-item>
<el-form-item label="原始单号">
<el-input v-model="dialogForm.originalNo" readonly placeholder="从待配载运单带出" />
</el-form-item>
<el-form-item label="运单批次号">
<el-input v-model="dialogForm.batchNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="数据来源">
<el-select v-model="dialogForm.dataSource" clearable placeholder="请选择">
<el-option
v-for="item in dataSourceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="计划名称">
<el-select
v-model="dialogForm.planName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadPlanOptions"
:loading="planLoading"
placeholder="请输入"
@visible-change="visible => visible && loadPlanOptions(dialogForm.planName)"
>
<el-option
v-for="item in planOptions"
:key="item.id || item.planName"
:label="item.planName"
:value="item.planName"
/>
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="dialogForm.startDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="dialogForm.endDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="配载子单号" class="loading-manage-dialog__span-2">
<el-input
v-model="dialogForm.loadingSubNos"
readonly
type="textarea"
:rows="2"
placeholder="添加待配载运单后自动生成"
/>
</el-form-item>
</div>
</section-card>
<section-card title="筛选待配载运单"> <section-card title="筛选待配载运单">
<template #extra> <template #extra>
<el-link v-if="!dialogReadonly" type="primary" @click="loadCandidateWaybills">筛选</el-link> <el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch">筛选</el-link>
</template> </template>
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible"> <div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
<el-row :gutter="16"> <el-row :gutter="16">
@@ -584,9 +513,9 @@
> >
<el-option <el-option
v-for="item in carrierOptions" v-for="item in carrierOptions"
:key="item.id || item.customerName || item.carrierName" :key="item.id || item.fullName || item.customerName || item.carrierName"
:label="item.customerName || item.carrierName || item.name" :label="item.fullName || item.customerName || item.carrierName || item.name"
:value="item.customerName || item.carrierName || item.name" :value="item.fullName || item.customerName || item.carrierName || item.name"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -641,15 +570,12 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div class="loading-manage-dialog__selected-summary">
<span>已选择 {{ selectedWaybillRows.length }} 条运单</span>
</div>
<el-table <el-table
ref="candidateTableRef" ref="candidateTableRef"
v-loading="candidateLoading" v-loading="candidateLoading"
:data="candidateRows" :data="candidateRows"
border border
height="260" height="287"
row-key="id" row-key="id"
@selection-change="candidateSelectionChange" @selection-change="candidateSelectionChange"
> >
@@ -723,7 +649,13 @@
show-overflow-tooltip show-overflow-tooltip
/> />
</el-table> </el-table>
<div class="loading-manage-dialog__pagination"> <div class="loading-manage-dialog__table-toolbar">
<el-button
type="primary"
:disabled="!candidateSelection.length || candidateAdding"
:loading="candidateAdding"
@click="addSelectedWaybills"
>添加配载</el-button>
<el-pagination <el-pagination
v-model:current-page="candidatePage.currentPage" v-model:current-page="candidatePage.currentPage"
v-model:page-size="candidatePage.pageSize" v-model:page-size="candidatePage.pageSize"
@@ -736,6 +668,7 @@
</div> </div>
</section-card> </section-card>
<div class="loading-manage-dialog__waybill-route-layout">
<section-card title="待配载列表"> <section-card title="待配载列表">
<el-table <el-table
:data="selectedWaybillRows" :data="selectedWaybillRows"
@@ -765,15 +698,15 @@
<template #default="{ $index }"> <template #default="{ $index }">
<el-link <el-link
v-if="!dialogReadonly" v-if="!dialogReadonly"
type="primary" type="danger"
@click="removeSelectedWaybill($index)" @click="removeSelectedWaybill($index)"
>移除</el-link> >删除</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<section-card title="路线信息"> <section-card title="线路编辑">
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length"> <div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<div <div
v-for="(node, index) in routeNodes" v-for="(node, index) in routeNodes"
@@ -794,6 +727,7 @@
</div> </div>
<el-empty v-else description="添加待配载运单后自动生成路线" :image-size="72" /> <el-empty v-else description="添加待配载运单后自动生成路线" :image-size="72" />
</section-card> </section-card>
</div>
<section-card title="货物信息"> <section-card title="货物信息">
<el-table :data="cargoRows" border class="loading-manage-dialog__cargo-table"> <el-table :data="cargoRows" border class="loading-manage-dialog__cargo-table">
@@ -805,11 +739,13 @@
<el-table-column prop="weight" label="重量" min-width="120" /> <el-table-column prop="weight" label="重量" min-width="120" />
<el-table-column prop="volume" 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="quantity" label="数量" min-width="100" />
<el-table-column prop="planDate" label="计划日期" min-width="130" sortable/>
<el-table-column prop="unitPrice" label="货物单价" min-width="150" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" /> <el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" /> <el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
<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="180" show-overflow-tooltip /> <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column prop="unitPrice" label="货物单价" min-width="150" />
<el-table-column prop="planDate" label="计划日期" min-width="130" sortable />
</el-table> </el-table>
</section-card> </section-card>
@@ -837,9 +773,9 @@
> >
<el-option <el-option
v-for="item in carrierOptions" v-for="item in carrierOptions"
:key="item.id || item.customerName || item.carrierName" :key="item.id || item.fullName || item.customerName || item.carrierName"
:label="item.customerName || item.carrierName || item.name" :label="item.fullName || item.customerName || item.carrierName || item.name"
:value="item.customerName || item.carrierName || item.name" :value="item.fullName || item.customerName || item.carrierName || item.name"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -939,6 +875,30 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="routeChangeVisible" title="变更运输路线" append-to-body destroy-on-close width="78%" class="loading-route-change-dialog">
<el-tabs v-model="routeChangeTab">
<el-tab-pane label="变更路线" name="change">
<section-card title="关联子运单地址">
<el-table :data="routeChangeRows" 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 prop="projectName" label="项目名称" min-width="160" />
<el-table-column label="发货地" min-width="310"><template #default="{ row }"><div class="loading-route-change-dialog__original">原:{{ row.originalDepartureAddress || '-' }}</div><el-input v-model="row.departureAddress" placeholder="请选择或输入地址" @input="updateRouteChangeNodes"><template #suffix><el-icon class="loading-route-change-dialog__location" @click="openCommonAddressDialog(row, 'departure')"><Location /></el-icon></template></el-input></template></el-table-column>
<el-table-column label="到货地" min-width="310"><template #default="{ row }"><div class="loading-route-change-dialog__original">原:{{ row.originalArrivalAddress || '-' }}</div><el-input v-model="row.arrivalAddress" placeholder="请选择或输入地址" @input="updateRouteChangeNodes"><template #suffix><el-icon class="loading-route-change-dialog__location" @click="openCommonAddressDialog(row, 'arrival')"><Location /></el-icon></template></el-input></template></el-table-column>
</el-table>
</section-card>
<section-card title="路线编辑"><template #extra><span class="loading-route-change-dialog__hint">拖拽调整顺序</span></template><div v-if="routeChangeNodes.length" class="loading-route-change-dialog__route-list"><div v-for="(node, index) in routeChangeNodes" :key="node.key || index" class="loading-route-change-dialog__route-item" draggable="true" @dragstart="handleRouteChangeDragStart(index)" @dragover.prevent @drop="handleRouteChangeDrop(index)"><span class="loading-route-change-dialog__route-type">{{ routeChangeTypeText(index) }}</span><span>{{ node.address }}</span><span class="loading-route-change-dialog__tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></span><el-icon><Rank /></el-icon></div></div><el-empty v-else description="暂无路线" :image-size="60" /></section-card>
<el-form label-width="auto"><el-form-item label="变更备注"><el-input v-model="routeChangeRemark" type="textarea" :rows="1" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-form>
</el-tab-pane>
<el-tab-pane label="变更记录" name="records"><el-table :data="routeChangeRecords" border><el-table-column prop="changeTime" label="变更时间" min-width="170"/><el-table-column prop="changeUser" label="变更人" min-width="120"/><el-table-column prop="content" label="变更内容" min-width="360"><template #default="{ row }"><div class="loading-route-change-dialog__record-content">{{ row.content || '-' }}</div></template></el-table-column><el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip/></el-table><el-empty v-if="!routeChangeRecords.length" description="暂无变更记录" :image-size="60"/></el-tab-pane>
</el-tabs>
<template #footer><el-button @click="routeChangeVisible = false">关闭</el-button><el-button v-if="routeChangeTab === 'change'" type="primary" @click="confirmRouteChange">确认变更</el-button></template>
</el-dialog>
<el-dialog v-model="commonAddressVisible" title="选择常用地址" append-to-body width="900px" class="loading-common-address-dialog">
<div class="loading-common-address-dialog__toolbar"><el-input v-model="commonAddressQuery.addressName" clearable placeholder="请输入地址名称" @keyup.enter="loadCommonAddresses"/><el-button type="primary" @click="loadCommonAddresses">查询</el-button></div>
<el-table v-loading="commonAddressLoading" :data="commonAddressRows" border @row-dblclick="selectCommonAddress"><el-table-column prop="addressName" label="地址名称" min-width="180"/><el-table-column prop="regionName" label="区域" min-width="180"/><el-table-column prop="detailAddress" label="详细地址" min-width="260"/><el-table-column label="操作" width="90"><template #default="{ row }"><el-link type="primary" @click="selectCommonAddress(row)">选择</el-link></template></el-table-column></el-table>
<template #footer><el-pagination v-model:current-page="commonAddressPage.currentPage" v-model:page-size="commonAddressPage.pageSize" layout="total, prev, pager, next" :total="commonAddressPage.total" @current-change="loadCommonAddresses"/><el-button @click="commonAddressVisible = false">关闭</el-button></template>
</el-dialog>
</basic-container> </basic-container>
</template> </template>
@@ -956,6 +916,8 @@ import {
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type'; import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import { import {
carrierTypeOptions, carrierTypeOptions,
loadingManageConfig, loadingManageConfig,
@@ -963,11 +925,14 @@ import {
loadingStatusOptions, loadingStatusOptions,
} from '@/option/business/loading-manage'; } from '@/option/business/loading-manage';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Rank, Refresh, Setting } from '@element-plus/icons-vue'; import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const defaultTransportType = '公路整车'; const defaultTransportType = '';
const defaultCandidateTransportType = '公路运输'; const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let detailAmapLoader;
const createSearchForm = () => ({ const createSearchForm = () => ({
loadingNo: '', loadingNo: '',
@@ -1091,6 +1056,7 @@ export default {
data() { data() {
return { return {
Rank, Rank,
Location,
Refresh, Refresh,
Setting, Setting,
loading: false, loading: false,
@@ -1102,7 +1068,7 @@ export default {
total: 0, total: 0,
pageSizes: [10, 20, 50, 100], pageSizes: [10, 20, 50, 100],
}, },
searchExpanded: true, searchExpanded: false,
searchForm: createSearchForm(), searchForm: createSearchForm(),
selectionRows: [], selectionRows: [],
dialogVisible: false, dialogVisible: false,
@@ -1110,9 +1076,10 @@ export default {
dialogLoading: false, dialogLoading: false,
dialogSaving: '', dialogSaving: '',
dialogForm: createDialogForm(), dialogForm: createDialogForm(),
candidateSearchVisible: true, candidateSearchVisible: false,
candidateQuery: createCandidateSearchForm(), candidateQuery: createCandidateSearchForm(),
candidateLoading: false, candidateLoading: false,
candidateAdding: false,
candidateRows: [], candidateRows: [],
candidateSelection: [], candidateSelection: [],
candidatePage: { candidatePage: {
@@ -1142,6 +1109,21 @@ export default {
statusOptions: loadingStatusOptions, statusOptions: loadingStatusOptions,
dataSourceOptions: loadingManageConfig.dataSourceOptions, dataSourceOptions: loadingManageConfig.dataSourceOptions,
config: loadingManageConfig, config: loadingManageConfig,
processNodes: [],
routeChangeVisible: false,
routeChangeTab: 'change',
routeChangeRows: [],
routeChangeNodes: [],
routeChangeRemark: '',
routeChangeRecords: [],
routeChangeDragIndex: -1,
commonAddressVisible: false,
commonAddressLoading: false,
commonAddressRows: [],
commonAddressQuery: { addressName: '' },
commonAddressPage: { currentPage: 1, pageSize: 10, total: 0 },
commonAddressTarget: null,
detailAmap: null,
}; };
}, },
computed: { computed: {
@@ -1158,6 +1140,20 @@ export default {
}; };
return titleMap[this.dialogMode] || '配载管理'; return titleMap[this.dialogMode] || '配载管理';
}, },
detailStatusType() {
const typeMap = { pending: 'success', running: 'success', completed: 'info', cancelled: 'info', draft: 'warning' };
return typeMap[this.dialogForm.businessStatus] || 'info';
},
taskSummary() {
return [this.dialogForm.carrierName, this.dialogForm.driverName, this.dialogForm.vehicleNo]
.filter(Boolean)
.join(' / ') || '-';
},
detailAttachments() {
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(item =>
typeof item === 'string' ? { name: item.split('/').pop(), url: item } : { name: item.name || item.fileName || '附件', url: item.url || item.link || item.fileUrl }
);
},
}, },
mounted() { mounted() {
this.loadTransportTypeOptions(); this.loadTransportTypeOptions();
@@ -1294,6 +1290,9 @@ export default {
this.dialogForm.loadingSubNos this.dialogForm.loadingSubNos
); );
this.restoreRouteAndCargo(); this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
await this.loadProcessNodes();
this.$nextTick(() => this.initDetailAmap());
await this.loadCandidateWaybills(); await this.loadCandidateWaybills();
} else { } else {
this.dialogForm = createDialogForm(); this.dialogForm = createDialogForm();
@@ -1310,6 +1309,8 @@ export default {
this.dialogLoading = false; this.dialogLoading = false;
}, },
resetDialogData() { resetDialogData() {
this.detailAmap?.destroy?.();
this.detailAmap = null;
this.dialogForm = createDialogForm(); this.dialogForm = createDialogForm();
this.candidateQuery = createCandidateSearchForm(); this.candidateQuery = createCandidateSearchForm();
this.candidateRows = []; this.candidateRows = [];
@@ -1319,7 +1320,8 @@ export default {
this.cargoRows = []; this.cargoRows = [];
this.candidatePage.currentPage = 1; this.candidatePage.currentPage = 1;
this.candidatePage.total = 0; this.candidatePage.total = 0;
this.candidateSearchVisible = true; this.candidateSearchVisible = false;
this.processNodes = [];
}, },
clearLoadingDialog() { clearLoadingDialog() {
this.resetDialogData(); this.resetDialogData();
@@ -1333,6 +1335,164 @@ export default {
: this.buildRouteNodes(this.selectedWaybillRows); : this.buildRouteNodes(this.selectedWaybillRows);
this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows); this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows);
}, },
restoreRouteChangeRecords() {
try {
const taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
this.routeChangeRecords = Array.isArray(taskInfo.routeChangeRecords)
? [...taskInfo.routeChangeRecords].sort((a, b) => String(b.changeTime).localeCompare(String(a.changeTime)))
: [];
} catch (error) {
this.routeChangeRecords = [];
}
},
async loadProcessNodes() {
const waybillProcess = parseJsonArray(this.selectedWaybillRows[0]?.processJson);
if (waybillProcess.length) {
this.processNodes = waybillProcess;
return;
}
try {
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.dialogForm.projectId, status: 1 }));
const config = rows.find(item => item.nodeConfigJson) || {};
this.processNodes = parseJsonArray(config.nodeConfigJson);
} catch (error) {
this.processNodes = [];
}
},
previewAttachment(file) {
if (!file.url) {
ElMessage.info('暂无可预览的附件地址');
return;
}
window.open(file.url, '_blank', 'noopener,noreferrer');
},
loadDetailAmap() {
if (window.AMap?.Map) return Promise.resolve();
if (!detailAmapLoader) {
detailAmapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return detailAmapLoader;
},
initDetailAmap() {
if (!this.$refs.detailAmap) return;
this.loadDetailAmap()
.then(() => {
if (!this.detailAmap) {
this.detailAmap = new window.AMap.Map(this.$refs.detailAmap, {
center: [116.40769, 39.89945],
zoom: 11,
});
window.AMap.plugin(['AMap.ToolBar'], () => this.detailAmap?.addControl(new window.AMap.ToolBar()));
} else {
this.detailAmap.resize?.();
}
})
.catch(() => ElMessage.warning('高德地图加载失败'));
},
handleTrackAction(action) {
ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
},
openRouteChangeDialog() {
this.routeChangeTab = 'change';
this.routeChangeRemark = '';
this.routeChangeRows = this.selectedWaybillRows.map(row => ({
...row,
originalDepartureAddress: row.departureAddress || row.departureName || '',
originalArrivalAddress: row.arrivalAddress || row.arrivalName || '',
departureAddress: row.departureAddress || row.departureName || '',
arrivalAddress: row.arrivalAddress || row.arrivalName || '',
}));
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
this.routeChangeVisible = true;
},
routeChangeTypeText(index) {
if (index === 0) return '起';
if (index === this.routeChangeNodes.length - 1) return '终';
return '经';
},
handleRouteChangeDragStart(index) {
this.routeChangeDragIndex = index;
},
handleRouteChangeDrop(index) {
if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return;
const nodes = [...this.routeChangeNodes];
const [node] = nodes.splice(this.routeChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.routeChangeNodes = nodes;
this.routeChangeDragIndex = -1;
},
updateRouteChangeNodes() {
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
},
openCommonAddressDialog(row, field) {
this.commonAddressTarget = { row, field };
this.commonAddressQuery = { addressName: '' };
this.commonAddressPage.currentPage = 1;
this.commonAddressVisible = true;
this.loadCommonAddresses();
},
async loadCommonAddresses() {
this.commonAddressLoading = true;
try {
const page = unwrapPage(await getCommonAddressList(this.commonAddressPage.currentPage, this.commonAddressPage.pageSize, { ...this.commonAddressQuery, allDept: 0 }));
this.commonAddressRows = page.records;
this.commonAddressPage.total = page.total;
} finally {
this.commonAddressLoading = false;
}
},
selectCommonAddress(address) {
const target = this.commonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] = [address.regionName, address.detailAddress || address.addressName].filter(Boolean).join('') || address.addressName || '';
this.updateRouteChangeNodes();
this.commonAddressVisible = false;
},
async confirmRouteChange() {
const changeRows = this.routeChangeRows.filter(row => row.departureAddress !== row.originalDepartureAddress || row.arrivalAddress !== row.originalArrivalAddress);
if (!changeRows.length && !this.routeChangeRemark) {
ElMessage.warning('请修改地址或填写变更备注');
return;
}
const content = changeRows.map(row => `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${row.departureAddress};到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`).join('') || '调整运输路线顺序';
this.routeChangeRecords.unshift({ changeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), changeUser: '当前用户', content, remark: this.routeChangeRemark });
let taskInfo = {};
try {
taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
} catch (error) {
taskInfo = {};
}
this.dialogForm.taskInfoJson = JSON.stringify({
...(typeof taskInfo === 'object' && !Array.isArray(taskInfo) ? taskInfo : {}),
routeChangeRecords: this.routeChangeRecords,
});
this.selectedWaybillRows = this.routeChangeRows.map(({ originalDepartureAddress, originalArrivalAddress, ...row }) => row);
this.routeNodes = this.routeChangeNodes;
this.syncRouteAddressFields();
try {
await loadingApi.changeRoute({
id: this.dialogForm.id,
routeJson: JSON.stringify(this.routeNodes),
departureAddress: this.dialogForm.departureAddress,
transitAddress: this.dialogForm.transitAddress,
arrivalAddress: this.dialogForm.arrivalAddress,
taskInfoJson: this.dialogForm.taskInfoJson,
});
this.routeChangeVisible = false;
ElMessage.success('路线变更已保存');
} catch (error) {
this.restoreRouteChangeRecords();
throw error;
}
},
async restoreWaybillRows(idsJson, loadingSubNos) { async restoreWaybillRows(idsJson, loadingSubNos) {
const ids = parseJsonArray(idsJson).filter(Boolean); const ids = parseJsonArray(idsJson).filter(Boolean);
if (ids.length) { if (ids.length) {
@@ -1369,6 +1529,10 @@ export default {
this.candidateLoading = false; this.candidateLoading = false;
} }
}, },
openCandidateSearch() {
this.candidateSearchVisible = true;
this.loadCandidateWaybills();
},
handleCandidateSearch() { handleCandidateSearch() {
this.candidatePage.currentPage = 1; this.candidatePage.currentPage = 1;
this.loadCandidateWaybills(); this.loadCandidateWaybills();
@@ -1390,10 +1554,25 @@ export default {
candidateSelectionChange(rows) { candidateSelectionChange(rows) {
this.candidateSelection = rows || []; this.candidateSelection = rows || [];
}, },
addSelectedWaybills() { async addSelectedWaybills() {
const exists = new Set(this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))); this.candidateAdding = true;
try {
const selectedRows = await Promise.all(
this.candidateSelection.map(async row => {
if (!row.id) return row;
try {
const res = await getWaybillDetail(row.id);
return { ...row, ...(res.data?.data || res.data || {}) };
} catch (error) {
return row;
}
})
);
const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
);
const nextRows = [...this.selectedWaybillRows]; const nextRows = [...this.selectedWaybillRows];
this.candidateSelection.forEach(row => { selectedRows.forEach(row => {
const key = String(row.id || row.waybillNo); const key = String(row.id || row.waybillNo);
if (!exists.has(key)) { if (!exists.has(key)) {
nextRows.push(row); nextRows.push(row);
@@ -1404,6 +1583,9 @@ export default {
this.rebuildSummaryFromWaybills(); this.rebuildSummaryFromWaybills();
this.$refs.candidateTableRef?.clearSelection?.(); this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = []; this.candidateSelection = [];
} finally {
this.candidateAdding = false;
}
}, },
removeSelectedWaybill(index) { removeSelectedWaybill(index) {
this.selectedWaybillRows.splice(index, 1); this.selectedWaybillRows.splice(index, 1);
@@ -1444,17 +1626,18 @@ export default {
}, },
buildRouteNodes(rows) { buildRouteNodes(rows) {
const nodeMap = new Map(); const nodeMap = new Map();
const appendNode = (address, tag) => {
if (!address) return;
if (!nodeMap.has(address)) nodeMap.set(address, { address, tags: [] });
nodeMap.get(address).tags.push(tag);
};
rows.forEach((row, rowIndex) => { rows.forEach((row, rowIndex) => {
const departure = row.departureAddress || row.departureName; const departure = row.departureAddress || row.departureName;
appendNode(departure, `装${rowIndex + 1}`);
});
rows.forEach((row, rowIndex) => {
const arrival = row.arrivalAddress || row.arrivalName; const arrival = row.arrivalAddress || row.arrivalName;
if (departure) { appendNode(arrival, `卸${rowIndex + 1}`);
if (!nodeMap.has(departure)) nodeMap.set(departure, { address: departure, tags: [] });
nodeMap.get(departure).tags.push(`装${rowIndex + 1}`);
}
if (arrival) {
if (!nodeMap.has(arrival)) nodeMap.set(arrival, { address: arrival, tags: [] });
nodeMap.get(arrival).tags.push(`卸${rowIndex + 1}`);
}
}); });
return [...nodeMap.values()].map((node, index) => ({ return [...nodeMap.values()].map((node, index) => ({
...node, ...node,
@@ -1489,7 +1672,7 @@ export default {
}, },
buildCargoRows(rows) { buildCargoRows(rows) {
return rows.flatMap(row => { return rows.flatMap(row => {
const goodsRows = parseJsonArray(row.goodsJson); const goodsRows = parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
if (goodsRows.length) { if (goodsRows.length) {
return goodsRows.map(item => this.normalizeCargoRow(row, item)); return goodsRows.map(item => this.normalizeCargoRow(row, item));
} }
@@ -1509,6 +1692,9 @@ export default {
unitPrice: item.unitPrice || waybill.unitPrice || '', unitPrice: item.unitPrice || waybill.unitPrice || '',
materialCode: item.materialCode || '', materialCode: item.materialCode || '',
equipmentCode: item.equipmentCode || '', equipmentCode: item.equipmentCode || '',
brand: item.brand || item.brandName || waybill.brand || '',
specificationModel:
item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '',
remark: item.remark || '', remark: item.remark || '',
}; };
}, },
@@ -1694,9 +1880,6 @@ export default {
label: item.dictValue || item.label || item.name, label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name, value: item.dictKey || item.value || item.dictValue || item.name,
})); }));
if (!this.transportTypeOptions.length) {
this.transportTypeOptions = [{ label: defaultTransportType, value: defaultTransportType }];
}
}, },
async loadProjectOptions(keyword = '') { async loadProjectOptions(keyword = '') {
this.projectLoading = true; this.projectLoading = true;
@@ -1710,7 +1893,7 @@ export default {
this.customerLoading = true; this.customerLoading = true;
try { try {
this.customerOptions = unwrapRecords( this.customerOptions = unwrapRecords(
await getCustomerList(1, 20, { customerName: keyword }) await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' })
); );
} finally { } finally {
this.customerLoading = false; this.customerLoading = false;
@@ -1892,10 +2075,17 @@ export default {
margin-bottom: 8px; margin-bottom: 8px;
} }
.loading-manage-dialog__waybill-route-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
gap: 16px;
align-items: start;
margin-bottom: 16px;
}
.loading-manage-dialog__candidate-actions, .loading-manage-dialog__candidate-actions,
.loading-manage-dialog__collapsed-search, .loading-manage-dialog__collapsed-search,
.loading-manage-dialog__table-toolbar, .loading-manage-dialog__table-toolbar,
.loading-manage-dialog__pagination,
.loading-manage-dialog__footer { .loading-manage-dialog__footer {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1908,16 +2098,18 @@ export default {
margin: 10px 0; margin: 10px 0;
} }
.loading-manage-dialog__pagination {
padding: 10px 0 4px;
}
.loading-manage-dialog__route-list { .loading-manage-dialog__route-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
@media (max-width: 1200px) {
.loading-manage-dialog__waybill-route-layout {
grid-template-columns: 1fr;
}
}
.loading-manage-dialog__route-item { .loading-manage-dialog__route-item {
display: grid; display: grid;
grid-template-columns: 28px 36px minmax(0, 1fr) auto; grid-template-columns: 28px 36px minmax(0, 1fr) auto;
@@ -1998,4 +2190,33 @@ export default {
--el-table-border-color: #eff1f7; --el-table-border-color: #eff1f7;
} }
} }
.loading-detail {
display: flex;
flex-direction: column;
gap: 8px;
color: #303133;
.loading-detail__summary { display: flex; align-items: center; flex-wrap: wrap; gap: 24px; padding: 4px 0 16px; }
.loading-detail__heading { font-size: 20px; font-weight: 600; margin-right: 8px; }
.loading-detail__route-title { display: inline-flex; align-items: center; gap: 16px; }
.loading-detail__label { color: #909399; margin-right: 10px; }
.loading-detail__task-row { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding-bottom: 4px; }
.loading-detail__field { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.loading-detail__field .loading-detail__label { margin-right: 0; }
.loading-detail__value { display: flex; flex-wrap: wrap; gap: 8px; text-align: left; }
.loading-detail__steps { display: flex; align-items: flex-start; overflow-x: auto; padding: 8px 4px 16px; }
.loading-detail__step { position: relative; min-width: 190px; flex: 1; text-align: center; }
.loading-detail__step:not(:last-child)::after { position: absolute; z-index: 0; top: 16px; left: calc(50% + 25px); width: calc(100% - 50px); height: 2px; content: ''; background: #606266; }
.loading-detail__step-line { display: flex; align-items: center; justify-content: center; }
.loading-detail__step-dot { position: relative; z-index: 1; display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; color: #fff; border-radius: 4px; font-weight: 600; }
.loading-detail__step-dot.start { background: #409eff; }.loading-detail__step-dot.middle { background: #67c23a; }.loading-detail__step-dot.end { background: #e6a23c; }
.loading-detail__step-address { margin-top: 8px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loading-detail__step-tags { display: flex; justify-content: center; gap: 4px; margin-top: 6px; }
.loading-detail__bottom-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.loading-detail__track-actions { display: flex; gap: 8px; }
.loading-detail__map { width: 100%; min-height: 260px; border: 1px solid #eff1f7; }
}
.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; } }
</style> </style>
@@ -1,5 +1,5 @@
<template> <template>
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" /> <business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="320" />
</template> </template>
<script> <script>
+112 -15
View File
@@ -147,9 +147,9 @@
:model="archiveForm" :model="archiveForm"
:rules="formRules" :rules="formRules"
label-position="right" label-position="right"
label-width="auto" label-width="calc(6em + 24px)"
:disabled="readonly" :disabled="readonly"
class="archive-form" class="archive-form dialog-form-label-fixed"
> >
<section-card title="工商信息"> <section-card title="工商信息">
<el-row :gutter="18"> <el-row :gutter="18">
@@ -614,7 +614,7 @@
</el-table-column> </el-table-column>
<el-table-column label="文件名称" min-width="180"> <el-table-column label="文件名称" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ row.originalName || row.name || '-' }}</span> <el-link type="primary" :disabled="!row.url" @click="previewQualificationFile(row)">{{ row.originalName || row.name || '-' }}</el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220"> <el-table-column label="附件描述" min-width="220">
@@ -664,6 +664,11 @@
</div> </div>
</section-card> </section-card>
<el-dialog v-model="qualificationDocumentPreviewVisible" :title="qualificationPreviewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
<open-file-viewer v-if="qualificationDocumentPreviewVisible && qualificationPreviewFile.url" :file="qualificationPreviewFile.url" :file-name="qualificationPreviewFile.name" :mime-type="qualificationPreviewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="qualificationViewerToolbar" :plugins="qualificationViewerPlugins" @unsupported="handleQualificationPreviewUnsupported" @error="handleQualificationPreviewError" />
</el-dialog>
<el-image-viewer v-if="qualificationImagePreviewVisible" :url-list="qualificationImagePreviewUrls" :initial-index="qualificationImagePreviewIndex" @close="qualificationImagePreviewVisible = false" />
<section-card title="变更记录"> <section-card title="变更记录">
<el-table :data="changeRecordPage.records" border> <el-table :data="changeRecordPage.records" border>
<el-table-column <el-table-column
@@ -1230,6 +1235,11 @@ import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload'; import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { normalizeSearchRangeParams } from '@/utils/search-range';
import { ArrowRight } from '@element-plus/icons-vue'; import { ArrowRight } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -1241,10 +1251,19 @@ const createTimeRangeMap = {
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader; let amapLoader;
const qualificationViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
export default { export default {
components: { components: {
ArrowRight, ArrowRight,
ElImageViewer,
OpenFileViewer,
}, },
data() { data() {
const nonNegativeLabelMap = { const nonNegativeLabelMap = {
@@ -1363,6 +1382,19 @@ export default {
qualificationFiles: [], qualificationFiles: [],
qualificationUploadFiles: [], qualificationUploadFiles: [],
selectedQualificationFiles: [], selectedQualificationFiles: [],
qualificationImagePreviewVisible: false,
qualificationImagePreviewUrls: [],
qualificationImagePreviewIndex: 0,
qualificationDocumentPreviewVisible: false,
qualificationPreviewFile: {},
qualificationViewerPlugins,
qualificationViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
deptTree: [], deptTree: [],
deptOptions: [], deptOptions: [],
regionOptions: [], regionOptions: [],
@@ -2748,10 +2780,53 @@ export default {
files: [file], files: [file],
}); });
}, },
getQualificationFileUrl(file = {}) {
return file.url || file.link || file.domain || '';
},
getQualificationFileExtension(file = {}) {
const name = String(file.originalName || file.name || this.getQualificationFileUrl(file) || '')
.split('?')[0];
const index = name.lastIndexOf('.');
return index > -1 ? name.slice(index + 1).toLowerCase() : '';
},
isQualificationImage(file) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
this.getQualificationFileExtension(file)
);
},
previewQualificationFile(file) {
const url = this.getQualificationFileUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isQualificationImage(file)) {
this.qualificationImagePreviewUrls = this.qualificationFiles
.filter(item => this.isQualificationImage(item) && this.getQualificationFileUrl(item))
.map(item => this.getQualificationFileUrl(item));
this.qualificationImagePreviewIndex = Math.max(
this.qualificationImagePreviewUrls.indexOf(url),
0
);
this.qualificationImagePreviewVisible = true;
return;
}
this.qualificationPreviewFile = {
name: file.originalName || file.name || '附件',
url,
mimeType: file.mimeType || file.contentType || '',
};
this.qualificationDocumentPreviewVisible = true;
},
handleQualificationPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleQualificationPreviewError() {
this.$message.error('附件预览失败');
},
resolveQualificationType(fileName) { resolveQualificationType(fileName) {
const name = String(fileName || '').toLocaleLowerCase(); const name = String(fileName || '').toLocaleLowerCase();
return ( const matchedType = [...this.qualificationTypeOptions]
[...this.qualificationTypeOptions]
.filter(item => item?.label || item?.value) .filter(item => item?.label || item?.value)
.sort( .sort(
(a, b) => (a, b) =>
@@ -2760,8 +2835,14 @@ export default {
.find(item => { .find(item => {
const typeText = String(item.label || item.value || '').toLocaleLowerCase(); const typeText = String(item.label || item.value || '').toLocaleLowerCase();
return typeText && name.includes(typeText); return typeText && name.includes(typeText);
})?.value || '' });
if (matchedType?.value) return matchedType.value;
const otherType = this.qualificationTypeOptions.find(item =>
String(item.label || item.value || '').includes('其他')
); );
if (otherType?.value) return otherType.value;
this.qualificationTypeOptions.push({ label: '其他附件', value: '其他附件' });
return '其他附件';
}, },
downloadAttachments() { downloadAttachments() {
const source = this.selectedQualificationFiles.length const source = this.selectedQualificationFiles.length
@@ -3617,12 +3698,26 @@ export default {
} }
.archive-form { .archive-form {
:deep(.el-input-number), :deep(.el-form-item__label) {
:deep(.el-select), flex: 0 0 calc(6em + 24px) !important;
:deep(.el-cascader), width: calc(6em + 24px) !important;
:deep(.el-date-editor) { height: auto;
width: 100%; min-height: 32px;
white-space: normal !important;
word-break: break-all;
overflow-wrap: anywhere;
line-height: 18px;
} }
:deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select),
:deep(.el-form-item__content > .el-cascader),
:deep(.el-form-item__content > .el-input-number),
:deep(.el-form-item__content > .el-date-editor) {
width: 220px;
max-width: 100%;
}
} }
business-term-field, business-term-field,
@@ -3635,7 +3730,8 @@ business-term-field,
.business-term-field { .business-term-field {
:deep(.el-date-editor) { :deep(.el-date-editor) {
flex: 1; flex: 0 0 220px;
width: 220px;
min-width: 0; min-width: 0;
} }
@@ -3646,14 +3742,15 @@ business-term-field,
} }
.archive-form__address { .archive-form__address {
.el-input, :deep(.el-input),
:deep(.el-cascader) { :deep(.el-cascader) {
flex: 1; flex: 0 0 220px;
width: 220px;
min-width: 0; min-width: 0;
} }
:deep(.el-cascader) { :deep(.el-cascader) {
flex: 0 0 36%; flex-basis: 220px;
} }
} }