"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Card } from "@/components/ui/Card"; import { TextSkeleton } from "@/components/ui/Skeleton"; import { ErrorState } from "@/components/shared/StateViews"; import { SundialDisk } from "@/components/time/SundialDisk"; import { charactersApi, timeApi } from "@/lib/api"; import { describeError } from "@/lib/utils/error"; import type { AcquaintanceInfo, Anniversary, CreateAnniversaryRequest, TimeConfig, TimeSnapshotResponse, } from "@/types/api"; // ============================================================ // 辰正 · 时态感知 // 实时时间展示 + 十二时辰流 + 自动刷新的后端数据 // ============================================================ // —— 十二时辰 —— const SHICHEN_KEYS = [ "zi", "chou", "yin", "mao", "chen", "si", "wu", "wei", "shen", "you", "xu", "hai", ] as const; const SHICHEN_HOURS: Record = { chou: 2, yin: 4, mao: 6, chen: 8, si: 10, wu: 12, wei: 14, shen: 16, you: 18, xu: 20, hai: 22, }; /** 由客户端本地时辰确定时辰 key(与后端逻辑一致) */ function shichenFromHour(hour: number): string { if (hour >= 23 || hour < 1) return "zi"; // 取各时辰起始值,反向查找 let prev = "zi"; for (const key of SHICHEN_KEYS) { const start = SHICHEN_HOURS[key]; if (hour >= start) prev = key; else break; } return prev; } /** 获取目标时区的当前时间组件 */ function getLocalTimeInTimezone(timezone: string) { const fmt = new Intl.DateTimeFormat("zh-CN", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }); const p = fmt.formatToParts(new Date()); const g = (t: string) => parseInt(p.find((x) => x.type === t)?.value ?? "0", 10); return { year: g("year"), month: g("month"), day: g("day"), hour: g("hour"), minute: g("minute"), second: g("second"), }; } function getDayPeriod(h: number): string { if (h >= 0 && h < 5) return "凌晨"; if (h >= 5 && h < 7) return "清晨"; if (h >= 7 && h < 11) return "上午"; if (h >= 11 && h < 13) return "中午"; if (h >= 13 && h < 17) return "下午"; if (h >= 17 && h < 19) return "傍晚"; if (h >= 19 && h < 22) return "夜晚"; return "深夜"; } const KIND_OPTIONS = [ { value: "anniversary", label: "纪念日" }, { value: "birthday", label: "生日" }, { value: "custom", label: "自定义" }, ]; // ============================================================ // 子组件 // ============================================================ /** 调制因子(设计稿 .factor 渐变轨道) */ function FactorFactor({ label, value, cls, }: { label: string; value: number; cls: string; }) { const pct = Math.round(Math.max(0, Math.min(1, value)) * 100); return (
{label}
{pct}%
); } // ============================================================ // 主页面 // ============================================================ export default function TimePage() { // —— 后端数据 —— const [snapshot, setSnapshot] = useState(null); const [acquaintance, setAcquaintance] = useState( null, ); const [config, setConfig] = useState(null); const [currentMood, setCurrentMood] = useState<{ mood: string; intensity: number; } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // —— 实时时钟 —— const [, setNow] = useState(new Date()); const [, setPrevShichen] = useState(null); const [shichenChanged, setShichenChanged] = useState(false); const shichenRef = useRef(""); // 每秒 tick useEffect(() => { const timer = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(timer); }, []); // 检测时辰变化 const timezone = snapshot?.snapshot?.timezone ?? config?.user_timezone ?? "Asia/Shanghai"; const local = getLocalTimeInTimezone(timezone); const currentShichen = shichenFromHour(local.hour); useEffect(() => { if (shichenRef.current && shichenRef.current !== currentShichen) { setPrevShichen(shichenRef.current); setShichenChanged(true); setTimeout(() => setShichenChanged(false), 2000); } shichenRef.current = currentShichen; }, [currentShichen]); // —— 数据获取 —— const fetchData = useCallback(async () => { try { const [s, acq, cfg] = await Promise.all([ timeApi.getTimeSnapshot(), timeApi.getAcquaintance(), timeApi.getTimeConfig(), ]); setSnapshot(s); setAcquaintance(acq); setConfig(cfg); } catch (e) { if (!snapshot) setError(describeError(e)); } finally { setLoading(false); } }, [snapshot]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchData(); // 获取当前人物心情 charactersApi .getActiveCharacter() .then((c) => { if (c.current_mood) setCurrentMood(c.current_mood); }) .catch(() => {}); }, []); // eslint-disable-line react-hooks/exhaustive-deps // 自动刷新:每 60 秒重新拉取后端数据(农历/节气/节日变化) useEffect(() => { if (loading) return; const timer = setInterval(fetchData, 60_000); return () => clearInterval(timer); }, [fetchData, loading]); // —— 渲染 —— if (loading) { return (
); } if (error && !snapshot) { return ; } const s = snapshot?.snapshot; const factors = snapshot?.factors; const context_text = snapshot?.context_text; // 实时时辰信息(优先用本地计算的,回退后端数据) const displayShichen = currentShichen; const displayDayPeriod = getDayPeriod(local.hour); return (
{/* ===== 页头 ===== */}

