M2b:导入改造产出可审阅灵胚(启镜物化世谱)与组装管线(符文展开/火候进入 IR),Kernel 打通采样参数链路
This commit is contained in:
1 parent
e6a3b7121f
commit
5f36e3785c
86 files changed
+424
-14455
No files matched your search
@@ -1,26 +0,0 @@
|
||||
import { ChatShell } from "@/components/chat/ChatShell";
|
||||
|
||||
import { serverApi } from "@/lib/api/server-client";
|
||||
import type { Character, ModelGatewayStatus } from "@/types/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* 对话页:Server Component 预取当前激活人物与模型网关后注入 ChatShell。
|
||||
* 任一不可达时仍渲染界面,由用户感知后端状态。
|
||||
*/
|
||||
export default async function ChatPage() {
|
||||
let character: Character | null = null;
|
||||
let gateway: ModelGatewayStatus | null = null;
|
||||
try {
|
||||
character = await serverApi.get<Character>("/characters/active");
|
||||
} catch {
|
||||
character = null;
|
||||
}
|
||||
try {
|
||||
gateway = await serverApi.get<ModelGatewayStatus>("/settings/models");
|
||||
} catch {
|
||||
gateway = null;
|
||||
}
|
||||
return <ChatShell initialCharacter={character} gateway={gateway} />;
|
||||
}
|
||||
@@ -1,691 +0,0 @@
|
||||
"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<string, number> = {
|
||||
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 (
|
||||
<div className={`factor ${cls}`}>
|
||||
<span className="fk">{label}</span>
|
||||
<div className="ftrack">
|
||||
<div className="ffill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="fv">{pct}%</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主页面
|
||||
// ============================================================
|
||||
|
||||
export default function TimePage() {
|
||||
// —— 后端数据 ——
|
||||
const [snapshot, setSnapshot] = useState<TimeSnapshotResponse | null>(null);
|
||||
const [acquaintance, setAcquaintance] = useState<AcquaintanceInfo | null>(
|
||||
null,
|
||||
);
|
||||
const [config, setConfig] = useState<TimeConfig | null>(null);
|
||||
const [currentMood, setCurrentMood] = useState<{
|
||||
mood: string;
|
||||
intensity: number;
|
||||
} | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// —— 实时时钟 ——
|
||||
const [, setNow] = useState<Date>(new Date());
|
||||
const [, setPrevShichen] = useState<string | null>(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 (
|
||||
<div className="mx-auto max-w-5xl px-4 py-8">
|
||||
<TextSkeleton lines={1} className="mb-8 w-48" />
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-5">
|
||||
<div className="md:col-span-3">
|
||||
<div className="glass rounded-2xl p-8">
|
||||
<TextSkeleton lines={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div className="glass rounded-2xl p-5">
|
||||
<TextSkeleton lines={6} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="glass mt-6 rounded-2xl p-5">
|
||||
<TextSkeleton lines={2} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !snapshot) {
|
||||
return <ErrorState message={error} onRetry={fetchData} />;
|
||||
}
|
||||
|
||||
const s = snapshot?.snapshot;
|
||||
const factors = snapshot?.factors;
|
||||
const context_text = snapshot?.context_text;
|
||||
|
||||
// 实时时辰信息(优先用本地计算的,回退后端数据)
|
||||
const displayShichen = currentShichen;
|
||||
const displayDayPeriod = getDayPeriod(local.hour);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl px-4 py-8">
|
||||
{/* ===== 页头 ===== */}
|
||||
<header className="mb-8 flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="font-serif text-2xl font-semibold text-fg">辰正</h1>
|
||||
<span className="rounded-full bg-gold/10 px-2.5 py-0.5 text-[10px] font-medium tracking-wider text-gold">
|
||||
时态感知
|
||||
</span>
|
||||
{shichenChanged && (
|
||||
<span className="animate-pulse rounded-full bg-gold/30 px-2 py-0.5 text-[10px] text-gold">
|
||||
更时
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{config && (
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
{config.default_city} · {config.user_timezone}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-muted">
|
||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-patina" />
|
||||
辰光实时
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ===== 日晷时辰盘 + 调制因子(设计稿 .sundial-region) ===== */}
|
||||
<div className="sundial-region">
|
||||
{/* 日晷 */}
|
||||
<SundialDisk
|
||||
currentShichen={displayShichen}
|
||||
hour={local.hour}
|
||||
minute={local.minute}
|
||||
lunarText={s?.lunar?.text}
|
||||
festivals={s?.festivals}
|
||||
solarTerm={s?.solar_term?.name}
|
||||
/>
|
||||
|
||||
{/* 调制因子(设计稿 .factor-card) */}
|
||||
{factors && (
|
||||
<div className="factor-card">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<h3 className="font-serif text-sm font-semibold text-fg">
|
||||
调制因子
|
||||
</h3>
|
||||
<span className="h-px flex-1 bg-rule" />
|
||||
</div>
|
||||
<div className="factor-list">
|
||||
<FactorFactor
|
||||
label="节日喜庆"
|
||||
value={factors.festival_joy}
|
||||
cls="f-joy"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="深夜脆弱"
|
||||
value={factors.late_night_fragility}
|
||||
cls="f-frag"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="重逢强度"
|
||||
value={factors.reunion_intensity}
|
||||
cls="f-reun"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="周末放松"
|
||||
value={factors.weekend_relax}
|
||||
cls="f-wknd"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="季节共鸣"
|
||||
value={factors.season_resonance}
|
||||
cls="f-season"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="同期共振"
|
||||
value={factors.same_period_resonance}
|
||||
cls="f-period"
|
||||
/>
|
||||
<FactorFactor
|
||||
label="关怀倾向"
|
||||
value={factors.care_suggest}
|
||||
cls="f-care"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentMood && (
|
||||
<div className="mt-4 rounded-xl bg-glass/40 px-4 py-3">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<h3 className="font-serif text-sm font-semibold text-fg">
|
||||
当前心情
|
||||
</h3>
|
||||
<span className="h-px flex-1 bg-rule" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">
|
||||
{currentMood.mood === "joy"
|
||||
? "😊"
|
||||
: currentMood.mood === "sadness"
|
||||
? "😢"
|
||||
: currentMood.mood === "anger"
|
||||
? "😠"
|
||||
: currentMood.mood === "fear"
|
||||
? "😨"
|
||||
: currentMood.mood === "surprise"
|
||||
? "😲"
|
||||
: currentMood.mood === "disgust"
|
||||
? "😣"
|
||||
: "😐"}
|
||||
</span>
|
||||
<span className="font-serif text-base font-medium text-fg">
|
||||
{currentMood.mood}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-muted">
|
||||
{Math.round(currentMood.intensity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{factors.care_kind && (
|
||||
<div className="care-note">
|
||||
<b>关怀</b> · {factors.care_kind}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ===== 相识 / 离线 / 里程碑 快照格(设计稿 .snap-grid) ===== */}
|
||||
{s && (
|
||||
<div className="snap-grid">
|
||||
<div className="snap-card">
|
||||
<div className="k">相识</div>
|
||||
<div className="v">
|
||||
{s.acquaintance.total_days}
|
||||
<small>天</small>
|
||||
</div>
|
||||
<div className="sub">{s.acquaintance.natural_language}</div>
|
||||
</div>
|
||||
<div className="snap-card">
|
||||
<div className="k">离线</div>
|
||||
<div className="v">
|
||||
{Math.round(s.offline.seconds / 86400)}
|
||||
<small>天</small>
|
||||
</div>
|
||||
<div className="sub">{s.offline.natural_language}</div>
|
||||
</div>
|
||||
<div className="snap-card milestone">
|
||||
<div className="k">下个纪念日</div>
|
||||
<div className="v">
|
||||
{s.next_milestone?.days_until ?? "—"}
|
||||
<small>天后</small>
|
||||
</div>
|
||||
<div className="sub">{s.next_milestone?.name ?? "无"}</div>
|
||||
</div>
|
||||
<div className="snap-card">
|
||||
<div className="k">辰光</div>
|
||||
<div className="v">
|
||||
{displayShichen}
|
||||
<small>· {displayDayPeriod}</small>
|
||||
</div>
|
||||
<div className="sub">{s.season ?? "—"}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== 时态感知文本(全宽) ===== */}
|
||||
{context_text && (
|
||||
<Card variant="surface" className="p-5">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="font-serif text-sm font-semibold text-fg">
|
||||
时态感知
|
||||
</h3>
|
||||
<span className="h-px flex-1 bg-rule" />
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-fg/80">
|
||||
{context_text}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ===== 底部双栏:相识 + 纪念日管理(设计稿 .lower-region) ===== */}
|
||||
<div className="lower-region">
|
||||
{acquaintance && (
|
||||
<Card variant="surface" className="p-6 event-main">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="font-serif text-sm font-semibold text-fg">相识</h3>
|
||||
<span className="h-px flex-1 bg-rule" />
|
||||
</div>
|
||||
<p className="font-serif text-4xl font-semibold text-fg">
|
||||
{acquaintance.total_days}
|
||||
<span className="ml-1 text-base font-normal text-muted">天</span>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{acquaintance.natural_language}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
<div className="anniv-rail">
|
||||
<AnniversaryManager />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 纪念日管理(保持原功能不变)
|
||||
// ============================================================
|
||||
|
||||
function AnniversaryManager() {
|
||||
const [anniversaries, setAnniversaries] = useState<Anniversary[]>([]);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [form, setForm] = useState<CreateAnniversaryRequest & { date: string }>(
|
||||
{
|
||||
name: "",
|
||||
date: "",
|
||||
kind: "anniversary",
|
||||
calendar: "solar",
|
||||
subject_type: "relationship",
|
||||
source: "manual",
|
||||
},
|
||||
);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Card variant="surface" className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-serif text-sm font-semibold text-fg">
|
||||
纪念日({anniversaries.length})
|
||||
</h3>
|
||||
{!showForm && !editingId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setShowForm(true);
|
||||
}}
|
||||
className="rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
|
||||
>
|
||||
+ 新增
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(showForm || editingId) && (
|
||||
<div className="mt-3 space-y-2 rounded-lg border border-border-glass bg-glass/50 p-3">
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="名称"
|
||||
className={inputCls}
|
||||
/>
|
||||
<input
|
||||
value={form.date}
|
||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
||||
type="date"
|
||||
className={inputCls}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={form.kind}
|
||||
onChange={(e) => setForm({ ...form, kind: e.target.value })}
|
||||
className={inputCls}
|
||||
>
|
||||
{KIND_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={form.calendar}
|
||||
onChange={(e) => setForm({ ...form, calendar: e.target.value })}
|
||||
className={inputCls}
|
||||
>
|
||||
<option value="solar">公历</option>
|
||||
<option value="lunar">农历</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={editingId ? () => handleUpdate(editingId) : handleCreate}
|
||||
disabled={busy}
|
||||
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
|
||||
>
|
||||
{editingId ? "保存" : "创建"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-cinnabar">{error}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
{anniversaries.length === 0 ? (
|
||||
<p className="text-sm text-muted">暂无纪念日。</p>
|
||||
) : (
|
||||
anniversaries.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="flex items-center justify-between rounded-lg bg-glass px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-sm text-fg">{a.name}</span>
|
||||
<span className="ml-2 text-[11px] text-muted">{a.date}</span>
|
||||
<span className="ml-2 rounded bg-gold/10 px-1.5 py-0.5 text-[10px] text-gold">
|
||||
{a.kind}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEdit(a)}
|
||||
className="text-[11px] text-patina hover:underline"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(a.id)}
|
||||
disabled={busy}
|
||||
className="text-[11px] text-cinnabar hover:underline disabled:opacity-50"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user