feat: persist heaven readings in unified dialog

This commit is contained in:
leefer
2026-07-23 09:51:33 +08:00
parent a517f9d859
commit c62670d46b
10 changed files with 739 additions and 22 deletions
+200 -18
View File
@@ -67,6 +67,12 @@ const state = {
personalField: null,
heavenPanel: "trend",
heavenInterpretations: { trend: "", fortune: "", heart: "" },
heavenReadingMode: "trend",
heavenReadingTab: "current",
heavenReadingHistory: { trend: [], fortune: [], heart: [] },
heavenReadingSelectedId: 0,
heavenReadingLoading: false,
heavenReadingError: "",
heartStage: "intro",
heartTimer: null,
heartSeconds: 30,
@@ -98,6 +104,7 @@ const elements = {
tradeLogDialog: document.querySelector("#tradeLogDialog"),
alertsDialog: document.querySelector("#alertsDialog"),
assistantDialog: document.querySelector("#assistantDialog"),
heavenReadingDialog: document.querySelector("#heavenReadingDialog"),
globalSearchDialog: document.querySelector("#globalSearchDialog"),
globalSearchInput: document.querySelector("#globalSearchInput"),
globalSearchResults: document.querySelector("#globalSearchResults"),
@@ -554,6 +561,8 @@ function bindEvents() {
});
document.querySelector("#interpretTrendButton").addEventListener("click", () => interpretHeaven("trend"));
document.querySelector("#interpretFortuneButton").addEventListener("click", () => interpretHeaven("fortune"));
document.querySelector("#historyTrendButton").addEventListener("click", () => openHeavenHistory("trend"));
document.querySelector("#historyFortuneButton").addEventListener("click", () => openHeavenHistory("fortune"));
document.querySelector("#qiObservationDate").addEventListener("change", () => {
state.personalField = null;
state.heavenManualData = null;
@@ -572,10 +581,18 @@ function bindEvents() {
document.querySelector("#startBreathingButton").addEventListener("click", startHeartBreathing);
document.querySelector("#beginCastingButton").addEventListener("click", beginHeartCasting);
document.querySelector("#heartSoundToggle").addEventListener("click", toggleHeartSound);
document.querySelector("#historyHeartButton").addEventListener("click", () => openHeavenHistory("heart"));
initializeHeartCoinHold();
initializeHeartLineInspection();
document.querySelector("#interpretHeartButton").addEventListener("click", () => interpretHeaven("heart"));
document.querySelector("#viewHeartReadingButton").addEventListener("click", () => openHeavenReading("heart"));
document.querySelector("#restartHeartButton").addEventListener("click", resetHeartRitual);
document.querySelector("#closeHeavenReadingDialog").addEventListener("click", () => elements.heavenReadingDialog.close());
document.querySelectorAll("[data-heaven-reading-tab]").forEach((button) => {
button.addEventListener("click", () => selectHeavenReadingTab(button.dataset.heavenReadingTab));
});
document.querySelector("#heavenReadingHistoryList").addEventListener("click", handleHeavenHistorySelection);
document.querySelector("#heavenReadingHistoryDetail").addEventListener("click", handleHeavenHistoryAction);
document.querySelectorAll("[data-heart-return]").forEach((button) => {
button.addEventListener("click", resetHeartRitual);
});
@@ -2225,6 +2242,7 @@ async function loadHeavenSetup(force = false, sector = "", stockCode = "") {
payload.requestedDate = requestedDate;
payload.requestedKey = requestedKey;
state.heavenSetup = payload;
state.heavenInterpretations.fortune = payload.daily_fortune_reading || "";
state.heavenManualData = Object.keys(payload.chart?.manual_data || {}).length
? payload.chart.manual_data
: null;
@@ -3095,12 +3113,159 @@ function hexagramLineGraphic(value) {
`;
}
const HEAVEN_READING_META = {
trend: { panel: "观势", action: "解势", done: "查看解势", status: "势已成", loading: "正在合参卦势" },
fortune: { panel: "观气", action: "解运", done: "已解运", status: "气已定", loading: "正在推演气机" },
heart: { panel: "观心", action: "我已察念,开始解卦", done: "查看解卦", status: "卦已解", loading: "正在整理卦辞" },
};
function heavenReadingMeta(mode = state.heavenReadingMode) {
return HEAVEN_READING_META[mode] || HEAVEN_READING_META.trend;
}
function openHeavenReading(mode, options = {}) {
state.heavenReadingMode = mode;
state.heavenReadingTab = "current";
state.heavenReadingError = options.error || "";
state.heavenReadingLoading = Object.hasOwn(options, "loading")
? Boolean(options.loading)
: false;
renderHeavenReadingDialog();
if (!elements.heavenReadingDialog.open) elements.heavenReadingDialog.showModal();
requestAnimationFrame(() => document.querySelector("#closeHeavenReadingDialog").focus());
}
async function openHeavenHistory(mode) {
state.heavenReadingMode = mode;
state.heavenReadingTab = "history";
state.heavenReadingSelectedId = 0;
renderHeavenReadingDialog();
if (!elements.heavenReadingDialog.open) elements.heavenReadingDialog.showModal();
await loadHeavenReadingHistory(mode);
}
function selectHeavenReadingTab(tab) {
state.heavenReadingTab = tab === "history" ? "history" : "current";
renderHeavenReadingDialog();
if (state.heavenReadingTab === "history") loadHeavenReadingHistory(state.heavenReadingMode);
}
async function loadHeavenReadingHistory(mode) {
const list = document.querySelector("#heavenReadingHistoryList");
list.innerHTML = '<div class="empty-state">正在读取历史记录</div>';
try {
const query = new URLSearchParams({ mode, limit: "100" });
const payload = await apiRequest(`/api/heaven/readings?${query}`);
state.heavenReadingHistory[mode] = payload.items || [];
if (!state.heavenReadingHistory[mode].some((item) => number(item.id) === state.heavenReadingSelectedId)) {
state.heavenReadingSelectedId = number(state.heavenReadingHistory[mode][0]?.id);
}
renderHeavenReadingHistory();
} catch (error) {
list.innerHTML = `<div class="empty-state">${escapeHtml(error.message || "历史记录加载失败")}</div>`;
}
}
function renderHeavenReadingDialog() {
const meta = heavenReadingMeta();
setText("heavenReadingEyebrow", `问天 · ${meta.panel}`);
setText("heavenReadingDialogTitle", state.heavenReadingTab === "history" ? "历史记录" : meta.status);
document.querySelectorAll("[data-heaven-reading-tab]").forEach((button) => {
const active = button.dataset.heavenReadingTab === state.heavenReadingTab;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
});
document.querySelector("#heavenReadingCurrent").hidden = state.heavenReadingTab !== "current";
document.querySelector("#heavenReadingHistory").hidden = state.heavenReadingTab !== "history";
if (state.heavenReadingTab === "current") renderHeavenReadingCurrent();
else renderHeavenReadingHistory();
refreshIcons();
}
function renderHeavenReadingCurrent() {
const reading = state.heavenInterpretations[state.heavenReadingMode];
const loading = document.querySelector("#heavenReadingLoading");
const empty = document.querySelector("#heavenReadingEmpty");
const result = document.querySelector("#heavenReadingResult");
const error = document.querySelector("#heavenReadingError");
loading.hidden = !state.heavenReadingLoading;
setText("heavenReadingLoadingTitle", heavenReadingMeta().loading);
error.hidden = !state.heavenReadingError;
error.textContent = state.heavenReadingError;
result.hidden = state.heavenReadingLoading || !reading;
empty.hidden = state.heavenReadingLoading || Boolean(reading) || Boolean(state.heavenReadingError);
if (!reading || state.heavenReadingLoading) return;
setText("heavenReadingResultStatus", heavenReadingMeta().status);
setText("heavenReadingSubject", reading.subject || `${heavenReadingMeta().panel}解读`);
setText("heavenReadingSubjectDetail", reading.subject_detail || displayCompactDate(reading.context_date || ""));
setText("heavenReadingCreatedAt", reading.created_at ? formatTimestamp(reading.created_at) : "刚刚完成");
document.querySelector("#heavenReadingAnswer").innerHTML = formatMentorAnswer(reading.answer || "");
}
function renderHeavenReadingHistory() {
const mode = state.heavenReadingMode;
const items = state.heavenReadingHistory[mode] || [];
setText("heavenReadingHistoryTitle", `${heavenReadingMeta(mode).panel}记录`);
setText("heavenReadingHistoryCount", `${items.length}`);
const list = document.querySelector("#heavenReadingHistoryList");
list.innerHTML = items.map((item) => `
<button class="heaven-reading-history-item ${number(item.id) === state.heavenReadingSelectedId ? "active" : ""}" type="button" data-heaven-reading-id="${number(item.id)}">
<span>${escapeHtml(item.subject)}</span>
<small>${escapeHtml(item.subject_detail || displayCompactDate(item.context_date))}</small>
<time>${formatTimestamp(item.created_at)}</time>
</button>
`).join("") || '<div class="empty-state">暂无历史解读</div>';
const selected = items.find((item) => number(item.id) === state.heavenReadingSelectedId);
const detail = document.querySelector("#heavenReadingHistoryDetail");
detail.innerHTML = selected ? `
<header><div><span>${escapeHtml(heavenReadingMeta(mode).status)}</span><h3>${escapeHtml(selected.subject)}</h3></div><time>${formatTimestamp(selected.created_at)}</time></header>
<p>${escapeHtml(selected.subject_detail || displayCompactDate(selected.context_date))}</p>
<div class="heaven-reading-answer">${formatMentorAnswer(selected.answer || "")}</div>
<footer><button class="button" type="button" data-delete-heaven-reading="${number(selected.id)}"><i data-lucide="trash-2"></i><span></span></button></footer>
` : '<div class="empty-state">选择一条记录查看完整解读</div>';
refreshIcons();
}
function handleHeavenHistorySelection(event) {
const button = event.target.closest("[data-heaven-reading-id]");
if (!button) return;
state.heavenReadingSelectedId = number(button.dataset.heavenReadingId);
renderHeavenReadingHistory();
}
async function handleHeavenHistoryAction(event) {
const button = event.target.closest("[data-delete-heaven-reading]");
if (!button || !window.confirm("确定删除这条解读记录吗?")) return;
const id = number(button.dataset.deleteHeavenReading);
try {
await apiRequest(`/api/heaven/readings/${id}`, "DELETE");
const mode = state.heavenReadingMode;
state.heavenReadingHistory[mode] = (state.heavenReadingHistory[mode] || []).filter((item) => number(item.id) !== id);
if (number(state.heavenInterpretations[mode]?.id) === id) {
state.heavenInterpretations[mode] = "";
if (mode === "fortune" && state.heavenSetup) state.heavenSetup.daily_fortune_reading = null;
updateHeavenInterpretationControls();
}
state.heavenReadingSelectedId = number(state.heavenReadingHistory[mode][0]?.id);
renderHeavenReadingHistory();
} catch (error) {
showToast(error.message || "解读记录删除失败");
}
}
async function interpretHeaven(mode) {
const existing = state.heavenInterpretations[mode];
if (existing) {
openHeavenReading(mode, { loading: false });
return;
}
const button = document.querySelector(mode === "trend" ? "#interpretTrendButton" : mode === "fortune" ? "#interpretFortuneButton" : "#interpretHeartButton");
if (button.disabled) return;
button.disabled = true;
const originalText = button.textContent;
button.textContent = mode === "trend" ? "正在观势" : mode === "fortune" ? "正在察运" : "正在解卦";
state.heavenReadingMode = mode;
state.heavenReadingLoading = true;
state.heavenReadingError = "";
openHeavenReading(mode, { loading: true });
updateHeavenInterpretationControls();
hideHeavenNotice();
try {
const payload = {
@@ -3112,10 +3277,15 @@ async function interpretHeaven(mode) {
if (mode === "trend" && state.heavenManualData) payload.manual_data = state.heavenManualData;
if (mode === "heart") payload.lines = state.heartLines;
const result = await apiRequest("/api/heaven/interpret", "POST", payload);
state.heavenInterpretations[mode] = {
state.heavenInterpretations[mode] = result.reading || {
answer: result.answer,
meta: mode === "trend" ? "观势已成" : mode === "fortune" ? "观气已成" : "解卦已成",
subject: `${heavenReadingMeta(mode).panel}解读`,
context_date: payload.trade_date,
created_at: new Date().toISOString(),
};
state.heavenReadingHistory[mode] = [];
state.heavenReadingLoading = false;
renderHeavenReadingDialog();
if (result.notice) showHeavenNotice(result.notice);
if (mode === "heart") {
if (await transitionHeartStage("interpretation")) await playHeartReadSequence();
@@ -3123,23 +3293,33 @@ async function interpretHeaven(mode) {
renderHeavenInterpretation(mode, state.heavenInterpretations[mode]);
}
} catch (error) {
showHeavenNotice(error.message || "问天解读失败");
showToast(error.message || "问天解读失败");
state.heavenReadingLoading = false;
state.heavenReadingError = error.message || "问天解读失败";
renderHeavenReadingDialog();
showHeavenNotice(state.heavenReadingError);
showToast(state.heavenReadingError);
} finally {
button.disabled = false;
button.textContent = originalText;
state.heavenReadingLoading = false;
updateHeavenInterpretationControls();
}
}
function renderHeavenInterpretation(mode, result) {
const container = document.querySelector(`#${mode}Interpretation`);
if (!result) {
container.hidden = true;
container.innerHTML = "";
return;
}
container.hidden = false;
container.innerHTML = `<div>${formatMentorAnswer(result.answer)}</div><small>${escapeHtml(result.meta)}</small>`;
function updateHeavenInterpretationControls() {
const loading = state.heavenReadingLoading;
const trendButton = document.querySelector("#interpretTrendButton");
const fortuneButton = document.querySelector("#interpretFortuneButton");
const heartButton = document.querySelector("#interpretHeartButton");
trendButton.disabled = loading || !state.heavenSetup?.chart?.available;
fortuneButton.disabled = loading || !state.heavenSetup?.field;
heartButton.disabled = loading || state.heartLines.length !== 6;
trendButton.textContent = loading && state.heavenReadingMode === "trend" ? "正在观势" : state.heavenInterpretations.trend ? HEAVEN_READING_META.trend.done : HEAVEN_READING_META.trend.action;
fortuneButton.textContent = loading && state.heavenReadingMode === "fortune" ? "正在察运" : state.heavenInterpretations.fortune ? HEAVEN_READING_META.fortune.done : HEAVEN_READING_META.fortune.action;
heartButton.textContent = loading && state.heavenReadingMode === "heart" ? "正在解卦" : state.heavenInterpretations.heart ? HEAVEN_READING_META.heart.done : HEAVEN_READING_META.heart.action;
document.querySelector("#viewHeartReadingButton").disabled = !state.heavenInterpretations.heart;
}
function renderHeavenInterpretation() {
updateHeavenInterpretationControls();
}
function initializeHeartAtmosphere() {
@@ -3455,6 +3635,7 @@ async function finalizeHeartHexagram() {
const payload = await apiRequest("/api/heaven/hexagram", "POST", { lines: state.heartLines });
if (stageToken !== state.heartStageToken || state.heartStage !== "casting") return;
state.heartHexagram = payload.hexagram;
updateHeavenInterpretationControls();
document.querySelector(".heart-hexagram-shell")?.classList.add("is-complete");
setText("castingPrompt", "卦成了");
heartSound.chime(660);
@@ -3658,6 +3839,7 @@ async function resetHeartRitual() {
state.heartThrows = [];
state.heartHexagram = null;
state.heavenInterpretations.heart = "";
updateHeavenInterpretationControls();
state.heartRevealToken += 1;
heartCastingBusy = false;
resetHeartCoins();