feat(web): 因子研究/因子组合/股票筛选接入统一作业反馈;阶段圆点按作业类型区分
接着 /backtest 的那次改造,把其余会跑异步 Job 的页面也切到同一套
`useJobRunner` + `JobProgress`(用户原话:包括因子测试等所有测试都帮我完善用户反馈):
- **/factors**(一个因子一个 Job 的批量场景):删掉 `running/processed/current/jobId` 与
按「已处理数 ÷ 总数」自算的 `Progress` **假百分比**;每轮把 `第 i/共 n 个` 交给反馈条,
阶段/作业号/已用秒数全部来自后端。取消 = 用户明确意图 → 保留已出的报告、不再跑后续因子,
不报错;单个因子失败仍继续跑其余因子,并把后端原文累积成**批级清单**(多因子时反馈条
只能显示最后一个作业,前几个失败不能丢)。
- **/factors/compose**:删掉 `value={30}` 的假进度条;`archiveId` / 复用 `BacktestResultView` /
「新页面放大」全部保留;failed/cancelled 交给反馈条,页面 error 只留参数与目录错误
(同一失败不在两处各说一遍)。
- **/selection**:异步选股切反馈条;**同步**的「执行选股」保留原 loading(`POST /selections`
没有 job_id,套上会去 `GET /jobs/{signal}` 撞 404)。
- **/signals**:`POST /api/signals` 是同步接口,**不套**作业反馈(不编作业号、不编阶段),
改为点击即现的 `role="status"` 提示,如实写明「同步请求、请求期间不能关页、无阶段无取消、
出错显示后端原文」。
- **阶段圆点按作业类型区分**(修掉一个真实缺陷):原来全站共用一张含 `queued/done` 的
`STAGE_ORDER`,因子测试页实测出现过**裸英文** `factor_calculation` 且 4 个圆点全灰
(`indexOf` = -1),还画出了因子测试根本不存在的「逐择股日选股 / 撮合与净值结算」。
现在 `STAGE_PIPELINES = { factor_test: [加载→计算因子值→汇总], backtest: [加载→撮合→汇总],
selection: [逐择股日选股] }`(阶段序列**不含 queued/done**,那是作业状态不是阶段),
未知阶段显示「执行中(stage)」并保留已推进的圆点,不整排灰、不露裸枚举。
验证(真实浏览器 CDP,读数原文已记录):
- /factors:点击后 0.2s 内 `submitting`→`queued` + 作业号;+30s Pill「计算因子值」、
圆点 `✓ 加载行情与因子数据 / ● 计算因子值 / ○ 汇总指标与曲线`(无「选股/撮合」、无英文枚举);
成功态给「去对比 / 打开归档」。
- /selection:圆点只有 `逐择股日选股` 一段;取消 → 「已取消,没有归档」;
另实测撞并发上限时如实显示后端原文「系统繁忙:并发研究任务已达上限」。
- /factors/compose:`submitting→queued→撮合与净值结算→success(EXP-…)`,业务结果与 5 处放大入口照旧。
- /backtest 回归:圆点由 4 个变 3 个(去掉后端**从不上报**的 selection 阶段),取消仍「已取消 + 没归档」。
- 自检产生的 12 个实验归档已全部删除(bulk-delete count:12,复查无残留)。
This commit is contained in:
@@ -13,12 +13,12 @@
|
||||
* 不滚一下用户就看不见(这正是「没有反馈」错觉的来源之一)。
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Btn, Pill } from "@/components/ui";
|
||||
import { Icon } from "@/components/icons";
|
||||
import { STAGE_LABEL, STAGE_ORDER, type JobRunState } from "@/lib/jobs";
|
||||
import { STAGE_LABEL, STAGE_PIPELINES, type JobKind, type JobRunState } from "@/lib/jobs";
|
||||
|
||||
/** 已用时间:< 60s 显示秒,否则 mm:ss(跑几分钟时更好读) */
|
||||
function fmtElapsed(ms: number): string {
|
||||
@@ -38,15 +38,36 @@ export function JobProgress<T>({
|
||||
onDismiss,
|
||||
/** 完成后是否自动滚到视野(提交时恒滚,完成后不打断阅读) */
|
||||
anchorId = "job-progress",
|
||||
/** 作业类型:决定阶段小圆点画哪几条 —— 因子测试没有「逐择股日选股 / 撮合与净值结算」,
|
||||
* 选股也只有一个真实阶段,全站共用一张表会把这些不存在的阶段画成「○ 待开始」。 */
|
||||
kind = "backtest",
|
||||
}: {
|
||||
state: JobRunState<T>;
|
||||
onCancel?: () => void;
|
||||
onDismiss?: () => void;
|
||||
anchorId?: string;
|
||||
kind?: JobKind;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const scrolled = useRef(false);
|
||||
const active = state.phase === "submitting" || state.phase === "queued" || state.phase === "running";
|
||||
/** 本类作业真实会走的阶段(不含 queued / done —— 那是作业状态,不是后端阶段) */
|
||||
const pipeline = STAGE_PIPELINES[kind];
|
||||
/**
|
||||
* 后端阶段是前端还不认识的枚举时(后端先加、前端文案没跟上),
|
||||
* 保留**上一个已知阶段**的进度而不是把整排圆点打成灰(那看起来像「什么都没在跑」);
|
||||
* 原始枚举另在 Pill 上兜底显示(如「执行中(factor_calculation)」),不出现裸英文。
|
||||
*/
|
||||
const [lastKnown, setLastKnown] = useState("");
|
||||
useEffect(() => {
|
||||
// 新一轮提交(submitting)先清空:否则同一页第二次运行时,会拿上一轮的最后阶段当进度,
|
||||
// 新作业明明刚排队却显示「已完成到分析的 ✓」——那是假的进度。
|
||||
if (state.phase === "submitting") {
|
||||
setLastKnown("");
|
||||
return;
|
||||
}
|
||||
if (pipeline.includes(state.stage)) setLastKnown(state.stage);
|
||||
}, [state.phase, state.stage, pipeline]);
|
||||
|
||||
// 提交瞬间(含同一页面第二次提交)把反馈条滚进视野:按钮在长表单底部时,
|
||||
// 顶部出现的反馈条不滚过去就等于没显示。
|
||||
@@ -66,9 +87,9 @@ export function JobProgress<T>({
|
||||
const stageText = state.stage
|
||||
? state.stage === "cancelling"
|
||||
? "正在取消…"
|
||||
: (STAGE_LABEL[state.stage] ?? state.stage)
|
||||
: (STAGE_LABEL[state.stage] ?? `执行中(${state.stage})`)
|
||||
: "等待后端回报阶段";
|
||||
const cur = STAGE_ORDER.indexOf(state.stage);
|
||||
const cur = pipeline.indexOf(pipeline.includes(state.stage) ? state.stage : lastKnown);
|
||||
const archiveHref = state.experimentId
|
||||
? `/experiments?exp=${encodeURIComponent(state.experimentId)}`
|
||||
: null;
|
||||
@@ -158,8 +179,8 @@ export function JobProgress<T>({
|
||||
|
||||
{active ? (
|
||||
<div className="stages" style={{ marginTop: 8 }}>
|
||||
{["data_loading", "selection", "backtesting", "analysis"].map((st) => {
|
||||
const mine = STAGE_ORDER.indexOf(st);
|
||||
{pipeline.map((st) => {
|
||||
const mine = pipeline.indexOf(st);
|
||||
const cls = cur >= 0 && mine < cur ? "stage is-done" : mine === cur ? "stage is-active" : "stage";
|
||||
return (
|
||||
<span className={cls} key={st}>
|
||||
|
||||
Reference in New Issue
Block a user