fff77dac3f
Add liuyao/bazi/combined flows with shared calc and AI infrastructure, 64-gua learn routes, and update Ubuntu PM2 deployment docs for port 3130. Co-authored-by: Cursor <cursoragent@cursor.com>
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { bool } from "aimless.js";
|
||
import Coin from "@/components/coin";
|
||
import Hexagram from "@/components/hexagram";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
buildHexagramList,
|
||
COIN_OPTIONS,
|
||
computeGuaResult,
|
||
type GuaResult,
|
||
YAO_LABELS,
|
||
} from "@/lib/calc/hexagram";
|
||
|
||
const AUTO_DELAY = 600;
|
||
|
||
interface HexagramInputProps {
|
||
mode: "online" | "offline";
|
||
enabled: boolean;
|
||
onResult: (data: GuaResult) => void;
|
||
onClear: () => void;
|
||
}
|
||
|
||
export default function HexagramInput({
|
||
mode,
|
||
enabled,
|
||
onResult,
|
||
onClear,
|
||
}: HexagramInputProps) {
|
||
const [frontList, setFrontList] = useState([true, true, true]);
|
||
const [rotation, setRotation] = useState(false);
|
||
const [hexagramList, setHexagramList] = useState<
|
||
GuaResult["list"]
|
||
>([]);
|
||
const [count, setCount] = useState(0);
|
||
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
|
||
Array(6).fill(null),
|
||
);
|
||
|
||
useEffect(() => {
|
||
setHexagramList([]);
|
||
setCount(0);
|
||
setOfflineCounts(Array(6).fill(null));
|
||
setRotation(false);
|
||
}, [mode]);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
mode !== "online" ||
|
||
!enabled ||
|
||
rotation ||
|
||
hexagramList.length >= 6 ||
|
||
count >= 6
|
||
) {
|
||
return;
|
||
}
|
||
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
|
||
return () => clearTimeout(timer);
|
||
}, [mode, enabled, rotation, count, hexagramList.length]);
|
||
|
||
function finishList(list: GuaResult["list"]) {
|
||
const result = computeGuaResult(list);
|
||
if (result) {
|
||
onResult({ list, result });
|
||
}
|
||
}
|
||
|
||
function onTransitionEnd() {
|
||
setRotation(false);
|
||
const frontCount = frontList.reduce(
|
||
(acc, val) => (val ? acc + 1 : acc),
|
||
0,
|
||
);
|
||
setHexagramList((list) => {
|
||
const newList = [
|
||
...list,
|
||
{
|
||
change: frontCount === 0 || frontCount === 3,
|
||
yang: frontCount >= 2,
|
||
separate: list.length === 3,
|
||
},
|
||
];
|
||
if (newList.length === 6) {
|
||
finishList(newList);
|
||
}
|
||
return newList;
|
||
});
|
||
}
|
||
|
||
function startOnlineCast() {
|
||
if (rotation || !enabled || hexagramList.length >= 6) {
|
||
return;
|
||
}
|
||
setFrontList([bool(), bool(), bool()]);
|
||
setRotation(true);
|
||
setCount((c) => c + 1);
|
||
}
|
||
|
||
function handleOfflineSelect(index: number, frontCount: number) {
|
||
const next = [...offlineCounts];
|
||
next[index] = frontCount;
|
||
setOfflineCounts(next);
|
||
}
|
||
|
||
function confirmOffline() {
|
||
if (offlineCounts.some((v) => v === null)) {
|
||
return;
|
||
}
|
||
const list = buildHexagramList(offlineCounts as number[]);
|
||
finishList(list);
|
||
setHexagramList(list);
|
||
}
|
||
|
||
function handleReset() {
|
||
setHexagramList([]);
|
||
setCount(0);
|
||
setOfflineCounts(Array(6).fill(null));
|
||
setRotation(false);
|
||
onClear();
|
||
}
|
||
|
||
const complete = hexagramList.length === 6;
|
||
const offlineReady = offlineCounts.every((v) => v !== null);
|
||
|
||
if (!enabled) {
|
||
return (
|
||
<p className="text-center text-sm text-muted-foreground">
|
||
请先填写问事、地域和起卦时辰
|
||
</p>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex w-full flex-col items-center gap-4">
|
||
{mode === "online" && !complete && (
|
||
<>
|
||
<Coin
|
||
onTransitionEnd={onTransitionEnd}
|
||
frontList={frontList}
|
||
rotation={rotation}
|
||
/>
|
||
<span className="text-lg font-medium">
|
||
🎲 第{" "}
|
||
<span className="font-mono text-xl font-bold text-orange-500">
|
||
{count === 0 ? "-/-" : `${count}/6`}
|
||
</span>{" "}
|
||
次卜筮
|
||
</span>
|
||
</>
|
||
)}
|
||
|
||
{mode === "offline" && !complete && (
|
||
<div className="w-full max-w-md space-y-2">
|
||
<p className="text-center text-xs text-muted-foreground">
|
||
人工抛铜钱后,按从下到上(初爻→上爻)录入每爻结果
|
||
</p>
|
||
{YAO_LABELS.map((label, index) => (
|
||
<div
|
||
key={label}
|
||
className="flex flex-wrap items-center gap-2 rounded-md border p-2"
|
||
>
|
||
<span className="w-10 shrink-0 text-sm font-medium">{label}</span>
|
||
{COIN_OPTIONS.map((opt) => (
|
||
<button
|
||
key={opt.count}
|
||
type="button"
|
||
onClick={() => handleOfflineSelect(index, opt.count)}
|
||
className={`rounded border px-2 py-1 text-xs transition ${
|
||
offlineCounts[index] === opt.count
|
||
? "border-primary bg-primary/10 text-primary"
|
||
: "hover:bg-accent"
|
||
}`}
|
||
>
|
||
{opt.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
))}
|
||
<Button
|
||
className="w-full"
|
||
disabled={!offlineReady}
|
||
onClick={confirmOffline}
|
||
>
|
||
确认卦象
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{hexagramList.length > 0 && (
|
||
<div className="flex flex-col items-center gap-3 sm:flex-row">
|
||
<Hexagram list={hexagramList} />
|
||
{complete && (
|
||
<Button size="sm" variant="outline" onClick={handleReset}>
|
||
重新起卦
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|