879 lines
31 KiB
Vue
879 lines
31 KiB
Vue
<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>
|