Fix learn 404, coin animation, full regions, and AI key errors.
Use numeric /learn/{num} routes, register Tailwind coin animations with方孔铜钱 UI, expand to 34 provinces, and surface missing OPENAI_API_KEY clearly.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+23
-18
@@ -2,7 +2,13 @@ import React, { useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
const rotationDuration = 3800;
|
||||
const bezier = "cubic-bezier(0.645,0.045,0.355,1)";
|
||||
|
||||
const COIN_ANIM: Record<string, string> = {
|
||||
"front-front": "animate-coin-front-front",
|
||||
"front-back": "animate-coin-front-back",
|
||||
"back-front": "animate-coin-back-front",
|
||||
"back-back": "animate-coin-back-back",
|
||||
};
|
||||
|
||||
function Coin(props: {
|
||||
frontList: boolean[];
|
||||
@@ -15,7 +21,6 @@ function Coin(props: {
|
||||
if (!props.rotation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setTimeout(() => {
|
||||
setLastFront(props.frontList);
|
||||
props.onTransitionEnd();
|
||||
@@ -37,23 +42,30 @@ function Coin(props: {
|
||||
);
|
||||
}
|
||||
|
||||
/** 方孔铜钱造型 */
|
||||
function CoinFace({ side }: { side: "front" | "back" }) {
|
||||
const isFront = side === "front";
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute inset-0 flex items-center justify-center rounded-full border-[3px] shadow-inner",
|
||||
"absolute inset-0 rounded-full border-[3px] shadow-md",
|
||||
isFront
|
||||
? "border-amber-500 bg-gradient-to-br from-amber-200 to-amber-400 text-amber-950"
|
||||
: "border-stone-500 bg-gradient-to-br from-stone-300 to-stone-500 text-stone-800",
|
||||
? "border-amber-600 bg-gradient-to-br from-amber-300 via-amber-400 to-amber-600"
|
||||
: "border-stone-600 bg-gradient-to-br from-stone-400 via-stone-500 to-stone-700",
|
||||
)}
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
transform: isFront ? undefined : "rotateY(180deg)",
|
||||
}}
|
||||
>
|
||||
<span className="select-none text-lg font-bold sm:text-xl">
|
||||
{isFront ? "正" : "反"}
|
||||
<div className="absolute inset-[18%] rounded-sm border-2 border-amber-800/40 bg-amber-900/10" />
|
||||
<span
|
||||
className={clsx(
|
||||
"absolute bottom-1 left-0 right-0 text-center text-[10px] font-bold sm:text-xs",
|
||||
isFront ? "text-amber-950/80" : "text-stone-200/90",
|
||||
)}
|
||||
>
|
||||
{isFront ? "乾" : "坤"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -64,24 +76,17 @@ function CoinItem(props: {
|
||||
lastFront: boolean;
|
||||
rotation: boolean;
|
||||
}) {
|
||||
let animate = "";
|
||||
if (props.rotation) {
|
||||
animate = `animate-[coin-${getFront(props.lastFront)}-${getFront(
|
||||
props.front,
|
||||
)}_${rotationDuration / 1000}s_${bezier}]`;
|
||||
}
|
||||
const animKey = `${getFront(props.lastFront)}-${getFront(props.front)}`;
|
||||
const animClass = props.rotation ? COIN_ANIM[animKey] : "";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-16 w-16 sm:h-20 sm:w-20"
|
||||
style={{ perspective: "800px" }}
|
||||
>
|
||||
<div className="h-[4.5rem] w-[4.5rem] sm:h-20 sm:w-20" style={{ perspective: "900px" }}>
|
||||
<div
|
||||
style={{
|
||||
transform: `rotateY(${props.front ? 0 : 180}deg)`,
|
||||
transformStyle: "preserve-3d",
|
||||
}}
|
||||
className={clsx("relative h-full w-full", animate)}
|
||||
className={clsx("relative h-full w-full", animClass)}
|
||||
>
|
||||
<CoinFace side="front" />
|
||||
<CoinFace side="back" />
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { guaNumFromMark } from "@/lib/content/zhouyi";
|
||||
|
||||
export default function GuaFooter({ guaMark }: { guaMark: string }) {
|
||||
export default function GuaFooter({
|
||||
guaMark,
|
||||
guaNum,
|
||||
}: {
|
||||
guaMark: string;
|
||||
guaNum?: string;
|
||||
}) {
|
||||
const num = guaNum ?? guaNumFromMark(guaMark);
|
||||
return (
|
||||
<div className="mt-8 flex flex-wrap gap-3 border-t pt-6">
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/liuyao?gua=${encodeURIComponent(guaMark)}`}>
|
||||
<Link href={`/liuyao?gua=${num}`}>
|
||||
<Sparkles size={16} className="mr-1" />
|
||||
以此卦起卦
|
||||
</Link>
|
||||
|
||||
@@ -24,9 +24,12 @@ function resolveLearnHref(
|
||||
if (href.endsWith("/index.md")) {
|
||||
const mark = href.replace(/\/index\.md$/, "").replace(/^\.\//, "");
|
||||
if (mark.startsWith("other/")) {
|
||||
return `/learn/other/${mark.slice("other/".length)}`;
|
||||
const folder = mark.slice("other/".length);
|
||||
const num = folder.split(".")[0];
|
||||
return `/learn/other/${num}`;
|
||||
}
|
||||
return `${base}/${mark}`;
|
||||
const num = mark.split(".")[0];
|
||||
return `${base}/${num}`;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { guaNumFromMark } from "@/lib/content/zhouyi";
|
||||
|
||||
export interface ResultObj {
|
||||
guaTitle: string;
|
||||
@@ -8,16 +10,17 @@ export interface ResultObj {
|
||||
}
|
||||
|
||||
function Result(props: ResultObj) {
|
||||
const guaNum = guaNumFromMark(props.guaMark);
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-center gap-2 sm:gap-3">
|
||||
{props.guaTitle}
|
||||
<a
|
||||
<Link
|
||||
className="group flex items-center gap-1 font-medium text-primary/80 underline underline-offset-4 transition-colors hover:text-primary/100"
|
||||
href={`/learn/${props.guaMark}`}
|
||||
href={`/learn/${guaNum}`}
|
||||
>
|
||||
<div className="mt-1 h-[90%] w-1.5 bg-blue-400/80 transition-colors group-hover:bg-blue-400/100" />
|
||||
<span>{props.guaResult}</span>
|
||||
</a>
|
||||
</Link>
|
||||
<span className="text-sm italic text-muted-foreground">
|
||||
{props.guaChange}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { bool } from "aimless.js";
|
||||
import Coin from "@/components/coin";
|
||||
import Hexagram from "@/components/hexagram";
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
YAO_LABELS,
|
||||
} from "@/lib/calc/hexagram";
|
||||
|
||||
const AUTO_DELAY = 600;
|
||||
const AUTO_DELAY = 800;
|
||||
|
||||
interface HexagramInputProps {
|
||||
mode: "online" | "offline";
|
||||
@@ -30,43 +30,30 @@ export default function HexagramInput({
|
||||
}: 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 [hexagramList, setHexagramList] = useState<GuaResult["list"]>([]);
|
||||
const [offlineCounts, setOfflineCounts] = useState<(number | null)[]>(
|
||||
Array(6).fill(null),
|
||||
);
|
||||
|
||||
const complete = hexagramList.length === 6;
|
||||
|
||||
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]);
|
||||
const finishList = useCallback(
|
||||
(list: GuaResult["list"]) => {
|
||||
const result = computeGuaResult(list);
|
||||
if (result) {
|
||||
onResult({ list, result });
|
||||
}
|
||||
},
|
||||
[onResult],
|
||||
);
|
||||
|
||||
function finishList(list: GuaResult["list"]) {
|
||||
const result = computeGuaResult(list);
|
||||
if (result) {
|
||||
onResult({ list, result });
|
||||
}
|
||||
}
|
||||
|
||||
function onTransitionEnd() {
|
||||
const onTransitionEnd = useCallback(() => {
|
||||
setRotation(false);
|
||||
const frontCount = frontList.reduce(
|
||||
(acc, val) => (val ? acc + 1 : acc),
|
||||
@@ -86,16 +73,23 @@ export default function HexagramInput({
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
}, [frontList, finishList]);
|
||||
|
||||
function startOnlineCast() {
|
||||
const startOnlineCast = useCallback(() => {
|
||||
if (rotation || !enabled || hexagramList.length >= 6) {
|
||||
return;
|
||||
}
|
||||
setFrontList([bool(), bool(), bool()]);
|
||||
setRotation(true);
|
||||
setCount((c) => c + 1);
|
||||
}
|
||||
}, [rotation, enabled, hexagramList.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "online" || !enabled || rotation || complete) {
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(startOnlineCast, AUTO_DELAY);
|
||||
return () => clearTimeout(timer);
|
||||
}, [mode, enabled, rotation, complete, hexagramList.length, startOnlineCast]);
|
||||
|
||||
function handleOfflineSelect(index: number, frontCount: number) {
|
||||
const next = [...offlineCounts];
|
||||
@@ -114,13 +108,11 @@ export default function HexagramInput({
|
||||
|
||||
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) {
|
||||
@@ -133,7 +125,7 @@ export default function HexagramInput({
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-4">
|
||||
{mode === "online" && !complete && (
|
||||
{mode === "online" && (
|
||||
<>
|
||||
<Coin
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
@@ -143,9 +135,12 @@ export default function HexagramInput({
|
||||
<span className="text-lg font-medium">
|
||||
🎲 第{" "}
|
||||
<span className="font-mono text-xl font-bold text-orange-500">
|
||||
{count === 0 ? "-/-" : `${count}/6`}
|
||||
{complete ? "6/6" : `${hexagramList.length + (rotation ? 1 : 0)}/6`}
|
||||
</span>{" "}
|
||||
次卜筮
|
||||
{rotation && (
|
||||
<span className="ml-2 text-sm text-muted-foreground">铜钱旋转中…</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -71,10 +71,10 @@ export default function RegionSelect({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{cityOptional && provinceCode && (
|
||||
{cityOptional && provinceCode && location && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
城市可不选,默认使用省份经度;已选:
|
||||
{location ? `${location.name}(${location.longitude}°)` : "—"}
|
||||
城市可选;未选城市时使用省份经度。当前:
|
||||
{location.name}({location.longitude}°)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user