Merge remote-tracking branch 'websoft/master'

# Conflicts:
#	src/views/business/project-apply.vue
This commit is contained in:
2026-09-18 18:26:13 +08:00
22 changed files with 697 additions and 257 deletions
@@ -3020,6 +3020,10 @@ export default {
Location,
OfficeBuilding,
Search,
// watcher /
// dataOptions API data computed watch(immediate) created
// created immediate watcher
routePathLocked: this.$route.path,
form: {},
suppressTransportTypeClear: false,
query: {},
@@ -3596,6 +3600,11 @@ export default {
!this.dialogReadonly
);
},
// tab.js fullPath path
// keep-alive $route id
isOwnedRouteActive() {
return this.$route.path === this.routePathLocked;
},
billingCargoTypeCascaderProps() {
return {
label: 'cargoName',
@@ -3673,6 +3682,9 @@ export default {
detailId: {
immediate: true,
handler(id) {
// 宿 view $route.query.detailId
// id
if (!this.isOwnedRouteActive) return;
if (id !== undefined && id !== null && id !== '') {
this.$nextTick(() => this.openDetail({ id }));
}
@@ -199,13 +199,17 @@
</el-dialog>
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
<el-form :inline="true" :model="detailQuery"
><el-form-item label="车牌号/船号"
<section-card class="waybill-search">
<el-form :inline="true" :model="detailQuery"
>
<el-form-item label="车牌号/船号"
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
><el-form-item label="司机/船长姓名"
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
><el-form-item label="司机/船长姓名"
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
><el-button type="primary" @click="loadDetails">查询</el-button></el-form
>
>
</section-card>
<el-table :data="details" border
><el-table-column type="index" label="序号" width="65" /><el-table-column
prop="batchNo"
@@ -593,6 +597,7 @@ import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz';
import * as api from '@/api/business/waybill-manage';
import { downloadXls } from '@/utils/util';
import SectionCard from '@/components/section-card/main.vue';
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
const emit = defineEmits(['update:modelValue', 'closed']);
@@ -1617,4 +1622,7 @@ const confirmImport = async () => {
:global(.avue-layout--horizontal .waybill-import-create__footer) {
left: 0;
}
.waybill-search .el-form-item{
margin-bottom: 0 !important;
}
</style>
@@ -3,21 +3,21 @@
:class="[
'waybill-manage-page',
{
'waybill-manage-page--form-page': isStandaloneWaybillFormPage,
'waybill-manage-page--detail-page': isStandaloneWaybillDetailPage,
'waybill-manage-page--form-page': formPageLocked,
'waybill-manage-page--detail-page': detailPageLocked,
},
]"
>
<component
v-if="!isStandaloneWaybillDetailPage"
v-if="!detailPageLocked"
:is="crudContainer"
:option="pageFormOption"
:form-page-title="isStandaloneWaybillFormPage ? formPageTitle : undefined"
:form-page-title="formPageLocked ? formPageTitle : undefined"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
:status="isStandaloneWaybillFormPage ? dialogReadonly : undefined"
:status="formPageLocked ? dialogReadonly : undefined"
ref="crud"
:permission="permissionList"
:before-open="beforeOpen"
@@ -1546,7 +1546,7 @@
</el-link>
</div>
<div
v-if="isStandaloneWaybillFormPage"
v-if="formPageLocked"
class="waybill-manage-page__standalone-menu-actions"
>
<el-button class="waybill-manage-page__form-close" @click="closeCrudDialog">
@@ -1747,7 +1747,7 @@
</el-dialog>
<empty-pagination
v-show="!isStandaloneWaybillFormPage && !isStandaloneWaybillDetailPage"
v-show="!formPageLocked && !detailPageLocked"
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@@ -1758,18 +1758,18 @@
:is="detailContainer"
v-model="detailBox"
:title="`${config.title}详情`"
:append-to-body="!isStandaloneWaybillDetailPage"
:append-to-body="!detailPageLocked"
top="10px"
width="96%"
show-close
:class="[
'waybill-manage-page__detail-dialog',
{ 'waybill-manage-page__detail-page': isStandaloneWaybillDetailPage },
{ 'waybill-manage-page__detail-page': detailPageLocked },
$attrs.option?.dialogCustomClass,
]"
>
<div v-loading="detailLoading" class="waybill-manage-page__detail-content">
<template v-if="detailBox || isStandaloneWaybillDetailPage">
<template v-if="detailBox || detailPageLocked">
<section-card class="waybill-manage-page__waybill-detail-summary">
<div class="waybill-manage-page__waybill-heading">
<strong>运单详情</strong>
@@ -1923,6 +1923,12 @@
}}</strong
>
</div>
<div
v-if="!waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong>
</div>
</template>
<template v-else-if="waybillIsNonRoadTransport(detailRow)">
<div class="waybill-manage-page__waybill-detail-field">
@@ -2029,8 +2035,8 @@
<div v-loading="waybillPunchRecordsLoading">
<el-timeline v-if="waybillPunchRecords.length">
<el-timeline-item
v-for="(record, index) in waybillPunchRecords"
:key="record.nodeCode || record.id || index"
v-for="(record, index) in orderedWaybillPunchRecords"
:key="`${record.nodeCode || record.id || 'punch'}-${index}`"
:timestamp="record.punchTime || record.statusName || '未打卡'"
:type="record.punched ? 'primary' : 'info'"
:color="record.punched ? '#2b6ce8' : '#c0c4cc'"
@@ -2217,10 +2223,10 @@
</div>
</template>
</div>
<div v-if="isStandaloneWaybillDetailPage" class="waybill-manage-page__detail-footer">
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer">
<el-button type="primary" @click="closeDetail">关闭</el-button>
</div>
<template v-if="!isStandaloneWaybillDetailPage" #footer>
<template v-if="!detailPageLocked" #footer>
<el-button type="primary" @click="closeDetail">关闭</el-button>
</template>
</component>
@@ -3512,9 +3518,26 @@ export default {
selectionList: [],
attachmentUploadMode: false,
standaloneFormKey: '',
// $route
// data created Vue Options API
// data computed watch(immediate) created created
// immediate watcher
routePathLocked: this.$route.path,
// / / $route
// keep-alive deactivate $route
// / append-to-body el-dialog
// Teleport body KeepAlive.deactivate move DOM
// /
formPageLocked: false,
detailPageLocked: false,
// add / edit $route query option
formModeLocked: '',
};
},
created() {
this.formPageLocked = this.isStandaloneWaybillFormPage;
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
this.formModeLocked = this.$route.query.mode || '';
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
@@ -3538,6 +3561,14 @@ export default {
this.consumeTemplateCreatePayload();
}
},
// keep-alive append-to-body
// Teleport DOM body
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() {
this.closeInnerDialogs();
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
@@ -3545,6 +3576,25 @@ export default {
addBtn: this.canCreate,
};
},
//
//
orderedWaybillPunchRecords() {
const records = Array.isArray(this.waybillPunchRecords) ? this.waybillPunchRecords : [];
return records
.map((record, index) => ({
record,
index,
punchedAt: this.waybillPunchRecordTimestamp(record),
}))
.sort((a, b) => {
const aPunched = a.punchedAt !== null;
const bPunched = b.punchedAt !== null;
if (aPunched && bPunched) return a.punchedAt - b.punchedAt || a.index - b.index;
if (aPunched !== bPunched) return aPunched ? -1 : 1;
return a.index - b.index;
})
.map(item => item.record);
},
isStandaloneWaybillPage() {
return this.standaloneFormPage && standaloneWaybillFormPaths.includes(this.$route.path);
},
@@ -3554,8 +3604,15 @@ export default {
isStandaloneWaybillDetailPage() {
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
},
// $route data formPageLocked
// tab.js fullPath path
// keep-alive $route id
// id
isOwnedRouteActive() {
return this.$route.path === this.routePathLocked;
},
detailContainer() {
return this.isStandaloneWaybillDetailPage ? 'PageDetail' : 'el-dialog';
return this.detailPageLocked ? 'PageDetail' : 'el-dialog';
},
formPageTitle() {
if (this.isStandaloneWaybillFormPage) {
@@ -3567,13 +3624,13 @@ export default {
return '';
},
crudContainer() {
return this.isStandaloneWaybillFormPage ? 'PageAvueForm' : 'avue-crud';
return this.formPageLocked ? 'PageAvueForm' : 'avue-crud';
},
pageFormOption() {
if (!this.isStandaloneWaybillFormPage) return this.option;
if (!this.formPageLocked) return this.option;
const base = {
...this.option,
boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add',
boxType: this.formModeLocked === 'edit' ? 'edit' : 'add',
menuBtn: true,
submitBtn: true,
emptyBtn: false,
@@ -3821,6 +3878,10 @@ export default {
detailId: {
immediate: true,
handler(id) {
// 宿 view $route.query.id / detailId
// id
// openDetail id
if (!this.isOwnedRouteActive) return;
if (id !== undefined && id !== null && id !== '') {
this.$nextTick(() => this.openDetail({ id }));
}
@@ -3837,6 +3898,25 @@ export default {
},
},
methods: {
// keep-alive / append-to-body
// Teleport DOM body data
closeInnerDialogs() {
this.detailBox = false;
this.mileageDialog.visible = false;
this.attachmentDocumentPreviewVisible = false;
this.attachmentImagePreviewVisible = false;
this.waybillRouteChangeBox = false;
this.waybillCommonAddressBox = false;
this.flowBox = false;
this.transportRouteBox = false;
this.transportAddressBox = false;
this.transportStationBox = false;
this.transportMapBox = false;
this.commonCargoBox = false;
this.cargoImportBox = false;
this.waybillProcessConfigBox = false;
this.excelBox = false;
},
initStandaloneFormPage() {
if (!this.isStandaloneWaybillFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -4384,6 +4464,9 @@ export default {
},
openDetail(row) {
if (!this.isStandaloneWaybillDetailPage) {
// row.id id
// push
if (!this.isOwnedRouteActive) return;
this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } });
return;
}
@@ -4463,7 +4546,10 @@ export default {
this.$message.info('当前配载单暂无详情数据');
return;
}
this.$router.push({ path: '/business/loading-manage', query: { detailId: id } });
this.$router.push({
path: '/business/loading-manage/detail',
query: { id, name: '配载单详情' },
});
},
loadWaybillDetailProcessNodes(projectId) {
this.waybillDetailProcessNodes = [];
@@ -4492,6 +4578,19 @@ export default {
})
.catch(() => []);
},
// null
waybillPunchRecordTimestamp(record = {}) {
if (record.punched === false || record.statusName === '未打卡') return null;
const raw = record.punchTime || record.punchAt || record.punchDateTime || '';
const text = String(raw).trim();
if (!text) return null;
const parsed = this.$dayjs ? this.$dayjs(text) : null;
if (parsed && typeof parsed.isValid === 'function' && parsed.isValid()) {
return parsed.valueOf();
}
const fallback = new Date(text.replace(/-/g, '/')).getTime();
return Number.isNaN(fallback) ? null : fallback;
},
loadWaybillPunchRecords() {
const waybillId = this.detailRow?.id;
if (!waybillId || this.waybillPunchRecordsLoading) return;
+126 -97
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="loading-manage-page">
<div v-if="!isStandaloneFormPage" class="loading-manage-page__search">
<div v-if="!isStandalonePage" class="loading-manage-page__search">
<el-form :model="searchForm" label-position="right" label-width="88px">
<div class="loading-manage-page__search-grid">
<el-form-item label="配载单号">
@@ -222,7 +222,7 @@
</el-form>
</div>
<div v-if="!isStandaloneFormPage" class="loading-manage-page__toolbar">
<div v-if="!isStandalonePage" class="loading-manage-page__toolbar">
<div class="loading-manage-page__toolbar-left">
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
@@ -242,7 +242,7 @@
</div>
<el-table
v-if="!isStandaloneFormPage"
v-if="!isStandalonePage"
ref="tableRef"
v-loading="loading"
:data="tableData"
@@ -259,7 +259,7 @@
<el-link
type="primary"
class="loading-manage-page__link"
@click="openLoadingDialog('view', row)"
@click="gotoLoadingDetail(row)"
>{{ row.loadingNo }}</el-link
>
</template>
@@ -363,7 +363,7 @@
</el-table-column>
</el-table>
<div v-if="!isStandaloneFormPage" class="loading-manage-page__pagination">
<div v-if="!isStandalonePage" class="loading-manage-page__pagination">
<el-pagination
v-model:current-page="page.currentPage"
v-model:page-size="page.pageSize"
@@ -376,27 +376,19 @@
/>
</div>
<component
:is="isStandaloneFormPage ? 'div' : 'el-dialog'"
:model-value="isStandaloneFormPage ? undefined : dialogVisible"
:title="isStandaloneFormPage ? undefined : dialogTitle"
:append-to-body="isStandaloneFormPage ? undefined : true"
:destroy-on-close="isStandaloneFormPage ? undefined : true"
:width="isStandaloneFormPage ? undefined : '96%'"
:top="isStandaloneFormPage ? undefined : '3vh'"
:class="[
'loading-manage-dialog',
{ 'loading-manage-form-dialog': isStandaloneFormPage },
{ 'loading-manage-form-page': isStandaloneFormPage },
]"
:modal="isStandaloneFormPage ? undefined : true"
:show-close="isStandaloneFormPage ? undefined : true"
:close-on-click-modal="isStandaloneFormPage ? undefined : true"
:close-on-press-escape="isStandaloneFormPage ? undefined : true"
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
@closed="resetLoadingDialog"
<!--
配载单的新增/编辑/重新派单/详情全部是独立整页,这里固定渲染普通容器,不再按路由切换 el-dialog
原因:标签页 keep-alive 会把本页实例缓存下来,若容器随 $route 由 div 变回 el-dialog
被缓存的实例会在离开本页后渲染出 append-to-body 的弹窗并永久停留在 body 上(Vue 的
KeepAlive.deactivate → Teleport.move 不会搬走 append-to-body 的弹窗 DOM),表现为“点左侧菜单又弹出弹窗”。
-->
<div
v-if="isStandalonePage"
class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page"
>
<div v-if="isStandaloneFormPage" class="archive-page-form__title">{{ formPageTitle }}</div>
<div class="archive-page-form__title">{{
isStandaloneDetailPage ? detailPageTitle : formPageTitle
}}</div>
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情">
@@ -1229,7 +1221,7 @@
</el-form>
</div>
<div v-if="isStandaloneFormPage" class="loading-manage-dialog__footer">
<div class="loading-manage-dialog__footer">
<template v-if="dialogReadonly">
<el-button @click="closeLoadingDialog">关闭</el-button>
</template>
@@ -1243,23 +1235,7 @@
</el-button>
</template>
</div>
<template v-if="!isStandaloneFormPage" #footer>
<div class="loading-manage-dialog__footer">
<template v-if="dialogReadonly">
<el-button @click="closeLoadingDialog">关闭</el-button>
</template>
<template v-else>
<el-button @click="clearLoadingDialog">清空</el-button>
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
暂存
</el-button>
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
</el-button>
</template>
</div>
</template>
</component>
</div>
<el-dialog
v-model="routeChangeVisible"
title="变更运输路线"
@@ -1268,7 +1244,7 @@
width="78%"
class="loading-route-change-dialog"
>
<el-tabs v-model="routeChangeTab">
<el-tabs type="border-card" v-model="routeChangeTab">
<el-tab-pane label="变更路线" name="change">
<section-card title="关联子运单地址">
<el-table :data="routeChangeRows" border>
@@ -1634,7 +1610,6 @@ export default {
searchExpanded: false,
searchForm: createSearchForm(),
selectionRows: [],
dialogVisible: false,
dialogMode: 'add',
dialogLoading: false,
dialogSaving: '',
@@ -1704,15 +1679,6 @@ export default {
if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同';
return '请选择';
},
dialogTitle() {
const titleMap = {
add: '新建配载',
edit: '编辑配载单',
view: '配载单详情',
reassign: '重新派单',
};
return titleMap[this.dialogMode] || '配载管理';
},
detailStatusType() {
const typeMap = {
pending: 'success',
@@ -1741,51 +1707,81 @@ export default {
}
);
},
// 独立表单页:/business/loading-manage/form?mode=add|edit|reassign
isStandaloneFormPage() {
return (
this.$route.path === '/business/loading-manage/form' &&
['add', 'edit'].includes(this.$route.query.mode)
['add', 'edit', 'reassign'].includes(this.$route.query.mode)
);
},
// 独立详情页:/business/loading-manage/detail?id=xxx
isStandaloneDetailPage() {
return this.$route.path === '/business/loading-manage/detail';
},
// 列表容器隐藏搜索栏 / 工具栏 / 表格 / 分页的统一开关
isStandalonePage() {
return this.isStandaloneFormPage || this.isStandaloneDetailPage;
},
formPageDefaultTitle() {
const titleMap = {
add: '新增配载管理',
edit: '编辑配载管理',
reassign: '重新派单',
};
return titleMap[this.$route.query.mode] || '新增配载管理';
},
formPageTitle() {
return (
this.$route.query.name || `${this.$route.query.mode === 'edit' ? '编辑' : '新增'}配载管理`
);
return this.$route.query.name || this.formPageDefaultTitle;
},
detailPageTitle() {
return this.$route.query.name || '配载单详情';
},
},
mounted() {
this.loadTransportTypeOptions();
this.loadTable();
if (this.isStandaloneFormPage) {
if (this.isStandalonePage) {
this.syncStandaloneTagTitle();
this.initStandaloneFormPage();
if (this.isStandaloneDetailPage) this.initStandaloneDetailPage();
else this.initStandaloneFormPage();
}
// 兼容旧链接:/business/loading-manage?detailId=xxx 直接跳独立详情页
const detailId = this.$route.query.detailId;
if (detailId) this.openLoadingDialog('view', { id: detailId });
if (detailId) this.gotoLoadingDetail({ id: detailId });
},
// 标签切走(实例被 keep-alive 缓存)或销毁时,先收起页内子弹窗
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() {
this.closeInnerDialogs();
},
watch: {
$route(to, from) {
if (this.isStandaloneFormPage) {
this.syncStandaloneTagTitle();
if (
to.query.mode !== from?.query?.mode ||
String(to.query.id || '') !== String(from?.query.id || '') ||
String(to.query.copyId || '') !== String(from?.query.copyId || '')
) {
this.initStandaloneFormPage();
}
} else if (from?.query?.mode && this.dialogVisible) {
this.closeLoadingDialog();
if (!this.isStandalonePage) return;
this.syncStandaloneTagTitle();
if (this.isStandaloneDetailPage) {
// 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情
const idChanged = String(to.query.id || '') !== String(from?.query?.id || '');
if (idChanged || this.dialogMode !== 'view') this.initStandaloneDetailPage();
return;
}
if (
to.query.mode !== from?.query?.mode ||
String(to.query.id || '') !== String(from?.query.id || '') ||
String(to.query.copyId || '') !== String(from?.query.copyId || '')
) {
this.initStandaloneFormPage();
}
},
'$route.query.detailId'(detailId) {
if (detailId) this.openLoadingDialog('view', { id: detailId });
if (detailId) this.gotoLoadingDetail({ id: detailId });
},
},
methods: {
syncStandaloneTagTitle() {
if (!this.isStandaloneFormPage) return;
const title = this.$route.query.mode === 'edit' ? '编辑配载管理' : '新增配载管理';
if (!this.isStandalonePage) return;
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
if (this.$route.query.name === title) return;
this.$store.commit('SET_TAG', {
fullPath: this.$route.fullPath,
@@ -2189,9 +2185,9 @@ export default {
},
handleRowAction(type, row) {
const actionMap = {
view: () => this.openLoadingDialog('view', row),
view: () => this.gotoLoadingDetail(row),
edit: () => this.openLoadingDialog('edit', row),
reassign: () => this.openLoadingDialog('reassign', row),
reassign: () => this.gotoLoadingReassign(row),
delete: () => this.deleteLoading(row),
start: () => this.startLoading(row),
cancel: () => this.cancelLoading(row),
@@ -2199,9 +2195,44 @@ export default {
};
actionMap[type]?.();
},
// 详情改为独立页面(与新增/编辑一致)
gotoLoadingDetail(row = {}) {
const id = row.id || row.loadingId || row.loadingManageId;
if (!id) {
ElMessage.info('当前配载单暂无详情数据');
return;
}
if (this.isStandaloneDetailPage && String(this.$route.query.id || '') === String(id)) return;
this.$router.push({
path: '/business/loading-manage/detail',
query: { id, name: '配载单详情' },
});
},
// 重新派单改为独立页面
gotoLoadingReassign(row = {}) {
if (!row.id) {
ElMessage.warning('配载单不存在,无法重新派单');
return;
}
this.$router.push({
path: '/business/loading-manage/form',
query: { mode: 'reassign', id: row.id, name: '重新派单' },
});
},
async initStandaloneDetailPage() {
if (!this.isStandaloneDetailPage) return;
const id = this.$route.query.id;
if (!id) {
ElMessage.warning('缺少配载单参数,无法查看详情');
this.closeLoadingDialog();
return;
}
await this.openLoadingDialog('view', { id });
},
async initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
const queryMode = this.$route.query.mode;
const mode = ['edit', 'reassign'].includes(queryMode) ? queryMode : 'add';
const copyId = mode === 'add' ? this.$route.query.copyId : '';
await this.openLoadingDialog(mode, {
id: copyId || this.$route.query.id,
@@ -2222,7 +2253,12 @@ export default {
}
},
async openLoadingDialog(mode, row) {
if (['add', 'edit'].includes(mode) && !this.isStandaloneFormPage) {
// 列表页进入新增/编辑/重新派单:统一走独立页面
if (['add', 'edit', 'reassign'].includes(mode) && !this.isStandaloneFormPage) {
if (mode === 'reassign') {
this.gotoLoadingReassign(row || {});
return;
}
this.$router.push({
path: '/business/loading-manage/form',
query: {
@@ -2234,7 +2270,6 @@ export default {
return;
}
this.dialogMode = mode;
this.dialogVisible = true;
this.dialogLoading = true;
this.resetDialogData();
if (mode === 'add') this.candidateQuery.transportType = 'road';
@@ -2267,17 +2302,9 @@ export default {
}
},
closeLoadingDialog() {
if (this.isStandaloneFormPage) {
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/loading-manage', query: {} });
return;
}
this.dialogVisible = false;
},
resetLoadingDialog() {
this.resetDialogData();
this.dialogSaving = '';
this.dialogLoading = false;
if (!this.isStandalonePage) return;
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/loading-manage', query: {} });
},
resetDialogData() {
this.detailAmap?.destroy?.();
@@ -2299,10 +2326,11 @@ export default {
this.processProjectId = '';
this.processProjectOptions = [];
},
clearLoadingDialog() {
this.resetDialogData();
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
this.loadCandidateWaybills();
// 离开本页(标签缓存/卸载)时收起页内子弹窗,避免 append-to-body 的弹窗 DOM 残留在 body 上
closeInnerDialogs() {
this.routeChangeVisible = false;
this.commonAddressVisible = false;
this.candidateSearchVisible = false;
},
restoreRouteAndCargo() {
const routeRows = parseJsonArray(this.dialogForm.routeJson);
@@ -3433,7 +3461,6 @@ export default {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.loading-manage-dialog__table-toolbar {
@@ -3767,7 +3794,9 @@ export default {
box-sizing: border-box;
background: #f5f6fa;
}
:global(.loading-manage-form-page .loading-manage-dialog__body) {
/* 独立页面(新增/编辑/重新派单/详情):内容随 #avue-view 自然滚动,不做内嵌 78vh 滚动盒。
此处权重必须高于 .loading-manage-dialog .loading-manage-dialog__body,故带上 .loading-manage-page 外壳。 */
:global(.loading-manage-page .loading-manage-form-dialog.loading-manage-form-page .loading-manage-dialog__body) {
max-height: none;
overflow: visible;
padding-right: 0;
-12
View File
@@ -1354,13 +1354,6 @@ export default {
.filter(fileType => !uploadedTypes.has(fileType));
},
},
watch: {
'$route.fullPath'() {
if (this.isProjectFormPage) {
this.openProjectFormPage();
}
},
},
created() {
this.loadDeptOptions();
this.loadCargoTypeOptions();
@@ -1370,11 +1363,6 @@ export default {
this.openProjectFormPage();
}
},
activated() {
if (this.isProjectFormPage && !this.projectBox) {
this.openProjectFormPage();
}
},
methods: {
buildTableOption() {
return {
+10 -1
View File
@@ -1,9 +1,11 @@
<template>
<!-- waybill-manage-detail.vue只在当前路由属于本页时才透传 detailId
避免标签页缓存的实例被其它页面的 detailId 污染后误开详情 -->
<transport-plan-page
:api="api"
:config="config"
:crud-option="option"
:detail-id="$route.query.detailId"
:detail-id="detailId"
:menu-width="220"
standalone-form-page
/>
@@ -14,6 +16,8 @@ import TransportPlanPage from './components/transport-plan-page.vue';
import * as api from '@/api/business/transport-plan';
import { config, option } from '@/option/business/transport-plan';
const listRoutePath = '/business/transport-plan';
export default {
components: { TransportPlanPage },
data() {
@@ -23,5 +27,10 @@ export default {
option,
};
},
computed: {
detailId() {
return this.$route.path === listRoutePath ? this.$route.query.detailId : '';
},
},
};
</script>
+14 -1
View File
@@ -1,9 +1,15 @@
<template>
<!--
detailId 必须做路由守卫本组件对应 /business/waybill-manage/detail
$route 是全局响应式的标签页 keep-alive 会缓存本实例路由切到别的详情页后
实例仍会重渲染若无守卫就会把当前页面的 id灌进子组件导致它拿别的单据 id
去查运单 运单管理不存在并误生成一个又一个运单详情标签
-->
<waybill-manage-page
:api="api"
:config="config"
:crud-option="option"
:detail-id="$route.query.id"
:detail-id="detailId"
:menu-width="250"
standalone-detail-page
/>
@@ -14,10 +20,17 @@ import WaybillManagePage from './components/waybill-manage-page.vue';
import * as api from '@/api/business/waybill-manage';
import { config, option } from '@/option/business/waybill-manage';
const detailRoutePath = '/business/waybill-manage/detail';
export default {
components: { WaybillManagePage },
data() {
return { api, config, option };
},
computed: {
detailId() {
return this.$route.path === detailRoutePath ? this.$route.query.id : '';
},
},
};
</script>
+10 -1
View File
@@ -1,9 +1,11 @@
<template>
<!-- waybill-manage-detail.vue保留旧链接 /business/waybill-manage?detailId=xxx 的能力
但只在当前路由确实属于本页时才透传避免被其它页面的 detailId 污染 -->
<waybill-manage-page
:api="api"
:config="config"
:crud-option="option"
:detail-id="$route.query.detailId"
:detail-id="detailId"
:menu-width="250"
standalone-form-page
/>
@@ -14,6 +16,8 @@ import WaybillManagePage from './components/waybill-manage-page.vue';
import * as api from '@/api/business/waybill-manage';
import { config, option } from '@/option/business/waybill-manage';
const listRoutePath = '/business/waybill-manage';
export default {
components: { WaybillManagePage },
data() {
@@ -23,5 +27,10 @@ export default {
option,
};
},
computed: {
detailId() {
return this.$route.path === listRoutePath ? this.$route.query.detailId : '';
},
},
};
</script>
@@ -882,7 +882,7 @@ export default {
item.receiverName !== first.receiverName
);
if (incompatible) {
this.$message.warning('合并开票的结算单必须属于同一合同、项目、组织及收付款方');
this.$message.warning('合并开票的结算单必须属于同一合同');
return;
}
this.form.settlements = rows.map(item => ({
@@ -1260,7 +1260,7 @@ export default {
}
.invoice-form-page__dialog-search {
display: flex;
justify-content: flex-end;
justify-content: flex-start;
gap: 8px;
margin-bottom: 12px;
}
+1 -1
View File
@@ -216,7 +216,7 @@
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号、项目或合同"
placeholder="3结算单号、项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
+5 -2
View File
@@ -1892,10 +1892,13 @@ export default {
overflow: hidden;
}
// 大尺寸模式:缩成 1.586:1 比例,左对齐(不再 100% 占栏)
// 大尺寸模式:跟随列宽自适应(最大 240px,约 1.586:1 证件比例),
// 保证不同分辨率 / 系统缩放下都与上方输入框左右对齐
:deep(.driver-uploader--large) {
display: block;
width: 240px;
width: 100%;
max-width: 240px;
flex: none;
margin: 0;
}
+5 -1
View File
@@ -1349,9 +1349,13 @@ export default {
height: 151px;
}
// 大尺寸模式:跟随列宽自适应(最大 248px),
// 保证不同分辨率 / 系统缩放下都与上方输入框左右对齐
:deep(.ship-uploader--large) {
display: block;
width: 248px;
width: 100%;
max-width: 248px;
flex: none;
margin: 0;
}
+26
View File
@@ -21,6 +21,15 @@
@on-load="onLoad"
>
<template #menu-left>
<el-button
v-if="hasPermission('equipment_ledger_delete')"
type="danger"
icon="el-icon-delete"
plain
@click="handleDelete"
>
批量删除
</el-button>
<el-button
v-if="hasPermission('equipment_ledger_import')"
type="primary"
@@ -310,6 +319,23 @@ export default {
this.$message.success('操作成功');
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确认删除选中的数据?删除后将不可恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(this.ids))
.then(() => {
this.onLoad(this.page);
this.$message.success('操作成功');
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
this.boxType = type;
if (type === 'add') {
+91 -2
View File
@@ -217,7 +217,7 @@ import { getList as getOcrTemplateList } from '@/api/base/insurance-ocr-template
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { handleImportExcel } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
@@ -540,7 +540,96 @@ export default {
});
},
handleImport() {
openImportDialog(this, '保险记录');
const column = this.findColumn(this.excelOption.column, 'excelFile');
if (!column) {
this.$message.error('导入配置异常,请稍后重试');
return;
}
// 上传前先归一化日期列(2026-9-1 → 2026-09-01),兼容两种格式
column.httpRequest = async (uploadOption, uploadColumn) => {
try {
const normalizedFile = await this.normalizeExcelDates(uploadOption.file);
await handleImportExcel(
this,
{ ...uploadOption, file: normalizedFile },
uploadColumn,
'保险记录',
null,
{ timeout: 60000 }
);
} catch (error) {
this.$message.error(error.message || '导入失败');
if (typeof uploadOption.onError === 'function') {
uploadOption.onError(error);
}
}
};
this.excelBox = true;
},
// 读取 Excel,把指定日期列统一归一化为 YYYY-MM-DD,再写回新文件
async normalizeExcelDates(file) {
const rows = await this.readExcelRows(file);
const dateHeaders = ['开始日期', '结束日期', '开票日期'];
const headerIndex = (rows[0] || []).reduce((map, label, index) => {
const key = String(label || '').trim();
if (key) map[key] = index;
return map;
}, {});
const targetIndexes = dateHeaders
.map(label => headerIndex[label])
.filter(index => index !== undefined);
if (!targetIndexes.length) return file;
rows.forEach((row, rowIndex) => {
if (rowIndex === 0) return;
targetIndexes.forEach(index => {
if (row[index] !== undefined && row[index] !== '') {
const normalized = this.normalizeDateString(String(row[index]));
if (normalized) row[index] = normalized;
}
});
});
return this.writeExcelFile(rows, file.name);
},
async readExcelRows(file) {
const XLSX = await import('xlsx');
const buffer = await file.arrayBuffer();
const workbook = XLSX.read(buffer, { type: 'array', cellDates: false });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
return XLSX.utils.sheet_to_json(worksheet, {
header: 1,
defval: '',
raw: false,
blankrows: false,
});
},
async writeExcelFile(rows, fileName) {
const XLSX = await import('xlsx');
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), 'Sheet1');
const binary = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
return new File([binary], fileName || 'insurance-record.xlsx', {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
},
// 兼容 YYYY-M-D / YYYY-MM-DD / YYYY/M/D / YYYY.年.月.日,非法或已标准格式原样返回 null
normalizeDateString(value) {
const text = String(value || '').trim();
if (!text) return null;
const match = text.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const date = new Date(year, month - 1, day);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return null;
}
const pad = n => String(n).padStart(2, '0');
return `${year}-${pad(month)}-${pad(day)}`;
},
handleExport() {
this.$confirm('是否导出保险记录数据?', '提示', {