/** * 语录博客流:按交易日分组 · 截断展开 · 当日盈亏摘要 · AI 复盘跳转. */ (function () { const page = document.getElementById("page-quotes"); if (!page) return; const elFeed = document.getElementById("quotes-feed"); const elStatus = document.getElementById("quotes-status"); const elBtnRefresh = document.getElementById("quotes-btn-refresh"); const elLinkArchive = document.getElementById("quotes-link-archive"); const RECENT_LIMIT = 20; const PREVIEW_LEN = 140; const ARCHIVE_QUOTE_AI_KEY = "hub_archive_quote_ai"; let quotes = []; let dayStats = {}; let expanded = {}; let inited = false; let loading = false; function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } async function apiFetch(url, opts) { return fetch(url, Object.assign({ credentials: "same-origin" }, opts || {})); } function setStatus(text) { if (elStatus) elStatus.textContent = text || ""; } function findQuote(id) { return ( quotes.find(function (q) { return String(q.id) === String(id); }) || null ); } function fmtPnl(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; return (n >= 0 ? "+" : "") + n.toFixed(2) + "U"; } function pnlClass(v) { const n = Number(v); if (!Number.isFinite(n) || n === 0) return ""; return n > 0 ? "pnl-pos" : "pnl-neg"; } function fmtWinRate(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; return n.toFixed(1) + "%"; } function daySummaryHtml(day, st) { if (!st) { return '当日统计加载中…'; } const openN = Number(st.open_count) || 0; const pnl = st.pnl_total; return ( '' + openN + " 笔 · 盈亏 ' + esc(fmtPnl(pnl)) + " · 胜率 " + esc(fmtWinRate(st.win_rate)) + "" ); } function previewText(raw) { const text = String(raw || "").trim(); if (text.length <= PREVIEW_LEN) return { text: text, truncated: false }; return { text: text.slice(0, PREVIEW_LEN).trim() + "…", truncated: true }; } function groupByDay(rows) { const map = {}; const order = []; rows.forEach(function (q) { const day = String(q.quote_date || "").slice(0, 10) || "—"; if (!map[day]) { map[day] = []; order.push(day); } map[day].push(q); }); return { map: map, order: order }; } function renderFeed() { if (!elFeed) return; if (!quotes.length) { elFeed.innerHTML = '

暂无复盘语录.可在「内照明心 → 复盘语录」中添加.

'; return; } const grouped = groupByDay(quotes); elFeed.innerHTML = grouped.order .map(function (day) { const list = grouped.map[day] || []; const cards = list .map(function (q) { const id = String(q.id); const full = String(q.content || "").trim(); const isOpen = !!expanded[id]; const prev = previewText(full); const showExpand = prev.truncated; const body = isOpen || !showExpand ? full : prev.text; return ( '
' + '
' + esc(body) + "
" + '
' + (showExpand ? '" : "") + '' + "
" ); }) .join(""); return ( '
' + '
' + '

' + esc(day) + "

" + daySummaryHtml(day, dayStats[day]) + "
" + '
' + cards + "
" ); }) .join(""); elFeed.querySelectorAll(".quotes-expand-btn").forEach(function (btn) { btn.addEventListener("click", function () { const id = btn.getAttribute("data-id"); expanded[id] = !expanded[id]; renderFeed(); }); }); elFeed.querySelectorAll(".quotes-ai-btn").forEach(function (btn) { btn.addEventListener("click", function () { startQuoteAiChat(btn.getAttribute("data-id")); }); }); } function startQuoteAiChat(quoteId) { const q = findQuote(quoteId); const content = q && String(q.content || "").trim(); if (!q || !content) { setStatus("语录内容为空,无法发起 AI 对话"); return; } try { sessionStorage.setItem( ARCHIVE_QUOTE_AI_KEY, JSON.stringify({ quote_date: q.quote_date || "", content: content, }) ); } catch (_) { setStatus("无法保存跳转数据"); return; } if (typeof window.hubNavigateTo === "function") { window.hubNavigateTo("/ai"); return; } location.href = "/ai"; } async function loadDayStats(days) { const uniq = []; const seen = {}; (days || []).forEach(function (d) { const day = String(d || "").slice(0, 10); if (!day || day === "—" || seen[day]) return; seen[day] = true; uniq.push(day); }); await Promise.all( uniq.map(async function (day) { if (dayStats[day]) return; try { const q = new URLSearchParams(); q.set("period", "today"); q.set("trading_day", day); const r = await apiFetch("/api/archive/daily-trades?" + q.toString()); const j = await r.json(); if (r.ok) { dayStats[day] = j.stats || { open_count: 0, pnl_total: 0, win_rate: null }; } else { dayStats[day] = { open_count: 0, pnl_total: 0, win_rate: null }; } } catch (_) { dayStats[day] = { open_count: 0, pnl_total: 0, win_rate: null }; } }) ); } async function loadQuotes() { if (loading) return; loading = true; setStatus("加载语录…"); try { const r = await apiFetch("/api/archive/quotes"); const j = await r.json(); if (!r.ok) { setStatus(j.detail || "加载失败"); return; } quotes = (j.quotes || []).slice(0, RECENT_LIMIT); const days = quotes.map(function (q) { return q.quote_date; }); renderFeed(); await loadDayStats(days); renderFeed(); setStatus("最近 " + quotes.length + " 条 · " + new Date().toLocaleTimeString()); } catch (e) { setStatus(String(e && e.message ? e.message : e) || "加载失败"); } finally { loading = false; } } function bindEvents() { if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadQuotes); if (elLinkArchive) { elLinkArchive.addEventListener("click", function (ev) { if (typeof window.hubNavigateTo === "function") { ev.preventDefault(); window.hubNavigateTo("/archive"); } }); } } async function init() { if (!page || page.classList.contains("hidden")) return; if (!inited) { bindEvents(); inited = true; } await loadQuotes(); } function destroy() {} window.hubQuotesPage = { init: init, destroy: destroy }; })();