diff --git a/src/option/system/user.js b/src/option/system/user.js index 93e3152..cd0a0ee 100644 --- a/src/option/system/user.js +++ b/src/option/system/user.js @@ -72,6 +72,7 @@ export const userOption = safe => { }, { label: '岗位', prop: 'postName', overHidden: true, display: false }, { label: '性别', prop: 'sexName', width: 80, display: false }, + { label: 'OA登录名', prop: 'oaLoginId', minWidth: 140, overHidden: true, display: false }, { label: 'OA人员id', prop: 'oaPersonId', minWidth: 120, overHidden: true, display: false }, { label: '备注', prop: 'remark', minWidth: 160, overHidden: true, display: false }, { diff --git a/src/views/business/components/shipping-template-page.vue b/src/views/business/components/shipping-template-page.vue index 21f0015..5b4df82 100644 --- a/src/views/business/components/shipping-template-page.vue +++ b/src/views/business/components/shipping-template-page.vue @@ -388,7 +388,7 @@ :disabled="dialogReadonly" @visible-change="visible => visible && loadFeeUnitOptions()" > sum + Number(this.freightAmount(item) || 0), @@ -1530,6 +1552,18 @@ export default { }, watch: { $route(to, from) { + // 列表实例:从其他页进入列表时刷新(activated 负责标签切回)。 + if (!this.isFormPageInstance) { + const enteringList = + (to.path === this.routePathLocked || to.path === shippingTemplateListRoute) && + from?.path && + from.path !== to.path; + if (enteringList) { + sessionStorage.removeItem(shippingTemplateListRefreshKey); + this.$nextTick(() => this.onLoad(this.page, this.query)); + } + return; + } if ( this.isStandaloneFormPage && (to.query.mode !== from?.query?.mode || @@ -1550,6 +1584,10 @@ export default { this.handleTransportTypeChange(value); this.syncFreightItems(); }, + 'form.templateType'() { + // 切换到运单模板时立即按货物数量单位生成公路运费明细。 + this.$nextTick(() => this.syncFreightItems()); + }, }, created() { this.loadProjectOptions(); @@ -1559,8 +1597,16 @@ export default { this.loadCurrencyOptions(); if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage()); }, + activated() { + if (this.isFormPageInstance) return; + sessionStorage.removeItem(shippingTemplateListRefreshKey); + this.$nextTick(() => this.onLoad(this.page, this.query)); + }, methods: { formatContractOptionLabel, + markShippingTemplateListNeedRefresh() { + sessionStorage.setItem(shippingTemplateListRefreshKey, '1'); + }, loadQuantityUnitOptions() { if (this.quantityUnitLoading) return this.quantityUnitLoadPromise; this.quantityUnitLoading = true; @@ -1591,12 +1637,8 @@ export default { }); } if (this.menuWidth != null) next.menuWidth = this.menuWidth; - if ( - this.standaloneFormPage && - ['/business/shipping-template', '/business/shipping-template/form'].includes( - this.$route.path - ) - ) { + // 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。 + if (this.standaloneFormPage && this.$route.path === shippingTemplateFormRoute) { next.dialogAppendToBody = false; next.dialogModal = false; next.dialogCustomClass = 'shipping-template-form-page-dialog'; @@ -1776,13 +1818,22 @@ export default { this.onLoad(this.page, this.query); }, onLoad(page, params = {}) { + if (this.isFormPageInstance) return; this.loading = true; + const currentPage = page?.currentPage || this.page.currentPage || 1; + const pageSize = page?.pageSize || this.page.pageSize || 10; this.api - .getList(page.currentPage, page.pageSize, { ...this.query, ...params }) + .getList(currentPage, pageSize, { ...this.query, ...params }) .then(res => { - const data = res?.data?.data || {}; - this.data = extractRecords(res); - this.page.total = data.total || this.data.length; + const data = res?.data?.data || res?.data || {}; + const records = extractRecords(res); + this.data = records; + this.page.total = Number(data.total ?? records.length) || 0; + }) + .catch(error => { + window.console.error(error); + this.data = []; + this.page.total = 0; }) .finally(() => { this.loading = false; @@ -1821,9 +1872,10 @@ export default { .submit(submit) .then(() => { this.$message.success('操作成功'); + this.markShippingTemplateListNeedRefresh(); done?.(); this.closeCrudDialog(); - this.onLoad(this.page, this.query); + if (!this.isFormPageInstance) this.onLoad(this.page, this.query); }) .catch(() => loading?.()); }, @@ -1837,9 +1889,10 @@ export default { .submit(submit) .then(() => { this.$message.success('操作成功'); + this.markShippingTemplateListNeedRefresh(); done?.(); this.closeCrudDialog(); - this.onLoad(this.page, this.query); + if (!this.isFormPageInstance) this.onLoad(this.page, this.query); }) .catch(() => loading?.()); }, @@ -1883,6 +1936,8 @@ export default { this.attachmentRows = []; this.shippingTemplateFreight = defaultFreight(); this.loadCurrencyOptions(); + this.loadFeeUnitOptions(); + this.$nextTick(() => this.syncFreightItems()); if (typeof this.api.nextCode === 'function') this.api .nextCode() @@ -2005,20 +2060,47 @@ export default { return false; } if (this.transportMode === 'road') { - const invalid = this.shippingTemplateFreight.freightItems.findIndex( - item => - Number(item.unitPrice || 0) < 0 || - !item.priceUnit || - (!this.isFreightAutoCalculable(item) && - (item.freightAmount === '' || - item.freightAmount === null || - item.freightAmount === undefined)) || - Number(item.freightAmount || 0) < 0 - ); - if (invalid > -1) { - this.$message.warning(`第${invalid + 1}条货物的单价、计价单位或运费无效`); + if (!this.shippingTemplateFreight.freightItems.length) { + this.$message.warning('请先完善货物数量单位,以便生成运费明细'); return false; } + for (const [index, item] of this.shippingTemplateFreight.freightItems.entries()) { + const label = `第${index + 1}条运费明细`; + if ( + item.unitPrice === '' || + item.unitPrice === null || + item.unitPrice === undefined + ) { + this.$message.warning(`请填写${label}的单价`); + return false; + } + if (Number(item.unitPrice) < 0) { + this.$message.warning(`${label}的单价不能小于0`); + return false; + } + if (!String(item.priceUnit || '').trim()) { + this.$message.warning(`请选择${label}的计价单位`); + return false; + } + if (this.isFreightAutoCalculable(item)) { + if (!item.freightAmountManual) { + item.freightAmount = this.freightAmount(item); + } + } else if ( + item.freightAmount === '' || + item.freightAmount === null || + item.freightAmount === undefined + ) { + this.$message.warning( + `${label}的计价单位与货物数量单位不匹配,无法自动计算运费,请手动填写运费` + ); + return false; + } + if (Number(item.freightAmount || 0) < 0) { + this.$message.warning(`${label}的运费不能小于0`); + return false; + } + } } else if (Number(this.shippingTemplateFreight.freightAmount || 0) < 0) { this.$message.warning('运费不能小于0'); return false; @@ -2033,8 +2115,11 @@ export default { return true; }, closeCrudDialog() { - if (this.isStandaloneBusinessPage) { - this.$router.push({ path: '/business/shipping-template', query: {} }); + if (this.isStandaloneBusinessPage || this.isFormPageInstance) { + // 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景) + this.markShippingTemplateListNeedRefresh(); + this.$router.$avueRouter?.closeTag?.(); + this.$router.push({ path: shippingTemplateListRoute, query: {} }); return; } this.$refs.crud?.closeDialog?.(); @@ -2294,7 +2379,16 @@ export default { }, loadFeeUnitOptions() { getDictionary({ code: 'unit_fee' }).then(res => { - this.feeUnitOptions = this.normalizeDictOptions(res?.data?.data || []); + // 计价单位必须保存可读文案(如 元/吨),才能与货物数量单位匹配并自动计算运费。 + this.feeUnitOptions = (res?.data?.data || []).map(item => { + const label = String(item.dictValue || item.label || item.value || '').trim(); + const key = String(item.dictKey || item.value || '').trim(); + return { + label: label || key, + value: label.includes('/') ? label : key || label, + remark: item.remark || '', + }; + }); }); }, loadCurrencyOptions() { @@ -3098,24 +3192,53 @@ export default { : []; const quantityUnits = []; this.transportCargoRows.forEach(row => { - const quantityUnit = row.quantityUnit || ''; + const quantityUnit = String(row.quantityUnit || '').trim(); + if (!quantityUnit) return; if (!quantityUnits.some(unit => unit === quantityUnit)) quantityUnits.push(quantityUnit); }); + const currency = this.shippingTemplateFreight.currency || 'RMB'; this.shippingTemplateFreight.freightItems = quantityUnits.map((quantityUnit, index) => { const matched = previousItems.find(item => String(item.quantityUnit || '') === String(quantityUnit)) || previousItems[index] || {}; + const defaultPriceUnit = resolvePriceUnitByQuantityUnit(quantityUnit, currency); + const priceUnit = this.normalizeFreightPriceUnit( + matched.priceUnit || defaultPriceUnit || '', + quantityUnit, + currency + ); return { quantityUnit, unitPrice: matched.unitPrice || '', - priceUnit: matched.priceUnit || '', + priceUnit, freightAmount: matched.freightAmount ?? '', freightAmountManual: matched.freightAmountManual === true || matched.manualFreightAmount === true, }; }); }, + normalizeFreightPriceUnit(priceUnit, quantityUnit, currency = 'RMB') { + const text = this.resolvePriceUnitText(priceUnit); + if (text && this.priceUnitMatchesQuantity(text, quantityUnit)) return text; + return ( + resolvePriceUnitByQuantityUnit(quantityUnit, currency) || + text || + String(priceUnit || '').trim() + ); + }, + resolvePriceUnitText(priceUnit) { + const raw = String(priceUnit || '').trim(); + if (!raw) return ''; + if (raw.includes('/')) return raw; + const options = this.feeUnitSelectOptions.length + ? this.feeUnitSelectOptions + : this.feeUnitOptions; + const matched = options.find( + item => String(item.value) === raw || String(item.label) === raw + ); + return matched?.label || raw; + }, quantityUnits() { return [ ...new Set( @@ -3131,7 +3254,7 @@ export default { .replace(/\s+/g, '') .replace(/立方米|立方公尺|方/g, '立方'); const unit = normalizeUnit(quantityUnit).replace(/[·*.]/g, ''); - const value = String(priceUnit || '') + const value = String(this.resolvePriceUnitText(priceUnit) || '') .trim() .toLowerCase() .replace(/\s+/g, ''); @@ -3163,6 +3286,10 @@ export default { return this.formatFreightNumber(item.freightAmount); } if (!this.isFreightAutoCalculable(item)) { + // 无法自动计算且未手工填写时返回空,避免把空值显示成 0 造成“已填写”的错觉。 + if (item.freightAmount === '' || item.freightAmount === null || item.freightAmount === undefined) { + return ''; + } return this.formatFreightNumber(item.freightAmount); } return this.formatFreightNumber( diff --git a/src/views/business/components/transport-plan-page.vue b/src/views/business/components/transport-plan-page.vue index 4d12b86..e4af27c 100644 --- a/src/views/business/components/transport-plan-page.vue +++ b/src/views/business/components/transport-plan-page.vue @@ -2807,8 +2807,11 @@ import 'nprogress/nprogress.css'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; let amapLoader; +const transportPlanListRoute = '/business/transport-plan'; +const transportPlanFormRoute = '/business/transport-plan/form'; +const transportPlanListRefreshKey = 'transport-plan-list-refresh'; const standaloneTransportPlanFormRoutes = { - '/business/transport-plan': '/business/transport-plan/form', + [transportPlanListRoute]: transportPlanFormRoute, }; const standaloneTransportPlanFormPaths = [ ...Object.keys(standaloneTransportPlanFormRoutes), @@ -3072,6 +3075,10 @@ export default { // 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created, // 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。 routePathLocked: this.$route.path, + // 表单页实例在创建时锁定,避免 keep-alive 列表实例随全局 $route 误切到表单态。 + isFormPageInstance: Object.values(standaloneTransportPlanFormRoutes).includes( + this.$route.path + ), form: {}, suppressTransportTypeClear: false, query: {}, @@ -3112,10 +3119,11 @@ export default { option: (() => { const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config)); if (this.menuWidth != null) opt.menuWidth = this.menuWidth; - if ( - this.standaloneFormPage && - standaloneTransportPlanFormPaths.includes(this.$route.path) - ) { + // 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。 + const isFormInstance = Object.values(standaloneTransportPlanFormRoutes).includes( + this.$route.path + ); + if (this.standaloneFormPage && isFormInstance) { opt.dialogAppendToBody = false; opt.dialogModal = false; opt.dialogCustomClass = 'transport-plan-form-page-dialog'; @@ -3289,6 +3297,12 @@ export default { } this.loadTaskQuantityUnitOptions(); }, + // keep-alive 重新激活列表时强制刷新,避免缓存脏数据;标记位用于兼容旧跳转逻辑。 + activated() { + if (this.isFormPageInstance) return; + sessionStorage.removeItem(transportPlanListRefreshKey); + this.$nextTick(() => this.onLoad(this.page, this.query)); + }, computed: { ...mapGetters(['permission', 'userInfo']), permissionList() { @@ -3300,10 +3314,20 @@ export default { return this.config.detailSections || []; }, isStandaloneBusinessPage() { - return this.standaloneFormPage && standaloneTransportPlanFormPaths.includes(this.$route.path); + return ( + this.standaloneFormPage && + (this.isFormPageInstance || + standaloneTransportPlanFormPaths.includes(this.routePathLocked) || + standaloneTransportPlanFormPaths.includes(this.$route.path)) + ); }, isStandaloneFormPage() { - return this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode); + // 用实例锁定标志,避免列表 keep-alive 实例被全局路由误判成表单页。 + return ( + this.standaloneFormPage && + this.isFormPageInstance && + ['add', 'edit'].includes(this.$route.query.mode) + ); }, formPageTitle() { if (this.isStandaloneFormPage) { @@ -3752,6 +3776,18 @@ export default { }, watch: { $route(to, from) { + // 列表实例:从其他页进入列表时刷新(activated 负责标签切回)。 + if (!this.isFormPageInstance) { + const enteringList = + (to.path === this.routePathLocked || to.path === transportPlanListRoute) && + from?.path && + from.path !== to.path; + if (enteringList) { + sessionStorage.removeItem(transportPlanListRefreshKey); + this.$nextTick(() => this.onLoad(this.page, this.query)); + } + return; + } if (!this.isStandaloneFormPage) return; if ( to.query.mode !== from?.query?.mode || @@ -4484,6 +4520,9 @@ export default { loading(); } }, + markTransportPlanListNeedRefresh() { + sessionStorage.setItem(transportPlanListRefreshKey, '1'); + }, rowSave(row, done, loading) { const submitRow = this.normalizeRow(row); if (!submitRow) { @@ -4492,6 +4531,7 @@ export default { } this.getSaveRequest()(submitRow).then( () => { + this.markTransportPlanListNeedRefresh(); this.onLoad(this.page); this.$message({ type: 'success', message: '操作成功!' }); done(); @@ -4511,6 +4551,7 @@ export default { } this.getSaveRequest()(submitRow).then( () => { + this.markTransportPlanListNeedRefresh(); this.onLoad(this.page); this.$message({ type: 'success', message: '操作成功!' }); done(); @@ -4540,7 +4581,7 @@ export default { .submit(submitRow) .then(() => { this.$message.success(action === 'draft' ? '暂存成功' : '创建成功'); - this.onLoad(this.page, this.query); + this.markTransportPlanListNeedRefresh(); this.closeCrudDialog(); }) .finally(() => { @@ -4548,12 +4589,14 @@ export default { }); }, closeCrudDialog() { - if (this.isStandaloneBusinessPage) { + if (this.isStandaloneBusinessPage || this.isFormPageInstance) { + // 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景) + this.markTransportPlanListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); const listPath = Object.entries(standaloneTransportPlanFormRoutes).find( - ([, formPath]) => formPath === this.$route.path - )?.[0] || this.$route.path; + ([, formPath]) => formPath === this.$route.path || formPath === this.routePathLocked + )?.[0] || transportPlanListRoute; this.$router.push({ path: listPath, query: {} }); return; } @@ -6634,20 +6677,33 @@ export default { return query; }, onLoad(page, params = {}) { + if (this.isFormPageInstance) return; this.loading = true; + const currentPage = page?.currentPage || this.page.currentPage || 1; + const pageSize = page?.pageSize || this.page.pageSize || 10; const query = this.normalizeQueryParams({ ...params, ...this.query, allDept: 0, }); this.api - .getList(page.currentPage, page.pageSize, query) + .getList(currentPage, pageSize, query) .then(res => { - const result = res.data.data; - this.page.total = result.total; - this.data = result.records; + const result = res?.data?.data || res?.data || {}; + const records = Array.isArray(result.records) + ? result.records + : Array.isArray(result) + ? result + : []; + this.page.total = Number(result.total ?? records.length) || 0; + this.data = records; this.selectionClear(); }) + .catch(error => { + window.console.error(error); + this.data = []; + this.page.total = 0; + }) .finally(() => { this.loading = false; }); diff --git a/src/views/business/transport-plan-import.vue b/src/views/business/transport-plan-import.vue index 4944c48..f49195a 100644 --- a/src/views/business/transport-plan-import.vue +++ b/src/views/business/transport-plan-import.vue @@ -602,6 +602,8 @@ export default { if (result.code !== 200) throw new Error(result.msg || '导入失败'); this.$message.success('导入完成'); } + // 与运单/配载一致:通知 keep-alive 列表实例重新拉取 + sessionStorage.setItem('transport-plan-list-refresh', '1'); this.$router.push('/business/transport-plan'); } catch (error) { this.$message.error(error.message || '导入失败'); diff --git a/src/views/settlement/components/settlement-import-page.vue b/src/views/settlement/components/settlement-import-page.vue index 3b0d6b9..7c3d6a4 100644 --- a/src/views/settlement/components/settlement-import-page.vue +++ b/src/views/settlement/components/settlement-import-page.vue @@ -135,7 +135,7 @@ - + - 上传 Excel + 上传 Excel 下载模板 @@ -298,15 +298,10 @@ -