提单费用均摊处理

This commit is contained in:
liuminghui
2026-11-10 17:24:52 +08:00
parent 3c49175cea
commit fbc138f041
23 changed files with 1355 additions and 32 deletions
+37
View File
@@ -36,6 +36,43 @@ export function listAirBillFeeLog(query) {
})
}
/** 勾选提单后,查询需要分摊到订单的费用 */
export function previewAirBillAlloc(billCodes) {
return request({
url: '/price/airBill/fee/alloc/preview',
method: 'get',
params: { billCodes: (billCodes || []).join(',') }
})
}
/** 按重量或订单数把费用分摊到订单 */
export function allocateAirBillFee(billCodes) {
return request({
url: '/price/airBill/fee/alloc',
method: 'post',
data: { billCodes },
timeout: 120000
})
}
/** 已分摊的费用改为已确定 */
export function confirmAirBillFee(billCode, feeId) {
return request({
url: '/price/airBill/fee/confirm',
method: 'post',
params: { billCode, feeId }
})
}
/** 作废已分摊或已确定的费用,并逻辑删除订单上的分摊明细 */
export function voidAirBillFee(billCode, feeId) {
return request({
url: '/price/airBill/fee/void',
method: 'post',
params: { billCode, feeId }
})
}
/** 只提交新增、修改、删除的费用 */
export function saveAirBillFee(data) {
return request({
+28 -3
View File
@@ -238,6 +238,11 @@ const props = defineProps({
type: Boolean,
default: true
},
// 最多勾选条数,0 表示不限制
selectionMax: {
type: Number,
default: 0
},
// 是否显示序号列
showIndex: {
type: Boolean,
@@ -436,10 +441,30 @@ function resetQuery() {
getList()
}
// 选中变化
// 选中变化。超过上限时只保留先勾到的行,并同步虚拟表和普通表的勾选状态。
let cappingSelection = false
function handleSelectionChange(val) {
selectionList.value = val
emit('selection-change', val)
if (cappingSelection) return
const rows = Array.isArray(val) ? val.slice() : []
const max = Number(props.selectionMax) || 0
if (max > 0 && rows.length > max) {
const kept = rows.slice(0, max)
cappingSelection = true
selectionList.value = kept
emit('selection-change', kept)
proxy.$modal.msgWarning(`单次最多勾选 ${max} 条`)
nextTick(() => {
const table = tableRef.value
if (table?.toggleRowSelection) {
const keys = new Set(kept.map((row) => virtualRowId(row)))
tableData.value.forEach((row) => table.toggleRowSelection(row, keys.has(virtualRowId(row))))
}
cappingSelection = false
})
return
}
selectionList.value = rows
emit('selection-change', rows)
}
const selectionIds = computed(() =>
selectionList.value.map((r) => (props.rowKey ? r[props.rowKey] : r.id)).filter((v) => v !== undefined)
@@ -11,7 +11,20 @@
:page-size="100"
:height="tableHeight"
virtual
selection
:selection-max="5"
>
<template #toolbar="{ selection }">
<el-col :span="1.5">
<el-button
type="primary"
plain
:disabled="!selection.length"
v-hasPermi="['price:airBill:fee']"
@click="openAlloc(selection)"
>费用处理</el-button>
</el-col>
</template>
<template #search-billCodes="{ model }">
<el-input
v-model="model.billCodes"
@@ -55,6 +68,7 @@
<air-bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" />
<air-bill-fee-dialog ref="feeRef" />
<air-bill-alloc-dialog ref="allocRef" @done="tableRef?.refresh()" />
</div>
</template>
@@ -62,9 +76,11 @@
import { listAirBill, getAirBill } from '@/api/price/airBill'
import AirBillDetail from './components/AirBillDetail.vue'
import AirBillFeeDialog from './components/AirBillFeeDialog.vue'
import AirBillAllocDialog from './components/AirBillAllocDialog.vue'
const tableRef = ref()
const feeRef = ref()
const allocRef = ref()
const tableHeight = ref(480)
let tableResizeObserver = null
@@ -171,6 +187,10 @@ function openFee(row) {
feeRef.value?.open(row)
}
function openAlloc(rows) {
allocRef.value?.open((rows || []).map(row => row.billCode))
}
function openDetail(row) {
if (!row?.billCode) return
open.value = true
@@ -0,0 +1,154 @@
<template>
<el-dialog v-model="visible" width="980px" append-to-body destroy-on-close @closed="reset">
<template #header>
<div class="alloc-title">
<span>费用处理</span>
<span class="alloc-hint">本次操作是将费用均摊到订单</span>
</div>
</template>
<div v-loading="loading">
<div class="alloc-summary">
已选 {{ billCodes.length }} 个提单,待分摊 {{ rows.length }} 条费用
</div>
<el-alert
v-if="blockText"
:title="blockText"
type="warning"
:closable="false"
show-icon
class="alloc-alert"
/>
<el-table :data="rows" border size="small" max-height="420" empty-text="所选提单没有需要分摊的费用">
<el-table-column prop="billCode" label="提单号" min-width="150" show-overflow-tooltip />
<el-table-column label="费用阶段" width="150" align="center">
<template #default="{ row }">
<dict-tag :options="bms_fee_level" :value="row.feeLevel" />
</template>
</el-table-column>
<el-table-column label="费用状态" width="100" align="center">
<template #default="{ row }">
<dict-tag :options="bill_fee_status" :value="row.billFeeStatus || '0'" />
</template>
</el-table-column>
<el-table-column prop="feeName" label="费用项" min-width="140" show-overflow-tooltip />
<el-table-column label="价格" width="110" align="right">
<template #default="{ row }">{{ priceText(row.price) }}</template>
</el-table-column>
<el-table-column label="币种" width="90" align="center">
<template #default="{ row }">
<dict-tag v-if="row.currency" :options="bms_currency" :value="row.currency" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="分摊维度" width="100" align="center">
<template #default="{ row }">
<dict-tag :options="bill_fee_alloc_dim" :value="row.allocDim" />
</template>
</el-table-column>
<el-table-column prop="orderCount" label="订单数" width="80" align="center" />
</el-table>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmit" :loading="saving" @click="submit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { previewAirBillAlloc, allocateAirBillFee } from '@/api/price/airBill'
import { useDict } from '@/utils/dict'
const emit = defineEmits(['done'])
const { proxy } = getCurrentInstance()
const { bms_fee_level, bill_fee_status, bms_currency, bill_fee_alloc_dim } = useDict(
'bms_fee_level', 'bill_fee_status', 'bms_currency', 'bill_fee_alloc_dim'
)
const visible = ref(false)
const loading = ref(false)
const saving = ref(false)
const billCodes = ref([])
const rows = ref([])
const blockText = computed(() => {
const blocked = rows.value.filter(row => row.blockReason)
if (!blocked.length) return ''
return blocked.slice(0, 3).map(row => `${row.billCode || '-'} ${row.feeName || '费用'}:${row.blockReason}`).join(';')
})
const canSubmit = computed(() => !loading.value && rows.value.length > 0 && !blockText.value)
function open(codes) {
const picked = [...new Set((codes || []).map(code => String(code || '').trim()).filter(Boolean))]
if (!picked.length) {
proxy.$modal.msgWarning('请先勾选提单')
return
}
if (picked.length > 5) {
proxy.$modal.msgWarning('单次最多勾选 5 条提单')
return
}
billCodes.value = picked
rows.value = []
visible.value = true
loading.value = true
previewAirBillAlloc(picked).then(res => {
rows.value = res.data || []
}).finally(() => {
loading.value = false
})
}
function priceText(value) {
if (value === null || value === undefined || value === '') return '-'
const number = Number(value)
return Number.isFinite(number) ? number.toFixed(2) : value
}
async function submit() {
if (!canSubmit.value) return
try {
await proxy.$modal.confirm('确认将费用分摊到订单吗?确定后会按重量或订单数量摊到每一票,费用状态改为已分摊。')
} catch {
return
}
saving.value = true
try {
const res = await allocateAirBillFee(billCodes.value)
proxy.$modal.msgSuccess(res.msg || '分摊完成')
visible.value = false
emit('done')
} finally {
saving.value = false
}
}
function reset() {
loading.value = false
saving.value = false
billCodes.value = []
rows.value = []
}
defineExpose({ open })
</script>
<style scoped>
.alloc-title {
display: flex;
align-items: baseline;
gap: 12px;
}
.alloc-hint {
font-size: 12px;
font-weight: 400;
color: var(--el-text-color-secondary);
}
.alloc-summary {
margin-bottom: 12px;
color: var(--el-text-color-regular);
}
.alloc-alert {
margin-bottom: 12px;
}
</style>
@@ -2,14 +2,14 @@
<el-dialog v-model="visible" :title="title" width="1360px" append-to-body destroy-on-close @closed="reset">
<div v-loading="loading">
<div class="fee-toolbar">
<el-button type="primary" plain icon="Plus" @click="addRow">新增</el-button>
<el-button type="primary" plain icon="Plus" v-hasPermi="['price:airBill:fee']" @click="addRow">新增</el-button>
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
</div>
<el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增">
<el-table-column min-width="180">
<template #header><span class="col-label"><i v-if="allocRequired" class="req-star">*</i>费用阶段</span></template>
<template #default="{ row }">
<el-select v-model="row.feeLevel" filterable clearable placeholder="请选择" style="width: 100%">
<el-select v-model="row.feeLevel" filterable clearable placeholder="请选择" style="width: 100%" :disabled="locked(row)">
<el-option v-for="item in bms_fee_level" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
@@ -17,7 +17,7 @@
<el-table-column width="120">
<template #header><span class="col-label"><i class="req-star">*</i>是否分摊</span></template>
<template #default="{ row }">
<el-select v-model="row.allocFlag" placeholder="请选择" style="width: 100%" @change="changeAlloc(row)">
<el-select v-model="row.allocFlag" placeholder="请选择" style="width: 100%" :disabled="locked(row)" @change="changeAlloc(row)">
<el-option v-for="item in bill_fee_alloc" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
@@ -25,7 +25,7 @@
<el-table-column v-if="allocRequired" min-width="150">
<template #header><span class="col-label"><i class="req-star">*</i>分摊维度(票)</span></template>
<template #default="{ row }">
<el-select v-if="String(row.allocFlag) === '1'" v-model="row.allocDim" placeholder="请选择" style="width: 100%">
<el-select v-if="String(row.allocFlag) === '1'" v-model="row.allocDim" placeholder="请选择" style="width: 100%" :disabled="locked(row)">
<el-option v-for="item in bill_fee_alloc_dim" :key="item.value" :label="item.label" :value="String(item.value)" />
</el-select>
</template>
@@ -39,7 +39,7 @@
<template #header><span class="col-label"><i class="req-star">*</i>费用项</span></template>
<template #default="{ row }">
<el-select v-model="row.feeType" filterable remote clearable reserve-keyword placeholder="输入费用名称"
:remote-method="searchSurcharge" :loading="surchargeLoading" style="width: 100%" @change="name => onFeeChange(row, name)">
:remote-method="searchSurcharge" :loading="surchargeLoading" :disabled="locked(row)" style="width: 100%" @change="name => onFeeChange(row, name)">
<el-option v-for="item in surchargeOptions" :key="item.scId" :label="item.scName" :value="item.scId" />
</el-select>
</template>
@@ -47,25 +47,27 @@
<el-table-column width="150">
<template #header><span class="col-label"><i class="req-star">*</i>价格</span></template>
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" />
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" :disabled="locked(row)" placeholder="价格" style="width: 100%" />
</template>
</el-table-column>
<el-table-column width="140">
<template #header><span class="col-label"><i class="req-star">*</i>收费币种</span></template>
<template #default="{ row }">
<el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%">
<el-select v-model="row.currency" filterable clearable placeholder="请选择" style="width: 100%" :disabled="locked(row)">
<el-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="备注" min-width="180">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="500" clearable placeholder="备注" />
<el-input v-model="row.remark" maxlength="500" clearable placeholder="备注" :disabled="locked(row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="removeRow(row)">删除</el-button>
<el-button v-if="row.billFeeStatus === '1'" v-hasPermi="['price:airBill:feeConfirm']" link type="primary" :disabled="acting" @click="confirmRow(row)">确定</el-button>
<el-button v-if="row.billFeeStatus === '1' || row.billFeeStatus === '3'" v-hasPermi="['price:airBill:feeVoid']" link type="danger" :disabled="acting" @click="voidRow(row)">作废</el-button>
<el-button v-if="!locked(row)" v-hasPermi="['price:airBill:fee']" link type="danger" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -77,14 +79,14 @@
<el-table-column label="操作时间" width="168">
<template #default="{ row }">{{ logTime(row) }}</template>
</el-table-column>
<el-table-column label="操作类型" width="90" align="center">
<el-table-column label="操作类型" width="110" align="center">
<template #default="{ row }">
<el-tag size="small" :type="logType(row.operationType)">{{ logAction(row.operationType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="更改前" min-width="200">
<template #default="{ row }">
<div v-if="blankSide(row, 'old')" class="log-empty">空</div>
<div v-if="blankSide(row, 'old')" class="log-empty">{{ emptyText(row) }}</div>
<div v-else class="log-change">
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.oldValue) }}</div>
</div>
@@ -92,7 +94,7 @@
</el-table-column>
<el-table-column label="更改后" min-width="200">
<template #default="{ row }">
<div v-if="blankSide(row, 'new')" class="log-empty">空</div>
<div v-if="blankSide(row, 'new')" class="log-empty">{{ emptyText(row) }}</div>
<div v-else class="log-change">
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.newValue) }}</div>
</div>
@@ -117,13 +119,13 @@
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存</el-button>
<el-button type="primary" v-hasPermi="['price:airBill:fee']" :disabled="!dirty" :loading="saving" @click="save">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee, confirmAirBillFee, voidAirBillFee } from '@/api/price/airBill'
import { listSurchargeOptions } from '@/api/price/surcharge'
import { parseTime } from '@/utils/bms'
@@ -134,6 +136,7 @@ const visible = ref(false)
const loading = ref(false)
const logLoading = ref(false)
const saving = ref(false)
const acting = ref(false)
const billCode = ref('')
const rows = ref([])
const deletedIds = ref([])
@@ -172,6 +175,7 @@ function reset() {
surchargeOptions.value = []
surchargeLoading.value = false
saving.value = false
acting.value = false
loading.value = false
logLoading.value = false
}
@@ -307,6 +311,37 @@ function changeAlloc(row) {
if (String(row.allocFlag) !== '1') row.allocDim = ''
}
function locked(row) {
return row?.billFeeStatus === '1' || row?.billFeeStatus === '3'
}
function confirmRow(row) {
if (!row?.id || acting.value) return
acting.value = true
confirmAirBillFee(billCode.value, row.id).then(res => {
proxy.$modal.msgSuccess(res.msg || '费用已确定')
load()
}).finally(() => {
acting.value = false
})
}
async function voidRow(row) {
if (!row?.id || acting.value) return
try {
await proxy.$modal.confirm('确认作废这条费用吗?已分摊到订单的费用明细会逻辑删除,这条提单费用也会逻辑删除。')
} catch {
return
}
acting.value = true
voidAirBillFee(billCode.value, row.id).then(res => {
proxy.$modal.msgSuccess(res.msg || '费用已作废')
load()
}).finally(() => {
acting.value = false
})
}
function removeRow(row) {
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
deletedIds.value.push(row.id)
@@ -404,15 +439,21 @@ function save() {
}
function logAction(type) {
return { 1: '新增', 2: '修改', 3: '删除' }[String(type)] || '维护'
return { 1: '新增', 2: '修改', 3: '删除', 4: '确定费用', 5: '作废' }[String(type)] || '维护'
}
function logType(type) {
if (String(type) === '1') return 'success'
if (String(type) === '3') return 'danger'
const value = String(type)
if (value === '1' || value === '4') return 'success'
if (value === '3' || value === '5') return 'danger'
return 'warning'
}
function emptyText(row) {
const value = String(row?.operationType)
return value === '4' || value === '5' ? '无' : '空'
}
function logTime(item) {
return parseTime(item.createTime) || ''
}