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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user