diff --git a/src/api/business/loading-manage.js b/src/api/business/loading-manage.js index e051fa0..6938bc3 100644 --- a/src/api/business/loading-manage.js +++ b/src/api/business/loading-manage.js @@ -89,3 +89,10 @@ export const batchComplete = ids => ids, }, }); + +export const changeRoute = row => + request({ + url: `${baseUrl}/change-route`, + method: 'post', + data: row, + }); diff --git a/src/config/website.js b/src/config/website.js index 7a0c20f..5172e82 100644 --- a/src/config/website.js +++ b/src/config/website.js @@ -23,7 +23,7 @@ export default { //配置首页不可关闭 setting: { sidebar: 'vertical', - tag: false, + tag: true, debug: false, collapse: true, search: false, diff --git a/src/main.js b/src/main.js index 195571f..b7979d8 100644 --- a/src/main.js +++ b/src/main.js @@ -15,6 +15,7 @@ import crudCommon from '@/mixins/crud.js'; import { getScreen, findColumn } from './utils/util'; import { setupPaginationEnhancer } from './utils/pagination'; import { setupDialogFormLabelAlign } from './utils/dialog-form-label'; +import { setupTableColumnDrag } from './utils/table-column-drag'; import './permission'; import error from './error'; import App from './App.vue'; @@ -107,6 +108,7 @@ app.use(VueClipboard, { autoSetContainer: true, appendToBody: true, // 这可以帮助解决一些更复杂的使用场景下的问题 }); +setupTableColumnDrag(app); app.mount('#app'); setupPaginationEnhancer(); setupDialogFormLabelAlign(); diff --git a/src/option/business/temporary-credit-limit.js b/src/option/business/temporary-credit-limit.js index 553dad0..0ace898 100644 --- a/src/option/business/temporary-credit-limit.js +++ b/src/option/business/temporary-credit-limit.js @@ -32,6 +32,7 @@ export const config = { temporaryCreditLimitSelectable: true, }, enableAttachmentTable: true, + detailAttachmentUploadTimeWidth: 220, enableDraftSave: true, saveAsDraft: true, defaultForm: { @@ -271,6 +272,8 @@ export const option = { }, ...auditColumns.map(column => ({ ...column, hide: true })), ]), - dialogWidth: 1280, + dialogWidth: 1400, + menuFixed: 'right', + menuWidth: 320, index: false, }; diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss index 9aba441..cb30892 100644 --- a/src/styles/element-ui.scss +++ b/src/styles/element-ui.scss @@ -574,12 +574,10 @@ gap: 8px; } -// 全站统一:表格操作列文字链接统一为主色(蓝色) -// 覆盖 Avue #menu 槽位与手写表格操作列中 el-link 的 warning / success / danger 等类型, -// 同时统一 hover / active / disabled 态,保证全站操作列风格一致。 +// 全站统一:表格普通操作链接使用主色(蓝色),删除等危险操作保持危险色(红色)。 // 选择器同时覆盖 Avue 行菜单(.avue-crud__menu)与任意含 el-link 的表格体单元格(手写操作列)。 -.avue-crud__menu .el-link, -.el-table .el-table__body .cell:has(.el-link) .el-link { +.avue-crud__menu .el-link:not(.el-link--danger), +.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 中等粗), // 操作列文字统一为常规字重,避免「查看/编辑/删除」等链接显得加粗。 font-weight: normal; @@ -589,6 +587,14 @@ --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 叠加导致文字间距过宽 .avue-crud .avue-crud__menu .el-link, .el-table .el-table__body .cell:not(.avue-crud__menu):has(> .el-link) .el-link { @@ -652,3 +658,13 @@ gap: 6px; } } + +// 全局表格列拖拽:仅对可移动的普通列显示拖拽反馈。 +.el-table th.tms-table-column-draggable { + cursor: grab; + + &.is-column-dragging { + cursor: grabbing; + opacity: 0.55; + } +} diff --git a/src/utils/dialog-form-label.js b/src/utils/dialog-form-label.js index 89739a0..7e06544 100644 --- a/src/utils/dialog-form-label.js +++ b/src/utils/dialog-form-label.js @@ -86,7 +86,8 @@ const collectLabelGroups = form => { }; const alignFormLabelsByColumn = form => { - const groups = collectLabelGroups(form); + if (form.classList.contains('dialog-form-label-fixed')) return; + const groups = collectLabelGroups(form); groups.forEach(group => { group.labels.forEach(label => { diff --git a/src/utils/table-column-drag.js b/src/utils/table-column-drag.js new file mode 100644 index 0000000..0e074e4 --- /dev/null +++ b/src/utils/table-column-drag.js @@ -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; + }); +}; diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index e1fb1f0..3288965 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -2179,7 +2179,12 @@ - + diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index 04ad443..11a8e9a 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -138,9 +138,9 @@ > @@ -214,7 +214,7 @@
查询 重置 - {{ searchExpanded ? '收起筛选' : '展开筛选' }} + {{ searchExpanded ? '收起' : '展开' }}
@@ -350,7 +350,29 @@ @closed="resetLoadingDialog" >
+
+ +
+
配载单详情
+
{{ dialogForm.loadingNo || '-' }}
+
{{ statusText(dialogForm) }}
+
{{ dialogForm.transportType || '-' }}
+
+
+
任务信息{{ taskSummary }}
+
附件{{ file.name }}-
+
+
+ + +
{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}
{{ node.address }}
{{ tag }}
+
+ + +
{{ item.name || item.nodeName || item.label }}
+
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- 已选择 {{ selectedWaybillRows.length }} 条运单 -
@@ -723,7 +649,13 @@ show-overflow-tooltip /> -
+
+ 添加配载 - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + - -
-
- - {{ routeTypeText(index) }} - {{ node.address }} - - {{ tag }} - -
-
- -
+ +
+
+ + {{ routeTypeText(index) }} + {{ node.address }} + + {{ tag }} + +
+
+ +
+
@@ -805,11 +739,13 @@ - - + + + + @@ -837,9 +773,9 @@ > @@ -939,6 +875,30 @@
+ + + + + + + + + + + + +
{{ routeChangeTypeText(index) }}{{ node.address }}{{ tag }}
+ +
+ +
+ +
+ +
查询
+ + +
@@ -956,6 +916,8 @@ import { import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type'; import { getList as getCustomerList } from '@/api/vehicle/customer-archive'; 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 { carrierTypeOptions, loadingManageConfig, @@ -963,11 +925,14 @@ import { loadingStatusOptions, } from '@/option/business/loading-manage'; 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'; -const defaultTransportType = '公路整车'; -const defaultCandidateTransportType = '公路运输'; +const defaultTransportType = ''; +const defaultCandidateTransportType = ''; +const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; +const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; +let detailAmapLoader; const createSearchForm = () => ({ loadingNo: '', @@ -1091,6 +1056,7 @@ export default { data() { return { Rank, + Location, Refresh, Setting, loading: false, @@ -1102,7 +1068,7 @@ export default { total: 0, pageSizes: [10, 20, 50, 100], }, - searchExpanded: true, + searchExpanded: false, searchForm: createSearchForm(), selectionRows: [], dialogVisible: false, @@ -1110,9 +1076,10 @@ export default { dialogLoading: false, dialogSaving: '', dialogForm: createDialogForm(), - candidateSearchVisible: true, + candidateSearchVisible: false, candidateQuery: createCandidateSearchForm(), candidateLoading: false, + candidateAdding: false, candidateRows: [], candidateSelection: [], candidatePage: { @@ -1142,6 +1109,21 @@ export default { statusOptions: loadingStatusOptions, dataSourceOptions: loadingManageConfig.dataSourceOptions, 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: { @@ -1158,6 +1140,20 @@ export default { }; 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() { this.loadTransportTypeOptions(); @@ -1294,6 +1290,9 @@ export default { this.dialogForm.loadingSubNos ); this.restoreRouteAndCargo(); + this.restoreRouteChangeRecords(); + await this.loadProcessNodes(); + this.$nextTick(() => this.initDetailAmap()); await this.loadCandidateWaybills(); } else { this.dialogForm = createDialogForm(); @@ -1310,6 +1309,8 @@ export default { this.dialogLoading = false; }, resetDialogData() { + this.detailAmap?.destroy?.(); + this.detailAmap = null; this.dialogForm = createDialogForm(); this.candidateQuery = createCandidateSearchForm(); this.candidateRows = []; @@ -1319,7 +1320,8 @@ export default { this.cargoRows = []; this.candidatePage.currentPage = 1; this.candidatePage.total = 0; - this.candidateSearchVisible = true; + this.candidateSearchVisible = false; + this.processNodes = []; }, clearLoadingDialog() { this.resetDialogData(); @@ -1333,6 +1335,164 @@ export default { : this.buildRouteNodes(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) { const ids = parseJsonArray(idsJson).filter(Boolean); if (ids.length) { @@ -1369,6 +1529,10 @@ export default { this.candidateLoading = false; } }, + openCandidateSearch() { + this.candidateSearchVisible = true; + this.loadCandidateWaybills(); + }, handleCandidateSearch() { this.candidatePage.currentPage = 1; this.loadCandidateWaybills(); @@ -1390,20 +1554,38 @@ export default { candidateSelectionChange(rows) { this.candidateSelection = rows || []; }, - addSelectedWaybills() { - const exists = new Set(this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))); - const nextRows = [...this.selectedWaybillRows]; - this.candidateSelection.forEach(row => { - const key = String(row.id || row.waybillNo); - if (!exists.has(key)) { - nextRows.push(row); - exists.add(key); - } - }); - this.selectedWaybillRows = nextRows; - this.rebuildSummaryFromWaybills(); - this.$refs.candidateTableRef?.clearSelection?.(); - this.candidateSelection = []; + async addSelectedWaybills() { + 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]; + selectedRows.forEach(row => { + const key = String(row.id || row.waybillNo); + if (!exists.has(key)) { + nextRows.push(row); + exists.add(key); + } + }); + this.selectedWaybillRows = nextRows; + this.rebuildSummaryFromWaybills(); + this.$refs.candidateTableRef?.clearSelection?.(); + this.candidateSelection = []; + } finally { + this.candidateAdding = false; + } }, removeSelectedWaybill(index) { this.selectedWaybillRows.splice(index, 1); @@ -1444,17 +1626,18 @@ export default { }, buildRouteNodes(rows) { 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) => { const departure = row.departureAddress || row.departureName; + appendNode(departure, `装${rowIndex + 1}`); + }); + rows.forEach((row, rowIndex) => { const arrival = row.arrivalAddress || row.arrivalName; - if (departure) { - 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}`); - } + appendNode(arrival, `卸${rowIndex + 1}`); }); return [...nodeMap.values()].map((node, index) => ({ ...node, @@ -1489,7 +1672,7 @@ export default { }, buildCargoRows(rows) { return rows.flatMap(row => { - const goodsRows = parseJsonArray(row.goodsJson); + const goodsRows = parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson); if (goodsRows.length) { return goodsRows.map(item => this.normalizeCargoRow(row, item)); } @@ -1509,6 +1692,9 @@ export default { unitPrice: item.unitPrice || waybill.unitPrice || '', materialCode: item.materialCode || '', equipmentCode: item.equipmentCode || '', + brand: item.brand || item.brandName || waybill.brand || '', + specificationModel: + item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '', remark: item.remark || '', }; }, @@ -1694,9 +1880,6 @@ export default { label: item.dictValue || item.label || item.name, value: item.dictKey || item.value || item.dictValue || item.name, })); - if (!this.transportTypeOptions.length) { - this.transportTypeOptions = [{ label: defaultTransportType, value: defaultTransportType }]; - } }, async loadProjectOptions(keyword = '') { this.projectLoading = true; @@ -1710,7 +1893,7 @@ export default { this.customerLoading = true; try { this.customerOptions = unwrapRecords( - await getCustomerList(1, 20, { customerName: keyword }) + await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' }) ); } finally { this.customerLoading = false; @@ -1892,10 +2075,17 @@ export default { 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__collapsed-search, .loading-manage-dialog__table-toolbar, - .loading-manage-dialog__pagination, .loading-manage-dialog__footer { display: flex; align-items: center; @@ -1908,16 +2098,18 @@ export default { margin: 10px 0; } - .loading-manage-dialog__pagination { - padding: 10px 0 4px; - } - .loading-manage-dialog__route-list { display: flex; flex-direction: column; gap: 8px; } + @media (max-width: 1200px) { + .loading-manage-dialog__waybill-route-layout { + grid-template-columns: 1fr; + } + } + .loading-manage-dialog__route-item { display: grid; grid-template-columns: 28px 36px minmax(0, 1fr) auto; @@ -1998,4 +2190,33 @@ export default { --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; } } diff --git a/src/views/business/temporary-credit-limit.vue b/src/views/business/temporary-credit-limit.vue index fccf057..38ae7e7 100644 --- a/src/views/business/temporary-credit-limit.vue +++ b/src/views/business/temporary-credit-limit.vue @@ -1,5 +1,5 @@