fix(HEL-104): 修复第四批P3a八处返工——引导胶囊入消息区/导师胶囊置顶/问师返回与空态/选股三视图按策略过滤/入选日列/空态切策略按钮
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+139
-59
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user