diff --git a/components/coin.tsx b/components/coin.tsx index 61501e8..f0c365c 100644 --- a/components/coin.tsx +++ b/components/coin.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import clsx from "clsx"; -import Image from "next/image"; const rotationDuration = 3800; const bezier = "cubic-bezier(0.645,0.045,0.355,1)"; @@ -8,21 +7,21 @@ const bezier = "cubic-bezier(0.645,0.045,0.355,1)"; function Coin(props: { frontList: boolean[]; rotation: boolean; - onTransitionEnd: any; + onTransitionEnd: () => void; }) { const [lastFront, setLastFront] = useState(props.frontList); - useEffect(function () { + useEffect(() => { if (!props.rotation) { return; } - let id = setTimeout(function () { + const id = setTimeout(() => { setLastFront(props.frontList); props.onTransitionEnd(); }, rotationDuration); return () => clearTimeout(id); - }); + }, [props.rotation, props.frontList, props.onTransitionEnd]); return (
@@ -38,50 +37,55 @@ function Coin(props: { ); } +function CoinFace({ side }: { side: "front" | "back" }) { + const isFront = side === "front"; + return ( +
+ + {isFront ? "正" : "反"} + +
+ ); +} + function CoinItem(props: { front: boolean; lastFront: boolean; rotation: boolean; - onTransitionEnd?: any; }) { let animate = ""; if (props.rotation) { - // animate-[coin-front-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)] - // animate-[coin-front-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)] - // animate-[coin-back-front_3.8s_cubic-bezier(0.645,0.045,0.355,1)] - // animate-[coin-back-back_3.8s_cubic-bezier(0.645,0.045,0.355,1)] animate = `animate-[coin-${getFront(props.lastFront)}-${getFront( props.front, )}_${rotationDuration / 1000}s_${bezier}]`; } + return (
- coin - coin +
+ + +
); } diff --git a/components/modes/liuyao-form.tsx b/components/modes/liuyao-form.tsx index 223ccda..2858dbf 100644 --- a/components/modes/liuyao-form.tsx +++ b/components/modes/liuyao-form.tsx @@ -1,8 +1,8 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { readStreamableValue } from "ai/rsc"; -import { Compass, ListRestart } from "lucide-react"; +import { BrainCircuit, ListRestart } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import Result from "@/components/result"; @@ -35,18 +35,26 @@ export default function LiuyaoForm() { const [isLoading, setIsLoading] = useState(false); const [showAi, setShowAi] = useState(false); + const actionRef = useRef(null); + const location = useRegionLocation(provinceCode, cityCode); const formReady = question.trim() !== "" && location !== null; + useEffect(() => { + if (guaData && actionRef.current) { + actionRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); + } + }, [guaData]); + function validate(): string | null { if (!question.trim()) { return "请输入问事"; } if (!location) { - return "请选择起卦地域"; + return "请选择起卦省份"; } if (!guaData) { - return "请先完成起卦(线上摇卦或线下录入)"; + return "请先完成 6 次起卦(线上摇卦或线下录入)"; } return null; } @@ -115,6 +123,7 @@ export default function LiuyaoForm() { setGuaData(data); setShowAi(false); setCompletion(""); + setError(""); } return ( @@ -197,16 +206,25 @@ export default function LiuyaoForm() { /> {guaData && ( -
+
+

+ 起卦完成 · 请点击下方「AI 解读」 +

)} - {error && !showAi && ( + {!guaData && formReady && castMode === "online" && ( +

+ 线上模式将自动摇卦 6 次,完成后出现 AI 解读按钮 +

+ )} + + {error && (

{error}

)} -
+
diff --git a/components/shared/hexagram-input.tsx b/components/shared/hexagram-input.tsx index 871b7a9..e7e496f 100644 --- a/components/shared/hexagram-input.tsx +++ b/components/shared/hexagram-input.tsx @@ -191,9 +191,14 @@ export default function HexagramInput({
{complete && ( - + <> +

+ 六爻已齐,请在下方点击「AI 解读」 +

+ + )}
)} diff --git a/components/shared/region-select.tsx b/components/shared/region-select.tsx index e2c9558..2f3def1 100644 --- a/components/shared/region-select.tsx +++ b/components/shared/region-select.tsx @@ -1,6 +1,10 @@ "use client"; -import { getProvinces, getCities, getRegionLocation } from "@/lib/data/regions"; +import { + getProvinces, + getCities, + getRegionLocation, +} from "@/lib/data/regions"; interface RegionSelectProps { provinceCode: string; @@ -8,6 +12,7 @@ interface RegionSelectProps { onProvinceChange: (code: string) => void; onCityChange: (code: string) => void; label?: string; + cityOptional?: boolean; } export default function RegionSelect({ @@ -16,9 +21,23 @@ export default function RegionSelect({ onProvinceChange, onCityChange, label = "出生地域", + cityOptional = true, }: RegionSelectProps) { const provinces = getProvinces(); const cities = provinceCode ? getCities(provinceCode) : []; + const location = provinceCode + ? getRegionLocation(provinceCode, cityCode) + : null; + + function handleProvinceChange(code: string) { + onProvinceChange(code); + if (!code) { + onCityChange(""); + return; + } + const list = getCities(code); + onCityChange(list[0]?.code ?? ""); + } return (
@@ -27,10 +46,7 @@ export default function RegionSelect({
+ {cityOptional && provinceCode && ( +

+ 城市可不选,默认使用省份经度;已选: + {location ? `${location.name}(${location.longitude}°)` : "—"} +

+ )}
); } diff --git a/lib/data/regions.json b/lib/data/regions.json index 8504302..fa8cac0 100644 --- a/lib/data/regions.json +++ b/lib/data/regions.json @@ -73,7 +73,11 @@ "longitude": 117.0009, "children": { "370100": { "name": "济南市", "longitude": 117.1205 }, - "370200": { "name": "青岛市", "longitude": 120.3826 } + "370200": { "name": "青岛市", "longitude": 120.3826 }, + "370300": { "name": "淄博市", "longitude": 118.0550 }, + "370600": { "name": "烟台市", "longitude": 121.4479 }, + "370700": { "name": "潍坊市", "longitude": 119.1619 }, + "371300": { "name": "临沂市", "longitude": 118.3565 } } }, "430000": { diff --git a/lib/data/regions.ts b/lib/data/regions.ts index 9a47c2c..680aa75 100644 --- a/lib/data/regions.ts +++ b/lib/data/regions.ts @@ -11,10 +11,12 @@ export type RegionsData = Record; export const regions = regionsData as RegionsData; export function getProvinces(): { code: string; name: string }[] { - return Object.entries(regions).map(([code, node]) => ({ - code, - name: node.name, - })); + return Object.entries(regions) + .map(([code, node]) => ({ + code, + name: node.name, + })) + .sort((a, b) => a.name.localeCompare(b.name, "zh-CN")); } export function getCities(provinceCode: string): { code: string; name: string }[] { @@ -22,10 +24,12 @@ export function getCities(provinceCode: string): { code: string; name: string }[ if (!province?.children) { return []; } - return Object.entries(province.children).map(([code, node]) => ({ - code, - name: node.name, - })); + return Object.entries(province.children) + .map(([code, node]) => ({ + code, + name: node.name, + })) + .sort((a, b) => a.name.localeCompare(b.name, "zh-CN")); } export function getRegionLocation(