302 lines
11 KiB
Vue
302 lines
11 KiB
Vue
<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>
|