From 8f4f564733484e62287af4d3ed212e4288812eca Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sun, 30 Aug 2026 10:17:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(factor):=20=E8=BF=9B=E5=BA=A6=E9=A1=B5?= =?UTF-8?q?=E5=BF=83=E8=B7=B3=E5=88=A4=E6=B4=BB+=E5=9B=A0=E5=AD=90?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=BB=98=E8=AE=A4=E6=8A=98=E5=8F=A0(?= =?UTF-8?q?=E7=94=A8=E6=88=B708-30=E5=8F=8D=E9=A6=88=E4=B8=A4=E9=A1=B9)=20?= =?UTF-8?q?[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 痛点:因子分析分钟级运行,进度条停着看不出进行中还是死掉;240因子罗列乱。 ①跨进程心跳链路:analyzer._report_progress 写 {output_dir}/{tid}.progress (stage/detail/ts;IO故障静默——锦上添花不能伤主流程),埋点=行情加载i/N逐只 +因子特征+逐因子i/M+tears✓+完成;runner._factor_worker 透传 task_id (默认空串兼容旧调用);GET /task/{id} 合并 _read_factor_progress(factor_ 前缀才读,age=距上次活动秒数)。 ②Progress.vue终端风重构:心跳区=呼吸灯(绿≤30s/琥珀≤180s静默期/红更久, prefers-reduced-motion停动画)+「Xs前·detail」+运行时长秒表+步骤%大数字; 因子步骤=行情加载→因子计算→逐因子分析→完成(心跳stage驱动);回测/优化 保持原5步推导+进度条;useTask/TaskStatus 透传 progress。 ③FactorPicker默认折叠:groups max-height 128px两行预览+渐隐底边+ 「展开全部N个因子▾」按钮,搜索时自动展开。 +5测试(_report_progress写/静默/心跳读roundtrip/非factor/损坏JSON),323绿+build绿 --- frontend/src/api/backtest.ts | 7 + frontend/src/composables/useTask.ts | 12 +- frontend/src/views/backtest/Progress.vue | 239 ++++++++++++++------- frontend/src/views/factor/FactorPicker.vue | 30 ++- sanguo_api/routes.py | 25 +++ sanguo_factor/analyzer.py | 30 ++- sanguo_orchestrator/runner.py | 6 +- tests/api/test_factor_routes.py | 27 +++ tests/factor/test_analyzer.py | 25 +++ 9 files changed, 315 insertions(+), 86 deletions(-) diff --git a/frontend/src/api/backtest.ts b/frontend/src/api/backtest.ts index c0a24d1..3275145 100644 --- a/frontend/src/api/backtest.ts +++ b/frontend/src/api/backtest.ts @@ -19,6 +19,13 @@ export interface TaskStatus { task_id: string status: string stage: string + /** 因子分析心跳(仅 factor_ 任务运行中有值) */ + progress?: { + stage: string + detail: string + ts: number + age: number + } | null } export interface EquityPoint { diff --git a/frontend/src/composables/useTask.ts b/frontend/src/composables/useTask.ts index b1cd6e5..5e0ef40 100644 --- a/frontend/src/composables/useTask.ts +++ b/frontend/src/composables/useTask.ts @@ -4,6 +4,14 @@ import { useAuthStore } from '@/stores/auth' export type TaskState = 'pending' | 'running' | 'done' | 'failed' | 'unknown' +export interface TaskProgress { + stage: string + detail: string + ts: number + /** 距上次心跳秒数(后端算) */ + age: number +} + /** * Track a task's status + stage via polling (2s) and WebSocket (real-time). * Auto-stops on unmount. @@ -11,6 +19,7 @@ export type TaskState = 'pending' | 'running' | 'done' | 'failed' | 'unknown' export function useTask(taskId: string) { const status = ref('unknown') const stage = ref('') + const progress = ref(null) let timer: ReturnType | null = null let ws: WebSocket | null = null @@ -19,6 +28,7 @@ export function useTask(taskId: string) { const s = await getStatus(taskId) status.value = s.status as TaskState stage.value = s.stage + progress.value = (s as { progress?: TaskProgress | null }).progress ?? null } catch { /* transient — keep last known state */ } @@ -51,5 +61,5 @@ export function useTask(taskId: string) { if (ws) ws.close() }) - return { status, stage, start } + return { status, stage, progress, start } } diff --git a/frontend/src/views/backtest/Progress.vue b/frontend/src/views/backtest/Progress.vue index 798bf51..983f44a 100644 --- a/frontend/src/views/backtest/Progress.vue +++ b/frontend/src/views/backtest/Progress.vue @@ -1,4 +1,12 @@ diff --git a/frontend/src/views/factor/FactorPicker.vue b/frontend/src/views/factor/FactorPicker.vue index b20dedb..0a245ad 100644 --- a/frontend/src/views/factor/FactorPicker.vue +++ b/frontend/src/views/factor/FactorPicker.vue @@ -11,6 +11,16 @@ const emit = defineEmits<{ (e: 'update:modelValue', v: string[]): void }>() const q = ref('') const picked = ref>(new Set(props.modelValue)) +// 默认收起(1-2 行预览);搜索或点「展开全部」后放开 +const collapsed = ref(true) + +function toggleCollapse(): void { + collapsed.value = !collapsed.value +} + +function onSearch(): void { + if (q.value.trim()) collapsed.value = false +} const grouped = computed(() => { const map = new Map() @@ -68,11 +78,11 @@ defineExpose({ hydrate })