From c62670d46b7b56d298e3edb73c477df6b85119fa Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 23 Jul 2026 09:51:33 +0800 Subject: [PATCH] feat: persist heaven readings in unified dialog --- api_access.py | 3 + database.py | 121 ++++++++++++++++++ server.py | 105 +++++++++++++++ static/app.js | 218 +++++++++++++++++++++++++++++--- static/index.html | 44 ++++++- static/styles.css | 112 ++++++++++++++++ tests/e2e/app-shell.spec.js | 59 +++++++++ tests/test_api_access.py | 2 + tests/test_frontend_contract.py | 9 ++ tests/test_heaven_readings.py | 88 +++++++++++++ 10 files changed, 739 insertions(+), 22 deletions(-) create mode 100644 tests/test_heaven_readings.py diff --git a/api_access.py b/api_access.py index ad27193..2df7024 100644 --- a/api_access.py +++ b/api_access.py @@ -13,6 +13,7 @@ MEMBER_GET_PATHS = frozenset( "/api/mentors/setup", "/api/mentors/messages", "/api/heaven/setup", + "/api/heaven/readings", "/api/assistant/messages", } ) @@ -60,4 +61,6 @@ def required_role(method: str, path: str) -> AccessRole: r"/api/screener/strategies/\d+", path ): return "member" + if re.fullmatch(r"/api/heaven/readings/\d+", path): + return "member" return "authenticated" diff --git a/database.py b/database.py index b086046..f0368ca 100644 --- a/database.py +++ b/database.py @@ -334,6 +334,24 @@ class ReviewDatabase: CREATE INDEX IF NOT EXISTS idx_assistant_messages_user ON assistant_messages(user_id, id DESC); + + CREATE TABLE IF NOT EXISTS heaven_readings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + mode TEXT NOT NULL, + context_date TEXT NOT NULL, + subject TEXT NOT NULL, + subject_detail TEXT NOT NULL DEFAULT '', + answer TEXT NOT NULL, + context_snapshot TEXT NOT NULL DEFAULT '{}', + dedupe_key TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(user_id, dedupe_key), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_heaven_readings_user_mode + ON heaven_readings(user_id, mode, context_date DESC, id DESC); """ ) user_columns = { @@ -1720,6 +1738,109 @@ class ReviewDatabase: ) return int(cursor.rowcount) + @staticmethod + def _heaven_reading_dict(row: sqlite3.Row | None) -> dict[str, Any] | None: + if not row: + return None + return { + "id": int(row["id"]), + "mode": str(row["mode"]), + "context_date": str(row["context_date"]), + "subject": str(row["subject"]), + "subject_detail": str(row["subject_detail"]), + "answer": str(row["answer"]), + "created_at": str(row["created_at"]), + } + + def save_heaven_reading( + self, + user_id: int, + mode: str, + context_date: str, + subject: str, + subject_detail: str, + answer: str, + context_snapshot: dict[str, Any], + dedupe_key: str, + ) -> dict[str, Any]: + now = datetime.now().astimezone().isoformat(timespec="seconds") + snapshot_json = json.dumps( + context_snapshot, ensure_ascii=False, separators=(",", ":") + ) + with self.connect() as connection: + connection.execute( + """ + INSERT INTO heaven_readings + (user_id, mode, context_date, subject, subject_detail, answer, + context_snapshot, dedupe_key, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id, dedupe_key) DO NOTHING + """, + ( + int(user_id), mode, context_date, subject, subject_detail, + answer, snapshot_json, dedupe_key, now, + ), + ) + row = connection.execute( + """ + SELECT id, mode, context_date, subject, subject_detail, answer, created_at + FROM heaven_readings WHERE user_id = ? AND dedupe_key = ? + """, + (int(user_id), dedupe_key), + ).fetchone() + connection.execute( + """ + DELETE FROM heaven_readings + WHERE user_id = ? AND mode = ? AND id NOT IN ( + SELECT id FROM heaven_readings + WHERE user_id = ? AND mode = ? ORDER BY id DESC LIMIT 100 + ) + """, + (int(user_id), mode, int(user_id), mode), + ) + result = self._heaven_reading_dict(row) + if not result: + raise ValueError("解读记录保存失败。") + return result + + def list_heaven_readings( + self, + user_id: int, + mode: str, + context_date: str = "", + limit: int = 100, + ) -> list[dict[str, Any]]: + clauses = ["user_id = ?", "mode = ?"] + parameters: list[Any] = [int(user_id), mode] + if context_date: + clauses.append("context_date = ?") + parameters.append(context_date) + parameters.append(max(1, min(100, int(limit)))) + with self.connect() as connection: + rows = connection.execute( + f""" + SELECT id, mode, context_date, subject, subject_detail, answer, created_at + FROM heaven_readings WHERE {' AND '.join(clauses)} + ORDER BY context_date DESC, id DESC LIMIT ? + """, + parameters, + ).fetchall() + return [self._heaven_reading_dict(row) for row in rows if row] + + def latest_heaven_reading( + self, user_id: int, mode: str, context_date: str = "" + ) -> dict[str, Any] | None: + items = self.list_heaven_readings(user_id, mode, context_date, 1) + return items[0] if items else None + + def delete_heaven_reading(self, user_id: int, reading_id: int) -> bool: + with self.connect() as connection: + cursor = connection.execute( + "DELETE FROM heaven_readings WHERE id = ? AND user_id = ?", + (int(reading_id), int(user_id)), + ) + return cursor.rowcount > 0 + def start_sync(self, trade_date: str, source: str) -> int: started_at = datetime.now().astimezone().isoformat(timespec="seconds") with self.connect() as connection: diff --git a/server.py b/server.py index c3f9e41..1194281 100644 --- a/server.py +++ b/server.py @@ -2023,6 +2023,9 @@ class DashboardService: "chart": chart, "field": field, "personal_profile": personal_profile, + "daily_fortune_reading": self.database.latest_heaven_reading( + self.current_user_id, "fortune", normalized_date + ), "sector_phase_overrides": [ {"name": name, "element": element} for name, element in sector_phase_overrides.items() @@ -2286,11 +2289,69 @@ class DashboardService: raise ValueError("六爻必须由六、七、八、九组成。") from exc return hexagram_from_lines(lines) + def heaven_readings( + self, mode: str, context_date: str = "", limit: int = 100 + ) -> dict[str, Any]: + mode = str(mode or "").strip() + if mode not in {"trend", "fortune", "heart"}: + raise ValueError("解读记录类型不正确。") + normalized_date = normalize_date(context_date) if context_date else "" + return { + "mode": mode, + "items": self.database.list_heaven_readings( + self.current_user_id, mode, normalized_date, limit + ), + } + + @staticmethod + def _heaven_reading_identity( + mode: str, context_date: str, context: dict[str, Any] + ) -> tuple[str, str]: + display_date = DashboardService._display_compact_date(context_date) + if mode == "trend": + stock = (context.get("selected_focus") or {}).get("stock") or {} + code = str(stock.get("code") or "").strip() + name = str(stock.get("name") or "").strip() + hexagram = context.get("hexagram") or {} + transformed = hexagram.get("transformed") or {} + subject = " ".join(item for item in (code, name) if item) or "观势" + detail = f"{display_date} · {hexagram.get('name') or '--'} → {transformed.get('name') or '--'}" + return subject, detail + if mode == "fortune": + field = context.get("five_phase_field") or {} + pillars = field.get("pillars") or {} + dominant = (field.get("balance") or [{}])[0] + subject = f"{display_date} 观气" + detail = ( + f"{pillars.get('year') or '--'}年 · {pillars.get('month') or '--'}月 · " + f"{pillars.get('day') or '--'}日 · {dominant.get('element') or '--'}气偏显" + ) + return subject, detail + hexagram = context.get("hexagram") or {} + transformed = hexagram.get("transformed") or {} + return ( + f"{display_date} 观心", + f"{hexagram.get('name') or '--'} → {transformed.get('name') or '--'}", + ) + def heaven_interpret(self, payload: dict[str, Any]) -> dict[str, Any]: mode = str(payload.get("mode") or "").strip() if mode not in {"trend", "fortune", "heart"}: raise ValueError("问天解读模式不正确。") trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) + if mode == "fortune": + existing = self.database.latest_heaven_reading( + self.current_user_id, "fortune", trade_date + ) + if existing: + return { + "answer": existing["answer"], + "mode": mode, + "compiler": "stored", + "notice": "", + "reading": existing, + "reused": True, + } if mode in {"trend", "fortune"}: setup = self.heaven_setup( trade_date, @@ -2351,17 +2412,41 @@ class DashboardService: "five_phase_field": fortune_field, "personal_profile": personal_profile, } + context_date = setup["calendar_date"] + if mode == "trend": + context_date = setup["trade_date"] else: context = { "hexagram": self.heaven_hexagram(payload.get("lines")), "ritual": "用户已完成30秒静心、六次三枚铜钱起卦,并在心中察看第一念。问题未输入。", } + context_date = trade_date result, compiler = self._call_heaven_agent(mode, context) + subject, subject_detail = self._heaven_reading_identity( + mode, context_date, context + ) + dedupe_key = ( + f"fortune:{context_date}" + if mode == "fortune" + else f"{mode}:{context_date}:{secrets.token_urlsafe(12)}" + ) + reading = self.database.save_heaven_reading( + self.current_user_id, + mode, + context_date, + subject, + subject_detail, + str(result.get("answer") or ""), + context, + dedupe_key, + ) return { **result, "mode": mode, "compiler": compiler, "notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "", + "reading": reading, + "reused": False, } def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]: @@ -3704,6 +3789,19 @@ class RequestHandler(BaseHTTPRequestHandler): except ValueError as exc: self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) return + if parsed.path == "/api/heaven/readings": + query = parse_qs(parsed.query) + try: + self.send_json( + SERVICE.heaven_readings( + query.get("mode", [""])[0], + query.get("context_date", [""])[0], + int(query.get("limit", ["100"])[0]), + ) + ) + except (TypeError, ValueError) as exc: + self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) + return if parsed.path == "/api/heaven/setup": query = parse_qs(parsed.query) trade_date = query.get("trade_date", [date.today().isoformat()])[0] @@ -3891,6 +3989,13 @@ class RequestHandler(BaseHTTPRequestHandler): {"ok": True, **SERVICE.delete_trade_entry(int(trade_match.group(1)))} ) return + heaven_reading_match = re.fullmatch(r"/api/heaven/readings/(\d+)", parsed.path) + if heaven_reading_match: + deleted = SERVICE.database.delete_heaven_reading( + SERVICE.current_user_id, int(heaven_reading_match.group(1)) + ) + self.send_json({"ok": True, "deleted": deleted}) + return sector_phase_match = re.fullmatch(r"/api/heaven/sector-phases/(.+)", parsed.path) if sector_phase_match: name = unquote(sector_phase_match.group(1)).strip() diff --git a/static/app.js b/static/app.js index 17476a0..0614c7a 100644 --- a/static/app.js +++ b/static/app.js @@ -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 = '
正在读取历史记录
'; + 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 = `
${escapeHtml(error.message || "历史记录加载失败")}
`; + } +} + +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) => ` + + `).join("") || '
暂无历史解读
'; + const selected = items.find((item) => number(item.id) === state.heavenReadingSelectedId); + const detail = document.querySelector("#heavenReadingHistoryDetail"); + detail.innerHTML = selected ? ` +
${escapeHtml(heavenReadingMeta(mode).status)}

${escapeHtml(selected.subject)}

+

${escapeHtml(selected.subject_detail || displayCompactDate(selected.context_date))}

+
${formatMentorAnswer(selected.answer || "")}
+ + ` : '
选择一条记录查看完整解读
'; + 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 = `
${formatMentorAnswer(result.answer)}
${escapeHtml(result.meta)}`; +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(); diff --git a/static/index.html b/static/index.html index 2866792..322f75f 100644 --- a/static/index.html +++ b/static/index.html @@ -425,6 +425,7 @@ +
请输入股票代码或股票名称
@@ -478,7 +479,6 @@ -
@@ -486,6 +486,7 @@
--

--

+
@@ -518,7 +519,6 @@
风险与制衡--
-

个人影响

日主、十神喜恶、五行喜忌与当日作用
@@ -568,6 +568,9 @@ + @@ -649,14 +652,13 @@
观心 · 五

解卦

--
- +

--

-

一念既察,卦只是镜。

观心用于观察念头与执着,不用于替代交易计划或预测涨跌。

@@ -982,6 +984,40 @@ + +
+
问天 · 观势

解读

+ +
+
+ + +
+
+ +
尚无本次解读
+ + +
+ +
+