⚡ 修复运输计划/发货模板列表缓存不刷新,并优化结算导入查看与费用弹窗

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 16:56:03 +08:00
parent ccaf944782
commit 5cb7eb16d5
9 changed files with 320 additions and 311 deletions
+1
View File
@@ -72,6 +72,7 @@ export const userOption = safe => {
}, },
{ label: '岗位', prop: 'postName', overHidden: true, display: false }, { label: '岗位', prop: 'postName', overHidden: true, display: false },
{ label: '性别', prop: 'sexName', width: 80, 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: 'OA人员id', prop: 'oaPersonId', minWidth: 120, overHidden: true, display: false },
{ label: '备注', prop: 'remark', minWidth: 160, overHidden: true, display: false }, { label: '备注', prop: 'remark', minWidth: 160, overHidden: true, display: false },
{ {
@@ -388,7 +388,7 @@
:disabled="dialogReadonly" :disabled="dialogReadonly"
@visible-change="visible => visible && loadFeeUnitOptions()" @visible-change="visible => visible && loadFeeUnitOptions()"
><el-option ><el-option
v-for="unit in feeUnitOptions" v-for="unit in feeUnitSelectOptions"
:key="unit.value" :key="unit.value"
:label="unit.label" :label="unit.label"
:value="unit.value" /></el-select :value="unit.value" /></el-select
@@ -1124,6 +1124,10 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import {
mapFeeUnitOptions,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit'; import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search'; import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
@@ -1264,6 +1268,10 @@ const PageAvueForm = {
}, },
}; };
const shippingTemplateListRoute = '/business/shipping-template';
const shippingTemplateFormRoute = '/business/shipping-template/form';
const shippingTemplateListRefreshKey = 'shipping-template-list-refresh';
export default { export default {
name: 'ShippingTemplatePage', name: 'ShippingTemplatePage',
components: { PageAvueForm, ElImageViewer, OpenFileViewer }, components: { PageAvueForm, ElImageViewer, OpenFileViewer },
@@ -1280,6 +1288,9 @@ export default {
return { return {
Location, Location,
OfficeBuilding, OfficeBuilding,
// 实例所属路由快照,避免 keep-alive 列表实例被全局 $route 误判成表单页。
routePathLocked: this.$route.path,
isFormPageInstance: this.$route.path === shippingTemplateFormRoute,
form: {}, form: {},
query: {}, query: {},
loading: true, loading: true,
@@ -1401,13 +1412,18 @@ export default {
isStandaloneBusinessPage() { isStandaloneBusinessPage() {
return ( return (
this.standaloneFormPage && this.standaloneFormPage &&
['/business/shipping-template', '/business/shipping-template/form'].includes( (this.isFormPageInstance ||
this.$route.path [shippingTemplateListRoute, shippingTemplateFormRoute].includes(this.routePathLocked) ||
) [shippingTemplateListRoute, shippingTemplateFormRoute].includes(this.$route.path))
); );
}, },
isStandaloneFormPage() { 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() { formPageTitle() {
return this.isStandaloneFormPage return this.isStandaloneFormPage
@@ -1464,6 +1480,12 @@ export default {
); );
return this.currencyUnit(option?.label || this.shippingTemplateFreight.currency); return this.currencyUnit(option?.label || this.shippingTemplateFreight.currency);
}, },
feeUnitSelectOptions() {
return mapFeeUnitOptions(
this.feeUnitOptions,
this.shippingTemplateFreight.currency || 'RMB'
);
},
roadFreightTotal() { roadFreightTotal() {
const total = this.shippingTemplateFreight.freightItems.reduce( const total = this.shippingTemplateFreight.freightItems.reduce(
(sum, item) => sum + Number(this.freightAmount(item) || 0), (sum, item) => sum + Number(this.freightAmount(item) || 0),
@@ -1530,6 +1552,18 @@ export default {
}, },
watch: { watch: {
$route(to, from) { $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 ( if (
this.isStandaloneFormPage && this.isStandaloneFormPage &&
(to.query.mode !== from?.query?.mode || (to.query.mode !== from?.query?.mode ||
@@ -1550,6 +1584,10 @@ export default {
this.handleTransportTypeChange(value); this.handleTransportTypeChange(value);
this.syncFreightItems(); this.syncFreightItems();
}, },
'form.templateType'() {
// 切换到运单模板时立即按货物数量单位生成公路运费明细。
this.$nextTick(() => this.syncFreightItems());
},
}, },
created() { created() {
this.loadProjectOptions(); this.loadProjectOptions();
@@ -1559,8 +1597,16 @@ export default {
this.loadCurrencyOptions(); this.loadCurrencyOptions();
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage()); if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
}, },
activated() {
if (this.isFormPageInstance) return;
sessionStorage.removeItem(shippingTemplateListRefreshKey);
this.$nextTick(() => this.onLoad(this.page, this.query));
},
methods: { methods: {
formatContractOptionLabel, formatContractOptionLabel,
markShippingTemplateListNeedRefresh() {
sessionStorage.setItem(shippingTemplateListRefreshKey, '1');
},
loadQuantityUnitOptions() { loadQuantityUnitOptions() {
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise; if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
this.quantityUnitLoading = true; this.quantityUnitLoading = true;
@@ -1591,12 +1637,8 @@ export default {
}); });
} }
if (this.menuWidth != null) next.menuWidth = this.menuWidth; if (this.menuWidth != null) next.menuWidth = this.menuWidth;
if ( // 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。
this.standaloneFormPage && if (this.standaloneFormPage && this.$route.path === shippingTemplateFormRoute) {
['/business/shipping-template', '/business/shipping-template/form'].includes(
this.$route.path
)
) {
next.dialogAppendToBody = false; next.dialogAppendToBody = false;
next.dialogModal = false; next.dialogModal = false;
next.dialogCustomClass = 'shipping-template-form-page-dialog'; next.dialogCustomClass = 'shipping-template-form-page-dialog';
@@ -1776,13 +1818,22 @@ export default {
this.onLoad(this.page, this.query); this.onLoad(this.page, this.query);
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
if (this.isFormPageInstance) return;
this.loading = true; this.loading = true;
const currentPage = page?.currentPage || this.page.currentPage || 1;
const pageSize = page?.pageSize || this.page.pageSize || 10;
this.api this.api
.getList(page.currentPage, page.pageSize, { ...this.query, ...params }) .getList(currentPage, pageSize, { ...this.query, ...params })
.then(res => { .then(res => {
const data = res?.data?.data || {}; const data = res?.data?.data || res?.data || {};
this.data = extractRecords(res); const records = extractRecords(res);
this.page.total = data.total || this.data.length; 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(() => { .finally(() => {
this.loading = false; this.loading = false;
@@ -1821,9 +1872,10 @@ export default {
.submit(submit) .submit(submit)
.then(() => { .then(() => {
this.$message.success('操作成功'); this.$message.success('操作成功');
this.markShippingTemplateListNeedRefresh();
done?.(); done?.();
this.closeCrudDialog(); this.closeCrudDialog();
this.onLoad(this.page, this.query); if (!this.isFormPageInstance) this.onLoad(this.page, this.query);
}) })
.catch(() => loading?.()); .catch(() => loading?.());
}, },
@@ -1837,9 +1889,10 @@ export default {
.submit(submit) .submit(submit)
.then(() => { .then(() => {
this.$message.success('操作成功'); this.$message.success('操作成功');
this.markShippingTemplateListNeedRefresh();
done?.(); done?.();
this.closeCrudDialog(); this.closeCrudDialog();
this.onLoad(this.page, this.query); if (!this.isFormPageInstance) this.onLoad(this.page, this.query);
}) })
.catch(() => loading?.()); .catch(() => loading?.());
}, },
@@ -1883,6 +1936,8 @@ export default {
this.attachmentRows = []; this.attachmentRows = [];
this.shippingTemplateFreight = defaultFreight(); this.shippingTemplateFreight = defaultFreight();
this.loadCurrencyOptions(); this.loadCurrencyOptions();
this.loadFeeUnitOptions();
this.$nextTick(() => this.syncFreightItems());
if (typeof this.api.nextCode === 'function') if (typeof this.api.nextCode === 'function')
this.api this.api
.nextCode() .nextCode()
@@ -2005,20 +2060,47 @@ export default {
return false; return false;
} }
if (this.transportMode === 'road') { if (this.transportMode === 'road') {
const invalid = this.shippingTemplateFreight.freightItems.findIndex( if (!this.shippingTemplateFreight.freightItems.length) {
item => this.$message.warning('请先完善货物数量单位,以便生成运费明细');
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}条货物的单价、计价单位或运费无效`);
return false; 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) { } else if (Number(this.shippingTemplateFreight.freightAmount || 0) < 0) {
this.$message.warning('运费不能小于0'); this.$message.warning('运费不能小于0');
return false; return false;
@@ -2033,8 +2115,11 @@ export default {
return true; return true;
}, },
closeCrudDialog() { closeCrudDialog() {
if (this.isStandaloneBusinessPage) { if (this.isStandaloneBusinessPage || this.isFormPageInstance) {
this.$router.push({ path: '/business/shipping-template', query: {} }); // 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景)
this.markShippingTemplateListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: shippingTemplateListRoute, query: {} });
return; return;
} }
this.$refs.crud?.closeDialog?.(); this.$refs.crud?.closeDialog?.();
@@ -2294,7 +2379,16 @@ export default {
}, },
loadFeeUnitOptions() { loadFeeUnitOptions() {
getDictionary({ code: 'unit_fee' }).then(res => { 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() { loadCurrencyOptions() {
@@ -3098,24 +3192,53 @@ export default {
: []; : [];
const quantityUnits = []; const quantityUnits = [];
this.transportCargoRows.forEach(row => { 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); if (!quantityUnits.some(unit => unit === quantityUnit)) quantityUnits.push(quantityUnit);
}); });
const currency = this.shippingTemplateFreight.currency || 'RMB';
this.shippingTemplateFreight.freightItems = quantityUnits.map((quantityUnit, index) => { this.shippingTemplateFreight.freightItems = quantityUnits.map((quantityUnit, index) => {
const matched = const matched =
previousItems.find(item => String(item.quantityUnit || '') === String(quantityUnit)) || previousItems.find(item => String(item.quantityUnit || '') === String(quantityUnit)) ||
previousItems[index] || previousItems[index] ||
{}; {};
const defaultPriceUnit = resolvePriceUnitByQuantityUnit(quantityUnit, currency);
const priceUnit = this.normalizeFreightPriceUnit(
matched.priceUnit || defaultPriceUnit || '',
quantityUnit,
currency
);
return { return {
quantityUnit, quantityUnit,
unitPrice: matched.unitPrice || '', unitPrice: matched.unitPrice || '',
priceUnit: matched.priceUnit || '', priceUnit,
freightAmount: matched.freightAmount ?? '', freightAmount: matched.freightAmount ?? '',
freightAmountManual: freightAmountManual:
matched.freightAmountManual === true || matched.manualFreightAmount === true, 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() { quantityUnits() {
return [ return [
...new Set( ...new Set(
@@ -3131,7 +3254,7 @@ export default {
.replace(/\s+/g, '') .replace(/\s+/g, '')
.replace(/立方米|立方公尺|方/g, '立方'); .replace(/立方米|立方公尺|方/g, '立方');
const unit = normalizeUnit(quantityUnit).replace(/[·*.]/g, ''); const unit = normalizeUnit(quantityUnit).replace(/[·*.]/g, '');
const value = String(priceUnit || '') const value = String(this.resolvePriceUnitText(priceUnit) || '')
.trim() .trim()
.toLowerCase() .toLowerCase()
.replace(/\s+/g, ''); .replace(/\s+/g, '');
@@ -3163,6 +3286,10 @@ export default {
return this.formatFreightNumber(item.freightAmount); return this.formatFreightNumber(item.freightAmount);
} }
if (!this.isFreightAutoCalculable(item)) { if (!this.isFreightAutoCalculable(item)) {
// 无法自动计算且未手工填写时返回空,避免把空值显示成 0 造成“已填写”的错觉。
if (item.freightAmount === '' || item.freightAmount === null || item.freightAmount === undefined) {
return '';
}
return this.formatFreightNumber(item.freightAmount); return this.formatFreightNumber(item.freightAmount);
} }
return this.formatFreightNumber( return this.formatFreightNumber(
@@ -2807,8 +2807,11 @@ import 'nprogress/nprogress.css';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader; let amapLoader;
const transportPlanListRoute = '/business/transport-plan';
const transportPlanFormRoute = '/business/transport-plan/form';
const transportPlanListRefreshKey = 'transport-plan-list-refresh';
const standaloneTransportPlanFormRoutes = { const standaloneTransportPlanFormRoutes = {
'/business/transport-plan': '/business/transport-plan/form', [transportPlanListRoute]: transportPlanFormRoute,
}; };
const standaloneTransportPlanFormPaths = [ const standaloneTransportPlanFormPaths = [
...Object.keys(standaloneTransportPlanFormRoutes), ...Object.keys(standaloneTransportPlanFormRoutes),
@@ -3072,6 +3075,10 @@ export default {
// 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created, // 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created,
// 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。 // 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。
routePathLocked: this.$route.path, routePathLocked: this.$route.path,
// 表单页实例在创建时锁定,避免 keep-alive 列表实例随全局 $route 误切到表单态。
isFormPageInstance: Object.values(standaloneTransportPlanFormRoutes).includes(
this.$route.path
),
form: {}, form: {},
suppressTransportTypeClear: false, suppressTransportTypeClear: false,
query: {}, query: {},
@@ -3112,10 +3119,11 @@ export default {
option: (() => { option: (() => {
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config)); const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
if (this.menuWidth != null) opt.menuWidth = this.menuWidth; if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
if ( // 仅表单页实例套用全屏表单弹窗样式,避免列表页误用后表格区域异常。
this.standaloneFormPage && const isFormInstance = Object.values(standaloneTransportPlanFormRoutes).includes(
standaloneTransportPlanFormPaths.includes(this.$route.path) this.$route.path
) { );
if (this.standaloneFormPage && isFormInstance) {
opt.dialogAppendToBody = false; opt.dialogAppendToBody = false;
opt.dialogModal = false; opt.dialogModal = false;
opt.dialogCustomClass = 'transport-plan-form-page-dialog'; opt.dialogCustomClass = 'transport-plan-form-page-dialog';
@@ -3289,6 +3297,12 @@ export default {
} }
this.loadTaskQuantityUnitOptions(); this.loadTaskQuantityUnitOptions();
}, },
// keep-alive 重新激活列表时强制刷新,避免缓存脏数据;标记位用于兼容旧跳转逻辑。
activated() {
if (this.isFormPageInstance) return;
sessionStorage.removeItem(transportPlanListRefreshKey);
this.$nextTick(() => this.onLoad(this.page, this.query));
},
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
permissionList() { permissionList() {
@@ -3300,10 +3314,20 @@ export default {
return this.config.detailSections || []; return this.config.detailSections || [];
}, },
isStandaloneBusinessPage() { isStandaloneBusinessPage() {
return this.standaloneFormPage && standaloneTransportPlanFormPaths.includes(this.$route.path); return (
this.standaloneFormPage &&
(this.isFormPageInstance ||
standaloneTransportPlanFormPaths.includes(this.routePathLocked) ||
standaloneTransportPlanFormPaths.includes(this.$route.path))
);
}, },
isStandaloneFormPage() { 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() { formPageTitle() {
if (this.isStandaloneFormPage) { if (this.isStandaloneFormPage) {
@@ -3752,6 +3776,18 @@ export default {
}, },
watch: { watch: {
$route(to, from) { $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 (!this.isStandaloneFormPage) return;
if ( if (
to.query.mode !== from?.query?.mode || to.query.mode !== from?.query?.mode ||
@@ -4484,6 +4520,9 @@ export default {
loading(); loading();
} }
}, },
markTransportPlanListNeedRefresh() {
sessionStorage.setItem(transportPlanListRefreshKey, '1');
},
rowSave(row, done, loading) { rowSave(row, done, loading) {
const submitRow = this.normalizeRow(row); const submitRow = this.normalizeRow(row);
if (!submitRow) { if (!submitRow) {
@@ -4492,6 +4531,7 @@ export default {
} }
this.getSaveRequest()(submitRow).then( this.getSaveRequest()(submitRow).then(
() => { () => {
this.markTransportPlanListNeedRefresh();
this.onLoad(this.page); this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' }); this.$message({ type: 'success', message: '操作成功!' });
done(); done();
@@ -4511,6 +4551,7 @@ export default {
} }
this.getSaveRequest()(submitRow).then( this.getSaveRequest()(submitRow).then(
() => { () => {
this.markTransportPlanListNeedRefresh();
this.onLoad(this.page); this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' }); this.$message({ type: 'success', message: '操作成功!' });
done(); done();
@@ -4540,7 +4581,7 @@ export default {
.submit(submitRow) .submit(submitRow)
.then(() => { .then(() => {
this.$message.success(action === 'draft' ? '暂存成功' : '创建成功'); this.$message.success(action === 'draft' ? '暂存成功' : '创建成功');
this.onLoad(this.page, this.query); this.markTransportPlanListNeedRefresh();
this.closeCrudDialog(); this.closeCrudDialog();
}) })
.finally(() => { .finally(() => {
@@ -4548,12 +4589,14 @@ export default {
}); });
}, },
closeCrudDialog() { closeCrudDialog() {
if (this.isStandaloneBusinessPage) { if (this.isStandaloneBusinessPage || this.isFormPageInstance) {
// 关闭独立表单页时标记列表需刷新(含直接点关闭未保存的场景)
this.markTransportPlanListNeedRefresh();
this.$router.$avueRouter?.closeTag?.(); this.$router.$avueRouter?.closeTag?.();
const listPath = const listPath =
Object.entries(standaloneTransportPlanFormRoutes).find( Object.entries(standaloneTransportPlanFormRoutes).find(
([, formPath]) => formPath === this.$route.path ([, formPath]) => formPath === this.$route.path || formPath === this.routePathLocked
)?.[0] || this.$route.path; )?.[0] || transportPlanListRoute;
this.$router.push({ path: listPath, query: {} }); this.$router.push({ path: listPath, query: {} });
return; return;
} }
@@ -6634,20 +6677,33 @@ export default {
return query; return query;
}, },
onLoad(page, params = {}) { onLoad(page, params = {}) {
if (this.isFormPageInstance) return;
this.loading = true; this.loading = true;
const currentPage = page?.currentPage || this.page.currentPage || 1;
const pageSize = page?.pageSize || this.page.pageSize || 10;
const query = this.normalizeQueryParams({ const query = this.normalizeQueryParams({
...params, ...params,
...this.query, ...this.query,
allDept: 0, allDept: 0,
}); });
this.api this.api
.getList(page.currentPage, page.pageSize, query) .getList(currentPage, pageSize, query)
.then(res => { .then(res => {
const result = res.data.data; const result = res?.data?.data || res?.data || {};
this.page.total = result.total; const records = Array.isArray(result.records)
this.data = result.records; ? result.records
: Array.isArray(result)
? result
: [];
this.page.total = Number(result.total ?? records.length) || 0;
this.data = records;
this.selectionClear(); this.selectionClear();
}) })
.catch(error => {
window.console.error(error);
this.data = [];
this.page.total = 0;
})
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
}); });
@@ -602,6 +602,8 @@ export default {
if (result.code !== 200) throw new Error(result.msg || '导入失败'); if (result.code !== 200) throw new Error(result.msg || '导入失败');
this.$message.success('导入完成'); this.$message.success('导入完成');
} }
// 与运单/配载一致:通知 keep-alive 列表实例重新拉取
sessionStorage.setItem('transport-plan-list-refresh', '1');
this.$router.push('/business/transport-plan'); this.$router.push('/business/transport-plan');
} catch (error) { } catch (error) {
this.$message.error(error.message || '导入失败'); this.$message.error(error.message || '导入失败');
@@ -135,7 +135,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="结算单上传" prop="file"> <el-form-item v-if="!readonly" label="结算单上传" prop="file">
<div class="settlement-import-create__upload"> <div class="settlement-import-create__upload">
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
@@ -143,13 +143,13 @@
accept=".xls,.xlsx" accept=".xls,.xlsx"
:auto-upload="false" :auto-upload="false"
:limit="1" :limit="1"
:disabled="readonly || !formReady" :disabled="!formReady"
:file-list="files" :file-list="files"
:on-change="onFileChange" :on-change="onFileChange"
:on-exceed="onFileExceed" :on-exceed="onFileExceed"
:on-remove="onFileRemove" :on-remove="onFileRemove"
> >
<el-button type="primary" :disabled="readonly || !formReady">上传 Excel</el-button> <el-button type="primary" :disabled="!formReady">上传 Excel</el-button>
</el-upload> </el-upload>
<el-link type="primary" :underline="false" @click="downloadTemplate"> <el-link type="primary" :underline="false" @click="downloadTemplate">
下载模板 下载模板
@@ -298,15 +298,10 @@
</el-table> </el-table>
</section> </section>
<div class="settlement-import-create__footer"> <div v-if="!readonly" class="settlement-import-create__footer">
<el-button @click="goBack">取消</el-button> <el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" :loading="draftLoading" @click="saveDraft">保存草稿</el-button> <el-button :loading="draftLoading" @click="saveDraft">保存草稿</el-button>
<el-button <el-button type="primary" :loading="confirmLoading" @click="confirmImport">
v-if="!readonly"
type="primary"
:loading="confirmLoading"
@click="confirmImport"
>
确认导入 确认导入
</el-button> </el-button>
</div> </div>
@@ -577,6 +572,8 @@ const syncEditedText = (row, column, value) => {
if (column.prop !== 'vehicleNo') return; if (column.prop !== 'vehicleNo') return;
applyVehicleNo(row, value); applyVehicleNo(row, value);
}; };
const feeColumn = name => ({
prop: name, prop: name,
label: name, label: name,
editor: 'number', editor: 'number',
@@ -1411,12 +1408,18 @@ const loadDetail = async id => {
const detailRows = Array.isArray(detail.rows) ? detail.rows : []; const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index)); rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
refreshDuplicateFlags(); refreshDuplicateFlags();
readonly.value = detail.importStatus && detail.importStatus !== 'draft'; // 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
readonly.value =
route.query.mode === 'view' ||
Boolean(detail.importStatus && detail.importStatus !== 'draft');
}; };
const initPage = async () => { const initPage = async () => {
await loadOptions(); await loadOptions();
const id = route.query.id; const id = route.query.id;
if (route.query.mode === 'view') {
readonly.value = true;
}
if (id) { if (id) {
await loadDetail(id); await loadDetail(id);
return; return;
@@ -450,32 +450,48 @@
class="settlement-detail-page__dialog-form" class="settlement-detail-page__dialog-form"
> >
<el-form-item label="更新范围" prop="contractId"> <el-form-item label="更新范围" prop="contractId">
<el-input <el-tooltip
:model-value="updateContractName" :content="updateContractName"
class="settlement-detail-page__generate-select" :disabled="!updateContractName"
readonly placement="top"
placeholder="请选择合同" :show-after="200"
> >
<template #append> <el-input
<el-button @click="openUpdateContractDialog">选择</el-button> :model-value="updateContractName"
</template> class="settlement-detail-page__update-fee-control"
</el-input> readonly
placeholder="请选择合同"
>
<template #append>
<el-button @click="openUpdateContractDialog">选择</el-button>
</template>
</el-input>
</el-tooltip>
</el-form-item> </el-form-item>
<el-form-item label="合同计费方案" prop="billingPlanId"> <el-form-item label="合同计费方案" prop="billingPlanId">
<el-select <el-tooltip
v-model="updateFeeForm.billingPlanId" :content="updateBillingPlanName"
clearable :disabled="!updateBillingPlanName"
filterable placement="top"
:disabled="!updateFeeForm.contractId" :show-after="200"
placeholder="请选择"
> >
<el-option <el-select
v-for="item in billingPlanOptions" v-model="updateFeeForm.billingPlanId"
:key="item.id" class="settlement-detail-page__update-fee-control"
:label="item.name" clearable
:value="item.id" filterable
/> :disabled="!updateFeeForm.contractId"
</el-select> placeholder="请选择"
>
<el-option
v-for="item in billingPlanOptions"
:key="item.id"
:label="item.name"
:value="item.id"
:title="item.name"
/>
</el-select>
</el-tooltip>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -1203,6 +1219,14 @@ export default {
); );
return formatContractOptionLabel(contract); return formatContractOptionLabel(contract);
}, },
updateBillingPlanName() {
const billingPlanId = this.updateFeeForm.billingPlanId;
if (!billingPlanId) return '';
const plan = this.billingPlanOptions.find(
item => String(item.id) === String(billingPlanId)
);
return plan?.name || '';
},
generateContractCategory() { generateContractCategory() {
return this.settlementType === 'payable' ? '承运商合同' : '客户合同'; return this.settlementType === 'payable' ? '承运商合同' : '客户合同';
}, },
@@ -3144,11 +3168,24 @@ export default {
} }
} }
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-tooltip__trigger) {
display: block;
width: 100%;
}
:deep(.el-select) { :deep(.el-select) {
width: 100%; width: 100%;
} }
} }
.settlement-detail-page__update-fee-control {
width: 100%;
}
.settlement-detail-page__adjust-input { .settlement-detail-page__adjust-input {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
@@ -11,7 +11,10 @@ import { useRoute } from 'vue-router';
import SettlementImportPage from './components/settlement-import-page.vue'; import SettlementImportPage from './components/settlement-import-page.vue';
const route = useRoute(); const route = useRoute();
const pageTitle = computed(() => (route.query.id ? '编辑导入结算单' : '新增导入结算单')); const pageTitle = computed(() => {
if (route.query.mode === 'view') return '查看导入结算单';
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+5 -226
View File
@@ -129,97 +129,6 @@
</div> </div>
</section> </section>
<el-dialog
v-model="detailVisible"
title="导入结算单详情"
width="1100px"
append-to-body
destroy-on-close
class="settlement-import-detail-dialog"
>
<el-descriptions :column="3" border>
<el-descriptions-item label="导入批次号">{{ detail.batchNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
{{ detail.importStatusName || importStatusLabel(detail.importStatus) }}
</el-descriptions-item>
<el-descriptions-item label="导入类型">
{{ detail.settlementTypeName || settlementTypeLabel(detail.settlementType) }}
</el-descriptions-item>
<el-descriptions-item label="结算单">
{{ detail.settlementBillTypeName || settlementBillTypeLabel(detail.settlementBillType) }}
</el-descriptions-item>
<el-descriptions-item label="运单类型">
{{ detail.waybillModeName || waybillModeLabel(detail.waybillMode) }}
</el-descriptions-item>
<el-descriptions-item label="明细行数">
{{ detail.detailCount ?? detail.waybillCount ?? detailRows.length }}
</el-descriptions-item>
<el-descriptions-item label="项目">{{ detail.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ detail.customerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户合同">
{{ detail.customerContractName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="承运商">{{ detail.carrierName || '-' }}</el-descriptions-item>
<el-descriptions-item label="承运商合同">
{{ detail.carrierContractName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detail.createUserName || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
<el-descriptions-item label="关联结算单" :span="3">
<el-link
v-if="detail.formalSettlementId"
type="primary"
@click="goFormalSettlement(detail)"
>
{{ detail.formalSettlementNo || detail.formalSettlementId }}
</el-link>
<el-link
v-else-if="detail.preSettlementId"
type="primary"
@click="goPreSettlement(detail)"
>
{{ detail.preSettlementNo || detail.preSettlementId }}
</el-link>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<div class="dialog-section-title settlement-import-page__detail-title">导入明细</div>
<el-table
v-loading="detailLoading"
:data="detailRows"
border
max-height="420"
class="settlement-import-page__table"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in detailDisplayColumns"
:key="column.prop"
:prop="column.fee ? undefined : column.prop"
:label="column.label"
:min-width="column.minWidth || 120"
show-overflow-tooltip
>
<template #default="{ row }">
{{ column.fee ? formatFeeAmount(row, column.prop) : formatDetailCell(row, column) }}
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
v-if="detail.importStatus === 'draft'"
type="primary"
@click="editDraft(detail)"
>
编辑草稿
</el-button>
</template>
</el-dialog>
<el-dialog v-model="voidVisible" title="作废导入结算单" width="480px" append-to-body> <el-dialog v-model="voidVisible" title="作废导入结算单" width="480px" append-to-body>
<el-form <el-form
ref="voidFormRef" ref="voidFormRef"
@@ -289,11 +198,6 @@ export default {
rows: [], rows: [],
page: { current: 1, size: 10, total: 0 }, page: { current: 1, size: 10, total: 0 },
batchNoOptions: [], batchNoOptions: [],
detailVisible: false,
detailLoading: false,
detail: {},
detailRows: [],
detailFeeItemNames: [],
voidVisible: false, voidVisible: false,
voidLoading: false, voidLoading: false,
voidForm: { id: null, reason: '' }, voidForm: { id: null, reason: '' },
@@ -339,52 +243,6 @@ export default {
visibleSearchFields() { visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4); return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
}, },
detailDisplayColumns() {
const before = [
{ prop: 'originalNo', label: '原始单号', minWidth: 130 },
{ prop: 'loadingIdentifier', label: '配载标识号', minWidth: 140 },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', minWidth: 190 },
{ prop: 'transportType', label: '运输方式', minWidth: 120 },
{ prop: 'driverName', label: '司机/船长姓名', minWidth: 130 },
{ prop: 'driverPhone', label: '司机/船长手机号', minWidth: 150 },
{ prop: 'departureAddress', label: '发货地址', minWidth: 160 },
{ prop: 'departureContact', label: '发货联系人', minWidth: 120 },
{ prop: 'departurePhone', label: '发货联系人电话', minWidth: 140 },
{ prop: 'arrivalAddress', label: '到货地址', minWidth: 160 },
{ prop: 'arrivalContact', label: '收货联系人', minWidth: 120 },
{ prop: 'arrivalPhone', label: '收货联系人电话', minWidth: 140 },
{ prop: 'cargoName', label: '货物名称', minWidth: 140 },
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
{ prop: 'packageType', label: '包装', minWidth: 100 },
{ prop: 'quantity', label: '数量', minWidth: 90 },
{ prop: 'quantityUnit', label: '数量单位', minWidth: 100 },
{ prop: 'specification', label: '规格', minWidth: 110 },
{ prop: 'model', label: '型号', minWidth: 110 },
{ prop: 'mileage', label: '里程(km)', minWidth: 110 },
{ prop: 'unitPrice', label: '运输单价', minWidth: 110 },
{ prop: 'transportFee', label: '运输费', minWidth: 110, money: true },
];
const fees = (this.detailFeeItemNames || []).map(name => ({
prop: name,
label: name,
minWidth: 110,
fee: true,
}));
const after = [
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
{ prop: 'actualStartDate', label: '实际发货时间', minWidth: 140 },
{ prop: 'actualEndDate', label: '实际完成时间', minWidth: 140 },
{ prop: 'planStartDate', label: '预计发货时间', minWidth: 140 },
{ prop: 'planEndDate', label: '预计完成时间', minWidth: 140 },
{ prop: 'remark', label: '备注', minWidth: 140 },
{ prop: 'waybillIdentifier', label: '同一运单标识号', minWidth: 150 },
];
const history =
this.detail.waybillMode === 'history'
? [{ prop: 'waybillNo', label: '运单号', minWidth: 140 }]
: [];
return [...history, ...before, ...fees, ...after];
},
}, },
created() { created() {
this.loadOptions(); this.loadOptions();
@@ -436,28 +294,10 @@ export default {
if (value === 'pre') return '预结算单'; if (value === 'pre') return '预结算单';
return '正式结算单'; return '正式结算单';
}, },
waybillModeLabel(value) {
if (value === 'history') return '历史运单';
return '新运单';
},
unwrapData(response) { unwrapData(response) {
const body = response?.data || response || {}; const body = response?.data || response || {};
return body?.data !== undefined ? body.data : body; return body?.data !== undefined ? body.data : body;
}, },
formatDetailCell(row, column) {
const value = row?.[column.prop];
if (value === null || value === undefined || value === '') return '';
if (!column.money) return value;
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : value;
},
formatFeeAmount(row, name) {
const feeItems = row?.feeItems || {};
const value = feeItems[name] ?? row?.[name];
if (value === null || value === undefined || value === '') return '';
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : value;
},
async loadOptions() { async loadOptions() {
try { try {
const response = await api.getOptions(); const response = await api.getOptions();
@@ -518,28 +358,14 @@ export default {
openCreate() { openCreate() {
this.$router.push({ path: '/settlement/settlement-import/form' }); this.$router.push({ path: '/settlement/settlement-import/form' });
}, },
async openDetail(row) { openDetail(row) {
if (!row?.id) return; if (!row?.id) return;
this.detailVisible = true; this.$router.push({
this.detailLoading = true; path: '/settlement/settlement-import/form',
this.detail = { ...row }; query: { id: row.id, mode: 'view' },
this.detailRows = []; });
this.detailFeeItemNames = [];
try {
const response = await api.getDetail(row.id);
const detail = this.unwrapData(response) || { ...row };
this.detail = detail;
this.detailRows = Array.isArray(detail.rows) ? detail.rows : [];
this.detailFeeItemNames = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
} catch (error) {
this.detail = { ...row };
this.$message.error(error?.message || '加载详情失败');
} finally {
this.detailLoading = false;
}
}, },
editDraft(row) { editDraft(row) {
this.detailVisible = false;
this.$router.push({ this.$router.push({
path: '/settlement/settlement-import/form', path: '/settlement/settlement-import/form',
query: { id: row.id }, query: { id: row.id },
@@ -560,30 +386,6 @@ export default {
this.$message.error(error?.message || '删除失败'); this.$message.error(error?.message || '删除失败');
} }
}, },
goFormalSettlement(row) {
if (!row?.formalSettlementId) return;
this.detailVisible = false;
this.$router.push({
path: '/settlement/formal-settlement/form',
query: {
mode: 'view',
id: row.formalSettlementId,
name: '正式结算详情',
},
});
},
goPreSettlement(row) {
if (!row?.preSettlementId) return;
this.detailVisible = false;
this.$router.push({
path: '/settlement/pre-settlement/form',
query: {
mode: 'view',
id: row.preSettlementId,
name: '预结算详情',
},
});
},
openVoid(row) { openVoid(row) {
this.voidForm = { id: row.id, reason: '' }; this.voidForm = { id: row.id, reason: '' };
this.voidVisible = true; this.voidVisible = true;
@@ -686,29 +488,6 @@ export default {
padding: 12px 0; padding: 12px 0;
} }
.settlement-import-page__detail-title {
margin: 16px 0 8px;
}
.dialog-section-title {
position: relative;
padding-left: 12px;
color: #303133;
font-size: 16px;
font-weight: 600;
&::before {
position: absolute;
top: 2px;
left: 0;
width: 4px;
height: 16px;
border-radius: 2px;
background: #409eff;
content: '';
}
}
.settlement-import-page :deep(.basic-container__card > .el-card__body) { .settlement-import-page :deep(.basic-container__card > .el-card__body) {
padding: 0; padding: 0;
} }
+1
View File
@@ -172,6 +172,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="OA登录名"><el-input v-model="form.oaLoginId" :disabled="isUserViewMode" maxlength="128" placeholder="请输入OA登录名" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col> <el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col> <el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col> <el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>