新增提单费用和费用日志

This commit is contained in:
liuminghui
2026-11-06 17:28:55 +08:00
committed by caojia
parent 6a02de8069
commit e0dc755ee3
13 changed files with 1115 additions and 1 deletions
+27
View File
@@ -17,3 +17,30 @@ export function getAirBill(billCode) {
params: { billCode }
})
}
/** 一票提单的费用明细 */
export function listAirBillFee(billCode) {
return request({
url: '/price/airBill/fee/list',
method: 'get',
params: { billCode }
})
}
/** 提单费用操作日志,分页参数 pageNum、pageSize */
export function listAirBillFeeLog(query) {
return request({
url: '/price/airBill/fee/log',
method: 'get',
params: query
})
}
/** 只提交新增、修改、删除的费用 */
export function saveAirBillFee(data) {
return request({
url: '/price/airBill/fee/save',
method: 'post',
data
})
}
@@ -49,18 +49,22 @@
</template>
<template #col-action="{ row }">
<el-button link type="primary" v-hasPermi="['price:airBill:query']" @click="openDetail(row)">详情</el-button>
<el-button link type="primary" v-hasPermi="['price:airBill:fee']" @click="openFee(row)">费用</el-button>
</template>
</basic-table>
<air-bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" />
<air-bill-fee-dialog ref="feeRef" />
</div>
</template>
<script setup name="AirBill">
import { listAirBill, getAirBill } from '@/api/price/airBill'
import AirBillDetail from './components/AirBillDetail.vue'
import AirBillFeeDialog from './components/AirBillFeeDialog.vue'
const tableRef = ref()
const feeRef = ref()
const tableHeight = ref(480)
let tableResizeObserver = null
@@ -102,7 +106,7 @@ const columns = [
{ label: '到达时间', prop: 'arriveTime', width: 170, parseTime: true },
{ label: '到仓时间', prop: 'arriveStockTime', width: 170, parseTime: true },
{ label: '清关时间', prop: 'clearanceTime', width: 170, parseTime: true },
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
{ label: '操作', prop: 'action', align: 'center', width: 130, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
]
const searchColumns = [
@@ -163,6 +167,10 @@ function queryList(params) {
return listAirBill(normalizeQuery(params))
}
function openFee(row) {
feeRef.value?.open(row)
}
function openDetail(row) {
if (!row?.billCode) return
open.value = true
@@ -0,0 +1,341 @@
<template>
<el-dialog v-model="visible" :title="title" width="980px" 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>
<span class="fee-hint">只保存本次新增、修改和删除的费用,未改动的行不会重写。</span>
</div>
<el-table :data="rows" border size="small" max-height="320" row-key="rowKey" empty-text="暂无费用,点击新增">
<el-table-column label="费用项" min-width="160">
<template #default="{ row }">
<el-select v-model="row.feeType" filterable clearable placeholder="请选择" style="width: 100%">
<el-option v-for="item in bms_fee_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="价格" width="150">
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0" :precision="4" :controls="false" :value-on-clear="null" placeholder="价格" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="收费币种" width="140">
<template #default="{ row }">
<el-select v-model="row.currency" filterable clearable placeholder="请选择" 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 placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button link type="danger" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="log-title">操作日志</div>
<div v-loading="logLoading">
<el-table :data="logs" border size="small" max-height="320" empty-text="暂无操作记录">
<el-table-column label="操作时间" width="168">
<template #default="{ row }">{{ logTime(row) }}</template>
</el-table-column>
<el-table-column label="操作类型" width="90" 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-else class="log-change">
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.oldValue) }}</div>
</div>
</template>
</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-else class="log-change">
<div v-for="change in row.changes" :key="change.field">{{ change.fieldLabel || change.field }}:{{ showValue(change.newValue) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作人" width="110" show-overflow-tooltip>
<template #default="{ row }">{{ row.createByName || row.createBy || '-' }}</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="logPageNum"
v-model:page-size="logPageSize"
class="log-pager"
:total="logTotal"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
background
small
@current-change="loadLogs"
@size-change="changeLogSize"
/>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
import { parseTime } from '@/utils/bms'
const { proxy } = getCurrentInstance()
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const visible = ref(false)
const loading = ref(false)
const logLoading = ref(false)
const saving = ref(false)
const billCode = ref('')
const rows = ref([])
const deletedIds = ref([])
const logs = ref([])
const logPageNum = ref(1)
const logPageSize = ref(10)
const logTotal = ref(0)
let rowSeed = 0
let logSeq = 0
const title = computed(() => billCode.value ? `提单费用 - ${billCode.value}` : '提单费用')
const dirty = computed(() => deletedIds.value.length > 0 || rows.value.some(row => rowChanged(row)))
function open(row) {
if (!row?.billCode) return
billCode.value = row.billCode
visible.value = true
load()
}
function reset() {
if (visible.value) return
billCode.value = ''
rows.value = []
deletedIds.value = []
logs.value = []
logPageNum.value = 1
logPageSize.value = 10
logTotal.value = 0
logSeq += 1
saving.value = false
loading.value = false
logLoading.value = false
}
function load() {
logPageNum.value = 1
loadFees()
loadLogs()
}
function loadFees() {
const code = billCode.value
loading.value = true
listAirBillFee(code).then(feeRes => {
if (code !== billCode.value) return
deletedIds.value = []
rows.value = (feeRes.data || []).map(item => ({
rowKey: `fee-${item.id}`,
id: item.id,
feeType: item.feeType == null ? '' : String(item.feeType),
price: item.price === null || item.price === undefined ? null : Number(item.price),
currency: item.currency || '',
remark: item.remark || '',
origin: snapshot(item)
}))
}).finally(() => {
if (code === billCode.value) loading.value = false
})
}
function unwrapLogs(res) {
const rows = res?.rows
if (Array.isArray(rows)) {
const total = Number(res.total)
return { list: rows, total: Number.isFinite(total) ? total : rows.length }
}
if (rows && Array.isArray(rows.list)) {
const total = Number(rows.total ?? res.total)
return { list: rows.list, total: Number.isFinite(total) ? total : rows.list.length }
}
if (Array.isArray(res?.data)) {
return { list: res.data, total: res.data.length }
}
return { list: [], total: 0 }
}
function loadLogs() {
const code = billCode.value
if (!code) return
const seq = ++logSeq
logLoading.value = true
listAirBillFeeLog({
billCode: code,
pageNum: logPageNum.value,
pageSize: logPageSize.value
}).then(logRes => {
if (seq !== logSeq || code !== billCode.value) return
const page = unwrapLogs(logRes)
logs.value = page.list
logTotal.value = page.total
}).finally(() => {
if (seq === logSeq && code === billCode.value) logLoading.value = false
})
}
function changeLogSize() {
logPageNum.value = 1
loadLogs()
}
function addRow() {
rowSeed += 1
rows.value.push({
rowKey: `new-${rowSeed}`,
id: null,
feeType: '',
price: null,
currency: '',
remark: '',
origin: null
})
}
function removeRow(row) {
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
deletedIds.value.push(row.id)
}
rows.value = rows.value.filter(item => item.rowKey !== row.rowKey)
}
function snapshot(item) {
return {
feeType: item.feeType || '',
price: normPrice(item.price),
currency: item.currency || '',
remark: (item.remark || '').trim()
}
}
function currentOf(row) {
return {
feeType: row.feeType || '',
price: normPrice(row.price),
currency: row.currency || '',
remark: (row.remark || '').trim()
}
}
function normPrice(value) {
if (value === null || value === undefined || value === '') return ''
const number = Number(value)
return Number.isNaN(number) ? String(value).trim() : String(number)
}
function rowChanged(row) {
if (!row.origin) {
const now = currentOf(row)
return !!(now.feeType || now.price || now.currency || now.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 save() {
const inserts = []
const updates = []
rows.value.forEach(row => {
if (!rowChanged(row)) return
const payload = {
feeType: row.feeType || '',
price: row.price,
currency: row.currency || '',
remark: (row.remark || '').trim()
}
if (row.id) updates.push({ id: row.id, ...payload })
else inserts.push(payload)
})
if (!inserts.length && !updates.length && !deletedIds.value.length) {
proxy.$modal.msgError('没有需要保存的变更')
return
}
saving.value = true
saveAirBillFee({ billCode: billCode.value, inserts, updates, deletes: deletedIds.value }).then(res => {
proxy.$modal.msgSuccess(res.msg || '保存成功')
load()
}).catch(() => {
load()
}).finally(() => {
saving.value = false
})
}
function logAction(type) {
return { 1: '新增', 2: '修改', 3: '删除' }[String(type)] || '维护'
}
function logType(type) {
if (String(type) === '1') return 'success'
if (String(type) === '3') return 'danger'
return 'warning'
}
function logTime(item) {
return parseTime(item.createTime) || ''
}
function showValue(value) {
return value === null || value === undefined || value === '' ? '空' : value
}
function blankSide(row, side) {
const changes = row.changes || []
if (!changes.length) return true
const key = side === 'old' ? 'oldValue' : 'newValue'
return changes.every(change => showValue(change[key]) === '空')
}
defineExpose({ open })
</script>
<style scoped>
.fee-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.fee-hint {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.log-title {
margin: 16px 0 8px;
font-weight: 600;
}
.log-change {
line-height: 1.7;
white-space: normal;
}
.log-empty {
color: var(--el-text-color-secondary);
}
.log-pager {
margin-top: 10px;
justify-content: flex-end;
}
</style>