diff --git a/frontend/src/views/backtest/Progress.vue b/frontend/src/views/backtest/Progress.vue index 983f44a..dbc9caa 100644 --- a/frontend/src/views/backtest/Progress.vue +++ b/frontend/src/views/backtest/Progress.vue @@ -180,7 +180,6 @@ const pageTitle = computed(() => (isFactor.value ? '因子分析' : isOptimize.v v-for="(s, i) in (isFactor ? fxSteps : btSteps)" :key="s.key" class="tl-node" :class="[isFactor ? fxStepState(i) : btStepState(i), { failed: status === 'failed' && i === (isFactor ? fxCurrentIdx : btCurrentIdx) }]" - :style="{ flex: i === (isFactor ? fxSteps : btSteps).length - 1 ? '0 0 auto' : '1' }" >
@@ -242,19 +241,22 @@ const pageTitle = computed(() => (isFactor.value ? '因子分析' : isOptimize.v .hb-pct { font-size: 30px; font-weight: 700; color: var(--brand); font-variant-numeric: tabular-nums; flex-shrink: 0; } .hb-pct .pct-sign { font-size: 14px; color: var(--text-3); } -/* ===== 时间线 ===== */ +/* ===== 时间线 ===== + 全节点等宽(flex:1)→dot 落在各等分区中心;连接线从本区 dot 右 16px 到 + 下一区 dot 左 16px(等宽下精确对齐;原「末节点不伸缩+bar 宽 100% 越界」 + 会把 4 顶到屏幕右缘且 bar 伸出容器)。 */ .timeline { display: flex; align-items: flex-start; } -.tl-node { display: flex; flex-direction: column; align-items: center; position: relative; padding: 0 8px; } +.tl-node { flex: 1; display: flex; flex-direction: column; align-items: center; position: relative; padding: 0 8px; min-width: 0; } .tl-dot { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; background: var(--bg-hover); color: var(--text-3); border: 1px solid var(--border-2); - z-index: 1; + z-index: 1; flex-shrink: 0; } .tl-label { margin-top: 8px; font-size: 12px; color: var(--text-3); white-space: nowrap; } -.tl-bar { position: absolute; top: 13px; left: 50%; width: 100%; height: 2px; background: var(--border-2); z-index: 0; } +.tl-bar { position: absolute; top: 13px; left: calc(50% + 16px); width: calc(100% - 32px); height: 2px; background: var(--border-2); z-index: 0; } .tl-node.done .tl-dot { background: var(--down); color: #fff; border-color: var(--down); } .tl-node.done .tl-label { color: var(--text); } .tl-node.done .tl-bar { background: var(--down); } diff --git a/sanguo_api/routes.py b/sanguo_api/routes.py index 54cd753..a6d9184 100644 --- a/sanguo_api/routes.py +++ b/sanguo_api/routes.py @@ -730,7 +730,21 @@ def daily_holdings(task_id: str): @router.get("/task/{task_id}/log", dependencies=[Depends(verify_token)]) def log_endpoint(task_id: str): - """Get backtest log text.""" + """Get backtest log text. + + 因子任务无传统日志文件 → 返回心跳流水(progress.log,由 analyzer + _report_progress 逐条 append),行情加载 i/N / 因子 i/M / tears ✓ + 即用户要看的"过程"。 + """ + if task_id.startswith("factor_"): + try: + lp = os.path.join(_FACTOR_PROGRESS_DIR, f"{task_id}.progress.log") + if os.path.exists(lp): + with open(lp, encoding="utf-8") as f: + return {"task_id": task_id, "log": f.read()} + except Exception: + pass + return {"task_id": task_id, "log": ""} orch = get_orchestrator() if hasattr(orch, 'db_path') and orch.db_path: file_dir = os.path.dirname(os.path.abspath(orch.db_path)) diff --git a/sanguo_factor/analyzer.py b/sanguo_factor/analyzer.py index 1d98a25..975c82d 100644 --- a/sanguo_factor/analyzer.py +++ b/sanguo_factor/analyzer.py @@ -62,7 +62,9 @@ class FactorReport: def _report_progress(task_id: str, output_dir: str, stage: str, detail: str) -> None: """跨进程进度心跳:worker 写 {output_dir}/{task_id}.progress, - GET /task/{id} 读它合并返回(前端显示"最近活动 Xs 前 · detail")。 + GET /task/{id} 读它合并返回(前端显示"最近活动 Xs 前 · detail"); + 同时 append 一行到 {task_id}.progress.log 作活动流水——因子任务 + 没有传统日志文件,心跳流水就是 /task/{id}/log 的内容。 失败静默——进度是可观测性锦上添花,不能影响分析主流程。""" if not task_id: return @@ -72,6 +74,10 @@ def _report_progress(task_id: str, output_dir: str, stage: str, detail: str) -> p = os.path.join(output_dir, f"{task_id}.progress") with open(p, "w", encoding="utf-8") as f: f.write(_j.dumps({"stage": stage, "detail": detail, "ts": _t.time()})) + lp = os.path.join(output_dir, f"{task_id}.progress.log") + stamp = _t.strftime("%H:%M:%S") + with open(lp, "a", encoding="utf-8") as f: + f.write(f"[{stamp}] {stage} · {detail}\n") except Exception: pass