feat(hub): refactor archive quotes to list-detail with top-form edit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 20:42:35 +08:00
parent e7e3a49151
commit cf1265763c
3 changed files with 140 additions and 113 deletions
+35 -40
View File
@@ -5532,37 +5532,49 @@ body.funds-fullscreen-open {
} }
.archive-quotes-list { .archive-quotes-list {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 420px; min-height: 160px;
overflow: auto; overflow: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.archive-quote-card { .archive-quote-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px;
align-items: center;
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
border-radius: 8px; border-radius: 8px;
background: var(--inset-surface); background: var(--inset-surface);
overflow: hidden; color: inherit;
} font: inherit;
.archive-quote-summary { text-align: left;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
padding: 8px 10px;
cursor: pointer; cursor: pointer;
list-style: none;
} }
.archive-quote-open-hint { .archive-quote-item:hover {
font-size: 0.7rem; border-color: color-mix(in srgb, var(--accent) 40%, var(--border-soft));
color: var(--accent);
white-space: nowrap;
} }
.archive-quote-card[open] .archive-quote-open-hint { .archive-quote-item.is-selected {
color: var(--muted); border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, var(--inset-surface));
} }
.archive-quote-summary::-webkit-details-marker { .archive-quote-detail {
display: none; flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 10px;
border-top: 1px solid var(--border-soft);
min-height: 140px;
max-height: 42vh;
}
.archive-quote-detail .archive-quote-full {
flex: 1 1 auto;
min-height: 96px;
max-height: 34vh;
overflow: auto;
} }
.archive-quote-date { .archive-quote-date {
font-weight: 600; font-weight: 600;
@@ -5577,12 +5589,6 @@ body.funds-fullscreen-open {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.archive-quote-body {
padding: 0 10px 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.archive-quote-full { .archive-quote-full {
padding: 10px 12px; padding: 10px 12px;
border-radius: 8px; border-radius: 8px;
@@ -5595,20 +5601,6 @@ body.funds-fullscreen-open {
word-break: break-word; word-break: break-word;
max-height: none; max-height: none;
} }
.archive-quote-edit {
width: 100%;
min-height: 160px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--border-soft);
background: var(--panel);
color: var(--text);
font-family: var(--font);
font-size: 0.8rem;
resize: vertical;
line-height: 1.55;
white-space: pre-wrap;
}
.archive-quote-actions { .archive-quote-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -5943,7 +5935,10 @@ body.funds-fullscreen-open {
max-height: none; max-height: none;
} }
.archive-quotes-list { .archive-quotes-list {
min-height: 220px; min-height: 120px;
}
.archive-quote-detail {
max-height: 36vh;
} }
} }
+95 -70
View File
@@ -25,6 +25,11 @@
const elQuoteForm = document.getElementById("archive-quote-form"); const elQuoteForm = document.getElementById("archive-quote-form");
const elQuoteDate = document.getElementById("archive-quote-date"); const elQuoteDate = document.getElementById("archive-quote-date");
const elQuoteContent = document.getElementById("archive-quote-content"); const elQuoteContent = document.getElementById("archive-quote-content");
const elQuoteSubmit = document.getElementById("archive-quote-submit");
const elQuoteDetail = document.getElementById("archive-quote-detail");
const elQuoteDetailFull = document.getElementById("archive-quote-detail-full");
const elQuoteEditBtn = document.getElementById("archive-quote-edit-btn");
const elQuoteDelBtn = document.getElementById("archive-quote-del-btn");
const elChartSection = document.getElementById("archive-chart-section"); const elChartSection = document.getElementById("archive-chart-section");
const elChartTitle = document.getElementById("archive-chart-title"); const elChartTitle = document.getElementById("archive-chart-title");
const elTfTabs = document.getElementById("archive-tf-tabs"); const elTfTabs = document.getElementById("archive-tf-tabs");
@@ -49,6 +54,8 @@
let meta = null; let meta = null;
let quotes = []; let quotes = [];
let selectedQuoteId = null;
let editingQuoteId = null;
let dailyTrades = []; let dailyTrades = [];
let dailyStats = { open_count: 0, by_exchange: {} }; let dailyStats = { open_count: 0, by_exchange: {} };
let periodMode = "today"; let periodMode = "today";
@@ -480,20 +487,57 @@
return s.length > 36 ? s.slice(0, 36) + "…" : s; return s.length > 36 ? s.slice(0, 36) + "…" : s;
} }
function quoteEditRows(text) { function findQuote(id) {
const t = String(text || ""); if (id == null || id === "") return null;
const lines = t.split(/\n/).length; return (
const wrapLines = Math.ceil(t.length / 26); quotes.find(function (q) {
return Math.min(32, Math.max(8, lines, wrapLines)); return String(q.id) === String(id);
}) || null
);
} }
function resizeQuoteTextarea(ta) { function updateQuoteSubmitBtn() {
if (!ta) return; if (!elQuoteSubmit) return;
ta.style.height = "auto"; elQuoteSubmit.textContent = editingQuoteId ? "修改保存" : "添加语录";
const lines = String(ta.value || "").split("\n").length; }
const wrapLines = Math.ceil(String(ta.value || "").length / 26);
ta.rows = Math.min(32, Math.max(8, lines, wrapLines)); function resetQuoteForm() {
ta.style.height = Math.max(ta.scrollHeight, 160) + "px"; editingQuoteId = null;
if (elQuoteContent) elQuoteContent.value = "";
updateQuoteSubmitBtn();
}
function startEditQuote() {
const q = findQuote(selectedQuoteId);
if (!q) return;
editingQuoteId = q.id;
if (elQuoteDate) elQuoteDate.value = q.quote_date || "";
if (elQuoteContent) {
elQuoteContent.value = q.content || "";
elQuoteContent.focus();
}
updateQuoteSubmitBtn();
}
function selectQuote(id) {
if (editingQuoteId != null && String(id) !== String(editingQuoteId)) {
resetQuoteForm();
}
selectedQuoteId = id;
renderQuotes();
renderQuoteDetail();
}
function renderQuoteDetail() {
if (!elQuoteDetail) return;
const q = findQuote(selectedQuoteId);
if (!q) {
elQuoteDetail.hidden = true;
if (elQuoteDetailFull) elQuoteDetailFull.textContent = "";
return;
}
elQuoteDetail.hidden = false;
if (elQuoteDetailFull) elQuoteDetailFull.textContent = q.content || "(空)";
} }
function renderQuotes() { function renderQuotes() {
@@ -503,79 +547,58 @@
} }
if (!quotes.length) { if (!quotes.length) {
elQuotesList.innerHTML = '<p class="archive-empty">暂无复盘语录,可在上方添加。</p>'; elQuotesList.innerHTML = '<p class="archive-empty">暂无复盘语录,可在上方添加。</p>';
if (elQuoteDetail) elQuoteDetail.hidden = true;
return; return;
} }
elQuotesList.innerHTML = quotes elQuotesList.innerHTML = quotes
.map(function (q) { .map(function (q) {
const selected = String(q.id) === String(selectedQuoteId);
return ( return (
'<details class="archive-quote-card">' + '<button type="button" class="archive-quote-item' +
'<summary class="archive-quote-summary">' + (selected ? " is-selected" : "") +
'" data-id="' +
q.id +
'">' +
'<span class="archive-quote-date">' + '<span class="archive-quote-date">' +
esc(q.quote_date) + esc(q.quote_date) +
"</span>" + "</span>" +
'<span class="archive-quote-preview">' + '<span class="archive-quote-preview">' +
esc(quotePreview(q.content)) + esc(quotePreview(q.content)) +
"</span>" + "</span>" +
'<span class="archive-quote-open-hint">查看</span>' + "</button>"
"</summary>" +
'<div class="archive-quote-body">' +
'<div class="archive-quote-full">' +
esc(q.content || "(空)") +
"</div>" +
'<textarea class="archive-quote-edit" data-id="' +
q.id +
'" rows="' +
quoteEditRows(q.content) +
'">' +
esc(q.content) +
"</textarea>" +
'<div class="archive-quote-actions">' +
'<button type="button" class="ghost archive-quote-save" data-id="' +
q.id +
'">保存</button>' +
'<button type="button" class="archive-del-btn archive-quote-del" data-id="' +
q.id +
'">删除</button>' +
"</div></div></details>"
); );
}) })
.join(""); .join("");
elQuotesList.querySelectorAll(".archive-quote-card").forEach(function (card) { elQuotesList.querySelectorAll(".archive-quote-item").forEach(function (btn) {
card.addEventListener("toggle", function () {
if (!card.open) return;
resizeQuoteTextarea(card.querySelector(".archive-quote-edit"));
});
});
elQuotesList.querySelectorAll(".archive-quote-save").forEach(function (btn) {
btn.addEventListener("click", function () { btn.addEventListener("click", function () {
const id = btn.getAttribute("data-id"); selectQuote(btn.getAttribute("data-id"));
const card = btn.closest(".archive-quote-card");
const ta = card && card.querySelector(".archive-quote-edit");
const dateEl = card && card.querySelector(".archive-quote-date");
if (!id || !ta) return;
void saveQuote(id, dateEl ? dateEl.textContent : "", ta.value, card);
});
});
elQuotesList.querySelectorAll(".archive-quote-del").forEach(function (btn) {
btn.addEventListener("click", function () {
void deleteQuote(btn.getAttribute("data-id"));
}); });
}); });
renderQuoteDetail();
} }
async function loadQuotes() { async function loadQuotes() {
const r = await apiFetch("/api/archive/quotes"); const r = await apiFetch("/api/archive/quotes");
const j = await r.json(); const j = await r.json();
quotes = j.quotes || []; quotes = j.quotes || [];
if (!quotes.length) {
selectedQuoteId = null;
} else if (!findQuote(selectedQuoteId)) {
selectedQuoteId = quotes[0].id;
}
renderQuotes(); renderQuotes();
} }
async function addQuote(ev) { async function submitQuoteForm(ev) {
if (ev) ev.preventDefault(); if (ev) ev.preventDefault();
const date = elQuoteDate && elQuoteDate.value; const date = elQuoteDate && elQuoteDate.value;
const content = elQuoteContent && elQuoteContent.value.trim(); const content = elQuoteContent && elQuoteContent.value.trim();
if (!date || !content) return; if (!date || !content) return;
if (editingQuoteId) {
await saveQuote(editingQuoteId, date, content);
return;
}
const r = await apiFetch("/api/archive/quotes", { const r = await apiFetch("/api/archive/quotes", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -586,34 +609,27 @@
setStatus(j.detail || "添加失败"); setStatus(j.detail || "添加失败");
return; return;
} }
if (elQuoteContent) elQuoteContent.value = ""; resetQuoteForm();
selectedQuoteId =
j.quote && j.quote.id != null ? j.quote.id : selectedQuoteId;
await loadQuotes(); await loadQuotes();
setStatus("语录已添加"); setStatus("语录已添加");
} }
async function saveQuote(id, quoteDate, content, cardEl) { async function saveQuote(id, quoteDate, content) {
let card = cardEl;
if (!card && elQuotesList) {
const ta = elQuotesList.querySelector('.archive-quote-edit[data-id="' + id + '"]');
card = ta ? ta.closest(".archive-quote-card") : null;
}
const date =
quoteDate ||
(card &&
card.querySelector(".archive-quote-date") &&
card.querySelector(".archive-quote-date").textContent) ||
"";
const r = await apiFetch("/api/archive/quotes/" + id, { const r = await apiFetch("/api/archive/quotes/" + id, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quote_date: date.trim(), content: content }), body: JSON.stringify({ quote_date: String(quoteDate || "").trim(), content: content }),
}); });
const j = await r.json(); const j = await r.json();
if (!r.ok) { if (!r.ok) {
setStatus(j.detail || "保存失败"); setStatus(j.detail || "保存失败");
return; return;
} }
if (card) card.open = false; const savedId = id;
resetQuoteForm();
selectedQuoteId = savedId;
await loadQuotes(); await loadQuotes();
setStatus("语录已保存"); setStatus("语录已保存");
} }
@@ -628,6 +644,8 @@
setStatus(j.detail || "删除失败"); setStatus(j.detail || "删除失败");
return; return;
} }
if (String(id) === String(editingQuoteId)) resetQuoteForm();
if (String(id) === String(selectedQuoteId)) selectedQuoteId = null;
await loadQuotes(); await loadQuotes();
setStatus("语录已删除"); setStatus("语录已删除");
} }
@@ -1363,7 +1381,14 @@
} }
}); });
} }
if (elQuoteForm) elQuoteForm.addEventListener("submit", addQuote); if (elQuoteForm) elQuoteForm.addEventListener("submit", submitQuoteForm);
if (elQuoteEditBtn) elQuoteEditBtn.addEventListener("click", startEditQuote);
if (elQuoteDelBtn) {
elQuoteDelBtn.addEventListener("click", function () {
if (!selectedQuoteId) return;
void deleteQuote(selectedQuoteId);
});
}
if (elTfTabs) { if (elTfTabs) {
elTfTabs.addEventListener("click", function (ev) { elTfTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-tf-btn"); const btn = ev.target.closest(".archive-tf-btn");
+10 -3
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260612-archive-quotes" /> <link rel="stylesheet" href="/assets/app.css?v=20260612-archive-quotes-v2" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" /> <link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head> </head>
<body> <body>
@@ -303,9 +303,16 @@
<form id="archive-quote-form" class="archive-quote-form"> <form id="archive-quote-form" class="archive-quote-form">
<input id="archive-quote-date" type="date" required /> <input id="archive-quote-date" type="date" required />
<textarea id="archive-quote-content" rows="5" placeholder="今日复盘心得…" required></textarea> <textarea id="archive-quote-content" rows="5" placeholder="今日复盘心得…" required></textarea>
<button type="submit" class="primary">添加语录</button> <button type="submit" id="archive-quote-submit" class="primary">添加语录</button>
</form> </form>
<div id="archive-quotes-list" class="archive-quotes-list"></div> <div id="archive-quotes-list" class="archive-quotes-list"></div>
<div id="archive-quote-detail" class="archive-quote-detail" hidden>
<div id="archive-quote-detail-full" class="archive-quote-full"></div>
<div class="archive-quote-actions">
<button type="button" id="archive-quote-edit-btn" class="ghost">修改</button>
<button type="button" id="archive-quote-del-btn" class="archive-del-btn">删除</button>
</div>
</div>
</aside> </aside>
<main class="archive-main-panel"> <main class="archive-main-panel">
<div id="archive-stats" class="archive-stats-bar"></div> <div id="archive-stats" class="archive-stats-bar"></div>
@@ -584,7 +591,7 @@
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script> <script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-market-day-split"></script> <script src="/assets/chart.js?v=20260609-market-day-split"></script>
<script src="/assets/archive.js?v=20260612-archive-quotes"></script> <script src="/assets/archive.js?v=20260612-archive-quotes-v2"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script> <script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script> <script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script> <script src="/assets/ai_review_render.js?v=3"></script>