新增提单管理和提单计费页面

This commit is contained in:
liuminghui
2026-11-11 16:36:17 +08:00
parent 7869205afc
commit efd92b5663
47 changed files with 1712 additions and 369 deletions
+40
View File
@@ -0,0 +1,40 @@
import request from '@/utils/request'
/** 按提单号查询可绑定的提单 */
export function listAirRoleBills(billCodes) {
return request({
url: '/price/airFee/bills',
method: 'get',
params: { billCodes }
})
}
/** 查询当前入口已绑定到这些提单的费用 */
export function listAirRoleFees(scene, billCodes) {
return request({
url: '/price/airFee/list',
method: 'get',
params: { scene, billCodes }
})
}
/** 保存当前入口的费用 */
export function saveAirRoleFees(data) {
return request({
url: '/price/airFee/save',
method: 'post',
data
})
}
/** 解析导入文件,示例行由后台跳过。空运、报关、清关共用,入口用 scene 区分。 */
export function importAirRoleFee(data) {
return request({
url: '/price/airFee/import',
method: 'post',
data,
// 必须声明 multipart,否则全局 application/json 会把文件流改写成空 JSON
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
timeout: 120000
})
}
@@ -171,7 +171,7 @@
</el-table-column>
<el-table-column prop="calcRule" label="计费条件" width="140">
<template #default="{ row }">
<el-select v-if="isRowEditing(row)" v-model="row.calcRule" size="small" placeholder="人工确认">
<el-select v-if="isRowEditing(row)" v-model="row.calcRule" size="small" placeholder="暂不计费">
<el-option v-for="item in calcRules" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
@@ -452,11 +452,11 @@ const calcRules = [
{ value: 'WEIGHT', label: '超重' },
{ value: 'SIZE', label: '超尺寸' },
{ value: 'RETURN', label: '退件' },
{ value: 'MANUAL', label: '人工确认' }
{ value: 'MANUAL', label: '暂不计费' }
]
function calcRuleLabel(value) {
return calcRules.find(item => item.value === value)?.label || '人工确认'
return calcRules.find(item => item.value === value)?.label || '暂不计费'
}
function conditionHint(row) {
@@ -0,0 +1,7 @@
<template>
<air-role-fee scene="air" />
</template>
<script setup name="AirLadingFee">
import AirRoleFee from '../components/AirRoleFee.vue'
</script>
@@ -0,0 +1,7 @@
<template>
<air-role-fee scene="clear" />
</template>
<script setup name="ClearLadingFee">
import AirRoleFee from '../components/AirRoleFee.vue'
</script>
@@ -0,0 +1,429 @@
<template>
<div class="app-container air-role-fee">
<div class="page-head">
<span class="page-title">{{ title }}</span>
</div>
<div class="bill-bar">
<el-input
v-model="billCodeText"
type="textarea"
:rows="2"
resize="none"
placeholder="提单号,多个用逗号或换行分隔,可多选"
/>
<el-button type="primary" :loading="billLoading" @click="searchBills">查询提单</el-button>
</div>
<el-table
ref="billTableRef"
:data="bills"
border
size="small"
max-height="220"
row-key="billCode"
empty-text="请输入提单号后查询"
@selection-change="onBillSelect"
>
<el-table-column type="selection" width="48" />
<el-table-column
v-for="col in billColumns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:align="col.align"
show-overflow-tooltip
>
<template #default="{ row }">{{ showCell(row, col) }}</template>
</el-table-column>
</el-table>
<div class="fee-toolbar">
<el-button type="primary" plain icon="Plus" v-hasPermi="[perm]" @click="addRows">新增</el-button>
<el-button type="primary" plain icon="Upload" v-hasPermi="[perm]" @click="openImport">导入</el-button>
<el-button v-hasPermi="[perm]" :loading="feeLoading" @click="loadFees">查询已有费用</el-button>
<span class="page-hint">已选 {{ selected.length }} 个提单。导入和新增都会写到每一个已选提单。</span>
</div>
<el-table :data="rows" border size="small" max-height="420" row-key="rowKey" empty-text="暂无费用">
<el-table-column prop="billCode" label="提单号" min-width="150" show-overflow-tooltip />
<el-table-column min-width="180">
<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" :disabled="locked(row)" style="width: 100%"
@change="id => onFeeChange(row, id)">
<el-option v-for="item in surchargeOptions" :key="item.scId" :label="item.scName" :value="item.scId" />
</el-select>
</template>
</el-table-column>
<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" :disabled="locked(row)" 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="请选择" :disabled="locked(row)" style="width: 100%">
<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 :disabled="locked(row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default="{ row }">
<span v-if="locked(row)" class="locked-text">{{ row.billFeeStatus === '3' ? '已确定' : '已分摊' }}</span>
<el-button v-else link type="danger" v-hasPermi="[perm]" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="fee-footer">
<el-button type="primary" v-hasPermi="[perm]" :disabled="!dirty" :loading="saving" @click="save">保存</el-button>
</div>
<excel-import-dialog
ref="importRef"
:title="`${title}导入`"
width="640px"
:show-update-support="false"
:import-desc="importDesc"
:template-download-handler="downloadTemplate"
:submit-handler="submitImport"
/>
</div>
</template>
<script setup>
import { listAirRoleBills, listAirRoleFees, saveAirRoleFees, importAirRoleFee } from '@/api/price/airFee'
import { listSurchargeOptions } from '@/api/price/surcharge'
import { download } from '@/utils/request'
import { parseTime } from '@/utils/bms'
import ExcelImportDialog from '@/components/ExcelImportDialog'
const props = defineProps({
scene: { type: String, required: true }
})
const { proxy } = getCurrentInstance()
const { bms_currency } = useDict('bms_currency')
const titles = { air: '空运费用', declare: '报关费用', clear: '清关费用' }
const perms = { air: 'price:airFee:air', declare: 'price:airFee:declare', clear: 'price:airFee:clear' }
const title = computed(() => titles[props.scene] || '提单费用')
const perm = computed(() => perms[props.scene] || '')
const billColumnMap = {
air: [
{ prop: 'billCode', label: '提单号', minWidth: 160 },
{ prop: 'airNo', label: '航班号', minWidth: 120 },
{ prop: 'actualAirNo', label: '实际航班号', minWidth: 120 },
{ prop: 'airCompany', label: '航空公司', minWidth: 120 },
{ prop: 'airCompanyAgent', label: '航空公司代理', minWidth: 150 },
{ prop: 'startAirport', label: '始发机场', width: 100 },
{ prop: 'endAirport', label: '目的机场', width: 100 },
{ prop: 'desPort', label: '目的港', width: 100 },
{ prop: 'routeName', label: '航线名称', minWidth: 120 },
{ prop: 'boardNo', label: '板号', width: 110 },
{ prop: 'pieceNumber', label: '总件数', width: 90, align: 'right' },
{ prop: 'actualWeight', label: '实际重量', width: 110, align: 'right' },
{ prop: 'volumeWeight', label: '体积重', width: 100, align: 'right' },
{ prop: 'bandBoxWeight', label: '打板重量', width: 110, align: 'right' },
{ prop: 'firstLandingTime', label: '首程落地时间', width: 170, time: true },
{ prop: 'startTime', label: '起飞时间', width: 170, time: true },
{ prop: 'arriveTime', label: '到达时间', width: 170, time: true },
{ prop: 'airOtherFee', label: '空运其他费用', width: 120, align: 'right' }
],
declare: [
{ prop: 'billCode', label: '提单号', minWidth: 160 },
{ prop: 'declareCode', label: '报关公司编码', minWidth: 140 },
{ prop: 'baoguanFee', label: '报关费', width: 120, align: 'right' },
{ prop: 'baoguanOtherFee', label: '报关其他费用', width: 130, align: 'right' }
],
clear: [
{ prop: 'billCode', label: '提单号', minWidth: 160 },
{ prop: 'clearanceCode', label: '清关编码', minWidth: 120 },
{ prop: 'qingguanAgent', label: '清关代理', minWidth: 140 },
{ prop: 'clearanceOpCode', label: '清关操作人', minWidth: 120 },
{ prop: 'desPort', label: '目的港', width: 100 },
{ prop: 'desPortAgentCode', label: '目的港代理编码', minWidth: 140 },
{ prop: 'desPortAgentFee', label: '目的港代理费', width: 130, align: 'right' },
{ prop: 'clearanceTime', label: '清关时间', width: 170, time: true },
{ prop: 'arriveStockTime', label: '到仓时间', width: 170, time: true },
{ prop: 'stockAddr', label: '出仓仓库地址', minWidth: 160 }
]
}
const billColumns = computed(() => billColumnMap[props.scene] || billColumnMap.air)
function showCell(row, col) {
const value = row?.[col.prop]
if (value === null || value === undefined || value === '') return '-'
if (col.time) return parseTime(value) || '-'
return value
}
const importDesc = [
'模板字段:费用名称、价格、币种、备注。',
'示例数据:燃油附加费、1000、EUR、导入时需清空这行示例数据。',
'导入前请删除示例行,否则这一行不会写入。',
'导入结果会添加到当前勾选的每一个提单,确认后点保存。'
]
const billCodeText = ref('')
const bills = ref([])
const selected = ref([])
const rows = ref([])
const deleted = ref([])
const billLoading = ref(false)
const feeLoading = ref(false)
const saving = ref(false)
const surchargeOptions = ref([])
const surchargeLoading = ref(false)
const importRef = ref()
let rowSeed = 0
let surchargeSeq = 0
const dirty = computed(() => deleted.value.length > 0 || rows.value.some(row => rowChanged(row)))
function parseCodes(raw) {
return [...new Set(String(raw || '').split(/[\s,,;;]+/).map(item => item.trim()).filter(Boolean))]
}
function searchBills() {
const codes = parseCodes(billCodeText.value)
if (!codes.length) {
proxy.$modal.msgWarning('请输入提单号')
return
}
billLoading.value = true
listAirRoleBills(codes.join(',')).then(res => {
const list = res.data || []
bills.value = list
selected.value = []
const found = new Set(list.map(item => item.billCode))
const missing = codes.filter(code => !found.has(code))
if (missing.length) proxy.$modal.msgWarning('未找到提单:' + missing.join('、'))
}).finally(() => {
billLoading.value = false
})
}
function onBillSelect(list) {
selected.value = list || []
}
async function loadFees(force = false) {
if (!selected.value.length) {
proxy.$modal.msgWarning('请先勾选提单')
return
}
if (force !== true && dirty.value) {
try {
await proxy.$modal.confirm('查询会覆盖当前未保存的费用,是否继续?')
} catch {
return
}
}
feeLoading.value = true
const codes = selected.value.map(item => item.billCode).join(',')
listAirRoleFees(props.scene, codes).then(res => {
deleted.value = []
rows.value = (res.data || []).map(item => toRow(item, true))
pinSelectedFees()
}).finally(() => {
feeLoading.value = false
})
searchSurcharge('')
}
function toRow(item, saved) {
const row = {
rowKey: saved ? `fee-${item.id}` : `new-${++rowSeed}`,
id: saved ? item.id : null,
billCode: item.billCode || '',
feeType: item.feeType == null ? '' : String(item.feeType),
feeName: item.feeName || '',
price: item.price === null || item.price === undefined ? null : Number(item.price),
currency: item.currency || '',
remark: item.remark || '',
billFeeStatus: item.billFeeStatus == null || item.billFeeStatus === '' ? '0' : String(item.billFeeStatus)
}
row.origin = saved ? snapshot(row) : null
return row
}
function snapshot(row) {
return {
feeType: row.feeType || '',
price: row.price === null || row.price === undefined || row.price === '' ? '' : String(Number(row.price)),
currency: row.currency || '',
remark: (row.remark || '').trim()
}
}
function currentOf(row) {
return {
feeType: row.feeType || '',
price: row.price === null || row.price === undefined || row.price === '' ? '' : String(Number(row.price)),
currency: row.currency || '',
remark: (row.remark || '').trim()
}
}
function rowChanged(row) {
if (!row.origin) return !!(row.feeType || row.price || row.currency || row.remark)
const now = currentOf(row)
return now.feeType !== row.origin.feeType || now.price !== row.origin.price
|| now.currency !== row.origin.currency || now.remark !== row.origin.remark
}
function locked(row) {
return row?.billFeeStatus === '1' || row?.billFeeStatus === '3'
}
function addRows() {
if (!selected.value.length) {
proxy.$modal.msgWarning('请先勾选提单')
return
}
selected.value.forEach(bill => rows.value.push(toRow({ billCode: bill.billCode }, false)))
}
function removeRow(row) {
if (row.id != null && !deleted.value.some(item => String(item.id) === String(row.id))) {
deleted.value.push({ id: row.id, billCode: row.billCode })
}
rows.value = rows.value.filter(item => item.rowKey !== row.rowKey)
}
function rowError(row) {
if (!row.billCode) return '请选择提单'
if (!row.feeType) return '请选择费用名称'
if (row.price === null || row.price === undefined || row.price === '') return '请填写价格'
if (Number(row.price) < 0) return '价格不能小于0'
if (!row.currency) return '请选择币种'
return ''
}
function save() {
const invalid = rows.value.find(row => rowChanged(row) && rowError(row))
if (invalid) {
proxy.$modal.msgError(rowError(invalid))
return
}
const bills = new Map()
function bucket(code) {
if (!bills.has(code)) bills.set(code, { billCode: code, inserts: [], updates: [], deletes: [] })
return bills.get(code)
}
rows.value.forEach(row => {
if (!rowChanged(row)) return
const payload = {
feeType: row.feeType || '',
price: row.price,
currency: row.currency || '',
remark: (row.remark || '').trim()
}
const bag = bucket(row.billCode)
if (row.id) bag.updates.push({ id: row.id, ...payload })
else bag.inserts.push(payload)
})
deleted.value.forEach(item => bucket(item.billCode).deletes.push(item.id))
const list = [...bills.values()].filter(item => item.inserts.length || item.updates.length || item.deletes.length)
if (!list.length) {
proxy.$modal.msgError('没有需要保存的变更')
return
}
saving.value = true
saveAirRoleFees({ scene: props.scene, bills: list }).then(res => {
proxy.$modal.msgSuccess(res.msg || '保存成功')
deleted.value = []
loadFees(true)
}).finally(() => {
saving.value = false
})
}
function openImport() {
if (!selected.value.length) {
proxy.$modal.msgWarning('请先勾选提单')
return
}
importRef.value?.open?.()
}
function downloadTemplate() {
return download('/price/airFee/importTemplate', {}, '提单费用导入模板.xlsx')
}
async function submitImport(file) {
const raw = file?.raw || file
const form = new FormData()
form.append('file', raw)
form.append('scene', props.scene)
const res = await importAirRoleFee(form)
const imported = res.data || []
if (!imported.length) throw new Error('没有可导入的费用')
selected.value.forEach(bill => {
imported.forEach(item => rows.value.push(toRow({ ...item, billCode: bill.billCode }, false)))
})
pinSelectedFees()
return res.msg || `已读入 ${imported.length} 条费用,请确认后保存`
}
function searchSurcharge(keyword) {
const seq = ++surchargeSeq
surchargeLoading.value = true
const params = { ssCode: 'on' }
const name = String(keyword || '').trim()
if (name) params.scName = name
listSurchargeOptions(params).then(res => {
if (seq !== surchargeSeq) return
surchargeOptions.value = (res.data || []).map(item => ({
scId: item.scId == null ? '' : String(item.scId),
scName: item.scName || ''
})).filter(item => item.scId)
pinSelectedFees()
}).finally(() => {
if (seq === surchargeSeq) surchargeLoading.value = false
})
}
function onFeeChange(row, id) {
const found = surchargeOptions.value.find(item => String(item.scId) === String(id || ''))
row.feeName = found ? found.scName : ''
}
function pinSelectedFees() {
const known = new Map(surchargeOptions.value.map(item => [String(item.scId), item]))
rows.value.forEach(row => {
if (row.feeType && !known.has(String(row.feeType))) {
known.set(String(row.feeType), { scId: String(row.feeType), scName: row.feeName || String(row.feeType) })
}
})
surchargeOptions.value = [...known.values()]
}
searchSurcharge('')
</script>
<style scoped>
.page-head, .bill-bar, .fee-toolbar, .fee-footer {
display: flex;
align-items: center;
gap: 12px;
}
.page-head { margin-bottom: 12px; }
.page-title { font-size: 16px; font-weight: 600; }
.page-hint { color: var(--el-text-color-secondary); font-size: 12px; }
.bill-bar { margin-bottom: 10px; align-items: flex-start; }
.bill-bar .el-textarea { max-width: 520px; }
.fee-toolbar { margin: 16px 0 10px; }
.fee-footer { margin-top: 12px; justify-content: flex-end; }
.col-label { display: inline-flex; align-items: flex-start; }
.req-star { color: var(--el-color-danger); font-style: normal; font-weight: 700; margin-right: 2px; }
.locked-text { color: var(--el-text-color-secondary); font-size: 12px; }
</style>
@@ -0,0 +1,7 @@
<template>
<air-role-fee scene="declare" />
</template>
<script setup name="DeclareLadingFee">
import AirRoleFee from '../components/AirRoleFee.vue'
</script>
@@ -20,16 +20,6 @@
/>
<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>
@@ -40,11 +30,6 @@
<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>
@@ -61,9 +46,7 @@ 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 { bms_currency } = useDict('bms_currency')
const visible = ref(false)
const loading = ref(false)
@@ -108,7 +91,7 @@ function priceText(value) {
async function submit() {
if (!canSubmit.value) return
try {
await proxy.$modal.confirm('确认将费用分摊到订单吗?确定后会按重量或订单数量摊到每一票,费用状态改为已分摊。')
await proxy.$modal.confirm('确认将费用分摊到订单吗?确定后会按重量摊到每一票,费用状态改为已分摊。')
} catch {
return
}
@@ -3,38 +3,9 @@
<div v-loading="loading">
<div class="fee-toolbar">
<el-button type="primary" plain icon="Plus" v-hasPermi="['price:airBill:fee']" @click="addRow">新增</el-button>
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
<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%" :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>
</el-table-column>
<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%" :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>
</el-table-column>
<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%" :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>
</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 min-width="160">
<template #header><span class="col-label"><i class="req-star">*</i>费用项</span></template>
<template #default="{ row }">
@@ -130,7 +101,7 @@ import { listSurchargeOptions } from '@/api/price/surcharge'
import { parseTime } from '@/utils/bms'
const { proxy } = getCurrentInstance()
const { bms_fee_level, bms_currency, bill_fee_status, bill_fee_alloc, bill_fee_alloc_dim } = useDict('bms_fee_level', 'bms_currency', 'bill_fee_status', 'bill_fee_alloc', 'bill_fee_alloc_dim')
const { bms_currency } = useDict('bms_currency')
const visible = ref(false)
const loading = ref(false)
@@ -151,7 +122,6 @@ let logSeq = 0
let surchargeSeq = 0
const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用')
const allocRequired = computed(() => rows.value.some(row => String(row.allocFlag) === '1'))
const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row)))
function open(row) {
@@ -307,10 +277,6 @@ function addRow() {
})
}
function changeAlloc(row) {
if (String(row.allocFlag) !== '1') row.allocDim = ''
}
function locked(row) {
return row?.billFeeStatus === '1' || row?.billFeeStatus === '3'
}
@@ -382,18 +348,14 @@ function normPrice(value) {
function rowChanged(row) {
if (!row.origin) {
const now = currentOf(row)
return !!(now.feeType || now.feeLevel || now.allocFlag === '1' || now.allocDim || now.price || now.currency || now.remark)
return !!(now.feeType || now.price || now.currency || now.remark)
}
const now = currentOf(row)
return now.feeType !== row.origin.feeType || now.feeLevel !== row.origin.feeLevel || now.allocFlag !== row.origin.allocFlag
|| now.allocDim !== row.origin.allocDim || now.price !== row.origin.price
return now.feeType !== row.origin.feeType || now.price !== row.origin.price
|| now.currency !== row.origin.currency || now.remark !== row.origin.remark
}
function rowError(row) {
if (String(row.allocFlag) !== '0' && String(row.allocFlag) !== '1') return '请选择是否分摊'
if (String(row.allocFlag) === '1' && !row.feeLevel) return '如需分摊费用至订单,请选择费用所属阶段'
if (String(row.allocFlag) === '1' && !row.allocDim) return '如需分摊费用至订单,请选择分摊维度'
if (!row.feeType) return '请选择费用项'
if (row.price === null || row.price === undefined || row.price === '') return '请填写价格'
if (Number(row.price) < 0) return '价格不能小于0'
@@ -414,8 +376,6 @@ function save() {
const payload = {
feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: row.price,
currency: row.currency || '',
remark: (row.remark || '').trim()
@@ -149,10 +149,10 @@
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ displayFeeName(row) }}</template>
</el-table-column>
<el-table-column label="费用类型" width="110" align="center">
<el-table-column label="费用类型" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tag size="small" :type="feeTypeTag(row.feeType)" effect="plain">
{{ feeTypeLabel(row.feeType) || row.feeType }}
{{ payFeeTypeLabel(row) }}
</el-tag>
</template>
</el-table-column>
@@ -247,21 +247,18 @@ watch(() => props.detail, () => {
function handlePayBill() {
const billId = props.detail?.billId
if (!billId) return
ElMessageBox.confirm('按订单的渠道服务商和已配置的应付产品计算应付?', '生成应付', { type: 'warning' })
ElMessageBox.confirm('确认生成应付?', '生成应付', { type: 'warning' })
.then(() => generatePayBill(billId))
.then(res => {
const data = res.data || {}
if (data.skipped && data.skipped.length) {
ElMessageBox.alert(data.skipped.join('\n'), data.message || '应付已生成', { type: 'warning' })
} else {
ElMessage.success(data.message || '应付已生成')
}
.then(() => {
ElMessage.success('操作成功')
return getBill(billId)
})
.then(res => {
if (res && res.data) emit('updated', res.data)
})
.catch(() => {})
.catch(err => {
if (err === 'cancel' || err === 'close') return
})
}
function canEditManualFee(row) {
@@ -422,6 +419,12 @@ function roundModeLabel(mode) {
return map[mode] || mode || '四舍五入'
}
function payFeeTypeLabel(row) {
const name = String(row?.feeName || row?.fee_name || '').trim()
if (row?.feeType === 'LADING' || row?.feeSource === 'LADING') return name || '提单费用'
return feeTypeLabel(row?.feeType) || row?.feeType || '-'
}
function feeTypeLabel(type) {
const found = (bms_fee_type.value || []).find(d => d.value === type)
return found ? found.label : ''