Merge remote-tracking branch 'origin/master'
# Conflicts: # src/views/settlement/receivable-payable-detail.vue
This commit is contained in:
@@ -24,6 +24,9 @@ export const getUpdateFeeContracts = params =>
|
||||
export const adjustFee = data =>
|
||||
request({ url: `${baseUrl}/adjust-fee`, method: 'post', data });
|
||||
|
||||
export const calculateAdjustedFee = data =>
|
||||
request({ url: `${baseUrl}/calculate-adjusted-fee`, method: 'post', data });
|
||||
|
||||
export const transferSettlement = data =>
|
||||
request({ url: `${baseUrl}/transfer-settlement`, method: 'post', data });
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const tableColumns = [
|
||||
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 140, link: true },
|
||||
{ label: '正式结算单号', prop: 'formalSettlementNo', minWidth: 140, link: true },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 140 },
|
||||
{ label: '运单号', prop: 'waybillNo', minWidth: 140, link: true },
|
||||
{ label: '车号', prop: 'vehicleNo', minWidth: 120 },
|
||||
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||
{ label: '货物名称', prop: 'cargoName', minWidth: 130 },
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-row :gutter="48">
|
||||
<el-col :span="12"><el-form-item label="起运地"><el-cascader v-model="matchForm.originPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.origin || '请选择起运地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('origin', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="目的地"><el-cascader v-model="matchForm.destinationPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.destination || '请选择目的地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('destination', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="运输方式"><el-select v-model="matchForm.transportMode" :disabled="readonly" clearable><el-option label="公路" value="公路" /><el-option label="铁路" value="铁路" /><el-option label="水路" value="水路" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="运输方式"><el-select v-model="matchForm.transportMode" :loading="transportModeLoading" :disabled="readonly" placeholder="请选择运输方式" clearable filterable><el-option v-for="item in transportModeOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="货物类型"><el-cascader v-model="matchForm.cargoTypePath" :options="cargoOptions" :props="cargoProps" :placeholder="matchForm.cargoType || '请选择货物类型'" :loading="cargoLoading" :disabled="readonly" clearable filterable :filter-method="filterCargo" @visible-change="v => v && ensureCargo()" @change="matchCargoChange" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -50,7 +50,7 @@ const defaultRule = () => ({ feeType: '', feeItem: '', billingElement: '', billi
|
||||
export default {
|
||||
props: { modelValue: Boolean, value: { type: Object, default: () => ({}) }, index: { type: Number, default: -1 }, readonly: Boolean },
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() { return { draft: this.normalizePlan(this.value), feeCategories: [], feeCategoryLoading: false, feeItems: {}, feeItemLoadingMap: {}, unitOptions: [], unitLoading: false, billingElements: ['按重量', '按体积', '按车辆', '按里程', '按吨·公里', '固定金额(整单一口价)', '按数量'], typeMap: { 按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按车辆: ['固定单价'], 按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '固定金额(整单一口价)': ['固定一口价'], 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'] }, matchVisible: false, matchIndex: -1, matchForm: defaultRule().matchCondition, regionOptions: [], regionLoading: false, regionRequest: null, cargoOptions: [], cargoFlatOptions: [], cargoLoading: false, cargoRequest: null, formRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] } }; },
|
||||
data() { return { draft: this.normalizePlan(this.value), feeCategories: [], feeCategoryLoading: false, feeItems: {}, feeItemLoadingMap: {}, unitOptions: [], unitLoading: false, billingElements: ['按重量', '按体积', '按车辆', '按里程', '按吨·公里', '固定金额(整单一口价)', '按数量'], typeMap: { 按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按车辆: ['固定单价'], 按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '固定金额(整单一口价)': ['固定一口价'], 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'] }, matchVisible: false, matchIndex: -1, matchForm: defaultRule().matchCondition, transportModeOptions: [], transportModeLoading: false, transportModeRequest: null, regionOptions: [], regionLoading: false, regionRequest: null, cargoOptions: [], cargoFlatOptions: [], cargoLoading: false, cargoRequest: null, formRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] } }; },
|
||||
computed: { visible: { get() { return this.modelValue; }, set(value) { this.$emit('update:modelValue', value); } }, regionProps() { return { label: 'title', value: 'id', children: 'children', leaf: 'leaf', emitPath: true }; }, cargoProps() { return { label: 'cargoName', value: 'id', children: 'children', disabled: (data, node) => node.level === 1, leaf: 'leaf', checkStrictly: true, emitPath: true }; } },
|
||||
mounted() { this.loadDictionaries(); },
|
||||
watch: { value: { deep: true, handler(value) { if (this.modelValue) { this.draft = this.normalizePlan(value); this.loadRuleItems(); } } } },
|
||||
@@ -86,7 +86,9 @@ export default {
|
||||
validateRules() { const groups = {}; const feeItemSet = new Set(); const required = [['feeType', '费用类型'], ['feeItem', '费用项'], ['billingElement', '计费要素'], ['billingType', '计费类型'], ['billingUnit', '计费单位']]; for (const [i, row] of this.draft.rules.entries()) { const empty = required.find(([key]) => row[key] === undefined || row[key] === null || String(row[key]).trim() === ''); if (empty) { this.$message.warning(`第${i + 1}行${empty[1]}不能为空`); return false; } const feeItem = String(row.feeItem).trim(); if (feeItemSet.has(feeItem)) { this.$message.warning(`费用项“${feeItem}”不能重复`); return false; } feeItemSet.add(feeItem); if (!this.usesRangeUnitPrice(row) && (row.unitPrice === undefined || row.unitPrice === null || String(row.unitPrice).trim() === '')) { this.$message.warning(`第${i + 1}行单价不能为空`); return false; } if (!this.billingTypes(row).includes(row.billingType)) { this.$message.warning('请选择计费要素对应的计费类型'); return false; } if (!this.canEditLimit(row)) continue; const key = row.billingElement; groups[key] = groups[key] || []; for (const range of this.getRanges(row)) { const lower = Number(range.lowerLimit); const upper = Number(range.upperLimit); if (range.lowerLimit === '' || range.upperLimit === '' || Number.isNaN(lower) || Number.isNaN(upper)) { this.$message.warning('请完整填写计费要素上下限'); return false; } if (this.usesRangeUnitPrice(row) && (range.unitPrice === undefined || range.unitPrice === null || String(range.unitPrice).trim() === '')) { this.$message.warning('请为每组计费要素区间填写单价'); return false; } if (lower > upper) { this.$message.warning('计费要素下限不能大于上限'); return false; } groups[key].push({ lower, upper }); } } return this.validateRangeGroups(groups); },
|
||||
validateRangeGroups(groups) { const precision = 0.000001; for (const [element, ranges] of Object.entries(groups)) { const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper); for (let i = 1; i < sorted.length; i += 1) { if (sorted[i].lower < sorted[i - 1].upper - precision) { this.$message.warning(`${element}的计费要素区间不能重叠`); return false; } if (sorted[i].lower > sorted[i - 1].upper + precision) { this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`); return false; } } } return true; },
|
||||
save() { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { ...rule, unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, limitRanges: ranges, lowerLimit: ranges[0]?.lowerLimit || '', upperLimit: ranges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? (this.usesRangeMinimum(rule) ? ranges[0]?.minimumBillingWeight || '' : rule.minimumBillingWeight) : '' }; }); this.$emit('save', plan, this.index); this.visible = false; }); },
|
||||
openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); if (path.length) this.matchCargoChange(path); }); },
|
||||
openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; this.ensureTransportModes().then(() => { this.matchForm.transportMode = this.normalizeTransportMode(this.matchForm.transportMode); }); this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); if (path.length) this.matchCargoChange(path); }); },
|
||||
ensureTransportModes() { if (this.transportModeOptions.length) return Promise.resolve(this.transportModeOptions); if (this.transportModeRequest) return this.transportModeRequest; this.transportModeLoading = true; this.transportModeRequest = getDictionary({ code: 'transport_type' }).then(res => { this.transportModeOptions = (res.data?.data || []).map(item => ({ label: item.dictValue || item.label || item.value || '', value: item.dictKey || item.value || item.dictValue || '' })); return this.transportModeOptions; }).finally(() => { this.transportModeLoading = false; this.transportModeRequest = null; }); return this.transportModeRequest; },
|
||||
normalizeTransportMode(value) { const transportMode = String(value || '').trim(); if (!transportMode) return ''; const option = this.transportModeOptions.find(item => { const optionValue = String(item.value || '').trim(); const optionLabel = String(item.label || '').trim(); return optionValue === transportMode || optionLabel === transportMode || optionLabel.includes(transportMode) || transportMode.includes(optionLabel); }); return option?.value || transportMode; },
|
||||
ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; this.regionLoading = true; this.regionRequest = getRegionLazyTree().then(res => { const raw = res.data?.data || []; this.regionOptions = this.normalizeRegionTree(raw); return this.regionOptions; }).finally(() => { this.regionLoading = false; this.regionRequest = null; }); return this.regionRequest; },
|
||||
normalizeRegionTree(list) { const map = new Map(); const flat = []; const walk = items => (items || []).forEach(item => { flat.push({ ...item, children: undefined }); if (item.children?.length) walk(item.children); }); walk(list); flat.forEach(item => map.set(String(item.id ?? item.code ?? item.value), { ...item, id: String(item.id ?? item.code ?? item.value), parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] })); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node) parent.children.push(node); else roots.push(node); }); const china = roots.find(item => ['中国', '中华人民共和国'].includes(item.title || item.name)); const source = china?.children?.length ? china.children : roots; const normalize = (node, level) => ({ ...node, title: node.title || node.name || '', leaf: level === 2, children: level === 1 ? (node.children || []).map(child => normalize(child, 2)) : undefined }); return source.map(node => normalize(node, 1)); },
|
||||
regionLabels(path) { let options = this.regionOptions; const labels = []; (path || []).forEach(value => { const option = options.find(item => String(item.id) === String(value)); if (option) { labels.push(option.title || option.name || ''); options = option.children || []; } }); return labels; },
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<el-link
|
||||
v-if="column.link && row[column.prop]"
|
||||
type="primary"
|
||||
@click="openDetailDialog(row)"
|
||||
@click="handleTableLink(row, column)"
|
||||
>
|
||||
{{ row[column.prop] }}
|
||||
</el-link>
|
||||
@@ -103,191 +103,166 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailDialog.visible"
|
||||
:title="detailDialog.title"
|
||||
width="92%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="settlement-detail-dialog"
|
||||
>
|
||||
<el-tabs type="border-card" style="background-color: #ffffff" v-model="detailDialog.activeTab" @tab-change="handleDetailTabChange">
|
||||
<el-tab-pane label="费用明细" name="fee">
|
||||
<el-table v-loading="detailDialog.loading" :data="feeRows" border>
|
||||
<teleport to="body">
|
||||
<section
|
||||
v-if="detailDialog.visible && !adjustDialog.visible"
|
||||
class="settlement-detail-page__detail-panel"
|
||||
aria-label="单据详情"
|
||||
>
|
||||
<header class="settlement-detail-page__detail-panel-header">
|
||||
<h3>单据详情:{{ detailDialog.title }}</h3>
|
||||
<el-tooltip content="关闭" placement="top">
|
||||
<el-button
|
||||
:icon="Close"
|
||||
text
|
||||
aria-label="关闭单据详情"
|
||||
@click="closeDetailPanel"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</header>
|
||||
<div class="settlement-detail-page__detail-panel-body">
|
||||
<el-tabs v-model="detailDialog.activeTab" @tab-change="handleDetailTabChange">
|
||||
<el-tab-pane label="费用明细" name="fee">
|
||||
<el-table v-loading="detailDialog.loading" :data="feeRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in feeDetailColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="变更记录" name="change">
|
||||
<el-table v-loading="changeDialog.loading" :data="changeRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in changeRecordColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
<div class="settlement-detail-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="changePage.current"
|
||||
v-model:page-size="changePage.size"
|
||||
:total="changePage.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadChangeRecords"
|
||||
@size-change="handleChangeSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<footer class="settlement-detail-page__detail-panel-footer">
|
||||
<el-button @click="closeDetailPanel">关闭</el-button>
|
||||
</footer>
|
||||
</section>
|
||||
</teleport>
|
||||
|
||||
<teleport to="body">
|
||||
<section
|
||||
v-if="adjustDialog.visible"
|
||||
class="settlement-detail-page__detail-panel"
|
||||
aria-label="调整费用"
|
||||
>
|
||||
<header class="settlement-detail-page__detail-panel-header">
|
||||
<h3>调整费用:{{ adjustDialog.row?.documentNo || '-' }}</h3>
|
||||
<el-tooltip content="关闭" placement="top">
|
||||
<el-button
|
||||
:icon="Close"
|
||||
text
|
||||
aria-label="关闭调整费用"
|
||||
@click="closeAdjustPanel"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</header>
|
||||
<div
|
||||
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body"
|
||||
>
|
||||
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in feeDetailColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
v-for="column in adjustFeeColumns"
|
||||
:key="column.prop || column.feeItemName"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ formatDetailCell(row, column.prop) }}</template>
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="column.prop === 'transportQuantityText' && !row.manualFee"
|
||||
class="settlement-detail-page__adjust-input"
|
||||
:model-value="row.transportQuantity"
|
||||
inputmode="decimal"
|
||||
@input="value => handleAdjustDecimalInput(row, 'transportQuantity', value)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'mileage' && !row.manualFee"
|
||||
class="settlement-detail-page__adjust-input"
|
||||
:model-value="row.mileage"
|
||||
inputmode="decimal"
|
||||
@input="value => handleAdjustDecimalInput(row, 'mileage', value)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'freightAmount' && !row.manualFee"
|
||||
class="settlement-detail-page__adjust-input"
|
||||
:model-value="row.freightAmount"
|
||||
inputmode="decimal"
|
||||
@input="value => handleAdjustDecimalInput(row, 'freightAmount', value, 'freight')"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.dynamic && !row.manualFee"
|
||||
class="settlement-detail-page__adjust-input"
|
||||
:model-value="row.feeItems[column.feeItemName]"
|
||||
inputmode="decimal"
|
||||
@input="value => handleAdjustFeeItemInput(row, column.feeItemName, value)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'remark'"
|
||||
v-model="row.remark"
|
||||
clearable
|
||||
maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'changeReason'"
|
||||
v-model="row.changeReason"
|
||||
clearable
|
||||
maxlength="300"
|
||||
placeholder="请输入变更原因"
|
||||
/>
|
||||
<span v-else-if="column.prop === 'adjustAmountText'">
|
||||
{{ fixedTwoDecimals(row.adjustAmount) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'afterAmountText'">
|
||||
{{ fixedTwoDecimals(row.afterAmount) }}
|
||||
</span>
|
||||
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="变更记录" name="change">
|
||||
<el-table v-loading="changeDialog.loading" :data="changeRows" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
v-for="column in changeRecordColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
<div class="settlement-detail-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="changePage.current"
|
||||
v-model:page-size="changePage.size"
|
||||
:total="changePage.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadChangeRecords"
|
||||
@size-change="handleChangeSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="adjustDialog.visible"
|
||||
title="调整费用"
|
||||
width="96%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="settlement-detail-page__adjust-toolbar">
|
||||
<el-button type="primary" plain @click="addManualAdjustRow">新增费用项</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="adjustDialog.loading"
|
||||
:data="adjustRows"
|
||||
border
|
||||
class="settlement-detail-page__adjust-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" header-align="center" />
|
||||
<el-table-column label="手工费用项目" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeItemName"
|
||||
clearable
|
||||
placeholder="请输入收费或扣费项目"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收费/扣费" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="row.manualFee"
|
||||
v-model="row.feeType"
|
||||
placeholder="请选择"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
>
|
||||
<el-option label="收费" value="charge" />
|
||||
<el-option label="扣费" value="deduct" />
|
||||
</el-select>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手工金额" width="150" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="row.manualFee"
|
||||
v-model="row.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateManualAdjustRow(row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.manualFee && !row.id"
|
||||
type="danger"
|
||||
@click="removeManualAdjustRow(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="column in adjustFeeColumns"
|
||||
:key="column.prop || column.feeItemName"
|
||||
:label="column.label"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="column.prop === 'transportQuantityText' && !row.manualFee"
|
||||
v-model="row.transportQuantity"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'mileage' && !row.manualFee"
|
||||
v-model="row.mileage"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row)"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.prop === 'freightAmount' && !row.manualFee"
|
||||
v-model="row.freightAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, 'freight')"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="column.dynamic && !row.manualFee"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@change="recalculateAdjustRow(row, column.feeItemName)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="column.prop === 'remark'"
|
||||
v-model="row.remark"
|
||||
clearable
|
||||
maxlength="200"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<span v-else-if="column.prop === 'adjustAmountText'">
|
||||
{{ fixedTwoDecimals(row.adjustAmount) }}
|
||||
</span>
|
||||
<span v-else-if="column.prop === 'afterAmountText'">
|
||||
{{ fixedTwoDecimals(row.afterAmount) }}
|
||||
</span>
|
||||
<span v-else>{{ formatDetailCell(row, column.prop) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="adjustDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<footer class="settlement-detail-page__detail-panel-footer">
|
||||
<el-button @click="closeAdjustPanel">取消</el-button>
|
||||
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee">
|
||||
保存
|
||||
</el-button>
|
||||
</footer>
|
||||
</section>
|
||||
</teleport>
|
||||
|
||||
<el-dialog v-model="updateFeeDialog.visible" title="更新费用" width="620px" append-to-body>
|
||||
<el-form
|
||||
@@ -414,8 +389,10 @@
|
||||
<el-form-item label="运单合同" required>
|
||||
<el-select
|
||||
v-model="generateQuery.contractId"
|
||||
class="settlement-detail-page__generate-select"
|
||||
filterable
|
||||
clearable
|
||||
:loading="contractLoading"
|
||||
placeholder="请选择合同"
|
||||
@change="handleGenerateContractChange"
|
||||
>
|
||||
@@ -428,7 +405,14 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="计费方案" required>
|
||||
<el-select v-model="generateQuery.billingPlanId" filterable clearable placeholder="请选择">
|
||||
<el-select
|
||||
v-model="generateQuery.billingPlanId"
|
||||
class="settlement-detail-page__generate-select"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="!generateQuery.contractId"
|
||||
:placeholder="generateQuery.contractId ? '请选择' : '请先选择合同'"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in billingPlanOptions"
|
||||
:key="item.id"
|
||||
@@ -493,7 +477,13 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="generateDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="previewDialog.loading"
|
||||
@click="openGeneratePreview"
|
||||
>
|
||||
生成费用
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -533,7 +523,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArrowDown, ArrowUp, Refresh, Setting } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, ArrowUp, Close, Refresh, Setting } from '@element-plus/icons-vue';
|
||||
import {
|
||||
changeRecordColumns,
|
||||
feeDetailBaseColumns,
|
||||
@@ -562,6 +552,7 @@ export default {
|
||||
return {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Close,
|
||||
Refresh,
|
||||
Setting,
|
||||
searchFields,
|
||||
@@ -582,6 +573,7 @@ export default {
|
||||
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
|
||||
adjustRows: [],
|
||||
adjustDynamicFeeColumns: [],
|
||||
adjustCalculateTimers: {},
|
||||
changeRows: [],
|
||||
changePage: { current: 1, size: 10, total: 0 },
|
||||
changeDialog: { loading: false },
|
||||
@@ -591,6 +583,7 @@ export default {
|
||||
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
|
||||
},
|
||||
contractOptions: [],
|
||||
contractLoading: false,
|
||||
updateContractOptions: [],
|
||||
transportTypeOptions: [],
|
||||
billingPlanOptions: [],
|
||||
@@ -644,12 +637,12 @@ export default {
|
||||
...feeDetailBaseColumns,
|
||||
...this.adjustDynamicFeeColumns,
|
||||
...feeDetailTailColumns,
|
||||
{ label: '变更原因', prop: 'changeReason', minWidth: 220 },
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadContracts();
|
||||
this.loadTable();
|
||||
},
|
||||
methods: {
|
||||
@@ -669,6 +662,7 @@ export default {
|
||||
},
|
||||
formatColumnValue(row, column) {
|
||||
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
|
||||
if (column.prop === 'transportQuantity') return this.fixedTwoDecimals(row[column.prop]);
|
||||
if (column.dynamic) {
|
||||
return this.money(this.normalizeFeeItems(row.feeItems)[column.feeItemName], row.currency || 'RMB');
|
||||
}
|
||||
@@ -693,12 +687,17 @@ export default {
|
||||
async loadContracts() {
|
||||
const contractCategory =
|
||||
this.settlementType === 'payable' ? '承运商合同' : '客户合同';
|
||||
const res = await getContractList(1, 999, {
|
||||
approvalStatus: 'approved',
|
||||
contractCategory,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.contractOptions = data.records || [];
|
||||
this.contractLoading = true;
|
||||
try {
|
||||
const res = await getContractList(1, 999, {
|
||||
approvalStatuses: 'approved,change_approved',
|
||||
contractCategory,
|
||||
});
|
||||
const data = this.unwrapPage(res);
|
||||
this.contractOptions = data.records || [];
|
||||
} finally {
|
||||
this.contractLoading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.page.current = 1;
|
||||
@@ -718,11 +717,40 @@ export default {
|
||||
selectionChange(selection) {
|
||||
this.selection = selection;
|
||||
},
|
||||
closeDetailPanel() {
|
||||
this.detailDialog.visible = false;
|
||||
this.detailDialog.row = null;
|
||||
},
|
||||
closeAdjustPanel() {
|
||||
Object.values(this.adjustCalculateTimers).forEach(timer => clearTimeout(timer));
|
||||
this.adjustCalculateTimers = {};
|
||||
this.adjustDialog.visible = false;
|
||||
this.adjustDialog.row = null;
|
||||
},
|
||||
handleTableLink(row, column) {
|
||||
if (column.prop === 'waybillNo') {
|
||||
this.openWaybillDetail(row);
|
||||
return;
|
||||
}
|
||||
this.openDetailDialog(row);
|
||||
},
|
||||
openWaybillDetail(row) {
|
||||
if (!row.waybillId) {
|
||||
this.$message.info('当前记录未关联运单详情');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/business/waybill-manage',
|
||||
query: { detailId: row.waybillId },
|
||||
});
|
||||
},
|
||||
async openDetailDialog(row) {
|
||||
this.closeAdjustPanel();
|
||||
this.detailDialog = { ...this.detailDialog, visible: true, row, title: row.documentNo, activeTab: 'fee' };
|
||||
await this.loadFeeDetail();
|
||||
},
|
||||
async openAdjustDialog(row) {
|
||||
this.closeDetailPanel();
|
||||
this.adjustDialog = { ...this.adjustDialog, visible: true, row, loading: true };
|
||||
try {
|
||||
const res = await api.getFeeDetail(row.id);
|
||||
@@ -766,25 +794,6 @@ export default {
|
||||
this.adjustDialog.loading = false;
|
||||
}
|
||||
},
|
||||
addManualAdjustRow() {
|
||||
this.adjustRows.push({
|
||||
id: null,
|
||||
manualFee: true,
|
||||
feeItemName: '',
|
||||
feeType: 'charge',
|
||||
amount: 0,
|
||||
transportQuantity: null,
|
||||
mileage: null,
|
||||
freightAmount: 0,
|
||||
originalAmount: 0,
|
||||
feeItems: {},
|
||||
remark: '',
|
||||
});
|
||||
},
|
||||
removeManualAdjustRow(row) {
|
||||
const index = this.adjustRows.indexOf(row);
|
||||
if (index >= 0) this.adjustRows.splice(index, 1);
|
||||
},
|
||||
recalculateManualAdjustRow(row) {
|
||||
const amount = Number(row.amount || 0);
|
||||
row.afterAmount = Number((row.feeType === 'deduct' ? -amount : amount).toFixed(2));
|
||||
@@ -793,6 +802,61 @@ export default {
|
||||
fixedTwoDecimals(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
normalizeAdjustDecimal(value) {
|
||||
const text = String(value ?? '').replace(/[^\d.]/g, '');
|
||||
const decimalIndex = text.indexOf('.');
|
||||
if (decimalIndex < 0) return text;
|
||||
const integer = text.slice(0, decimalIndex).replace(/\D/g, '') || '0';
|
||||
const decimal = text
|
||||
.slice(decimalIndex + 1)
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 2);
|
||||
return `${integer}.${decimal}`;
|
||||
},
|
||||
handleAdjustDecimalInput(row, prop, value, changedField) {
|
||||
row[prop] = this.normalizeAdjustDecimal(value);
|
||||
if (['transportQuantity', 'mileage'].includes(prop)) {
|
||||
this.scheduleAdjustedFeeCalculation(row);
|
||||
return;
|
||||
}
|
||||
this.recalculateAdjustRow(row, changedField);
|
||||
},
|
||||
handleAdjustFeeItemInput(row, feeItemName, value) {
|
||||
row.feeItems[feeItemName] = this.normalizeAdjustDecimal(value);
|
||||
this.recalculateAdjustRow(row, feeItemName);
|
||||
},
|
||||
scheduleAdjustedFeeCalculation(row) {
|
||||
const key = String(row.id);
|
||||
clearTimeout(this.adjustCalculateTimers[key]);
|
||||
row.calculateSequence = Number(row.calculateSequence || 0) + 1;
|
||||
const sequence = row.calculateSequence;
|
||||
this.adjustCalculateTimers[key] = setTimeout(
|
||||
() => this.calculateAdjustedFee(row, sequence),
|
||||
350
|
||||
);
|
||||
},
|
||||
async calculateAdjustedFee(row, sequence) {
|
||||
if (!this.adjustDialog.row || !row.id) return;
|
||||
row.calculating = true;
|
||||
try {
|
||||
const res = await api.calculateAdjustedFee({
|
||||
detailId: this.adjustDialog.row.id,
|
||||
feeId: row.id,
|
||||
transportQuantity: row.transportQuantity,
|
||||
mileage: row.mileage,
|
||||
freightAmount: row.freightAmount,
|
||||
feeItems: row.feeItems,
|
||||
});
|
||||
if (sequence !== row.calculateSequence || !this.adjustDialog.visible) return;
|
||||
const data = res.data?.data || res.data || res || {};
|
||||
row.freightAmount = Number(data.freightAmount || 0);
|
||||
row.feeItems = { ...row.feeItems, ...(data.feeItems || {}) };
|
||||
row.adjustAmount = Number(data.adjustAmount || 0);
|
||||
row.afterAmount = Number(data.afterAmount || 0);
|
||||
} finally {
|
||||
if (sequence === row.calculateSequence) row.calculating = false;
|
||||
}
|
||||
},
|
||||
recalculateAdjustRow(row, changedField) {
|
||||
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
|
||||
row.freightAmount = Number(row.feeItems[changedField] || 0);
|
||||
@@ -839,10 +903,11 @@ export default {
|
||||
feeType: row.feeType,
|
||||
amount: row.amount,
|
||||
remark: row.remark,
|
||||
changeReason: row.changeReason,
|
||||
})),
|
||||
});
|
||||
this.$message.success('保存成功');
|
||||
this.adjustDialog.visible = false;
|
||||
this.closeAdjustPanel();
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.adjustDialog.submitting = false;
|
||||
@@ -1054,9 +1119,12 @@ export default {
|
||||
openGenerateDialog() {
|
||||
this.generateDialog.visible = true;
|
||||
this.generateQuery = {};
|
||||
this.contractOptions = [];
|
||||
this.billingPlanOptions = [];
|
||||
this.generateRows = [];
|
||||
this.generateSelection = [];
|
||||
this.generatePage = { current: 1, size: 10, total: 0 };
|
||||
this.loadContracts();
|
||||
},
|
||||
handleGenerateContractChange(contractId) {
|
||||
this.syncBillingPlanOptions(contractId);
|
||||
@@ -1093,15 +1161,16 @@ export default {
|
||||
generateSelectionChange(selection) {
|
||||
this.generateSelection = selection;
|
||||
},
|
||||
openGeneratePreview() {
|
||||
async openGeneratePreview() {
|
||||
if (!this.generateSelection.length) {
|
||||
this.$message.warning('请选择需要生成费用的运单');
|
||||
return;
|
||||
}
|
||||
this.previewPage.current = 1;
|
||||
const loaded = await this.loadGeneratePreview();
|
||||
if (!loaded) return;
|
||||
this.previewDialog.visible = true;
|
||||
this.generateDialog.visible = false;
|
||||
this.previewPage.current = 1;
|
||||
this.loadGeneratePreview();
|
||||
},
|
||||
async loadGeneratePreview() {
|
||||
this.previewDialog.loading = true;
|
||||
@@ -1127,6 +1196,9 @@ export default {
|
||||
return { ...row, ...dynamic };
|
||||
});
|
||||
this.previewPage.total = data.total || 0;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
this.previewDialog.loading = false;
|
||||
}
|
||||
@@ -1173,7 +1245,13 @@ export default {
|
||||
const contract = contracts.find(item => String(item.id) === String(contractId));
|
||||
const plans = this.parseJson(contract?.billingPlanJson);
|
||||
this.billingPlanOptions = plans.map((item, index) => ({
|
||||
id: item.id || item.planId || item.name || `plan-${index}`,
|
||||
id:
|
||||
item.id ||
|
||||
item.planId ||
|
||||
item.name ||
|
||||
item.planName ||
|
||||
item.billingPlanName ||
|
||||
`plan-${index}`,
|
||||
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
|
||||
defaultPlan:
|
||||
item.defaultPlan === true || String(item.defaultPlan).toLowerCase() === 'true',
|
||||
@@ -1324,7 +1402,6 @@ export default {
|
||||
|
||||
:deep(th.el-table__cell) {
|
||||
background: #f5f7fa;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||
@@ -1364,22 +1441,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.settlement-detail-page__adjust-toolbar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settlement-detail-page__adjust-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.settlement-detail-page__adjust-table {
|
||||
:deep(th.el-table__cell) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
:deep(.el-input-number),
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.el-input-number .el-input__inner) {
|
||||
:deep(.el-input__inner) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -1392,6 +1458,10 @@ export default {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.settlement-detail-page__generate-select {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.settlement-detail-page__transfer-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 12px 4px;
|
||||
@@ -1412,16 +1482,81 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* 详情弹窗 tabs 白底 */
|
||||
.settlement-detail-dialog {
|
||||
.settlement-detail-page__detail-panel {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
bottom: 0;
|
||||
left: $sidebar_width;
|
||||
width: calc(100% - #{$sidebar_width});
|
||||
max-height: min(60vh, 560px);
|
||||
border-top: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
:global(.avue--collapse) .settlement-detail-page__detail-panel {
|
||||
left: $sidebar_collapse;
|
||||
width: calc(100% - #{$sidebar_collapse});
|
||||
}
|
||||
|
||||
:global(.avue-layout--horizontal) .settlement-detail-page__detail-panel {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settlement-detail-page__detail-panel-header {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #eff1f7;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid #409eff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.settlement-detail-page__detail-panel-body {
|
||||
max-height: calc(min(60vh, 560px) - 106px);
|
||||
padding: 0 20px 16px;
|
||||
overflow: auto;
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 0 0 6px 6px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
background: transparent;
|
||||
:deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
.settlement-detail-page__detail-panel-footer {
|
||||
display: flex;
|
||||
min-height: 57px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
border-top: 1px solid #eff1f7;
|
||||
}
|
||||
|
||||
.settlement-detail-page__adjust-panel-body {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.settlement-detail-page__detail-panel {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+13
-13
@@ -50,26 +50,26 @@ export default ({
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://localhost',
|
||||
// //target: 'https://saber3.bladex.cn/api',
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.228:8000',
|
||||
target: 'http://localhost',
|
||||
//target: 'https://saber3.bladex.cn/api',
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.228:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user