Files
BMS/bms-front/src/views/monitor/billingTask/index.vue
T
2026-09-28 11:07:09 +08:00

302 lines
11 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">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:default-params="defaultParams"
row-key="taskId"
selection
>
<template #toolbar="{ ids }">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Refresh"
:disabled="!ids.length"
:loading="batchLoading"
v-hasPermi="['monitor:billingTask:retry']"
@click="handleBatchRetry(ids)"
>批量重新同步</el-button>
</el-col>
</template>
<template #col-billType="{ row }">
<el-tag size="small">{{ billTypeLabel(row.billType) }}</el-tag>
</template>
<template #col-successStatus="{ row }">
<el-tag size="small" :type="statusTag(row.successStatus)">{{ statusLabel(row.successStatus) }}</el-tag>
</template>
<template #col-errorMessage="{ row }">
<span class="err-text" :title="row.errorMessage || ''">{{ row.errorMessage || '-' }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)" v-hasPermi="['monitor:billingTask:list']">查看</el-button>
<el-button
link
type="warning"
icon="Refresh"
:disabled="row.successStatus === 'SUCCESS'"
:loading="retryingId === row.taskId"
v-hasPermi="['monitor:billingTask:retry']"
@click="handleRetry(row)"
>重新同步</el-button>
</template>
</basic-table>
<el-drawer v-model="open" size="600px" :title="drawerTitle" append-to-body destroy-on-close class="billing-task-drawer">
<div v-loading="loading" class="detail-wrap">
<el-empty v-if="!detail" description="暂无详情" />
<template v-else>
<div class="detail-header">
<el-tag :type="statusTag(detail.successStatus)" effect="light">{{ statusLabel(detail.successStatus) }}</el-tag>
<span class="detail-header-type">{{ billTypeLabel(detail.billType) }}</span>
<span class="detail-header-time">计费时间:{{ detail.billTime || '-' }}</span>
</div>
<el-descriptions :column="1" border size="small" class="detail-desc">
<el-descriptions-item label="任务ID">{{ detail.taskId }}</el-descriptions-item>
<el-descriptions-item label="订单号">{{ detail.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ detail.trackingNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="转单号">{{ detail.transferNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费订单ID">{{ detail.billId || '-' }}</el-descriptions-item>
<el-descriptions-item label="失败码">{{ detail.failCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div v-if="detail.errorMessage" class="err-block">
<div class="err-block-head">
<span class="err-block-title">失败原因</span>
<el-button link type="primary" size="small" icon="CopyDocument" @click="copyError">复制</el-button>
</div>
<div class="err-block-body">{{ detail.errorMessage }}</div>
</div>
</template>
</div>
</el-drawer>
</div>
</template>
<script setup name="BillingTaskMonitor">
import { listBillingTask, getBillingTask, retryBillingTask, retryBillingTaskBatch } from '@/api/monitor/billingTask'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const open = ref(false)
const loading = ref(false)
const detail = ref(null)
const drawerTitle = ref('计费任务详情')
const retryingId = ref(null)
const batchLoading = ref(false)
const defaultParams = { successStatus: 'FAILED' }
const statusOptions = [
{ label: '失败', value: 'FAILED' },
{ label: '待处理', value: 'WAITING' },
{ label: '成功', value: 'SUCCESS' },
{ label: '全部', value: 'ALL' }
]
const failCodeOptions = [
{ label: 'NO_CUSTOMER_PACKAGE', value: 'NO_CUSTOMER_PACKAGE' },
{ label: 'NO_ACTIVE_PACKAGE_VERSION', value: 'NO_ACTIVE_PACKAGE_VERSION' },
{ label: 'ALREADY_IN_AR_BILL', value: 'ALREADY_IN_AR_BILL' },
{ label: 'PRODUCT_ARCHIVE_MISSING', value: 'PRODUCT_ARCHIVE_MISSING' },
{ label: 'PRICE_NOT_HIT', value: 'PRICE_NOT_HIT' },
{ label: 'ZONE_PRICE_NOT_HIT', value: 'ZONE_PRICE_NOT_HIT' },
{ label: 'POSTCODE_REQUIRED', value: 'POSTCODE_REQUIRED' },
{ label: 'POSTCODE_ZONE_NOT_FOUND', value: 'POSTCODE_ZONE_NOT_FOUND' },
{ label: 'ORACLE_ORDER_NOT_FOUND', value: 'ORACLE_ORDER_NOT_FOUND' },
{ label: 'ORACLE_ORDERNO_EMPTY', value: 'ORACLE_ORDERNO_EMPTY' },
{ label: 'ORACLE_CUSTOMER_EMPTY', value: 'ORACLE_CUSTOMER_EMPTY' },
{ label: 'ORACLE_PRODUCT_EMPTY', value: 'ORACLE_PRODUCT_EMPTY' },
{ label: 'ORACLE_WEIGHT_INVALID', value: 'ORACLE_WEIGHT_INVALID' },
{ label: 'BILL_TIME_EMPTY', value: 'BILL_TIME_EMPTY' },
{ label: 'REFUND_ORDER_UNRESOLVED', value: 'REFUND_ORDER_UNRESOLVED' },
{ label: 'CUSTOMER_CODE_REQUIRED', value: 'CUSTOMER_CODE_REQUIRED' },
{ label: 'CUSTOMER_DISABLED', value: 'CUSTOMER_DISABLED' },
{ label: 'FREIGHT_MISSING', value: 'FREIGHT_MISSING' },
{ label: 'UNKNOWN', value: 'UNKNOWN' }
]
const billTypeOptions = [
{ label: '待计费', value: 'PENDING' },
{ label: '重计费', value: 'REBILL' },
{ label: '退费', value: 'REFUND' }
]
const searchColumns = [
{ label: '状态', prop: 'successStatus', component: 'select', options: statusOptions, placeholder: '请选择状态', width: '140px', props: { clearable: true } },
{ label: '计费类型', prop: 'billType', component: 'select', options: billTypeOptions, placeholder: '请选择类型', width: '140px', props: { clearable: true } },
{ label: '失败码', prop: 'failCode', component: 'select', options: failCodeOptions, placeholder: '请选择失败码', width: '220px', props: { clearable: true, filterable: true } },
{ label: '订单号', prop: 'orderNo', component: 'input', placeholder: '订单号', width: '180px' },
{ label: '运单号', prop: 'trackingNo', component: 'input', placeholder: '运单号', width: '180px' },
{ label: '转单号', prop: 'transferNo', component: 'input', placeholder: '转单号', width: '180px' }
]
const columns = [
{ label: '任务ID', prop: 'taskId', align: 'center', width: 100 },
{ label: '订单号', prop: 'orderNo', minWidth: 160, showOverflowTooltip: true },
{ label: '运单号', prop: 'trackingNo', minWidth: 150, showOverflowTooltip: true },
{ label: '转单号', prop: 'transferNo', minWidth: 160, showOverflowTooltip: true },
{ label: '计费类型', prop: 'billType', align: 'center', width: 100, slot: 'billType' },
{ label: '状态', prop: 'successStatus', align: 'center', width: 100, slot: 'successStatus' },
{ label: '计费时间', prop: 'billTime', align: 'center', width: 170 },
{ label: '失败原因', prop: 'errorMessage', minWidth: 220, slot: 'errorMessage', showOverflowTooltip: true },
{ label: '失败码', prop: 'failCode', align: 'center', width: 180, showOverflowTooltip: true },
{ label: '创建时间', prop: 'createTime', align: 'center', width: 170 },
{ label: '操作', prop: 'action', align: 'center', width: 180, fixed: 'right', slot: 'action', showInToolbar: false }
]
function queryList(params) {
return listBillingTask(params)
}
function billTypeLabel(type) {
const found = billTypeOptions.find(item => item.value === type)
return found ? found.label : (type || '-')
}
function statusLabel(status) {
const found = statusOptions.find(item => item.value === status)
return found ? found.label : (status || '-')
}
function statusTag(status) {
if (status === 'SUCCESS') return 'success'
if (status === 'FAILED') return 'danger'
if (status === 'WAITING') return 'warning'
return 'info'
}
function handleView(row) {
drawerTitle.value = '计费任务 #' + row.taskId
open.value = true
loading.value = true
detail.value = null
getBillingTask(row.taskId).then(res => {
detail.value = res.data || row
}).finally(() => {
loading.value = false
})
}
function handleRetry(row) {
proxy.$modal.confirm('确认重新同步任务 #' + row.taskId + ' ?').then(() => {
retryingId.value = row.taskId
return retryBillingTask(row.taskId)
}).then(res => {
const task = res.data || {}
if (task.successStatus === 'SUCCESS') {
proxy.$modal.msgSuccess('同步成功')
} else {
proxy.$modal.msgError(task.errorMessage || '同步失败')
}
tableRef.value.refresh()
}).catch(() => {}).finally(() => {
retryingId.value = null
})
}
function copyError() {
const text = detail.value?.errorMessage || ''
if (!text) return
navigator.clipboard.writeText(text).then(() => {
proxy.$modal.msgSuccess('已复制')
}).catch(() => {
proxy.$modal.msgError('复制失败')
})
}
function handleBatchRetry(ids) {
if (!ids || !ids.length) return
proxy.$modal.confirm('确认批量重新同步选中的 ' + ids.length + ' 条任务?').then(() => {
batchLoading.value = true
return retryBillingTaskBatch(ids)
}).then(res => {
const data = res.data || {}
proxy.$modal.msgSuccess('完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0))
tableRef.value.refresh()
}).catch(() => {}).finally(() => {
batchLoading.value = false
})
}
</script>
<style scoped>
.err-text {
color: #f56c6c;
word-break: break-all;
}
.detail-wrap {
padding: 0 4px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 16px;
background: #f8f9fb;
border-radius: 6px;
}
.detail-header-type {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.detail-header-time {
margin-left: auto;
font-size: 12px;
color: #909399;
}
.detail-desc :deep(.el-descriptions__label) {
width: 96px;
min-width: 96px;
color: #909399;
}
.detail-desc :deep(.el-descriptions__content) {
word-break: break-all;
}
.err-block {
margin-top: 16px;
border: 1px solid #fde2e2;
border-radius: 6px;
overflow: hidden;
}
.err-block-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
background: #fef0f0;
}
.err-block-title {
font-size: 13px;
font-weight: 600;
color: #f56c6c;
}
.err-block-body {
max-height: 260px;
overflow-y: auto;
padding: 10px 12px;
font-size: 12px;
font-family: Consolas, Monaco, monospace;
line-height: 1.7;
color: #c45656;
word-break: break-all;
white-space: pre-wrap;
background: #fff;
}
</style>