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:
@@ -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>
|
||||
Reference in New Issue
Block a user