feat(app): report flow with polling and 32-dimension result view(F3)

This commit is contained in:
xsl
2026-08-15 00:49:47 +08:00
parent 9503c6e259
commit 995c18e7d5
5 changed files with 1573 additions and 10 deletions
@@ -0,0 +1,147 @@
/**
* TaskPollingPanel —— 任务轮询通用面板(F3,Create 提交后与 Result 重新生成共用)。
*
* - 每 3 秒 GET /api/v1/tasks/{id},展示 progress 进度条 + stage_text
* - SUCCEEDED / FAILED 通过回调上抛,由宿主页面决定跳转或失败态
* - 连续 3 次网络异常视为失败(避免瞬时抖动误杀长任务)
* - 定时器随组件销毁自动清理(页面返回/跳转即停);taskId 切换时
* 通过 generation 计数丢弃旧任务的在途响应
*/
import { ApiClient } from '../../core/ApiClient'
import { ApiError } from '../../core/ApiTypes'
import { TaskWire } from './ReportTypes'
const POLL_INTERVAL_MS: number = 3000
const MAX_CONSECUTIVE_ERRORS: number = 3
const POLL_PRIMARY_COLOR: string = '#C2185B'
@Component
export struct TaskPollingPanel {
@Prop @Watch('onTaskIdChanged') taskId: string = ''
onSucceeded: () => void = () => {}
onFailed: (reason: string) => void = () => {}
@State progress: number = 0
@State stageText: string = '任务已提交,等待调度…'
private timerId: number = -1
private generation: number = 0
private errCount: number = 0
aboutToAppear(): void {
if (this.taskId !== '') {
this.startPolling()
}
}
aboutToDisappear(): void {
// 页面销毁/返回清理定时器:离开页面不会残留轮询
this.stopTimer()
}
private onTaskIdChanged(): void {
this.startPolling()
}
private startPolling(): void {
this.stopTimer()
this.generation += 1
this.errCount = 0
this.progress = 0
this.stageText = '任务已提交,等待调度…'
this.pollOnce(this.generation)
}
private stopTimer(): void {
if (this.timerId !== -1) {
clearTimeout(this.timerId)
this.timerId = -1
}
}
private async pollOnce(gen: number): Promise<void> {
const tid = this.taskId
if (tid === '') {
return
}
try {
const t = await ApiClient.get<TaskWire>(`/api/v1/tasks/${tid}`)
if (gen !== this.generation) {
return // 已切换到新任务,丢弃旧任务的在途响应
}
this.errCount = 0
const p = t.progress
if (p !== undefined && p !== null) {
this.progress = Math.min(100, Math.max(0, p))
}
const st = t.stage_text
if (st !== undefined && st !== null && st !== '') {
this.stageText = st
}
if (t.status === 'SUCCEEDED') {
this.stopTimer()
this.onSucceeded()
return
}
if (t.status === 'FAILED') {
this.stopTimer()
this.onFailed(this.stageText)
return
}
this.scheduleNext(gen)
} catch (e) {
if (gen !== this.generation) {
return
}
this.errCount += 1
if (this.errCount >= MAX_CONSECUTIVE_ERRORS) {
this.stopTimer()
let reason = '网络异常,连续查询任务状态失败'
if (e instanceof ApiError) {
reason = `${reason}(${e.message})`
}
this.onFailed(reason)
return
}
this.scheduleNext(gen)
}
}
private scheduleNext(gen: number): void {
this.stopTimer()
this.timerId = setTimeout((): void => {
this.pollOnce(gen)
}, POLL_INTERVAL_MS)
}
build() {
Column({ space: 10 }) {
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('100%')
.height(8)
.color(POLL_PRIMARY_COLOR)
.backgroundColor('#F3D9E3')
.borderRadius(4)
Row({ space: 8 }) {
Text(this.stageText)
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`${Math.round(this.progress)}%`)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor(POLL_PRIMARY_COLOR)
}
.width('100%')
Text(`任务 ID:${this.taskId}|每 3 秒自动刷新,返回不会中断生成`)
.fontSize(11)
.fontColor('#AAAAAA')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
}
}