辰正

时态感知 {shichenChanged && ( 更时 )}
{config && (

{config.default_city} · {config.user_timezone}

)}
辰光实时
{/* ===== 日晷时辰盘 + 调制因子(设计稿 .sundial-region) ===== */}
{/* 日晷 */} {/* 调制因子(设计稿 .factor-card) */} {factors && (

调制因子

{currentMood && (

当前心情

{currentMood.mood === "joy" ? "😊" : currentMood.mood === "sadness" ? "😢" : currentMood.mood === "anger" ? "😠" : currentMood.mood === "fear" ? "😨" : currentMood.mood === "surprise" ? "😲" : currentMood.mood === "disgust" ? "😣" : "😐"} {currentMood.mood} {Math.round(currentMood.intensity * 100)}%
)} {factors.care_kind && (
关怀 · {factors.care_kind}
)}
)}
{/* ===== 相识 / 离线 / 里程碑 快照格(设计稿 .snap-grid) ===== */} {s && (
相识
{s.acquaintance.total_days} 天
{s.acquaintance.natural_language}
离线
{Math.round(s.offline.seconds / 86400)} 天
{s.offline.natural_language}
下个纪念日
{s.next_milestone?.days_until ?? "—"} 天后
{s.next_milestone?.name ?? "无"}
辰光
{displayShichen} · {displayDayPeriod}
{s.season ?? "—"}
)} {/* ===== 时态感知文本(全宽) ===== */} {context_text && (

时态感知

{context_text}

)} {/* ===== 底部双栏:相识 + 纪念日管理(设计稿 .lower-region) ===== */}
{acquaintance && (

相识

{acquaintance.total_days} 天

{acquaintance.natural_language}

)}
); } // ============================================================ // 纪念日管理(保持原功能不变) // ============================================================ function AnniversaryManager() { const [anniversaries, setAnniversaries] = useState([]); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState( { name: "", date: "", kind: "anniversary", calendar: "solar", subject_type: "relationship", source: "manual", }, ); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const fetch = useCallback(async () => { try { const data = await timeApi.listAnniversaries(); setAnniversaries(data); } catch { /* silent */ } }, []); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetch(); }, [fetch]); function resetForm() { setForm({ name: "", date: "", kind: "anniversary", calendar: "solar", subject_type: "relationship", source: "manual", }); setShowForm(false); setEditingId(null); setError(null); } async function handleCreate() { if (!form.name.trim() || !form.date) { setError("请填写名称与日期。"); return; } setBusy(true); setError(null); try { await timeApi.createAnniversary(form); resetForm(); await fetch(); } catch (e) { setError(e instanceof Error ? e.message : "创建失败。"); } finally { setBusy(false); } } async function handleUpdate(id: string) { if (!form.name.trim() || !form.date) { setError("请填写名称与日期。"); return; } setBusy(true); setError(null); try { await timeApi.updateAnniversary(id, form); resetForm(); await fetch(); } catch (e) { setError(e instanceof Error ? e.message : "更新失败。"); } finally { setBusy(false); } } async function handleDelete(id: string) { setBusy(true); setError(null); try { await timeApi.deleteAnniversary(id); await fetch(); } catch (e) { setError(e instanceof Error ? e.message : "删除失败。"); } finally { setBusy(false); } } function startEdit(a: Anniversary) { setEditingId(a.id); setShowForm(false); setForm({ name: a.name, date: a.date, kind: a.kind, calendar: a.calendar, subject_type: a.subject_type, source: a.source, }); setError(null); } const inputCls = "w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg outline-none focus:border-patina/60 focus:shadow-[0_0_0_2px] focus:shadow-patina/20"; return (

纪念日({anniversaries.length})

{!showForm && !editingId && ( )}
{(showForm || editingId) && (
setForm({ ...form, name: e.target.value })} placeholder="名称" className={inputCls} /> setForm({ ...form, date: e.target.value })} type="date" className={inputCls} />
{error &&

{error}

}
)}
{anniversaries.length === 0 ? (

暂无纪念日。

) : ( anniversaries.map((a) => (
{a.name} {a.date} {a.kind}
)) )}
); }