Persist how many return items each sync run flipped to "missing" and how many came back to "ok" (yeeke_sync_run.missing_marked_count / recovered_count, migration 1789801000000), return them from the sync-runs API and add 「标记不可用」「恢复可用」 columns to the sync-runs page. When the 20% safety valve skips marking the count stays 0 and the reason remains in error_message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
133 lines
6.9 KiB
Vue
133 lines
6.9 KiB
Vue
<template>
|
|
<BasicLayout>
|
|
<template #wrapper>
|
|
<el-card class="page-card" shadow="never">
|
|
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="search">
|
|
<el-form-item label="触发方式">
|
|
<el-select v-model="query.trigger" clearable placeholder="全部" style="width:120px">
|
|
<el-option label="手动" value="manual" />
|
|
<el-option label="定时" value="scheduled" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-select v-model="query.status" clearable placeholder="全部" style="width:140px">
|
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="开始时间"><el-date-picker v-model="startedRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
|
|
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button></el-form-item>
|
|
<el-form-item class="sync-action">
|
|
<el-button
|
|
v-if="canSync"
|
|
type="primary"
|
|
:loading="manualSyncStarting"
|
|
:disabled="manualSyncStarting"
|
|
@click="startManualSync"
|
|
>手动同步</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" class="notice"><template #default><el-button link type="primary" @click="load">重新加载</el-button></template></el-alert>
|
|
<div class="table-wrap">
|
|
<el-table v-loading="loading" :data="items" border stripe height="100%" empty-text="暂无同步记录">
|
|
<el-table-column label="运行ID" prop="id" width="90" />
|
|
<el-table-column label="触发方式" width="100"><template #default="{ row }">{{ triggerLabel(row.trigger) }}</template></el-table-column>
|
|
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="statusMeta(row.status).type">{{ statusMeta(row.status).label }}</el-tag></template></el-table-column>
|
|
<el-table-column label="开始时间" min-width="160"><template #default="{ row }">{{ formatTime(row.startedAt) }}</template></el-table-column>
|
|
<el-table-column label="结束时间" min-width="160"><template #default="{ row }">{{ formatTime(row.finishedAt) }}</template></el-table-column>
|
|
<el-table-column label="页数" prop="totalPages" width="70" />
|
|
<el-table-column label="读取" prop="readCount" width="70" />
|
|
<el-table-column label="新增" prop="createdCount" width="70" />
|
|
<el-table-column label="更新" prop="updatedCount" width="70" />
|
|
<el-table-column label="跳过" prop="skippedCount" width="70" />
|
|
<el-table-column label="失败" prop="failedCount" width="70" />
|
|
<el-table-column label="标记不可用" prop="missingMarkedCount" width="96" />
|
|
<el-table-column label="恢复可用" prop="recoveredCount" width="84" />
|
|
<el-table-column label="脱敏原因" min-width="200"><template #default="{ row }">{{ row.errorMessage || '—' }}</template></el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<pagination v-show="total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" @pagination="load" />
|
|
</el-card>
|
|
</template>
|
|
</BasicLayout>
|
|
</template>
|
|
|
|
<script>
|
|
import { Search, RefreshLeft } from '@element-plus/icons-vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { listYeekeSyncRuns, triggerYeekeSync } from '@/api/goauto/yeeke-returns'
|
|
|
|
export default {
|
|
name: 'GoAutoYeekeSyncRuns',
|
|
setup() { return { Search, RefreshLeft } },
|
|
data() {
|
|
return {
|
|
loading: false, loadError: '', items: [], total: 0, startedRange: null, manualSyncStarting: false,
|
|
query: { page: 1, pageSize: 20, trigger: '', status: '', startedFrom: '', startedTo: '' },
|
|
statusOptions: [
|
|
{ label: '执行中', value: 'running' }, { label: '成功', value: 'succeeded' },
|
|
{ label: '失败', value: 'failed' }, { label: '已中断', value: 'interrupted' }
|
|
]
|
|
}
|
|
},
|
|
computed: {
|
|
canSync() { return (this.$store.getters.roles || []).some(role => role === 'admin' || role === 'purchaser') },
|
|
hasRunningSync() { return this.items.some(item => item.status === 'running') }
|
|
},
|
|
created() { this.load() },
|
|
methods: {
|
|
triggerLabel(trigger) { return { manual: '手动', scheduled: '定时' }[trigger] || trigger || '—' },
|
|
statusMeta(status) { return { running: { label: '执行中', type: 'primary' }, succeeded: { label: '成功', type: 'success' }, failed: { label: '失败', type: 'danger' }, interrupted: { label: '已中断', type: 'warning' }}[status] || { label: status || '-', type: 'info' } },
|
|
formatTime(value) { if (!value) return '—'; return new Date(value).toLocaleString('zh-CN', { hour12: false }) },
|
|
async load() {
|
|
this.loading = true; this.loadError = ''
|
|
try {
|
|
const r = await listYeekeSyncRuns(this.query, { suppressNetworkError: true })
|
|
this.items = r.data.items
|
|
this.total = r.data.total
|
|
} catch (error) {
|
|
this.loadError = error?.response?.data?.message || error?.message || '同步记录加载失败'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
search() {
|
|
this.query.page = 1
|
|
const [from, to] = this.startedRange || ['', '']
|
|
this.query.startedFrom = from
|
|
this.query.startedTo = to
|
|
this.load()
|
|
},
|
|
reset() {
|
|
this.startedRange = null
|
|
this.query = { page: 1, pageSize: 20, trigger: '', status: '', startedFrom: '', startedTo: '' }
|
|
this.load()
|
|
},
|
|
async startManualSync() {
|
|
if (!this.canSync || this.manualSyncStarting || this.hasRunningSync) return
|
|
this.manualSyncStarting = true
|
|
try {
|
|
await triggerYeekeSync()
|
|
ElMessage.success('已开始同步')
|
|
await this.load()
|
|
} catch (error) {
|
|
ElMessage.error(error?.response?.data?.message || error?.message || '同步启动失败')
|
|
} finally {
|
|
this.manualSyncStarting = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
// The card fills the viewport and the table takes all remaining height, so
|
|
// the pagination sits at the bottom instead of floating mid-page.
|
|
.page-card{height:calc(100vh - 124px);display:flex;flex-direction:column}
|
|
.page-card :deep(.el-card__body){flex:1;min-height:0;display:flex;flex-direction:column}
|
|
.search-form{display:flex;flex-wrap:wrap;align-items:center;padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}
|
|
.sync-action{margin-left:auto}
|
|
.notice{margin-bottom:16px}
|
|
.table-wrap{flex:1;min-height:240px}
|
|
</style>
|