perf(frontend): 工具页分页/历史截断/轻量轮询/共享纯逻辑(审查 F5/F6/F9/F12/G3)

- F9 新增 useTablePaging composable 并接入 7 个工具页"匹配结果"表(只切渲染窗口,
  不加选择语义;每页 100 条)+ 深色分页样式
- F6 历史任务抽屉渲染截断(默认 50 条 + "显示全部/收起"),全选口径改为当前可见条目
- F5 新增 task-progress-polling 适配器:等待任务终态的紧循环走 /tasks/progress/light,
  异常或空响应回退重型 batch;已接入跟价/定时匹配的 waitForTaskTerminal
- F12 背景图 bg.jpg 251KB → 166KB(1920 宽 + quality 80,image-set 仍优先 webp)
- G3 抽出共享纯逻辑 task-queue-state(开始时间表序列化校验 + 记录缺失错误判定),
  5 个工具页删除逐字重复实现
- 新增 4 个单测文件(分页切片/历史截断/轻量轮询归一/任务队列纯逻辑)
This commit is contained in:
2026-09-14 06:23:46 +08:00
parent e76714c32e
commit 67223f8950
18 changed files with 684 additions and 69 deletions
@@ -162,7 +162,7 @@
</div>
<el-table
v-else
:data="matchedItems"
:data="pagedMatchedItems"
:row-key="rowKeyForMatch"
:highlight-current-row="false"
class="result-table match-table"
@@ -215,6 +215,14 @@
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-actions="{ item }">
<button type="button" class="btn-delete" @click="deleteTaskRecord(itemSource(item))">删除</button>
@@ -275,6 +283,8 @@ import {
import { formatDateTime } from '@/shared/utils/datetime'
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const MAX_TRANSIENT_ERRORS = 30;
/** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */
@@ -290,6 +300,14 @@ const taskConditionTexts = ref<string[]>([]);
const conditionSelectValue = ref("");
const selectedCountryCodes = ref<string[]>([...EU_COUNTRY_CODES]);
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
const dashboard = ref<PatrolDeleteDashboardVo>({
candidateCount: 0,
@@ -713,11 +731,6 @@ function clearActiveQueueTask() {
saveQueueState();
}
function isRecordMissingError(error: unknown) {
const message = error instanceof Error ? error.message : String(error || "");
return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message);
}
function removeMatchedRowLocally(row: PatrolDeleteShopQueueItem) {
const key = rowKeyForMatch(row);
matchedItems.value = matchedItems.value.filter((item) => rowKeyForMatch(item) !== key);
@@ -164,7 +164,7 @@
<template #cards-extra>
<div class="subsection-title">匹配结果</div>
<div v-if="!matchedItems.length" class="match-empty">完成匹配店铺在此展示匹配结果确认无误后再启动任务</div>
<el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
<el-table v-else :data="pagedMatchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
class="result-table match-table">
<el-table-column prop="shopName" label="店铺名" min-width="100" />
<el-table-column label="匹配" width="72" align="center">
@@ -187,6 +187,14 @@
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-actions="{ item }">
<button v-if="canDownload(itemSource(item))" type="button" class="download"
@@ -257,9 +265,12 @@ import {
type PriceTrackShopQueueItem,
type PriceTrackTaskDetailVo,
} from '@/shared/api/java-modules'
import { getPollingProgressBatch } from '@/shared/api/task-progress-polling.ts'
import { formatDateTime } from '@/shared/utils/datetime'
import { uploadPathsToJava } from '@/shared/utils/upload-to-java'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const ziniaoVersion = useZiniaoVersion()
@@ -277,6 +288,14 @@ const shopInput = ref('')
const candidates = ref<PriceTrackCandidateVo[]>([])
const selectedCandidates = ref<PriceTrackCandidateVo[]>([])
const matchedItems = ref<PriceTrackShopQueueItem[]>([])
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const adding = ref(false)
const matching = ref(false)
const pushing = ref(false)
@@ -809,11 +828,6 @@ function rowKeyForMatch(row: { shopName?: string; shopId?: number | string | nul
return `${(row.shopName || '').trim()}\u0001${row.shopId ?? ''}`
}
function isRecordMissingError(error: unknown) {
const message = error instanceof Error ? error.message : String(error || '')
return /记录不存在|不存在|已删除|not\s*found|404/i.test(message)
}
function priceTrackModeForAppClient() {
return statusModeEnabled.value ? 'status' : 'asin'
}
@@ -1006,7 +1020,10 @@ async function waitForTaskTerminal(taskId: number) {
while (true) {
if (disposed) return 'STOPPED'
try {
const batch = await getPriceTrackTaskProgressBatch([taskId])
// F5:等待终态的紧循环只读 status(轻量白名单覆盖),走 light 端点减小轮询响应体,异常回退重型 batch
const batch = await getPollingProgressBatch('priceTrack', [taskId], {
fallback: () => getPriceTrackTaskProgressBatch([taskId]),
})
if (transientErrorCount > 0) {
queuePushResult.value = `任务 ${taskId} 服务已恢复,继续等待执行结果...`
}
@@ -97,7 +97,7 @@
<template #cards-extra>
<div class="subsection-title">匹配结果</div>
<div v-if="!matchedItems.length" class="match-empty">完成匹配店铺在此展示匹配结果确认无误后再启动任务</div>
<el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
<el-table v-else :data="pagedMatchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
class="result-table match-table">
<el-table-column prop="shopName" label="店铺名" min-width="100" />
<el-table-column label="匹配" width="72" align="center">
@@ -120,6 +120,14 @@
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-actions="{ item }">
<button v-if="canDownload(itemSource(item))" type="button" class="download"
@@ -176,11 +184,21 @@ import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
import { formatDateTime } from '@/shared/utils/datetime'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const shopInput = ref('')
const candidates = ref<ProductRiskCandidateVo[]>([])
const selectedCandidates = ref<ProductRiskCandidateVo[]>([])
const matchedItems = ref<ProductRiskShopQueueItem[]>([])
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const adding = ref(false)
const matching = ref(false)
const pushing = ref(false)
@@ -443,11 +461,6 @@ function rowKeyForMatch(row: { shopName?: string; shopId?: number | string | nul
return `${name}\u0001${id}`
}
function isRecordMissingError(error: unknown) {
const message = error instanceof Error ? error.message : String(error || '')
return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message)
}
function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: number | string | null }>) {
if (!rows.length) return
const keys = new Set(rows.map((row) => rowKeyForMatch(row)))
@@ -111,7 +111,7 @@
</div>
<el-table
v-else
:data="matchedItems"
:data="pagedMatchedItems"
:row-key="rowKeyForMatch"
:highlight-current-row="false"
class="result-table match-table"
@@ -164,6 +164,14 @@
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-actions="{ item }">
<button type="button" class="btn-delete" @click="deleteTaskRecord(itemSource(item))">删除</button>
@@ -217,6 +225,8 @@ import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
import { formatDateTime } from '@/shared/utils/datetime'
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError, parseTaskStartTimes, removeTaskStartTime, upsertTaskStartTime } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
@@ -225,6 +235,14 @@ const shopInput = ref("");
const candidates = ref<QueryAsinCandidateVo[]>([]);
const selectedCandidates = ref<QueryAsinCandidateVo[]>([]);
const matchedItems = ref<QueryAsinShopQueueItem[]>([]);
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const historyItems = ref<QueryAsinHistoryItem[]>([]);
const dashboard = ref<QueryAsinDashboardVo>({
candidateCount: 0,
@@ -560,15 +578,7 @@ function loadTaskStartTimes() {
typeof window !== "undefined"
? window.localStorage.getItem(taskStartTimeStorageKey())
: null;
const parsed = raw ? JSON.parse(raw) : {};
const next: Record<number, string> = {};
for (const [taskId, value] of Object.entries(parsed || {})) {
const numericTaskId = Number(taskId);
if (!Number.isFinite(numericTaskId) || numericTaskId <= 0) continue;
if (typeof value !== "string" || !value.trim()) continue;
next[numericTaskId] = value;
}
taskStartTimes.value = next;
taskStartTimes.value = parseTaskStartTimes(raw);
} catch {
taskStartTimes.value = {};
}
@@ -576,18 +586,13 @@ function loadTaskStartTimes() {
function setTaskStartTime(taskId: number, startedAt = new Date().toISOString()) {
if (!Number.isFinite(taskId) || taskId <= 0) return;
taskStartTimes.value = {
...taskStartTimes.value,
[taskId]: startedAt,
};
taskStartTimes.value = upsertTaskStartTime(taskStartTimes.value, taskId, startedAt);
saveTaskStartTimes();
}
function clearTaskStartTime(taskId?: number | null) {
if (!taskId || !(taskId in taskStartTimes.value)) return;
const next = { ...taskStartTimes.value };
delete next[taskId];
taskStartTimes.value = next;
taskStartTimes.value = removeTaskStartTime(taskStartTimes.value, taskId);
saveTaskStartTimes();
}
@@ -613,11 +618,6 @@ function resetQueueWorkerIfIdle() {
saveQueueState();
}
function isRecordMissingError(error: unknown) {
const message = error instanceof Error ? error.message : String(error || "");
return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message);
}
function removeHistoryItemLocally(item: QueryAsinHistoryItem) {
historyItems.value = historyItems.value.filter((row) => {
if (item.resultId != null && row.resultId === item.resultId) return false;
@@ -78,7 +78,7 @@
<template #cards-extra>
<div class="subsection-title">匹配结果</div>
<div v-if="!matchedItems.length" class="match-empty">匹配后将在这里显示结果</div>
<el-table v-else :data="matchedItems" :row-key="rowKey" :highlight-current-row="false"
<el-table v-else :data="pagedMatchedItems" :row-key="rowKey" :highlight-current-row="false"
class="result-table match-table">
<el-table-column prop="shopName" label="店铺名" min-width="120" />
<el-table-column label="匹配" width="64" align="center">
@@ -93,6 +93,14 @@
<button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button>
</template></el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-extra="{ item }">
<div v-if="itemSource(item).error" class="files error-text">错误{{ itemSource(item).error }}</div>
@@ -153,6 +161,7 @@ import {
type ShopDataCrawlTaskDetailVo,
} from '@/shared/api/java-modules'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const COUNTRY_OPTIONS = [
{ code: 'UK', label: '英国' },
@@ -168,6 +177,14 @@ const shopInput = ref('')
const candidates = ref<ShopDataCrawlCandidateVo[]>([])
const selectedCandidates = ref<ShopDataCrawlCandidateVo[]>([])
const matchedItems = ref<ShopDataCrawlShopItem[]>([])
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const historyItems = ref<ShopDataCrawlHistoryItem[]>([])
const dashboard = ref<ShopDataCrawlDashboardVo>({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 0, failedTaskCount: 0 })
const orderedCountryCodes = ref<string[]>(COUNTRY_OPTIONS.map((row) => row.code))
@@ -77,7 +77,7 @@
<template #cards-extra>
<div class="subsection-title">匹配结果</div>
<div v-if="!matchedItems.length" class="match-empty">完成匹配店铺在此展示匹配结果确认无误后再启动任务</div>
<el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false" class="result-table match-table">
<el-table v-else :data="pagedMatchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false" class="result-table match-table">
<el-table-column prop="shopName" label="店铺名" min-width="100" />
<el-table-column label="匹配" width="72" align="center"><template #default="{ row }"><span :class="row.matched ? 'ok' : 'fail'">{{ row.matched ? '是' : '否' }}</span></template></el-table-column>
<el-table-column prop="shopId" label="店铺ID" min-width="120" show-overflow-tooltip />
@@ -87,6 +87,14 @@
<el-table-column label="说明" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ formatMatchRemark(row) }}</template></el-table-column>
<el-table-column label="操作" width="72" align="center"><template #default="{ row }"><button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button></template></el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</template>
<template #item-extra="{ item }">
<div v-if="currentTaskStageText(itemSource(item))" class="files">{{ currentTaskStageText(itemSource(item)) }}</div>
@@ -116,18 +124,28 @@ import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchCreateTaskItem, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { getPollingProgressBatch } from '@/shared/api/task-progress-polling.ts'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const COUNTRY_OPTIONS = [{ code: 'DE', label: '德国' }, { code: 'UK', label: '英国' }, { code: 'FR', label: '法国' }, { code: 'IT', label: '意大利' }, { code: 'ES', label: '西班牙' }] as const
const shopInput = ref('')
const candidates = ref<ShopMatchCandidateVo[]>([])
const selectedCandidates = ref<ShopMatchCandidateVo[]>([])
const matchedItems = ref<ShopMatchShopQueueItem[]>([])
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const shopMatchListingFilter = ref<ListingFilterValue>('Active')
const adding = ref(false)
const matching = ref(false)
@@ -439,7 +457,10 @@ function getPollIntervalMs() { return getTaskPollIntervalMs() }
function scheduleNextPoll(immediate = false) { if (disposed) return; if (pollTimer.value) { if (!immediate) return; timers.clearTimer('task-poll', pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (disposed) return; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (!disposed && pollingTaskIds.value.length) pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = timers.setTimeout('task-poll', run, getPollIntervalMs()) }
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
function stopPolling() { if (pollTimer.value) { timers.clearTimer('task-poll', pollTimer.value); pollTimer.value = null } }
async function waitForTaskTerminal(taskId: number) { let transientErrorCount = 0; const maxTransientErrors = 30; while (true) { if (disposed) return 'STOPPED'; try { const batch = await getShopMatchTaskProgressBatch([taskId]); if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 服务已恢复,继续等待执行结果...`; transientErrorCount = 0; if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { const prev = taskSnapshots.value[taskId]; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } } catch (error) { if (!isTransientBackendError(error)) throw error; transientErrorCount += 1; if (transientErrorCount >= maxTransientErrors) throw new Error(`任务 ${taskId} 等待服务恢复超时,请稍后手动刷新查看状态`); queuePushResult.value = `任务 ${taskId} 运行中,正在等待服务恢复(${transientErrorCount}/${maxTransientErrors}...`; if (transientErrorCount === 1 || transientErrorCount % 5 === 0) ElMessage.warning(`任务 ${taskId} 运行中,服务暂时不可用,正在自动重试`); await sleep(getPollIntervalMs()); continue } await sleep(getPollIntervalMs()) } }
async function waitForTaskTerminal(taskId: number) { let transientErrorCount = 0; const maxTransientErrors = 30; while (true) { if (disposed) return 'STOPPED'; try { // F5:等待终态的紧循环只读 status,走 light 端点减小轮询响应体,异常回退重型 batch
const batch = await getPollingProgressBatch('shopMatch', [taskId], {
fallback: () => getShopMatchTaskProgressBatch([taskId]),
}); if (transientErrorCount > 0) queuePushResult.value = `任务 ${taskId} 服务已恢复,继续等待执行结果...`; transientErrorCount = 0; if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { const prev = taskSnapshots.value[taskId]; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } } catch (error) { if (!isTransientBackendError(error)) throw error; transientErrorCount += 1; if (transientErrorCount >= maxTransientErrors) throw new Error(`任务 ${taskId} 等待服务恢复超时,请稍后手动刷新查看状态`); queuePushResult.value = `任务 ${taskId} 运行中,正在等待服务恢复(${transientErrorCount}/${maxTransientErrors}...`; if (transientErrorCount === 1 || transientErrorCount % 5 === 0) ElMessage.warning(`任务 ${taskId} 运行中,服务暂时不可用,正在自动重试`); await sleep(getPollIntervalMs()); continue } await sleep(getPollIntervalMs()) } }
async function waitForScheduledTaskStageExit(taskId: number) {
let transientErrorCount = 0
const maxTransientErrors = 30
@@ -115,7 +115,7 @@
</div>
<div v-else class="match-zone-scroll">
<el-table
:data="matchedItems"
:data="pagedMatchedItems"
:row-key="rowKeyForMatch"
:highlight-current-row="false"
class="result-table match-table"
@@ -168,6 +168,14 @@
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="matchedTotal > matchedPageSize"
class="matched-pagination"
layout="total, prev, pager, next"
:total="matchedTotal"
:page-size="matchedPageSize"
v-model:current-page="matchedPage"
/>
</div>
</div>
@@ -257,6 +265,8 @@ import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
import { formatDateTime } from '@/shared/utils/datetime'
import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError, parseTaskStartTimes, removeTaskStartTime, upsertTaskStartTime } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
@@ -276,6 +286,14 @@ const reservedAmount = ref(0);
const candidates = ref<WithdrawCandidateVo[]>([]);
const selectedCandidates = ref<WithdrawCandidateVo[]>([]);
const matchedItems = ref<WithdrawShopQueueItem[]>([]);
// F9:匹配结果表分页(只切渲染窗口,不加选择语义)
const {
page: matchedPage,
pageSize: matchedPageSize,
total: matchedTotal,
paged: pagedMatchedItems,
} = useTablePaging(matchedItems)
const historyItems = ref<WithdrawHistoryItem[]>([]);
const dashboard = ref<WithdrawDashboardVo>({
candidateCount: 0,
@@ -700,15 +718,7 @@ function loadTaskStartTimes() {
typeof window !== "undefined"
? window.localStorage.getItem(taskStartTimeStorageKey())
: null;
const parsed = raw ? JSON.parse(raw) : {};
const next: Record<number, string> = {};
for (const [taskId, value] of Object.entries(parsed || {})) {
const numericTaskId = Number(taskId);
if (!Number.isFinite(numericTaskId) || numericTaskId <= 0) continue;
if (typeof value !== "string" || !value.trim()) continue;
next[numericTaskId] = value;
}
taskStartTimes.value = next;
taskStartTimes.value = parseTaskStartTimes(raw);
} catch {
taskStartTimes.value = {};
}
@@ -716,18 +726,13 @@ function loadTaskStartTimes() {
function setTaskStartTime(taskId: number, startedAt = new Date().toISOString()) {
if (!Number.isFinite(taskId) || taskId <= 0) return;
taskStartTimes.value = {
...taskStartTimes.value,
[taskId]: startedAt,
};
taskStartTimes.value = upsertTaskStartTime(taskStartTimes.value, taskId, startedAt);
saveTaskStartTimes();
}
function clearTaskStartTime(taskId?: number | null) {
if (!taskId || !(taskId in taskStartTimes.value)) return;
const next = { ...taskStartTimes.value };
delete next[taskId];
taskStartTimes.value = next;
taskStartTimes.value = removeTaskStartTime(taskStartTimes.value, taskId);
saveTaskStartTimes();
}
@@ -753,11 +758,6 @@ function resetQueueWorkerIfIdle() {
saveQueueState();
}
function isRecordMissingError(error: unknown) {
const message = error instanceof Error ? error.message : String(error || "");
return /记录不存在|任务不存在|不存在|已删除|not\s*found|404/i.test(message);
}
function removeHistoryItemLocally(item: WithdrawHistoryItem) {
historyItems.value = historyItems.value.filter((row) => {
if (item.resultId != null && row.resultId === item.resultId) return false;