From a487b1716575b6b2e493328eab07a4879313667c Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 10 Jun 2026 21:32:20 +0800 Subject: [PATCH] Fix learn 404, coin animation, full regions, and AI key errors. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/learn/{[guaMark] => [slug]}/page.tsx | 35 +- .../other/{[guaMark] => [slug]}/page.tsx | 31 +- app/learn/other/page.tsx | 6 +- app/learn/page.tsx | 6 +- components/coin.tsx | 41 +- components/learn/gua-footer.tsx | 12 +- components/learn/markdown-content.tsx | 7 +- components/result.tsx | 9 +- components/shared/hexagram-input.tsx | 67 +- components/shared/region-select.tsx | 6 +- lib/ai/stream.ts | 4 + lib/content/zhouyi.ts | 15 + lib/data/regions.json | 781 ++++++++++++++++-- scripts/generate-regions.mjs | 62 ++ tailwind.config.ts | 8 + 15 files changed, 931 insertions(+), 159 deletions(-) rename app/learn/{[guaMark] => [slug]}/page.tsx (58%) rename app/learn/other/{[guaMark] => [slug]}/page.tsx (63%) create mode 100644 scripts/generate-regions.mjs diff --git a/app/learn/[guaMark]/page.tsx b/app/learn/[slug]/page.tsx similarity index 58% rename from app/learn/[guaMark]/page.tsx rename to app/learn/[slug]/page.tsx index a53a26b..5b375b8 100644 --- a/app/learn/[guaMark]/page.tsx +++ b/app/learn/[slug]/page.tsx @@ -6,29 +6,50 @@ import MarkdownContent from "@/components/learn/markdown-content"; import { getGuaName, getGuaNumber, + guaNumFromMark, listGuaMarks, + markFromNum, readLearnMarkdown, stripFrontmatter, } from "@/lib/content/zhouyi"; -export async function generateStaticParams() { +async function resolveGuaMark(slug: string): Promise { + const decoded = decodeURIComponent(slug); + if (/^\d{1,2}$/.test(decoded)) { + return markFromNum(decoded, "traditional"); + } const marks = await listGuaMarks("traditional"); - return marks.map((guaMark) => ({ guaMark })); + if (marks.includes(decoded)) { + return decoded; + } + if (/^\d{2}\./.test(decoded)) { + return marks.find((m) => m === decoded || m.startsWith(decoded.split(".")[0] + ".")) ?? null; + } + return null; } +export async function generateStaticParams() { + return Array.from({ length: 64 }, (_, i) => ({ + slug: String(i + 1).padStart(2, "0"), + })); +} + +export const dynamicParams = true; + export default async function GuaDetailPage({ params, }: { - params: Promise<{ guaMark: string }>; + params: Promise<{ slug: string }>; }) { - const { guaMark } = await params; - const marks = await listGuaMarks("traditional"); - if (!marks.includes(guaMark)) { + const { slug } = await params; + const guaMark = await resolveGuaMark(slug); + if (!guaMark) { notFound(); } const raw = await readLearnMarkdown(guaMark, "traditional"); const content = stripFrontmatter(raw); + const num = guaNumFromMark(guaMark); return ( @@ -41,7 +62,7 @@ export default async function GuaDetailPage({ 第 {getGuaNumber(guaMark)} 卦 · {getGuaName(guaMark)} - + ); } diff --git a/app/learn/other/[guaMark]/page.tsx b/app/learn/other/[slug]/page.tsx similarity index 63% rename from app/learn/other/[guaMark]/page.tsx rename to app/learn/other/[slug]/page.tsx index 884bea8..eaaecb1 100644 --- a/app/learn/other/[guaMark]/page.tsx +++ b/app/learn/other/[slug]/page.tsx @@ -6,24 +6,41 @@ import MarkdownContent from "@/components/learn/markdown-content"; import { getGuaName, getGuaNumber, + guaNumFromMark, listGuaMarks, + markFromNum, readLearnMarkdown, stripFrontmatter, } from "@/lib/content/zhouyi"; -export async function generateStaticParams() { +async function resolveGuaMark(slug: string): Promise { + const decoded = decodeURIComponent(slug); + if (/^\d{1,2}$/.test(decoded)) { + return markFromNum(decoded, "simplified"); + } const marks = await listGuaMarks("simplified"); - return marks.map((guaMark) => ({ guaMark })); + if (marks.includes(decoded)) { + return decoded; + } + return null; } +export async function generateStaticParams() { + return Array.from({ length: 64 }, (_, i) => ({ + slug: String(i + 1).padStart(2, "0"), + })); +} + +export const dynamicParams = true; + export default async function GuaOtherDetailPage({ params, }: { - params: Promise<{ guaMark: string }>; + params: Promise<{ slug: string }>; }) { - const { guaMark } = await params; - const marks = await listGuaMarks("simplified"); - if (!marks.includes(guaMark)) { + const { slug } = await params; + const guaMark = await resolveGuaMark(slug); + if (!guaMark) { notFound(); } @@ -41,7 +58,7 @@ export default async function GuaOtherDetailPage({ 第 {getGuaNumber(guaMark)} 卦 · {getGuaName(guaMark)} - + ); } diff --git a/app/learn/other/page.tsx b/app/learn/other/page.tsx index 8dc695a..e0f5cd6 100644 --- a/app/learn/other/page.tsx +++ b/app/learn/other/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import PageShell from "@/components/page-shell"; import { getGuaName, - getGuaNumber, + guaNumFromMark, listGuaMarks, } from "@/lib/content/zhouyi"; @@ -35,11 +35,11 @@ export default async function LearnOtherPage() { {guaMarks.map((mark) => ( - {getGuaNumber(mark)} + {guaNumFromMark(mark)} {getGuaName(mark)} diff --git a/app/learn/page.tsx b/app/learn/page.tsx index 5e96a9c..bc164b3 100644 --- a/app/learn/page.tsx +++ b/app/learn/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import PageShell from "@/components/page-shell"; import { getGuaName, - getGuaNumber, + guaNumFromMark, listGuaMarks, } from "@/lib/content/zhouyi"; @@ -35,11 +35,11 @@ export default async function LearnPage() { {guaMarks.map((mark) => ( - {getGuaNumber(mark)} + {guaNumFromMark(mark)} {getGuaName(mark)} diff --git a/components/coin.tsx b/components/coin.tsx index f0c365c..635aa0f 100644 --- a/components/coin.tsx +++ b/components/coin.tsx @@ -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 = { + "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 (
- - {isFront ? "正" : "反"} +
+ + {isFront ? "乾" : "坤"}
); @@ -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 ( -
+
diff --git a/components/learn/gua-footer.tsx b/components/learn/gua-footer.tsx index cef577f..6d79f33 100644 --- a/components/learn/gua-footer.tsx +++ b/components/learn/gua-footer.tsx @@ -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 (