330 lines
9.5 KiB
Vue
330 lines
9.5 KiB
Vue
<template>
|
||
<div class="app-container uuid-to-new-page">
|
||
<el-card shadow="never" class="progress-card">
|
||
<div class="progress-head">
|
||
<div class="title">UUID 阴影同步进度</div>
|
||
<div class="actions">
|
||
<span class="switch-label">线程</span>
|
||
<el-select
|
||
v-model="threadCount"
|
||
size="small"
|
||
style="width: 88px"
|
||
:disabled="configLocked || configLoading"
|
||
v-hasPermi="['monitor:uuidToNew:switch']"
|
||
@change="onConfigChange"
|
||
>
|
||
<el-option v-for="n in threadOptions" :key="n" :label="n + '线程'" :value="n" />
|
||
</el-select>
|
||
<span class="switch-label">批次</span>
|
||
<el-select
|
||
v-model="batchSize"
|
||
size="small"
|
||
style="width: 100px"
|
||
:disabled="configLocked || configLoading"
|
||
v-hasPermi="['monitor:uuidToNew:switch']"
|
||
@change="onConfigChange"
|
||
>
|
||
<el-option v-for="n in batchOptions" :key="n" :label="String(n)" :value="n" />
|
||
</el-select>
|
||
<span class="switch-label">连续同步</span>
|
||
<el-switch
|
||
v-model="switchOn"
|
||
:loading="switchLoading"
|
||
:disabled="switchLoading"
|
||
inline-prompt
|
||
active-text="开"
|
||
inactive-text="关"
|
||
v-hasPermi="['monitor:uuidToNew:switch']"
|
||
@change="onSwitchChange"
|
||
/>
|
||
<el-tag :type="progress.running ? 'success' : (progress.enabled ? 'warning' : 'info')" size="small">
|
||
{{ statusText }}
|
||
</el-tag>
|
||
<el-button size="small" icon="Refresh" :loading="progressLoading" @click="refreshAll">刷新</el-button>
|
||
</div>
|
||
</div>
|
||
<el-row :gutter="16" class="stat-row">
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num">{{ formatNum(progress.oldCnt) }}</div>
|
||
<div class="label">旧表订单</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num ok">{{ formatNum(progress.doneCnt) }}</div>
|
||
<div class="label">已同步 SUCCESS</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num warn">{{ formatNum(progress.pendingCnt) }}</div>
|
||
<div class="label">积压</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num">{{ formatNum(progress.newCnt) }}</div>
|
||
<div class="label">阴影订单行</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num">{{ progress.donePercent }}%</div>
|
||
<div class="label">完成比例</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<div class="stat">
|
||
<div class="num cfg">{{ progress.threadCount }} / {{ progress.batchSize }}</div>
|
||
<div class="label">线程 / 批次</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-progress
|
||
:percentage="Math.min(100, Number(progress.donePercent) || 0)"
|
||
:stroke-width="14"
|
||
:text-inside="true"
|
||
/>
|
||
<div class="hint-line">关闭开关后可改线程/批次;打开后按当前选项立即连续执行,一批结束立刻下一批。开关/线程/批次会记入 Redis,服务重启后自动恢复。</div>
|
||
</el-card>
|
||
|
||
<basic-table
|
||
ref="tableRef"
|
||
class="job-log-table"
|
||
:columns="columns"
|
||
:search-columns="searchColumns"
|
||
:query="queryList"
|
||
:selection="false"
|
||
show-index
|
||
>
|
||
<template #toolbar>
|
||
<el-col :span="1.5">
|
||
<el-button type="primary" plain icon="Refresh" @click="refreshAll">刷新列表</el-button>
|
||
</el-col>
|
||
</template>
|
||
<template #col-fail="{ row }">
|
||
<span :class="{ 'fail-num': row.fail > 0 }">{{ row.fail }}</span>
|
||
</template>
|
||
<template #col-cost="{ row }">
|
||
{{ formatCost(row.startedAt, row.finishedAt) }}
|
||
</template>
|
||
</basic-table>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="UuidToNewMonitor">
|
||
import { getUuidToNewProgress, listUuidToNewJobLog, setUuidToNewEnabled, updateUuidToNewConfig } from '@/api/monitor/uuidToNew'
|
||
|
||
const { proxy } = getCurrentInstance()
|
||
const tableRef = ref()
|
||
const progressLoading = ref(false)
|
||
const switchLoading = ref(false)
|
||
const configLoading = ref(false)
|
||
const switchOn = ref(false)
|
||
const threadCount = ref(2)
|
||
const batchSize = ref(200)
|
||
const threadOptions = [2, 4, 6, 8, 10, 12, 14, 16]
|
||
const batchOptions = [50, 100, 200, 300, 500, 1000]
|
||
|
||
const progress = reactive({
|
||
oldCnt: 0,
|
||
doneCnt: 0,
|
||
newCnt: 0,
|
||
pendingCnt: 0,
|
||
donePercent: 0,
|
||
enabled: false,
|
||
running: false,
|
||
batchSize: 200,
|
||
threadCount: 2
|
||
})
|
||
|
||
const configLocked = computed(() => !!(switchOn.value || progress.enabled || progress.running))
|
||
|
||
const statusText = computed(() => {
|
||
if (progress.running) return '正在同步'
|
||
if (progress.enabled) return '开启中(等待启动)'
|
||
return '已停止'
|
||
})
|
||
|
||
const searchColumns = [
|
||
{ label: '批次号', prop: 'batchNo', component: 'input', placeholder: '批次号', width: '220px' },
|
||
{ label: '开始时间', prop: 'dateRange', component: 'datetimerange', width: '360px' }
|
||
]
|
||
|
||
const columns = [
|
||
{ label: 'ID', prop: 'id', width: 90, align: 'center' },
|
||
{ label: '批次号', prop: 'batchNo', minWidth: 200, showOverflowTooltip: true },
|
||
{ label: '抓取', prop: 'fetched', width: 90, align: 'center' },
|
||
{ label: '成功', prop: 'success', width: 90, align: 'center' },
|
||
{ label: '失败', prop: 'fail', width: 90, align: 'center', slot: 'fail' },
|
||
{ label: '跳过', prop: 'skip', width: 90, align: 'center' },
|
||
{ label: '耗时', prop: 'cost', width: 110, align: 'center', slot: 'cost' },
|
||
{ label: '开始', prop: 'startedAt', width: 170, align: 'center', parseTime: true },
|
||
{ label: '结束', prop: 'finishedAt', width: 170, align: 'center', parseTime: true },
|
||
{ label: '备注', prop: 'remark', minWidth: 140, showOverflowTooltip: true }
|
||
]
|
||
|
||
function formatNum(v) {
|
||
const n = Number(v || 0)
|
||
return n.toLocaleString()
|
||
}
|
||
|
||
function formatCost(start, end) {
|
||
if (!start || !end) return '-'
|
||
const a = new Date(start).getTime()
|
||
const b = new Date(end).getTime()
|
||
if (!a || !b || b < a) return '-'
|
||
const sec = Math.round((b - a) / 1000)
|
||
if (sec < 60) return sec + 's'
|
||
return Math.floor(sec / 60) + 'm' + (sec % 60) + 's'
|
||
}
|
||
|
||
function syncLocalFromProgress() {
|
||
if (progress.threadCount) threadCount.value = Number(progress.threadCount)
|
||
if (progress.batchSize) batchSize.value = Number(progress.batchSize)
|
||
switchOn.value = !!progress.enabled
|
||
}
|
||
|
||
async function loadProgress() {
|
||
progressLoading.value = true
|
||
try {
|
||
const res = await getUuidToNewProgress()
|
||
Object.assign(progress, res.data || {})
|
||
syncLocalFromProgress()
|
||
} finally {
|
||
progressLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function onConfigChange() {
|
||
if (configLocked.value) {
|
||
syncLocalFromProgress()
|
||
return
|
||
}
|
||
configLoading.value = true
|
||
try {
|
||
const res = await updateUuidToNewConfig({
|
||
threadCount: threadCount.value,
|
||
batchSize: batchSize.value
|
||
})
|
||
Object.assign(progress, res.data || {})
|
||
syncLocalFromProgress()
|
||
} catch (e) {
|
||
syncLocalFromProgress()
|
||
} finally {
|
||
configLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function onSwitchChange(val) {
|
||
switchLoading.value = true
|
||
try {
|
||
const res = await setUuidToNewEnabled({
|
||
enabled: val,
|
||
threadCount: threadCount.value,
|
||
batchSize: batchSize.value
|
||
})
|
||
Object.assign(progress, res.data || {})
|
||
syncLocalFromProgress()
|
||
proxy.$modal.msgSuccess(val ? '已开启,按当前线程/批次连续同步' : '已关闭,当前批结束后停止')
|
||
tableRef.value && tableRef.value.refresh()
|
||
} catch (e) {
|
||
switchOn.value = !val
|
||
} finally {
|
||
switchLoading.value = false
|
||
}
|
||
}
|
||
|
||
function queryList(params) {
|
||
const query = {
|
||
pageNum: params.pageNum,
|
||
pageSize: params.pageSize,
|
||
batchNo: params.batchNo
|
||
}
|
||
if (params.dateRange && params.dateRange.length === 2) {
|
||
query.beginStartedAt = params.dateRange[0]
|
||
query.endStartedAt = params.dateRange[1]
|
||
}
|
||
return listUuidToNewJobLog(query)
|
||
}
|
||
|
||
function refreshAll() {
|
||
loadProgress()
|
||
tableRef.value && tableRef.value.refresh()
|
||
}
|
||
|
||
let timer = null
|
||
onMounted(() => {
|
||
loadProgress()
|
||
timer = setInterval(() => {
|
||
if (progress.enabled || progress.running) {
|
||
loadProgress()
|
||
tableRef.value && tableRef.value.refresh()
|
||
}
|
||
}, 5000)
|
||
})
|
||
onBeforeUnmount(() => {
|
||
if (timer) clearInterval(timer)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.progress-card {
|
||
margin-bottom: 16px;
|
||
}
|
||
.progress-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.progress-head .title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
.progress-head .actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.switch-label {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
.stat-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
.stat {
|
||
background: #f7f8fa;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.stat .num {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
.stat .num.ok { color: #67c23a; }
|
||
.stat .num.warn { color: #e6a23c; }
|
||
.stat .num.cfg { font-size: 18px; }
|
||
.stat .label {
|
||
margin-top: 4px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
.hint-line {
|
||
margin-top: 8px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
.fail-num {
|
||
color: #f56c6c;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|