Implement batch fee management features in BillingController, including preview, submission, and task retrieval. Update related services and mappers to support batch processing. Add necessary error messages in properties file and enhance front-end API for batch operations.

This commit is contained in:
wuweihua
2026-10-09 10:28:29 +08:00
parent 95df65b4f6
commit 5b4cbaffd6
15 changed files with 1014 additions and 23 deletions
+44 -8
View File
@@ -70,24 +70,30 @@
<template #col-payTotal="{ row }">
<span class="payable">{{ formatMoney(row.payTotal) }}</span>
</template>
<!-- <template #toolbar="{ selection, ids }">
<template #toolbar>
<el-col :span="1.5">
<el-button
type="warning"
type="primary"
plain
icon="Refresh"
:disabled="!selection.length"
v-hasPermi="['price:billing:arrearsUpdate']"
@click="handleUpdateArrears(ids, selection)"
>更新欠费状态</el-button>
icon="Plus"
v-hasPermi="['price:billing:feeAdd']"
@click="openBatchFee"
>批量增加费用</el-button>
</el-col>
</template> -->
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
</template>
</basic-table>
<bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" @updated="onDetailUpdated" />
<batch-add-income-fee-dialog
v-model="batchFeeOpen"
:list-query="batchFeeListQuery"
:selected-ids="batchFeeSelectedIds"
@success="onBatchFeeSuccess"
/>
</div>
</template>
@@ -96,6 +102,7 @@ import { listBill, getBill, listOrderNoOptions, listTrackingNoOptions, listTrans
import { ElMessage, ElMessageBox } from 'element-plus'
import { useProducts } from '@/components/ProductSelect/useProducts'
import BillDetail from './components/BillDetail.vue'
import BatchAddIncomeFeeDialog from './components/BatchAddIncomeFeeDialog.vue'
// 订单号下拉远程搜索:返回匹配的订单号列表(最多 20 条)
function searchOrderNoOptions(keyword) {
@@ -126,6 +133,9 @@ const open = ref(false)
const title = ref('计费详情')
const loading = ref(false)
const detail = ref(null)
const batchFeeOpen = ref(false)
const batchFeeListQuery = ref({})
const batchFeeSelectedIds = ref([])
const columns = [
{ label: '订单号', prop: 'orderNo', minWidth: 140, showOverflowTooltip: true },
@@ -213,6 +223,32 @@ function onDetailUpdated(data) {
tableRef.value?.refresh?.()
}
function hasBatchFeeQuery(q) {
const hasArr = (arr) => Array.isArray(arr) && arr.some(v => v != null && String(v).trim() !== '')
return hasArr(q.orderNos)
|| hasArr(q.trackingNos)
|| hasArr(q.transferNos)
|| hasArr(q.customerCodes)
|| hasArr(q.productCodes)
|| !!(q.creditArrearsStatus && String(q.creditArrearsStatus).trim())
}
function openBatchFee() {
const q = tableRef.value?.getQueryParams?.() || {}
const { pageNum, pageSize, ...rest } = q
if (!hasBatchFeeQuery(rest)) {
ElMessage.warning('请先设置列表查询条件')
return
}
batchFeeListQuery.value = { ...rest }
batchFeeSelectedIds.value = tableRef.value?.getSelectionIds?.() || []
batchFeeOpen.value = true
}
function onBatchFeeSuccess() {
tableRef.value?.refresh?.()
}
function formatMoney(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
@@ -0,0 +1,354 @@
<template>
<el-dialog
:model-value="modelValue"
:title="dlgTitle"
width="640px"
append-to-body
destroy-on-close
@update:model-value="$emit('update:modelValue', $event)"
>
<el-form label-width="110px" class="batch-scope">
<el-form-item :label="lblQuery">
<div v-if="queryTags.length" class="query-tags">
<el-tag
v-for="(t, i) in queryTags"
:key="i"
class="query-tag"
type="info"
effect="plain"
>{{ t }}</el-tag>
</div>
<span v-else class="empty-query">{{ emptyQuery }}</span>
</el-form-item>
<el-form-item :label="lblScope" required>
<el-radio-group v-model="scopeMode">
<el-radio value="ALL">{{ optAll }}</el-radio>
<el-radio value="SELECTED">{{ optSelected }}</el-radio>
</el-radio-group>
<div v-if="scopeMode === 'SELECTED'" class="selected-tip">{{ selectedTip }}</div>
</el-form-item>
</el-form>
<el-divider content-position="left">{{ feeSection }}</el-divider>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item :label="lblFeeName" prop="scId">
<el-select
v-model="form.scId"
filterable
clearable
:placeholder="phSurcharge"
style="width: 100%;"
@change="onSurchargeChange"
>
<el-option
v-for="s in surchargeOptions"
:key="s.scId"
:label="s.scName"
:value="s.scId"
>
<span>{{ s.scName }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">{{ s.chargeValue }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="lblFeeType" prop="feeType">
<el-select v-model="form.feeType" :placeholder="phFeeType" style="width: 100%;">
<el-option
v-for="d in (bms_fee_type || [])"
:key="d.value"
:label="d.label"
:value="d.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="lblAmount" prop="amount">
<el-input-number
v-model="form.amount"
:min="0"
:precision="4"
:step="0.1"
controls-position="right"
style="width: 100%;"
/>
<div class="amount-hint">{{ amountHint }}</div>
</el-form-item>
<el-form-item :label="lblCurrency" prop="currency">
<el-select v-model="form.currency" style="width: 100%;">
<el-option
v-for="d in (bms_currency || [])"
:key="d.value"
:label="d.label"
:value="d.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="lblRemark">
<el-input v-model="form.remark" type="textarea" :rows="2" :placeholder="phRemark" maxlength="200" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="$emit('update:modelValue', false)" :disabled="submitting">{{ btnCancel }}</el-button>
<el-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="submit">{{ btnOk }}</el-button>
</template>
</el-dialog>
<el-dialog
v-model="progressVisible"
:title="progressTitle"
width="420px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
append-to-body
>
<div class="batch-progress">
<el-progress :percentage="progressPercent" :status="progressStatus" />
<div class="batch-progress-msg">{{ progressMessage }}</div>
</div>
</el-dialog>
</template>
<script setup>
import { listSurchargeOptions } from '@/api/price/surcharge'
import { batchAddIncomeFeeAsync } from '@/api/price/billing'
const props = defineProps({
modelValue: { type: Boolean, default: false },
listQuery: { type: Object, default: () => ({}) },
selectedIds: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'success'])
const { proxy } = getCurrentInstance()
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const dlgTitle = "批量增加费用"
const lblScope = "添加范围"
const optAll = "全部加(当前列表查询条件)"
const optSelected = "只加列表勾选"
const lblQuery = "列表查询条件"
const emptyQuery = "无(无查询条件时不能添加费用)"
const feeSection = "费用信息"
const lblFeeName = "费用名称"
const lblFeeType = "费用类型"
const lblAmount = "金额"
const lblCurrency = "币种"
const lblRemark = "说明"
const phSurcharge = "请选择附加费"
const phFeeType = "请选择费用类型"
const phRemark = "选填"
const btnCancel = "取消"
const btnOk = "确认添加"
const amountHint = "留空则按附加费计费方式逐单计算;填写则所有订单使用该固定金额"
const progressTitle = "正在批量增加费用"
const formRef = ref()
const submitting = ref(false)
const surchargeOptions = ref([])
const scopeMode = ref('')
const progressVisible = ref(false)
const progressPercent = ref(0)
const progressMessage = ref('')
const progressStatus = ref(undefined)
const form = ref(emptyForm())
const rules = {
scId: [{ required: true, message: "请选择附加费", trigger: 'change' }],
feeType: [{ required: true, message: "请选择费用类型", trigger: 'change' }],
currency: [{ required: true, message: "请选择币种", trigger: 'change' }]
}
const selectedCount = computed(() => (props.selectedIds || []).filter(Boolean).length)
const selectedTip = computed(() => "已勾选 " + selectedCount.value + " 条")
const queryTags = computed(() => buildQueryTags(props.listQuery || {}))
const hasQuery = computed(() => queryTags.value.length > 0)
const canSubmit = computed(() => {
if (!hasQuery.value) return false
if (!scopeMode.value) return false
if (!form.value.scId) return false
if (scopeMode.value === 'SELECTED') return selectedCount.value > 0
return true
})
function emptyForm() {
return {
scId: undefined,
feeType: 'SURCHARGE',
amount: undefined,
currency: 'CNY',
remark: ''
}
}
function fmtList(arr, max = 5) {
const list = (arr || []).filter(v => v != null && String(v).trim() !== '')
if (!list.length) return ''
const shown = list.slice(0, max).map(String)
return shown.join(', ') + (list.length > max ? ' …(+'+ (list.length - max) + ')' : '')
}
function buildQueryTags(q) {
const tags = []
const order = fmtList(q.orderNos)
if (order) tags.push("订单号" + ':' + order)
const tracking = fmtList(q.trackingNos)
if (tracking) tags.push("运单号" + ':' + tracking)
const transfer = fmtList(q.transferNos)
if (transfer) tags.push("转单号" + ':' + transfer)
const customer = fmtList(q.customerCodes)
if (customer) tags.push("客户" + ':' + customer)
const product = fmtList(q.productCodes)
if (product) tags.push("产品" + ':' + product)
if (q.creditArrearsStatus) {
const label = q.creditArrearsStatus === 'ARREARS'
? "欠费"
: (q.creditArrearsStatus === 'NORMAL' ? "正常" : q.creditArrearsStatus)
tags.push("额度欠费" + ':' + label)
}
return tags
}
function loadSurcharges() {
return listSurchargeOptions({ ssCode: 'on' }).then(res => {
surchargeOptions.value = res.data || []
})
}
watch(() => props.modelValue, (open) => {
if (!open) return
scopeMode.value = ''
form.value = emptyForm()
loadSurcharges().then(() => nextTick(() => formRef.value?.clearValidate()))
})
function onSurchargeChange(scId) {
const s = (surchargeOptions.value || []).find(x => String(x.scId) === String(scId))
if (!s) return
form.value.feeType = s.feeType || 'SURCHARGE'
form.value.currency = 'CNY'
const type = String(s.chargeType || 'FIXED').toUpperCase()
if (type === 'FIXED' && s.chargeValue != null) {
form.value.amount = Number(Number(s.chargeValue).toFixed(4))
} else {
form.value.amount = undefined
}
if (s.scNote && !form.value.remark) {
form.value.remark = s.scNote
}
}
function buildPayload() {
const q = props.listQuery || {}
const payload = {
scopeMode: scopeMode.value,
scId: form.value.scId,
feeType: form.value.feeType,
amount: form.value.amount,
currency: form.value.currency || 'CNY',
remark: form.value.remark,
orderNos: q.orderNos || [],
trackingNos: q.trackingNos || [],
transferNos: q.transferNos || [],
customerCodes: q.customerCodes || [],
productCodes: q.productCodes || [],
creditArrearsStatus: q.creditArrearsStatus || undefined
}
if (scopeMode.value === 'SELECTED') {
payload.billIds = (props.selectedIds || []).filter(Boolean)
}
return payload
}
function submit() {
formRef.value?.validate(valid => {
if (!valid) return
if (!hasQuery.value) {
proxy.$modal.msgWarning("请先在列表设置查询条件")
return
}
if (!scopeMode.value) {
proxy.$modal.msgWarning("请选择添加范围")
return
}
if (scopeMode.value === 'SELECTED' && selectedCount.value <= 0) {
proxy.$modal.msgWarning("请先在列表勾选要增加费用的订单")
return
}
const confirmMsg = scopeMode.value === 'SELECTED'
? ("确认向勾选的 " + selectedCount.value + " 条订单批量增加费用?")
: "确认按当前列表查询条件批量增加费用?"
proxy.$modal.confirm(confirmMsg).then(() => {
submitting.value = true
progressVisible.value = true
progressPercent.value = 0
progressMessage.value = "任务提交中…"
progressStatus.value = undefined
return batchAddIncomeFeeAsync(buildPayload(), (task) => {
progressPercent.value = Math.min(100, Number(task.percent) || 0)
const total = Number(task.total) || 0
const success = Number(task.success) || 0
const fail = Number(task.fail) || 0
if (task.status === 'PENDING') progressMessage.value = "排队中…"
else if (task.status === 'RUNNING') {
progressMessage.value = "正在处理 " + success + '/' + total + (fail ? ("(失败 " + fail + ")") : '')
} else if (task.status === 'SUCCESS') {
progressMessage.value = "完成:成功 " + success + ",失败 " + fail
progressStatus.value = fail > 0 ? 'warning' : 'success'
}
}).then(({ task }) => {
progressPercent.value = 100
const success = Number(task.success) || 0
const fail = Number(task.fail) || 0
const tip = "批量增加完成:成功 " + success + ",失败 " + fail
if (fail > 0) proxy.$modal.msgWarning(tip + (task.error ? ';' + task.error : ''))
else proxy.$modal.msgSuccess(tip)
emit('update:modelValue', false)
emit('success', task)
}).catch((e) => {
progressStatus.value = 'exception'
const tip = (e && e.task && e.task.error) || (e && e.message) || "批量增加失败"
progressMessage.value = tip
proxy.$modal.msgError(tip)
}).finally(() => {
submitting.value = false
setTimeout(() => { progressVisible.value = false }, 600)
})
}).catch(() => {})
})
}
</script>
<style scoped>
.batch-scope { margin-bottom: 4px; }
.query-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.query-tag { max-width: 100%; }
.empty-query {
color: var(--el-color-danger);
font-size: 13px;
}
.selected-tip {
margin-top: 6px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.amount-hint {
margin-top: 4px;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.4;
}
.batch-progress { padding: 8px 4px 4px; }
.batch-progress-msg {
margin-top: 12px;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.5;
word-break: break-word;
max-height: 160px;
overflow: auto;
}
</style>