This commit is contained in:
wuweihua
2026-11-10 16:21:16 +08:00
16 changed files with 911 additions and 170 deletions
@@ -1,11 +1,6 @@
<template>
<el-dialog v-model="visible" title="导入应付价卡" width="1080px" append-to-body destroy-on-close @closed="reset">
<el-form label-width="88px" class="import-form">
<el-form-item label="支付类型" required>
<el-select v-model="productType" filterable placeholder="请选择支付类型" style="width: 420px">
<el-option v-for="item in bms_pay_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="产品">
<el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px">
<el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" />
@@ -115,14 +110,13 @@ import PayConfigDetail from './PayConfigDetail.vue'
const emit = defineEmits(['saved'])
const { proxy } = getCurrentInstance()
const { payment_product_type, bms_pay_type } = useDict('payment_product_type', 'bms_pay_type')
const { payment_product_type } = useDict('payment_product_type')
const visible = ref(false)
const recognizing = ref(false)
const saving = ref(false)
const overwrite = ref(false)
const dictValue = ref('')
const productType = ref('AP')
const preview = ref(null)
const selected = ref([])
const tableRef = ref()
@@ -150,7 +144,6 @@ function reset() {
selected.value = []
overwrite.value = false
dictValue.value = ''
productType.value = 'AP'
activeKey.value = ''
recognizing.value = false
saving.value = false
@@ -265,10 +258,6 @@ function onSelect(rows) {
}
async function save() {
if (!productType.value) {
proxy.$modal.msgError('请选择支付类型')
return
}
if (!selected.value.length) {
proxy.$modal.msgError('请勾选要保存的产品')
return
@@ -284,8 +273,7 @@ async function save() {
}
saving.value = true
try {
const products = selected.value.map(row => ({ ...row, productType: productType.value }))
const res = await savePay({ overwrite: overwrite.value, products })
const res = await savePay({ overwrite: overwrite.value, products: selected.value })
const message = res.msg || '保存成功'
reset()
visible.value = false
+118 -18
View File
@@ -61,12 +61,12 @@
<pay-import-dialog ref="importRef" @saved="tableRef.refresh()" />
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" destroy-on-close>
<el-drawer v-model="historyVisible" :title="historyTitle" size="980px" append-to-body destroy-on-close>
<div class="history-bar">
<el-button type="primary" plain :disabled="historyPicked.length !== 2" @click="openCompare">比对所选版本</el-button>
<span>勾选两个版本后比对。历史订单按生效时间取价,不看当前开关。</span>
</div>
<el-table v-loading="historyLoading" :data="historyRows" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table v-loading="historyLoading" :data="historyRows" row-key="termId" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table-column type="selection" width="42" align="center" />
<el-table-column prop="versionNo" label="版本" width="70" align="center" />
<el-table-column label="来源文件" min-width="160" show-overflow-tooltip>
@@ -123,15 +123,42 @@
</template>
</el-dialog>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body destroy-on-close>
<el-table v-loading="compareLoading" :data="compareRows" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="90" align="center" />
<el-table-column prop="change" label="变化" width="80" align="center" />
<el-table-column prop="name" label="项目" min-width="160" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column prop="oldValue" label="旧版" min-width="160" show-overflow-tooltip />
<el-table-column prop="newValue" label="新版" min-width="160" show-overflow-tooltip />
</el-table>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body :z-index="4000" destroy-on-close>
<div class="compare-panel">
<div v-if="compareReady && !compareLoading" class="history-bar">
<el-tag type="primary" effect="plain">变更 {{ compareStat.changed }}</el-tag>
<el-tag type="success" effect="plain">新增 {{ compareStat.added }}</el-tag>
<el-tag type="danger" effect="plain">删除 {{ compareStat.removed }}</el-tag>
<el-tag type="info" effect="plain">相同 {{ compareStat.same }}</el-tag>
</div>
<el-alert
v-if="compareReady && !compareLoading && !compareRows.length"
title="这两版的有效期、重量段和附加费一致"
type="success"
:closable="false"
show-icon
/>
<el-table v-show="compareLoading || compareRows.length" v-loading="compareLoading" :data="compareRows" :row-class-name="compareRowClass" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="100" align="center" />
<el-table-column label="变化" width="90" align="center">
<template #default="{ row }">
<el-tag :type="changeTagType(row.change)" effect="light">{{ row.change }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="name" label="项目" min-width="180" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column label="旧版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'old')">{{ row.oldValue || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="新版" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="changeTextClass(row.change, 'new')">{{ row.newValue || '-' }}</span>
</template>
</el-table-column>
</el-table>
</div>
</el-drawer>
<el-drawer
@@ -178,6 +205,12 @@ const compareVisible = ref(false)
const compareLoading = ref(false)
const compareTitle = ref('版本比对')
const compareRows = ref([])
const compareAll = ref([])
const compareReady = ref(false)
const compareStat = computed(() => {
const count = (name) => compareAll.value.filter(row => row.change === name).length
return { changed: count('变更'), added: count('新增'), removed: count('删除'), same: count('相同') }
})
const detailTitle = computed(() => {
if (!detail.value) return '应付配置'
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
@@ -355,23 +388,60 @@ function onHistoryPick(rows) {
function openCompare() {
if (historyPicked.value.length !== 2) return
const [first, second] = historyPicked.value
const [first, second] = [...historyPicked.value].sort((a, b) => (Number(a.versionNo) || 0) - (Number(b.versionNo) || 0))
if (!first.productId || !second.productId) {
proxy.$modal.msgError('选中的版本没有价格数据')
return
}
compareTitle.value = `第 ${first.versionNo || '-'} 版 对比 第 ${second.versionNo || '-'} 版`
compareVisible.value = true
compareLoading.value = true
compareRows.value = []
compareAll.value = []
compareReady.value = false
comparePay(first.productId, second.productId).then(res => {
const data = res.data || {}
const older = data.older?.versionNo || '-'
const newer = data.newer?.versionNo || '-'
compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const weights = (data.weightDiffs || []).map(row => ({ ...row, kind: '重量段' }))
const fees = (data.surchargeDiffs || []).map(row => ({ ...row, kind: '附加费' }))
compareRows.value = weights.concat(fees).filter(row => row.change !== '相同')
const older = data.older?.versionNo
const newer = data.newer?.versionNo
if (older && newer) compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const rows = []
.concat(markKind(data.versionDiffs, '版本'))
.concat(markKind(data.weightDiffs, '重量段'))
.concat(markKind(data.surchargeDiffs, '附加费'))
.concat(markKind(data.extraDiffs, '其他配置'))
compareAll.value = rows
compareRows.value = rows.filter(row => row.change !== '相同')
compareReady.value = true
}).finally(() => {
compareLoading.value = false
})
}
function markKind(rows, kind) {
return (rows || []).map(row => ({ ...row, kind: row.kind || kind }))
}
function changeTagType(change) {
if (change === '变更') return 'primary'
if (change === '新增') return 'success'
if (change === '删除') return 'danger'
return 'info'
}
function compareRowClass({ row }) {
if (row.change === '变更') return 'compare-changed'
if (row.change === '新增') return 'compare-added'
if (row.change === '删除') return 'compare-removed'
return ''
}
function changeTextClass(change, side) {
if (change === '变更') return 'compare-text-changed'
if (change === '删除' && side === 'old') return 'compare-text-removed'
if (change === '新增' && side === 'new') return 'compare-text-added'
return ''
}
function loadHistory() {
if (!historyProductId.value) return
historyLoading.value = true
@@ -445,4 +515,34 @@ function statusType(status) {
margin-bottom: 12px;
color: var(--el-text-color-secondary);
}
.compare-text-changed {
color: var(--el-color-primary);
font-weight: 600;
}
.compare-text-added {
color: var(--el-color-success);
font-weight: 600;
}
.compare-text-removed {
color: var(--el-color-danger);
font-weight: 600;
}
.compare-panel :deep(tr.compare-changed > td.el-table__cell) {
background: var(--el-color-primary-light-9);
}
.compare-panel :deep(tr.compare-added > td.el-table__cell) {
background: var(--el-color-success-light-9);
}
.compare-panel :deep(tr.compare-removed > td.el-table__cell) {
background: var(--el-color-danger-light-9);
}
.compare-panel :deep(tr.compare-changed:hover > td.el-table__cell) {
background: var(--el-color-primary-light-8);
}
.compare-panel :deep(tr.compare-added:hover > td.el-table__cell) {
background: var(--el-color-success-light-8);
}
.compare-panel :deep(tr.compare-removed:hover > td.el-table__cell) {
background: var(--el-color-danger-light-8);
}
</style>
@@ -1,24 +1,57 @@
<template>
<el-dialog v-model="visible" :title="title" width="980px" append-to-body destroy-on-close @closed="reset">
<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>
<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">
<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.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 v-model="row.feeLevel" filterable clearable placeholder="请选择" style="width: 100%">
<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 label="价格" width="150">
<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-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%">
<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 }">
<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)">
<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" placeholder="价格" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="收费币种" width="140">
<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-option v-for="item in bms_currency" :key="item.value" :label="item.label" :value="item.value" />
@@ -91,10 +124,11 @@
<script setup>
import { listAirBillFee, listAirBillFeeLog, saveAirBillFee } from '@/api/price/airBill'
import { listSurchargeOptions } from '@/api/price/surcharge'
import { parseTime } from '@/utils/bms'
const { proxy } = getCurrentInstance()
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
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 visible = ref(false)
const loading = ref(false)
@@ -107,10 +141,14 @@ const logs = ref([])
const logPageNum = ref(1)
const logPageSize = ref(10)
const logTotal = ref(0)
const surchargeOptions = ref([])
const surchargeLoading = ref(false)
let rowSeed = 0
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) {
@@ -130,6 +168,9 @@ function reset() {
logPageSize.value = 10
logTotal.value = 0
logSeq += 1
surchargeSeq += 1
surchargeOptions.value = []
surchargeLoading.value = false
saving.value = false
loading.value = false
logLoading.value = false
@@ -139,6 +180,7 @@ function load() {
logPageNum.value = 1
loadFees()
loadLogs()
searchSurcharge('')
}
function loadFees() {
@@ -151,16 +193,58 @@ function loadFees() {
rowKey: `fee-${item.id}`,
id: item.id,
feeType: item.feeType == null ? '' : String(item.feeType),
feeName: item.feeName || '',
feeLevel: item.feeLevel == null ? '' : String(item.feeLevel),
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim == null ? '' : String(item.allocDim),
billFeeStatus: item.billFeeStatus == null || item.billFeeStatus === '' ? '0' : String(item.billFeeStatus),
price: item.price === null || item.price === undefined ? null : Number(item.price),
currency: item.currency || '',
remark: item.remark || '',
origin: snapshot(item)
}))
pinSelectedFees()
}).finally(() => {
if (code === billCode.value) loading.value = false
})
}
function pinSelectedFees() {
const known = new Map(surchargeOptions.value.map(item => [String(item.scId), item]))
for (const row of rows.value) {
if (!row.feeType) continue
if (!known.has(String(row.feeType))) {
known.set(String(row.feeType), { scId: String(row.feeType), scName: row.feeName || String(row.feeType) })
}
}
surchargeOptions.value = [...known.values()]
}
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()
}).catch(() => {
if (seq === surchargeSeq) 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 unwrapLogs(res) {
const rows = res?.rows
if (Array.isArray(rows)) {
@@ -207,6 +291,11 @@ function addRow() {
rowKey: `new-${rowSeed}`,
id: null,
feeType: '',
feeName: '',
feeLevel: '',
allocFlag: '0',
allocDim: '',
billFeeStatus: '0',
price: null,
currency: '',
remark: '',
@@ -214,6 +303,10 @@ function addRow() {
})
}
function changeAlloc(row) {
if (String(row.allocFlag) !== '1') row.allocDim = ''
}
function removeRow(row) {
if (row.id != null && !deletedIds.value.some(id => String(id) === String(row.id))) {
deletedIds.value.push(row.id)
@@ -224,6 +317,9 @@ function removeRow(row) {
function snapshot(item) {
return {
feeType: item.feeType || '',
feeLevel: item.feeLevel || '',
allocFlag: item.allocFlag == null || item.allocFlag === '' ? '0' : String(item.allocFlag),
allocDim: item.allocDim || '',
price: normPrice(item.price),
currency: item.currency || '',
remark: (item.remark || '').trim()
@@ -233,6 +329,9 @@ function snapshot(item) {
function currentOf(row) {
return {
feeType: row.feeType || '',
feeLevel: row.feeLevel || '',
allocFlag: row.allocFlag || '',
allocDim: String(row.allocFlag) === '1' ? (row.allocDim || '') : '',
price: normPrice(row.price),
currency: row.currency || '',
remark: (row.remark || '').trim()
@@ -248,20 +347,40 @@ function normPrice(value) {
function rowChanged(row) {
if (!row.origin) {
const now = currentOf(row)
return !!(now.feeType || now.price || now.currency || now.remark)
return !!(now.feeType || now.feeLevel || now.allocFlag === '1' || now.allocDim || now.price || now.currency || now.remark)
}
const now = currentOf(row)
return now.feeType !== row.origin.feeType || now.price !== row.origin.price
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
|| 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'
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 inserts = []
const updates = []
rows.value.forEach(row => {
if (!rowChanged(row)) return
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()
@@ -323,6 +442,18 @@ defineExpose({ open })
color: var(--el-text-color-secondary);
font-size: 12px;
}
.col-label {
display: inline-flex;
align-items: flex-start;
line-height: 1.2;
}
.req-star {
color: var(--el-color-danger);
font-style: normal;
font-weight: 700;
margin-right: 2px;
line-height: 1;
}
.log-title {
margin: 16px 0 8px;
font-weight: 600;