fix(HEL-104): 修复第四批P3a八处返工——引导胶囊入消息区/导师胶囊置顶/问师返回与空态/选股三视图按策略过滤/入选日列/空态切策略按钮

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
施工员
2026-08-24 23:54:19 +08:00
co-authored by multica-agent
parent 20b05b1b78
commit c466014862
2 changed files with 161 additions and 65 deletions
+139 -59
View File
@@ -1620,16 +1620,8 @@
history: "入选历史"
};
function screenerViewKey(payload, view) {
if (!payload) return [];
if (view === "active") return payload.active_signals || [];
if (view === "history") return payload.candidate_history || [];
const latest = payload.latest_results || {};
const strategyLibrary = screenerCurrentLibrary(payload);
const mode = strategyLibrary || "smart";
const run = latest[mode] || latest.smart || latest.curated || null;
if (!run) return [];
return run.candidates || [];
function screenerCurrentStrategy(payload) {
return findScreenerStrategy(payload, state.screener.strategyId);
}
function screenerCurrentLibrary(payload) {
@@ -1639,13 +1631,71 @@
return (s.formula && s.formula.meta && s.formula.meta.library) || "smart";
}
function screenerLibraryMode(library) {
if (library === "curated") return "curated";
if (library === "quant" || library === "custom") return "quant";
return "smart";
}
// 按当前策略名在 recent_results / latest_results 中匹配最新一次运行(对齐电脑端 activeScreenerResultEntry
function screenerRunForCurrentStrategy(payload) {
if (!payload) return null;
const strategy = screenerCurrentStrategy(payload);
if (!strategy || !strategy.name) return null;
const name = strategy.name;
const recent = (payload.recent_results || []).slice().reverse();
for (let i = 0; i < recent.length; i += 1) {
const run = recent[i];
if (run && run.meta && run.meta.strategy_name === name) return run;
}
const latest = (payload.latest_results || {})[screenerLibraryMode(screenerCurrentLibrary(payload))];
if (latest && latest.meta && latest.meta.strategy_name === name) return latest;
return null;
}
// 持续有效 / 入选历史:对齐电脑端 selectedScreenerArchiveRows,按当前策略过滤 active_signals/candidate_history 组装行
function screenerArchiveRows(payload, view) {
if (!payload) return [];
const source = view === "active" ? (payload.active_signals || []) : (payload.candidate_history || []);
const strategy = screenerCurrentStrategy(payload);
const strategyName = strategy ? strategy.name : "";
const mode = screenerLibraryMode(screenerCurrentLibrary(payload));
return source.filter(function (row) {
if ((row.mode || "smart") !== mode) return false;
return (row.hits || []).some(function (hit) { return hit.strategy_name === strategyName; });
}).map(function (row) {
let hits = (row.hits || []).filter(function (hit) { return hit.strategy_name === strategyName; });
if (view === "active") hits = hits.filter(function (hit) { return hit.active; });
const latestHit = hits.slice().sort(function (a, b) {
return String(b.selection_date || "").localeCompare(String(a.selection_date || ""));
})[0] || {};
return {
code: row.code,
name: row.name,
sector: row.sector,
pct_chg: row.pct_chg,
return_5d: row.return_5d,
score_display: latestHit.score_display != null ? latestHit.score_display : row.score_display,
selection_date: displayCompactDate(latestHit.selection_date || row.selection_date || ""),
status: view === "active" ? "持续有效" : "已入选"
};
});
}
function screenerViewKey(payload, view) {
if (!payload) return [];
if (view === "active") return screenerArchiveRows(payload, "active");
if (view === "history") return screenerArchiveRows(payload, "history");
const run = screenerRunForCurrentStrategy(payload);
if (!run) return [];
return run.candidates || [];
}
function screenerCurrentRun(payload) {
if (!payload) return null;
if (state.screener.view === "active") return null;
if (state.screener.view === "history") return null;
const latest = payload.latest_results || {};
const mode = screenerCurrentLibrary(payload);
return latest[mode] || latest.smart || latest.curated || null;
return screenerRunForCurrentStrategy(payload);
}
function screenerCurrentRunId() {
@@ -1810,6 +1860,16 @@
return "暂无信号";
}
function screenerEmptyText(payload) {
if (state.screener.view === "active") return "该策略暂无持续有效信号";
if (state.screener.view === "history") return "该策略暂无入选历史";
const status = screenerStrategyStatusText(payload);
if (status === "数据不足") return "该策略数据不足,暂无法出候选";
if (status === "等待盘后") return "该策略等待盘后更新";
if (status === "选择策略") return "请先选择一个策略查看结果";
return "该策略当日暂无信号";
}
function formatScreenerUpdatedAt(value) {
if (!value) return "盘后已更新";
const text = String(value);
@@ -1829,8 +1889,9 @@
if (!rows.length) {
wrap.innerHTML = '<div class="m-state m-motion-rise-in">' +
'<span class="m-state-icon">' + icon("inbox", 26) + '</span>' +
'<p>当前策略当日暂无信号</p>' +
'<small>可点上方策略行切换其他策略</small>' +
'<p>' + escapeHtml(screenerEmptyText(payload)) + '</p>' +
'<small>可切换其他策略查看结果</small>' +
'<button class="m-btn-primary m-btn-retry" type="button" data-screener-open-drawer style="margin-top:16px;">切换策略</button>' +
'</div>';
return;
}
@@ -1838,15 +1899,20 @@
const followed = screenerFollowedCodes();
rows.forEach(function (r) { r.tracked = followed[String(r.code)] ? true : false; });
const primaryColumns = [];
if (state.screener.view === "history") {
primaryColumns.push({ key: "selection_date", label: "入选日", type: "text", width: 76 });
}
primaryColumns.push(
{ key: "score_display", label: "综合分", type: "score" },
{ key: "pct_chg", label: "涨幅%", type: "change" },
{ key: "sector", label: "板块", type: "text" }
);
const cols = {
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "score_display", label: "综合分", type: "score" },
{ key: "pct_chg", label: "涨幅%", type: "change" },
{ key: "sector", label: "板块", type: "text" }
],
primaryColumns: primaryColumns,
scrollColumns: [
{ key: "historical_probability", label: "历史胜率%", type: "rate" },
{ key: "return_5d", label: "5日%", type: "change" },
@@ -1941,6 +2007,20 @@
});
}
function screenerStrategyRegimeText(s) {
const regimes = (s && s.regimes) || [];
if (!regimes.length) return "";
const all = (state.screener.data && state.screener.data.regimes) || [];
const labels = [];
regimes.forEach(function (id) {
const r = all.find(function (x) { return x.id === id; });
labels.push(r ? r.label : id);
});
if (!labels.length) return "";
if (all.length > 0 && labels.length >= all.length) return "全阶段";
return labels.join(" / ");
}
function screenerStrategyStatusTextFor(s, payload) {
if (s.published_run) {
const pub = s.published_run;
@@ -1972,8 +2052,12 @@
const isCurrent = String(s.id) === String(state.screener.strategyId);
const status = screenerStrategyStatusTextFor(s, state.screener.data);
const statusCls = status === "数据不足" ? 'm-strategy-status--warn' : (status === "暂无信号" || status === "等待盘后" ? 'm-strategy-status--muted' : '');
const regimeText = screenerStrategyRegimeText(s);
return '<button class="m-screener-drawer-row" type="button" data-screener-row="' + escapeHtml(String(s.id)) + '">' +
'<span class="m-screener-drawer-main">' +
'<span class="m-screener-drawer-name">' + escapeHtml(s.name) + (isCurrent ? ' <span class="m-strategy-check">' + icon("check", 16) + '</span>' : '') + '</span>' +
(regimeText ? '<small class="m-screener-drawer-sub">' + escapeHtml(regimeText) + '</small>' : '') +
'</span>' +
'<span class="m-strategy-status ' + statusCls + '">' + escapeHtml(status) + '</span>' +
'</button>';
}).join("") || '<div class="m-state m-state--error"><p>无匹配策略</p></div>';
@@ -2151,6 +2235,23 @@
const runId = screenerCurrentRunId();
const followed = screenerFollowedCodes()[String(code)];
const isTracked = Boolean(followed);
const contributions = Array.isArray(row.contributions) ? row.contributions : [];
const riskFlags = Array.isArray(row.risk_flags) ? row.risk_flags : [];
const contributionGroup = (row.reason || contributions.length)
? appendDetailGroup("主要贡献", contributions.length
? contributions.map(function (c) {
return {
label: c.label || "",
value: (c.value != null && c.value !== "" ? escapeHtml(String(c.value)) : "--") + (c.points != null ? "+" + formatNumber(Number(c.points), 1) + "分)" : "")
};
})
: [{ label: "", value: '<span style="white-space:normal;">' + escapeHtml(row.reason || "") + '</span>' }])
: "";
const riskGroup = riskFlags.length
? appendDetailGroup("风险标记", riskFlags.map(function (f) {
return { label: "", value: '<span style="white-space:normal;">' + escapeHtml(f) + '</span>' };
}))
: "";
const html =
appendDetailGroup("选股信息", [
{ label: "综合分", value: formatNumber(Number(row.score_display || 0), 1), cls: Number(row.score_display || 0) >= 80 ? "up" : "" },
@@ -2158,7 +2259,8 @@
{ label: "历史胜率", value: row.historical_probability != null ? formatNumber(Number(row.historical_probability), 1) + "%" : "--" },
{ label: "板块", value: escapeHtml(row.sector || "--") }
]) +
(row.reason ? appendDetailGroup("主要贡献", [{ label: "", value: '<span style="white-space:normal;">' + escapeHtml(row.reason) + '</span>' }]) : '') +
contributionGroup +
riskGroup +
'<div class="m-screener-detail-actions">' +
(isTracked
? '<button class="m-btn-primary m-btn-disabled" type="button" disabled>已加入跟踪</button>'
@@ -2328,8 +2430,8 @@
aborter: null
};
const isAssistant = key === "assistant/chat";
// 聊天页是底栏直达:不要 back 按钮m-actions 由本函数自己控制
global.MobileRouter.updateHeader({ title: isAssistant ? "复盘助手" : "问师", back: false, actions: "" });
// 复盘助手是底栏直达(无 back);问师从智能工具图标页进入(有 back)m-actions 由本函数自己控制
global.MobileRouter.updateHeader({ title: isAssistant ? "复盘助手" : "问师", back: !isAssistant, actions: "" });
document.getElementById("m-view").innerHTML = buildChatShell(key);
const actions = [];
if (!isAssistant) {
@@ -2343,9 +2445,8 @@
function buildChatShell(key) {
const isAssistant = key === "assistant/chat";
return '<div class="m-page m-chat-page" data-page="' + escapeHtml(key) + '">' +
'<div class="m-chat-stream" id="m-chat-stream">' + chatSkeletonHtml() + '</div>' +
(isAssistant ? '' : '<div class="m-chat-mentor-bar" id="m-chat-mentor-bar"></div>') +
'<div class="m-chat-presets" id="m-chat-presets"></div>' +
'<div class="m-chat-stream" id="m-chat-stream">' + chatSkeletonHtml() + '</div>' +
'<div class="m-chat-input-bar" id="m-chat-input-bar">' +
'<div class="m-chat-input-wrap">' +
'<textarea id="m-chat-input" class="m-chat-input" rows="1" placeholder="问点什么…" maxlength="2000" enterkeyhint="send"></textarea>' +
@@ -2429,7 +2530,6 @@
});
state.chat.historyLoaded = true;
renderChatStream();
renderChatPresets();
}).catch(function (error) {
if (seq !== state.seq) return;
renderChatStreamError(error && error.message ? error.message : "对话历史加载失败");
@@ -2444,12 +2544,9 @@
global.MobileAPI.request("/api/mentors/setup?trade_date=" + encodeURIComponent(todayString())).then(function (payload) {
if (seq !== state.seq) return;
state.chat.mentorSetup = payload;
// 没有选定导师时,从偏好或首位开始
// 无已选导师时保持空态(spec §1.5):不自动选中导师,等用户点「选择导师」或顶栏 bot 开抽屉
if (!state.chat.mentorId) {
const mentors = (payload && payload.mentors) || [];
if (mentors.length) {
selectMentor(mentors[0].id, { silent: true });
}
// 空态,等待选择
} else {
// 已选导师,更新名称/标签
const cur = ((payload && payload.mentors) || []).find(function (m) { return m.id === state.chat.mentorId; });
@@ -2463,7 +2560,6 @@
}
renderChatMentorBar();
renderChatStream();
renderChatPresets();
}).catch(function (error) {
if (seq !== state.seq) return;
renderChatStreamError(error && error.message ? error.message : "导师目录加载失败");
@@ -2484,7 +2580,6 @@
});
state.chat.historyLoaded = true;
renderChatStream();
renderChatPresets();
}).catch(function (error) {
if (seq !== state.seq) return;
renderChatStreamError(error && error.message ? error.message : "对话历史加载失败");
@@ -2510,7 +2605,6 @@
if (typeof closeSheet === "function") closeSheet();
renderChatMentorBar();
renderChatStream();
renderChatPresets();
loadMentorHistory();
}
@@ -2519,7 +2613,6 @@
state.chat.messages = [];
state.chat.followUps = [];
renderChatStream();
renderChatPresets();
showToast("已清空对话");
}).catch(function (err) {
showToast((err && err.message) || "清空失败");
@@ -2532,7 +2625,6 @@
state.chat.messages = [];
state.chat.followUps = [];
renderChatStream();
renderChatPresets();
showToast("已清空当日对话");
}).catch(function (err) {
showToast((err && err.message) || "清空失败");
@@ -2558,7 +2650,8 @@
if (isAssistant) {
html =
chatBubbleHtml("ai", "我是你的复盘助手,可以问我市场位置、主线、交易复盘、明日清单。") +
'<div class="m-chat-empty-hint">从下面挑一个开始,或直接输入你的问题</div>';
'<div class="m-chat-empty-hint">从下面挑一个开始,或直接输入你的问题</div>' +
chatPresetPillsHtml();
} else if (state.chat.mentorId) {
// 显示导师 tagline 原文(spec §1.5
const tagline = state.chat.mentorTagline || "今天想问点什么?";
@@ -2664,8 +2757,9 @@
function renderChatMentorBar() {
const bar = document.getElementById("m-chat-mentor-bar");
if (!bar) return;
if (state.chat.page === "assistant/chat") { bar.innerHTML = ""; return; }
if (!state.chat.mentorId) { bar.innerHTML = ""; return; }
if (state.chat.page === "assistant/chat") { bar.innerHTML = ""; bar.style.display = "none"; return; }
if (!state.chat.mentorId) { bar.innerHTML = ""; bar.style.display = "none"; return; }
bar.style.display = "flex";
const grade = state.chat.mentorGrade || "";
const gradeBadge = grade ? '<span class="m-mentor-grade m-mentor-grade--' + escapeHtml(grade.toLowerCase()) + '">' + escapeHtml(grade) + '</span>' : "";
bar.innerHTML = '<button class="m-mentor-pill" type="button" data-chat-mentors>' +
@@ -2674,22 +2768,12 @@
'</button>';
}
function renderChatPresets() {
const wrap = document.getElementById("m-chat-presets");
if (!wrap) return;
const isAssistant = state.chat.page === "assistant/chat";
const hasHistory = (state.chat.messages || []).length > 0;
if (isAssistant) {
// 复盘助手:仅在空会话时显示引导问题
if (hasHistory || state.chat.streaming) { wrap.innerHTML = ""; return; }
wrap.innerHTML = '<div class="m-source-tabs m-chat-preset-list" role="tablist">' +
ASSISTANT_PRESET_QUESTIONS.map(function (q) {
return '<button class="m-source-tab" type="button" data-chat-preset="' + escapeHtml(q) + '">' + escapeHtml(q) + '</button>';
}).join("") + '</div>';
} else {
// 问师:无引导问题
wrap.innerHTML = "";
}
function chatPresetPillsHtml() {
return '<div class="m-chat-presets">' +
'<div class="m-chat-preset-list">' +
ASSISTANT_PRESET_QUESTIONS.map(function (q) {
return '<button class="m-source-tab" type="button" data-chat-preset="' + escapeHtml(q) + '">' + escapeHtml(q) + '</button>';
}).join("") + '</div></div>';
}
function renderChatFollowUps() {
@@ -2734,7 +2818,6 @@
const input = document.getElementById("m-chat-input");
if (input) { input.value = ""; autoSizeChatInput(); }
renderChatStream();
renderChatPresets();
startChatStream(text);
}
@@ -2805,7 +2888,6 @@
state.chat.streamBubble = null;
syncChatSendEnabled();
renderChatStream();
renderChatPresets();
}
function handleChatStreamError(err, aborted) {
@@ -2820,7 +2902,6 @@
state.chat.streamBuffer = "";
syncChatSendEnabled();
renderChatStream();
renderChatPresets();
return;
}
state.chat.streaming = false;
@@ -3660,7 +3741,6 @@
state.chat.messages = [];
renderChatStream();
renderChatMentorBar();
renderChatPresets();
return;
}
});