feat(self-operated): add charts and continuous collection status

This commit is contained in:
2026-10-13 20:44:31 +08:00
parent 698ca34529
commit 4c4a3db1eb
33 changed files with 780 additions and 211 deletions
+2 -21
View File
@@ -1,4 +1,4 @@
import type { BusinessQuery, DailyPoint, Progress, QueryContext, RetryCommand, Scope, Task } from './types'
import type { BusinessQuery, Progress, QueryContext, RetryCommand, Scope, Task } from './types'
// Aborting saves work; generation fencing also protects against adapters that return after abort.
export class RequestFence {
@@ -90,7 +90,7 @@ export const reasonLabels: Record<string, string> = {
}
export function reasonsText(reasons: readonly string[] | null | undefined) { return reasons?.map(reason => reasonLabels[reason] ?? statusLabels[reason] ?? '存在待核对项').join('、') || '—' }
export function phaseText(phase: string | null | undefined) {
const phases: Record<string, string> = { DIRECTORY: '获取出库目录', DISCOVERY: '获取出库目录', DETAIL: '获取出库详情', DETAILS: '获取出库详情', INVENTORY: '获取产品库存', VALIDATE: '校验', VALIDATION: '校验', MERGE: '归并', BUILD: '统计计算', PUBLISH: '发布', WAIT_UPSTREAM: '等待账号共享上游', WAIT_BUDGET: '预算等待', WAITING_SOURCE: '等待来源准备', WAITING: '等待执行', SAMPLE_DONE: '样本结束', FAILED: '执行失败', FETCH: '获取数据', PLANNED: '已规划', FETCHED: '数据已取完', VALIDATED: '已校验', BUILDING: '统计计算', BUILT: '计算完成', PUBLISHED: '已发布' }
const phases: Record<string, string> = { DIRECTORY: '获取出库目录', DISCOVERY: '获取出库目录', DETAIL: '获取出库详情', DETAILS: '获取出库详情', INVENTORY: '获取产品库存', VALIDATE: '校验', VALIDATION: '校验', MERGE: '更新本仓出库数据', BUILD: '统计计算', PUBLISH: '发布', WAIT_UPSTREAM: '等待账号共享上游', WAIT_BUDGET: '预算等待', WAITING_SOURCE: '等待来源准备', WAITING: '等待执行', SAMPLE_DONE: '样本结束', FAILED: '执行失败', FETCH: '获取数据', PLANNED: '已规划', FETCHED: '数据已取完', VALIDATED: '已校验', BUILDING: '统计计算', BUILT: '计算完成', PUBLISHED: '已发布' }
return phase ? phases[phase] ?? '阶段待核对' : '阶段待确认'
}
export function label(value: string | null | undefined) { return value ? statusLabels[value] ?? '待核对' : '—' }
@@ -142,22 +142,3 @@ export class RuntimePoller {
if (generation === this.generation && this.active && this.visible) this.timer = setTimeout(() => void this.tick(generation), busy ? 10000 : 60000)
}
}
export function trendSegments(points: DailyPoint[]) {
const max = Math.max(1, ...points.map(point => Number(shownQuantity(point) ?? 0)))
const segments: { path: string; partial: boolean }[] = []
let path = ''
let partial = false
const flush = () => { if (path) segments.push({ path, partial }); path = '' }
points.forEach((point, index) => {
const quantity = shownQuantity(point)
if (quantity == null || point.dataStatus === 'UNKNOWN') { flush(); return }
const x = points.length === 1 ? 400 : 30 + index / (points.length - 1) * 740
const y = 175 - Number(quantity) / max * 145
const pointPartial = point.dataStatus !== 'COMPLETE'
if (path && partial !== pointPartial) { flush() }
path += `${path ? ' L' : 'M'}${x},${y}`
partial = pointPartial
})
flush()
return segments
}