Files
BMS/bms-front/src/views/price/billing/bill.vue
T
2026-10-23 10:06:59 +08:00

879 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container billing-bill">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:search-per-row="4"
row-key="billId"
selection
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
:height="tableHeight"
virtual
>
<template #search-orderIdentifiers="{ model }">
<div class="order-identifier-search">
<el-input
v-model="model.orderIdentifiers"
type="textarea"
:rows="1"
resize="none"
placeholder="请输入订单号、运单号或转单号"
clearable
/>
</div>
</template>
<template #search-customerCodes="{ model }">
<customer-select
v-model="model.customerCodes"
type="customer"
value-key="customerCode"
label-key="customerName"
code-key="customerCode"
placeholder="客户名称/客户代码/对外代码"
multiple
remote-search
/>
</template>
<template #search-productCodes="{ model }">
<el-select
v-model="model.productCodes"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
placeholder="请输入"
style="width: 100%;"
>
<el-option v-for="p in productOptions" :key="p.code" :label="p.name" :value="p.code">
<span>{{ p.name }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">{{ p.code }}</span>
</el-option>
</el-select>
</template>
<template #col-product="{ row }">
{{ row.productName || '-' }} ({{ row.productCode }})
</template>
<template #col-country="{ row }">
{{ row.countryName || '-' }} ({{ row.countryCode || '-' }})
</template>
<template #col-chargeWeight="{ row }">
{{ formatWeight(row.chargeWeight) }}
</template>
<template #col-matchSource="{ row }">
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
</template>
<template #col-billingFailStatus="{ row }">
<el-tag size="small" :type="billingFailTagType(row.billingFailStatus)">{{ billingFailLabel(row.billingFailStatus) }}</el-tag>
</template>
<template #col-customerActiveStatus="{ row }">
<el-tag size="small" :type="customerActiveTagType(row.customerActiveStatus)">{{ customerActiveLabel(row.customerActiveStatus) }}</el-tag>
</template>
<template #col-creditArrearsStatus="{ row }">
<el-tag size="small" :type="creditArrearsTagType(row.creditArrearsStatus)">{{ creditArrearsLabel(row.creditArrearsStatus) }}</el-tag>
</template>
<template #col-refundStatus="{ row }">
<el-tag size="small" :type="refundTagType(row.refundStatus)">{{ refundLabel(row.refundStatus) }}</el-tag>
</template>
<template #col-incomeTotal="{ row }">
<span class="payable">{{ formatMoney(row.incomeTotal) }}</span>
</template>
<template #col-payTotal="{ row }">
<span class="payable">{{ formatMoney(row.payTotal) }}</span>
</template>
<template #toolbar>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
v-hasPermi="['price:billing:feeAdd']"
@click="openBatchFee"
>批量增加费用</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Upload"
v-hasPermi="['price:billing:chargeWeightEdit']"
@click="openChargeWeightImport"
>批量修改计费重</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Document"
v-hasPermi="['price:billing:chargeWeightEdit']"
@click="openChargeWeightImportAttach"
>导入记录</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Refresh"
v-hasPermi="['price:billing:recalc']"
@click="openRecalcDialog"
>重计费</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
v-hasPermi="['price:billing:export']"
@click="handleExport"
>导出</el-button>
</el-col>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
<el-button
link
type="primary"
icon="Edit"
:disabled="row.refundStatus === 'REFUNDED'"
v-hasPermi="['price:billing:chargeWeightEdit']"
@click="openEditChargeWeight(row)"
>改计费重</el-button>
<el-button
link
type="danger"
icon="RefreshLeft"
:loading="refundingKey === row.billId"
:disabled="row.refundStatus === 'REFUNDED'"
v-hasPermi="['price:billing:refund']"
@click="handleRefund(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"
/>
<edit-charge-weight-dialog
v-model="editCwOpen"
:bill-id="editCwRow?.billId || ''"
:order-no="editCwRow?.orderNo || ''"
:origin-weight="editCwRow?.weight"
:current-charge-weight="editCwRow?.chargeWeight"
@success="onChargeWeightUpdated"
/>
<excel-import-dialog
ref="chargeWeightImportRef"
title="批量修改计费重"
width="520px"
action="/price/billing/bill/chargeWeight/importData"
template-file-code="CHARGE_WEIGHT"
template-file-name="批量修改计费重导入模板"
:show-update-support="false"
:import-desc="chargeWeightImportDesc"
:template-download-handler="downloadChargeWeightImportTemplate"
:submit-handler="runChargeWeightImportAsync"
@success="onChargeWeightImportSuccess"
>
<template #extra>
<el-checkbox v-model="cwFillByFilter">按当前筛选条件带出订单号</el-checkbox>
<div class="cw-fill-hint">勾选后下载模板会写入筛选结果的订单号;人工修改计费重列留空待填</div>
</template>
</excel-import-dialog>
<charge-weight-import-attach-drawer ref="cwImportAttachRef" />
<el-dialog
v-model="recalcDialogOpen"
title="重计费"
width="420px"
append-to-body
destroy-on-close
>
<el-form label-width="90px">
<el-form-item label="计费类型" required>
<el-radio-group v-model="recalcMode">
<el-radio value="PRODUCT">产品价</el-radio>
<el-radio value="REDLINE">红线价</el-radio>
</el-radio-group>
</el-form-item>
<el-alert
type="info"
:closable="false"
show-icon
title="仅更新所选类型对应字段;有勾选按勾选,否则按当前筛选。已出账也可重算。"
/>
</el-form>
<template #footer>
<el-button @click="recalcDialogOpen = false">取消</el-button>
<el-button type="primary" :disabled="!recalcMode" @click="confirmRecalc">开始重计费</el-button>
</template>
</el-dialog>
<el-dialog
v-model="cwImportProgressVisible"
:title="cwImportProgressTitle"
width="420px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
append-to-body
>
<div class="import-progress">
<el-progress
:percentage="cwImportProgressPercent"
:status="cwImportProgressStatus"
:indeterminate="cwImportProgressIndeterminate"
:stroke-width="10"
/>
<div class="import-progress-msg">{{ cwImportProgressMessage }}</div>
</div>
</el-dialog>
</div>
</template>
<script setup name="BillingBill">
import { listBill, getBill, refundBill, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
import { exportAsyncFromTable } from '@/utils/excelExport'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useProducts } from '@/components/ProductSelect/useProducts'
import BillDetail from './components/BillDetail.vue'
import BatchAddIncomeFeeDialog from './components/BatchAddIncomeFeeDialog.vue'
import EditChargeWeightDialog from './components/EditChargeWeightDialog.vue'
import ExcelImportDialog from '@/components/ExcelImportDialog'
import ChargeWeightImportAttachDrawer from './components/ChargeWeightImportAttachDrawer.vue'
const MAX_ORDER_IDENTIFIERS = 1000
function parseOrderIdentifiers(raw) {
const values = Array.isArray(raw) ? raw : [raw]
const identifiers = values
.filter(v => v !== null && v !== undefined)
.flatMap(v => String(v).split(/[\s,,;;]+/))
.map(v => v.trim())
.filter(Boolean)
return [...new Set(identifiers)]
}
function getUniqueOrderIdentifierCount(raw) {
return parseOrderIdentifiers(raw).length
}
function validateOrderIdentifierLimit(raw) {
const count = getUniqueOrderIdentifierCount(raw)
if (count <= MAX_ORDER_IDENTIFIERS) return true
ElMessage.warning(`单号去重后最多输入 ${MAX_ORDER_IDENTIFIERS} 个,当前 ${count} 个`)
return false
}
// 产品下拉选项:复用 ProductSelect 的模块级产品缓存(全应用只拉一次)
const { productList, ensureProducts } = useProducts()
ensureProducts()
const productOptions = computed(() =>
productList.value
.filter(p => p.productCode)
.map(p => ({ code: p.productCode, name: p.productName || p.productCode }))
)
const tableRef = ref()
const tableHeight = ref(480)
let tableResizeObserver = null
function updateTableHeight() {
const tableEl = document.querySelector('.billing-bill > .basic-table .el-table, .billing-bill > .basic-table .basic-table-virtual')
if (!tableEl) return
const top = tableEl.getBoundingClientRect().top
const paginationReserve = 58
const bottomGap = 12
tableHeight.value = Math.max(240, Math.floor(window.innerHeight - top - paginationReserve - bottomGap))
}
onMounted(() => {
nextTick(() => {
updateTableHeight()
const form = document.querySelector('.billing-bill > .basic-table .basic-form')
if (form && typeof ResizeObserver !== 'undefined') {
tableResizeObserver = new ResizeObserver(() => updateTableHeight())
tableResizeObserver.observe(form)
}
})
window.addEventListener('resize', updateTableHeight)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateTableHeight)
if (tableResizeObserver) {
tableResizeObserver.disconnect()
tableResizeObserver = null
}
})
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 refundingKey = ref(null)
const editCwOpen = ref(false)
const editCwRow = ref(null)
const chargeWeightImportRef = ref()
const cwImportAttachRef = ref()
const chargeWeightImportDesc = [
'模板列:订单号、人工修改计费重(kg)',
'导入后异步写入计费重并重新计费;原计费重(weight)不覆盖',
'下载模板可选:按当前列表筛选条件带出订单号'
]
const cwFillByFilter = ref(false)
const cwImportProgressVisible = ref(false)
const cwImportProgressPercent = ref(0)
const cwImportProgressMessage = ref('')
const cwImportProgressStatus = ref(undefined)
const cwImportProgressIndeterminate = ref(false)
const cwImportProgressTitle = ref('正在导入修改计费重')
const recalcDialogOpen = ref(false)
const recalcMode = ref('PRODUCT')
const columns = [
{ label: '订单号', prop: 'orderNo', width: 160, fixed: 'left', showOverflowTooltip: true },
{ label: '运单号', prop: 'trackingNo', minWidth: 140, showOverflowTooltip: true },
{ label: '转单号', prop: 'transferNo', minWidth: 140, showOverflowTooltip: true },
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
{ label: '客户代码', prop: 'customerCode', width: 120 },
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
{ label: '件数', prop: 'pieces', align: 'center', width: 70 },
{ label: '计费重', prop: 'chargeWeight', align: 'center', width: 90, slot: 'chargeWeight' },
{ label: '原计费重', prop: 'weight', align: 'center', width: 100, slot: 'chargeWeight' },
{ label: '收货重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'chargeWeight' },
{ label: '应收总额', prop: 'incomeTotal', align: 'right', width: 120, slot: 'incomeTotal' },
{ label: '应付总额', prop: 'payTotal', align: 'right', width: 120, slot: 'payTotal' },
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 130, slot: 'matchSource' },
{ label: '计费状态', prop: 'billingFailStatus', align: 'center', width: 120, slot: 'billingFailStatus' },
{ label: '客户状态', prop: 'customerActiveStatus', align: 'center', width: 100, slot: 'customerActiveStatus' },
{ label: '额度欠费', prop: 'creditArrearsStatus', align: 'center', width: 100, slot: 'creditArrearsStatus' },
// { label: '退费状态', prop: 'refundStatus', align: 'center', width: 100, slot: 'refundStatus' },
{ label: '计费时间', prop: 'bizDate', align: 'center', width: 170, parseTime: true },
{ label: '下单时间', prop: 'orderTime', align: 'center', width: 170, parseTime: true },
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
{ label: '操作', prop: 'action', align: 'center', width: 240, fixed: 'right', slot: 'action', showInToolbar: false }
]
const arrearsStatusOptions = [
{ label: '正常', value: 'NORMAL' },
{ label: '欠费', value: 'ARREARS' }
]
const refundStatusOptions = [
{ label: '未退费', value: 'NONE' },
{ label: '已退费', value: 'REFUNDED' }
]
const billingFailStatusOptions = [
{ label: '计费成功', value: 0 },
{ label: '未匹配产品', value: 1 },
{ label: '未匹配国家', value: 2 },
{ label: '未匹配重量段', value: 3 },
]
const searchColumns = [
{ label: '单号', prop: 'orderIdentifiers', placeholder: '订单号/运单号/转单号' },
{ label: '客户', prop: 'customerCodes', placeholder: '客户名称/客户代码/对外代码' },
{ label: '产品代码', prop: 'productCodes', placeholder: '产品代码' },
{
label: '额度欠费',
prop: 'creditArrearsStatus',
component: 'select',
options: arrearsStatusOptions,
placeholder: '请选择',
props: { clearable: true }
},
{
label: '\u8ba1\u8d39\u5931\u8d25',
prop: 'billingFailStatuses',
component: 'select',
options: billingFailStatusOptions,
placeholder: '\u8bf7\u9009\u62e9',
props: {
multiple: true,
clearable: true,
collapseTags: true,
collapseTagsTooltip: true
}
},
{
label: '计费时间',
prop: 'bizDateRange',
component: 'datetimerange',
colSpan: 2,
props: {
clearable: true,
// 只选日期时自动补全为当天 00:00:00 ~ 23:59:59
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
}
}
]
function buildBillQuery(params) {
const { bizDateRange, orderIdentifiers: rawOrderIdentifiers, ...rest } = params || {}
const query = { ...rest }
const orderIdentifiers = parseOrderIdentifiers(rawOrderIdentifiers)
if (orderIdentifiers.length) query.orderIdentifiers = orderIdentifiers
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
query.params = {
...(query.params || {}),
beginBizDate: bizDateRange[0] || null,
endBizDate: bizDateRange[1] || null
}
}
return query
}
function queryList(params) {
if (!validateOrderIdentifierLimit(params?.orderIdentifiers)) {
return Promise.reject(new Error('order identifier limit exceeded'))
}
const query = buildBillQuery(params)
return listBill(query)
}
function handleExport() {
const params = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
return exportAsyncFromTable({
baseUrl: '/price/billing/bill/export',
tableRef: tableRef.value,
normalize: buildBillQuery,
filePrefix: 'billing_bill',
requireFilter: true
})
}
function handleUpdateArrears(ids, selection) {
const billIds = (ids && ids.length)
? ids
: (selection || []).map(row => row.billId).filter(Boolean)
if (!billIds.length) {
ElMessage.warning('请勾选要更新的订单')
return
}
const arrearsCount = (selection || []).filter(row => row.creditArrearsStatus === 'ARREARS').length
ElMessageBox.confirm(
'确认将勾选的 ' + billIds.length + ' 条订单更新欠费状态为正常?将逐单校验可用额度,校验不通过的不会更新' +
(arrearsCount < billIds.length ? '(当前勾选中欠费 ' + arrearsCount + ' 条)' : '') + '。',
'更新欠费状态',
{ type: 'warning' }
).then(() => {
return updateCreditArrears(billIds)
}).then(res => {
ElMessage.success(res.msg || '更新完成')
tableRef.value?.refresh?.()
}).catch(() => {})
}
function handleRefund(row) {
if (!row || !row.billId) return
if (row.refundStatus === 'REFUNDED') {
ElMessage.warning('该订单已退费')
return
}
ElMessageBox.confirm(
'确认对订单 ' + (row.orderNo || '') + ' 发起退费?',
'退费确认',
{ type: 'warning' }
).then(() => {
refundingKey.value = row.billId
return refundBill(row.billId)
}).then(() => {
ElMessage.success('退费成功')
tableRef.value?.refresh?.()
}).catch(() => {}).finally(() => {
refundingKey.value = null
})
}
function handleView(row) {
title.value = '计费详情'
open.value = true
loading.value = true
detail.value = null
getBill(row.billId).then(res => {
detail.value = res.data || null
}).finally(() => {
loading.value = false
})
}
function onDetailUpdated(data) {
detail.value = data
tableRef.value?.refresh?.()
}
function hasBatchFeeQuery(q) {
const hasArr = (arr) => Array.isArray(arr) && arr.some(v => v != null && String(v).trim() !== '')
return parseOrderIdentifiers(q.orderIdentifiers).length > 0
|| hasArr(q.orderNos)
|| hasArr(q.trackingNos)
|| hasArr(q.transferNos)
|| hasArr(q.customerCodes)
|| hasArr(q.productCodes)
|| hasArr(q.billingFailStatuses)
|| hasArr(q.bizDateRange)
|| !!(q.creditArrearsStatus && String(q.creditArrearsStatus).trim())
}
function openBatchFee() {
const q = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return
const { pageNum, pageSize, bizDateRange, ...rest } = q
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
ElMessage.warning('请先设置列表查询条件')
return
}
const listQuery = {
...rest,
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers)
}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
listQuery.beginBizDate = bizDateRange[0] || null
listQuery.endBizDate = bizDateRange[1] || null
}
batchFeeListQuery.value = listQuery
batchFeeSelectedIds.value = tableRef.value?.getSelectionIds?.() || []
batchFeeOpen.value = true
}
function onBatchFeeSuccess() {
tableRef.value?.refresh?.()
}
function buildRecalcPayload() {
const q = tableRef.value?.getQueryParams?.() || {}
if (!validateOrderIdentifierLimit(q.orderIdentifiers)) return null
const { pageNum, pageSize, bizDateRange, ...rest } = q
if (!hasBatchFeeQuery({ ...rest, bizDateRange })) {
ElMessage.warning('请先设置列表查询条件')
return null
}
const selectedIds = (tableRef.value?.getSelectionIds?.() || []).filter(Boolean)
const payload = {
scopeMode: selectedIds.length ? 'SELECTED' : 'ALL',
billIds: selectedIds,
orderNos: rest.orderNos || [],
trackingNos: rest.trackingNos || [],
transferNos: rest.transferNos || [],
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers),
customerCodes: rest.customerCodes || [],
productCodes: rest.productCodes || [],
creditArrearsStatus: rest.creditArrearsStatus || undefined
}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
payload.beginBizDate = bizDateRange[0] || null
payload.endBizDate = bizDateRange[1] || null
}
return payload
}
function openRecalcDialog() {
const payload = buildRecalcPayload()
if (!payload) return
recalcMode.value = 'PRODUCT'
recalcDialogOpen.value = true
}
function confirmRecalc() {
if (!recalcMode.value) {
ElMessage.warning('请选择计费类型')
return
}
recalcDialogOpen.value = false
runRecalc(recalcMode.value)
}
async function runRecalc(mode) {
const payload = buildRecalcPayload()
if (!payload) return
const label = mode === 'REDLINE' ? '红线价' : '产品价'
cwImportProgressTitle.value = '正在重算' + label
cwImportProgressVisible.value = true
cwImportProgressPercent.value = 0
cwImportProgressIndeterminate.value = false
cwImportProgressStatus.value = undefined
cwImportProgressMessage.value = '任务提交中…'
try {
const { task } = await recalcBillPriceAsync(mode, payload, (t) => {
cwImportProgressPercent.value = Math.min(100, Number(t.percent) || 0)
const total = Number(t.total) || 0
const success = Number(t.success) || 0
const fail = Number(t.fail) || 0
if (t.status === 'PENDING') cwImportProgressMessage.value = '排队中…'
else if (t.status === 'RUNNING') {
cwImportProgressMessage.value = '正在处理 ' + success + '/' + total +
(fail ? ('(失败 ' + fail + ')') : '')
}
})
const success = Number(task && task.success) || 0
const fail = Number(task && task.fail) || 0
const tip = label + '重算完成:成功 ' + success + ',失败 ' + fail
cwImportProgressPercent.value = 100
cwImportProgressStatus.value = fail > 0 ? 'warning' : 'success'
cwImportProgressMessage.value = tip + (fail && task.error ? (';' + task.error) : '')
if (fail > 0) ElMessage.warning(cwImportProgressMessage.value)
else ElMessage.success(tip)
tableRef.value?.refresh?.()
} catch (e) {
cwImportProgressStatus.value = 'exception'
const tip = (e && e.task && e.task.error) || (e && e.message) || '重算失败'
cwImportProgressMessage.value = tip
ElMessage.error(tip)
} finally {
setTimeout(() => { cwImportProgressVisible.value = false }, 800)
}
}
function openEditChargeWeight(row) {
if (!row || !row.billId) return
if (row.refundStatus === 'REFUNDED') {
ElMessage.warning('该订单已退费')
return
}
editCwRow.value = row
editCwOpen.value = true
}
function onChargeWeightUpdated() {
tableRef.value?.refresh?.()
}
function buildChargeWeightTemplateQuery() {
const q = tableRef.value?.getQueryParams?.() || {}
const { pageNum, pageSize, bizDateRange, ...rest } = q
const payload = {
fillByFilter: !!cwFillByFilter.value,
orderNos: rest.orderNos || [],
trackingNos: rest.trackingNos || [],
transferNos: rest.transferNos || [],
orderIdentifiers: parseOrderIdentifiers(rest.orderIdentifiers),
customerCodes: rest.customerCodes || [],
productCodes: rest.productCodes || [],
creditArrearsStatus: rest.creditArrearsStatus || undefined
}
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
payload.beginBizDate = bizDateRange[0] || null
payload.endBizDate = bizDateRange[1] || null
}
return payload
}
async function downloadChargeWeightImportTemplate() {
const currentQuery = tableRef.value?.getQueryParams?.() || {}
if (cwFillByFilter.value && !validateOrderIdentifierLimit(currentQuery.orderIdentifiers)) return
const payload = buildChargeWeightTemplateQuery()
// Checked: backend fills orderNos from list filters. Unchecked: OSS template CHARGE_WEIGHT.
if (payload.fillByFilter) {
if (!hasBatchFeeQuery(tableRef.value?.getQueryParams?.() || {})) {
throw new Error('请先设置列表筛选条件')
}
await downloadChargeWeightTemplate(payload)
return
}
const { downloadFileTemplate } = await import('@/api/system/fileTemplate')
await downloadFileTemplate('CHARGE_WEIGHT', '批量修改计费重导入模板')
}
function openChargeWeightImportAttach() {
cwImportAttachRef.value?.open?.()
}
function openChargeWeightImport() {
cwFillByFilter.value = false
chargeWeightImportRef.value?.open?.()
}
function applyCwImportProgress(t) {
cwImportProgressIndeterminate.value = false
cwImportProgressPercent.value = Math.min(100, Number(t.percent) || 0)
const total = Number(t.total) || 0
const success = Number(t.success) || 0
const fail = Number(t.fail) || 0
if (t.status === 'PENDING') {
cwImportProgressMessage.value = '\u6392\u961f\u4e2d\u2026'
} else if (t.status === 'RUNNING') {
if (total > 0) {
cwImportProgressMessage.value = '\u6b63\u5728\u5904\u7406 ' + success + '/' + total +
(fail ? ('\uff08\u5931\u8d25 ' + fail + '\uff09') : '')
} else {
cwImportProgressMessage.value = t.message || '\u6b63\u5728\u5bfc\u5165\u2026'
}
} else if (t.status === 'SUCCESS') {
cwImportProgressMessage.value = '\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail
cwImportProgressStatus.value = fail > 0 ? 'warning' : 'success'
} else if (t.status === 'FAIL') {
cwImportProgressMessage.value = t.error || t.message || '\u5bfc\u5165\u5931\u8d25'
cwImportProgressStatus.value = 'exception'
}
}
async function runChargeWeightImportAsync(uploadFile) {
const raw = uploadFile && (uploadFile.raw || uploadFile)
if (!raw) throw new Error('\u8bf7\u5148\u9009\u62e9\u6587\u4ef6')
// Close import dialog so progress dialog is the only visible wait UI
chargeWeightImportRef.value?.close?.()
cwImportProgressTitle.value = '正在导入修改计费重'
cwImportProgressVisible.value = true
cwImportProgressPercent.value = 0
cwImportProgressIndeterminate.value = true
cwImportProgressMessage.value = '\u6b63\u5728\u89e3\u6790Excel\u5e76\u6821\u9a8c\u8ba2\u5355\uff0c\u8bf7\u7a0d\u5019\u2026'
cwImportProgressStatus.value = undefined
try {
const { task } = await importChargeWeightAsync(raw, applyCwImportProgress)
cwImportProgressIndeterminate.value = false
cwImportProgressPercent.value = 100
const success = Number(task && task.success) || 0
const fail = Number(task && task.fail) || 0
const tip = '\u5bfc\u5165\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail
if (fail > 0) {
cwImportProgressStatus.value = 'warning'
cwImportProgressMessage.value = tip + (task && task.error ? ('\uff1b' + task.error) : '')
await new Promise(resolve => setTimeout(resolve, 1500))
return tip + (task && task.error ? ('\uff1b' + task.error) : '')
}
cwImportProgressStatus.value = 'success'
cwImportProgressMessage.value = tip
await new Promise(resolve => setTimeout(resolve, 1200))
return tip
} catch (e) {
cwImportProgressIndeterminate.value = false
cwImportProgressStatus.value = 'exception'
const tip = (e && e.task && e.task.error) || (e && e.message) || '\u5bfc\u5165\u5931\u8d25'
cwImportProgressMessage.value = tip
await new Promise(resolve => setTimeout(resolve, 1200))
throw e
} finally {
cwImportProgressVisible.value = false
cwImportProgressIndeterminate.value = false
}
}
function onChargeWeightImportSuccess() {
tableRef.value?.refresh?.()
}
function formatMoney(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(2)
}
function formatWeight(val) {
if (val === null || val === undefined || val === '') return '-'
const num = Number(val)
if (Number.isNaN(num)) return val
return num.toFixed(3)
}
function billingFailLabel(status) {
const code = status == null ? 0 : Number(status)
const hit = billingFailStatusOptions.find(item => item.value === code)
return hit ? hit.label : '未知失败'
}
function billingFailTagType(status) {
const code = status == null ? 0 : Number(status)
if (code > 0) return 'danger'
return 'success'
}
function sourceLabel(source) {
const map = {
PACKAGE_FIXED: '客户固定价套餐',
PACKAGE_DISCOUNT: '客户折扣套餐',
PRODUCT_DEFAULT: '产品默认价',
BILLING_FAIL: '计费失败',
FIXED: '客户固定价套餐',
DISCOUNT: '客户折扣套餐'
}
return map[source] || source || '-'
}
function sourceTagType(source) {
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
if (source === 'PRODUCT_DEFAULT') return 'info'
if (source === 'BILLING_FAIL') return 'danger'
return ''
}
function customerActiveLabel(status) {
const map = { ACTIVE: '正常', MISSING: '不存在', DISABLED: '停用' }
return map[status] || status || '-'
}
function customerActiveTagType(status) {
if (status === 'ACTIVE') return 'success'
if (status === 'DISABLED') return 'warning'
if (status === 'MISSING') return 'danger'
return 'info'
}
function creditArrearsLabel(status) {
const map = { NORMAL: '正常', ARREARS: '欠费' }
return map[status] || status || '-'
}
function creditArrearsTagType(status) {
if (status === 'NORMAL') return 'success'
if (status === 'ARREARS') return 'danger'
return 'info'
}
function refundLabel(status) {
const map = { NONE: '未退费', REFUNDED: '已退费' }
return map[status] || status || '未退费'
}
function refundTagType(status) {
if (status === 'REFUNDED') return 'danger'
return 'success'
}
</script>
<style scoped>
.billing-bill .payable {
color: #f56c6c;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.cw-fill-hint {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.import-progress { padding: 8px 4px 4px; }
.import-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;
}
/* 搜索栏走 BasicTable search-per-row=4 的栅格布局(固定 4 项一行,多余换行),
这里只补齐插槽渲染的控件宽度,不再覆盖 BasicForm 的布局 */
.billing-bill :deep(.basic-form .el-form-item .el-select),
.billing-bill :deep(.basic-form .el-form-item .el-input),
.billing-bill :deep(.basic-form .el-form-item .el-date-editor) {
width: 100%;
}
.order-identifier-search { width: 100%; }
</style>