guanwei 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +202 -0
  3. package/README.md +247 -0
  4. package/dist/assets/index-CsOD5rSo.js +102 -0
  5. package/dist/assets/index-CsOD5rSo.js.map +1 -0
  6. package/dist/assets/index-DQx62TUr.css +1 -0
  7. package/dist/favicon.svg +12 -0
  8. package/dist/index.html +15 -0
  9. package/eslint.config.js +28 -0
  10. package/index.html +25 -0
  11. package/package.json +96 -0
  12. package/postcss.config.js +10 -0
  13. package/public/favicon.svg +12 -0
  14. package/scripts/guanwei +245 -0
  15. package/scripts/install.sh +79 -0
  16. package/scripts/release.sh +42 -0
  17. package/scripts/setup.bat +36 -0
  18. package/scripts/setup.sh +123 -0
  19. package/server/src/data/db.json +12059 -0
  20. package/server/src/data/guanwei.db +0 -0
  21. package/server/src/data/lifeContext.ts +423 -0
  22. package/server/src/data/spreads.ts +255 -0
  23. package/server/src/data/tarotCards.ts +2353 -0
  24. package/server/src/index.ts +71 -0
  25. package/server/src/routes/ai.ts +376 -0
  26. package/server/src/routes/divine.ts +140 -0
  27. package/server/src/routes/hour.ts +40 -0
  28. package/server/src/routes/tarot.ts +129 -0
  29. package/server/src/routes/users.ts +140 -0
  30. package/server/src/services/chartBrief.ts +131 -0
  31. package/server/src/services/divineStore.ts +188 -0
  32. package/server/src/services/hourInference.ts +150 -0
  33. package/server/src/services/llmProvider.ts +181 -0
  34. package/server/src/services/promptBuilder.ts +327 -0
  35. package/server/src/services/relativesCheck.ts +139 -0
  36. package/server/src/services/sixRelatives.ts +70 -0
  37. package/server/src/services/skills.ts +116 -0
  38. package/server/src/types/index.ts +104 -0
  39. package/server/src/utils/semanticAnalyzer.ts +555 -0
  40. package/server/src/utils/tarotEngine.ts +849 -0
  41. package/server/tsconfig.json +18 -0
  42. package/shared/core/data/classics.ts +276 -0
  43. package/shared/core/data/courses.ts +211 -0
  44. package/shared/core/data/duanyu.ts +148 -0
  45. package/shared/core/data/ganzhi.ts +53 -0
  46. package/shared/core/data/gua64.ts +93 -0
  47. package/shared/core/data/liuren.ts +18 -0
  48. package/shared/core/data/liuyao.ts +73 -0
  49. package/shared/core/data/qimen.ts +27 -0
  50. package/shared/core/data/region-full.ts +3 -0
  51. package/shared/core/data/region.ts +310 -0
  52. package/shared/core/data/tarotSpreads.ts +78 -0
  53. package/shared/core/data/xiaoliuren.ts +18 -0
  54. package/shared/core/data/ziwei.ts +97 -0
  55. package/shared/core/data/zodiac.ts +20 -0
  56. package/shared/core/engine/astrology.ts +169 -0
  57. package/shared/core/engine/bazi.ts +306 -0
  58. package/shared/core/engine/calendar.ts +127 -0
  59. package/shared/core/engine/daily.ts +133 -0
  60. package/shared/core/engine/liuren.ts +86 -0
  61. package/shared/core/engine/liuyao.ts +93 -0
  62. package/shared/core/engine/meihua.ts +87 -0
  63. package/shared/core/engine/qimen.ts +78 -0
  64. package/shared/core/engine/questionFit.ts +94 -0
  65. package/shared/core/engine/tarot.ts +69 -0
  66. package/shared/core/engine/trueSolarTime.ts +86 -0
  67. package/shared/core/engine/xiaoliuren.ts +19 -0
  68. package/shared/core/engine/ziwei.ts +217 -0
  69. package/shared/core/engine/ziweiInterpret.ts +161 -0
  70. package/shared/core/types.ts +206 -0
  71. package/src/App.tsx +69 -0
  72. package/src/arts/registry.ts +31 -0
  73. package/src/assets/react.svg +1 -0
  74. package/src/components/Backdrop.tsx +27 -0
  75. package/src/components/BambooArt.tsx +41 -0
  76. package/src/components/CalendarInput.tsx +131 -0
  77. package/src/components/DateInput.tsx +103 -0
  78. package/src/components/Disclaimer.tsx +12 -0
  79. package/src/components/Empty.tsx +8 -0
  80. package/src/components/ErrorBoundary.tsx +38 -0
  81. package/src/components/HomeSideAnchor.tsx +62 -0
  82. package/src/components/InkVein.tsx +10 -0
  83. package/src/components/LifeEventsInput.tsx +47 -0
  84. package/src/components/LocationPicker.tsx +117 -0
  85. package/src/components/Motes.tsx +65 -0
  86. package/src/components/Navigation.tsx +80 -0
  87. package/src/components/ProfileForm.tsx +40 -0
  88. package/src/components/ProfilePicker.tsx +40 -0
  89. package/src/components/QuestionFields.tsx +47 -0
  90. package/src/components/ReportView.tsx +211 -0
  91. package/src/components/ResultCard.tsx +40 -0
  92. package/src/components/SealButton.tsx +18 -0
  93. package/src/components/SiteFooter.tsx +17 -0
  94. package/src/components/SiteNav.tsx +103 -0
  95. package/src/components/SongDialog.tsx +64 -0
  96. package/src/components/SongSearchSelect.tsx +81 -0
  97. package/src/components/SongSelect.tsx +61 -0
  98. package/src/components/StarField.tsx +119 -0
  99. package/src/components/TarotCard.tsx +129 -0
  100. package/src/components/TimeShichenInput.tsx +29 -0
  101. package/src/components/arts/AstrologyArt.tsx +141 -0
  102. package/src/components/arts/BaziArt.tsx +269 -0
  103. package/src/components/arts/LiurenArt.tsx +87 -0
  104. package/src/components/arts/LiuyaoArt.tsx +111 -0
  105. package/src/components/arts/MeihuaArt.tsx +105 -0
  106. package/src/components/arts/QimenArt.tsx +86 -0
  107. package/src/components/arts/TarotArt.tsx +110 -0
  108. package/src/components/arts/XiaoliurenArt.tsx +93 -0
  109. package/src/components/arts/ZiweiArt.tsx +296 -0
  110. package/src/data/arts.ts +84 -0
  111. package/src/data/lifeContext.ts +423 -0
  112. package/src/data/sample-report.json +146 -0
  113. package/src/data/shichen.ts +21 -0
  114. package/src/data/spreads.ts +255 -0
  115. package/src/data/tarotCards.ts +2353 -0
  116. package/src/hooks/useAIInterpret.ts +76 -0
  117. package/src/hooks/useDivine.ts +39 -0
  118. package/src/hooks/useTheme.ts +29 -0
  119. package/src/index.css +190 -0
  120. package/src/lib/utils.ts +6 -0
  121. package/src/main.tsx +12 -0
  122. package/src/pages/AboutPage.tsx +25 -0
  123. package/src/pages/AcademyPage.tsx +128 -0
  124. package/src/pages/AstrologyPage.tsx +33 -0
  125. package/src/pages/AuthPage.tsx +263 -0
  126. package/src/pages/BaZiPage.tsx +33 -0
  127. package/src/pages/ClassicsPage.tsx +120 -0
  128. package/src/pages/DemoPage.tsx +161 -0
  129. package/src/pages/HistoryPage.tsx +133 -0
  130. package/src/pages/Home.tsx +179 -0
  131. package/src/pages/HomePage.tsx +174 -0
  132. package/src/pages/ModulePage.tsx +320 -0
  133. package/src/pages/SpreadEditor.tsx +230 -0
  134. package/src/pages/SpreadEditorPage.tsx +105 -0
  135. package/src/pages/SpreadLibrary.tsx +199 -0
  136. package/src/pages/SpreadLibraryPage.tsx +17 -0
  137. package/src/pages/TarotPage.tsx +738 -0
  138. package/src/pages/ZiWeiPage.tsx +33 -0
  139. package/src/services/api.ts +342 -0
  140. package/src/stores/useDivinationStore.ts +22 -0
  141. package/src/styles/song.css +1066 -0
  142. package/src/styles/tokens.css +60 -0
  143. package/src/types/index.ts +104 -0
  144. package/src/utils/astrologyCalc.ts +61 -0
  145. package/src/utils/comboEngine.ts +134 -0
  146. package/src/utils/dst.ts +24 -0
  147. package/src/utils/panTone.ts +206 -0
  148. package/src/utils/recordStore.ts +60 -0
  149. package/src/utils/semanticAnalyzer.ts +555 -0
  150. package/src/utils/spreadStorage.ts +32 -0
  151. package/src/utils/storage.ts +27 -0
  152. package/src/utils/tarotEngine.ts +849 -0
  153. package/src/utils/userStore.ts +234 -0
  154. package/src/vite-env.d.ts +1 -0
  155. package/tailwind.config.js +52 -0
  156. package/tsconfig.json +40 -0
  157. package/vite.config.ts +34 -0
@@ -0,0 +1,133 @@
1
+ // 占问记录:列表(按术筛选/搜索/删除)+ 详情(复用九术结果渲染回看)
2
+ import { useMemo, useState } from 'react';
3
+ import { Link, useParams } from 'react-router-dom';
4
+ import { getRecords, deleteRecord, getRecord } from '@/utils/recordStore';
5
+ import { artPairOf } from '@/arts/registry';
6
+ import { artById } from '@/data/arts';
7
+ import SongSelect from '@/components/SongSelect';
8
+
9
+ function formatTime(ts: number) {
10
+ const d = new Date(ts);
11
+ const p = (n: number) => String(n).padStart(2, '0');
12
+ return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${p(d.getHours())}:${p(d.getMinutes())}`;
13
+ }
14
+
15
+ export default function HistoryPage() {
16
+ const { id } = useParams<{ id: string }>();
17
+ const [tick, setTick] = useState(0);
18
+ const [artFilter, setArtFilter] = useState('all');
19
+ const [search, setSearch] = useState('');
20
+
21
+ // 详情态:回看某次占问(复用对应术的结果渲染)
22
+ if (id) {
23
+ const rec = getRecord(id);
24
+ if (!rec) {
25
+ return (
26
+ <div className="wrap page-compact" style={{ paddingTop: 'var(--sp-4)', textAlign: 'center' }}>
27
+ <div className="result-placeholder"><div className="glyph">佚</div><p>此占无痕 · 请返记录再择</p></div>
28
+ <div className="mt-4"><Link className="btn-seal btn-ghost" to="/history">返 记 录</Link></div>
29
+ </div>
30
+ );
31
+ }
32
+ const art = artById(rec.artId);
33
+ const pair = artPairOf(rec.artId);
34
+ return (
35
+ <div className="wrap" style={{ paddingTop: 'var(--sp-7)', paddingBottom: 'var(--sp-6)' }}>
36
+ <Link className="back-link" to="/history">‹ 返记录</Link>
37
+ <div className="module-kicker">留痕 · {art?.name || rec.artId}</div>
38
+ <div className="module-title-row">
39
+ <h2 className="module-title"><span className="cn">{art?.name || '占问'}</span>{formatTime(rec.createdAt)}</h2>
40
+ </div>
41
+ {(rec.profile || rec.question) && (
42
+ <div className="result-card" style={{ marginTop: '1rem' }}>
43
+ {rec.question && <p className="result-text"><strong>所问:</strong>{rec.question}</p>}
44
+ {rec.profile && (
45
+ <p className="result-text tiny" style={{ marginTop: '.4rem' }}>
46
+ <strong>档案:</strong>{(rec.profile as any).gender || ''}{(rec.profile as any).birthDate ? ' · ' + (rec.profile as any).birthDate : ''}{(rec.profile as any).birthTime ? ' ' + (rec.profile as any).birthTime : ''}
47
+ {(() => { const l = (rec.profile as any).location; return l ? ' · ' + ((l as any).province || '') + ((l as any).city || '') + ((l as any).district || '') + '(' + (l as any).lng?.toFixed?.(2) + ',' + (l as any).lat?.toFixed?.(2) + ')' : ''; })()}
48
+ </p>
49
+ )}
50
+ </div>
51
+ )}
52
+ <div className="mt-4">
53
+ {pair?.Result ? <pair.Result data={rec.result} /> : <p className="result-text">此术结果暂无法回看。</p>}
54
+ </div>
55
+ <button className="btn-seal btn-ghost" style={{ marginTop: 'var(--sp-3)', fontSize: '.85rem', padding: '.4rem 1.2rem' }}
56
+ onClick={() => { if (confirm('确定删除此占问记录?')) { deleteRecord(rec.id); window.location.hash = '#/history'; } }}>
57
+ 删 此 记 录
58
+ </button>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ const records = useMemo(() => getRecords(), [tick, artFilter, search]);
64
+ const [profileFilter, setProfileFilter] = useState('all');
65
+ const filtered = records.filter(r => {
66
+ if (profileFilter !== 'all' && (r.profileId || 'main') !== profileFilter) return false;
67
+ if (artFilter !== 'all' && r.artId !== artFilter) return false;
68
+ if (search) {
69
+ const art = artById(r.artId);
70
+ if (!(art?.name || '').includes(search) && !r.id.includes(search)) return false;
71
+ }
72
+ return true;
73
+ });
74
+
75
+ return (
76
+ <div className="wrap" style={{ paddingTop: 'var(--sp-7)', paddingBottom: 'var(--sp-6)' }}>
77
+ <div className="section-eyebrow">留痕 · Records</div>
78
+ <h2 className="section-title" style={{ marginTop: '.8rem' }}>占问记录</h2>
79
+ <p className="section-note">每问留痕,观照来时路。共 {records.length} 次占问(本地上限 200 条)。</p>
80
+
81
+ <div className="field-row" style={{ marginTop: 'var(--sp-3)', gridTemplateColumns: '1fr 1fr' }}>
82
+ <div className="field"><input className="input-line" placeholder="搜术名…" value={search} onChange={e => setSearch(e.target.value)} /></div>
83
+ <div className="field"><SongSelect value={artFilter} options={[{ value: 'all', label: '全部九术' }, ...['bazi', 'ziwei', 'qimen', 'meihua', 'liuyao', 'liuren', 'xiaoliuren', 'astrology', 'tarot'].map(a => ({ value: a, label: artById(a)?.name || a }))]} onChange={setArtFilter} /></div>
84
+ </div>
85
+
86
+ {filtered.length === 0 ? (
87
+ <div className="result-placeholder" style={{ marginTop: 'var(--sp-5)' }}>
88
+ <div className="glyph">痕</div>
89
+ <p>{records.length ? '无契合之占 · 请易词再寻' : '尚无占问记录 · 诚心一问,方始留痕'}</p>
90
+ </div>
91
+ ) : (
92
+ <div className="mt-4" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(300px,1fr))', gap: 'var(--sp-3)' }}>
93
+ {filtered.map(r => {
94
+ const art = artById(r.artId);
95
+ const resultText = summarize(r.result);
96
+ return (
97
+ <div className="result-card" key={r.id} style={{ marginBottom: 0, display: 'flex', flexDirection: 'column' }}>
98
+ <h3 style={{ fontSize: '1rem' }}>{art?.name || r.artId}</h3>
99
+ <div className="tiny muted">{formatTime(r.createdAt)}</div>
100
+ <p className="result-text" style={{ flex: 1 }}>{resultText}</p>
101
+ <div style={{ display: 'flex', gap: '.6rem', marginTop: 'var(--sp-1)' }}>
102
+ <Link className="pill cool" to={'/history/' + r.id}>回 看</Link>
103
+ <button className="pill hot" onClick={() => { deleteRecord(r.id); setTick(t => t + 1); }}>删 除</button>
104
+ </div>
105
+ </div>
106
+ );
107
+ })}
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ }
113
+
114
+ // 从结果中提取一行摘要(按术别取关键字段)
115
+ function summarize(result: unknown): string {
116
+ const r = result as Record<string, any>;
117
+ if (!r) return '(结果不可读)';
118
+ if (r.yearGZ) return `${r.yearGZ}年 ${r.monthGZ}月 ${r.dayGZ}日 ${r.hourGZ}时 · ${r.strength || ''}`;
119
+ if (r.benGua) return `本卦${r.benGua.name} · ${r.bianGua ? '变' + r.bianGua.name : ''} · ${r.shengke || ''}`;
120
+ if (r.ju) return `${r.jqName} · ${r.yin ? '阴' : '阳'}遁${r.ju}局 · ${r.dayGZ}日`;
121
+ if (r.name && r.detail) return `${r.name} · ${r.detail.ji || ''} · ${r.detail.wx || ''}`;
122
+ if (r.jiang) return `${r.jiang}将加时 · 四课${r.ke1}/${r.ke2}/${r.ke3}/${r.ke4}`;
123
+ if (r.ming !== undefined) return `命宫@${r.ming} · ${r.juName || ''} · 紫微@${r.zwPos}`;
124
+ if (Array.isArray(r) && r[0] && r[0].name) return `${r.length} 张牌:${r.map((c: any) => c.name).join('、')}`;
125
+ if (r.planets) return `太阳落${signOf(r.sun)} · 月亮落${signOf(r.moon)}`;
126
+ if (r.yin !== undefined) return `${r.jqName || ''} · ${r.yin ? '阴' : '阳'}遁${r.ju}局`;
127
+ return JSON.stringify(r).slice(0, 40);
128
+ }
129
+
130
+ function signOf(lon: number): string {
131
+ const signs = ['白羊', '金牛', '双子', '巨蟹', '狮子', '处女', '天秤', '天蝎', '射手', '摩羯', '水瓶', '双鱼'];
132
+ return signs[Math.floor(((lon % 360) + 360) % 360 / 30)];
133
+ }
@@ -0,0 +1,179 @@
1
+ import { Link } from 'react-router-dom';
2
+ import { Sparkles, Star, CircleDot, Calendar, Moon, Sun } from 'lucide-react';
3
+ import { getCurrentZodiac, getMoonPhase, getDailyFortune } from '@/utils/astrologyCalc';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ const features = [
7
+ {
8
+ title: '塔罗占卜',
9
+ description: '78张神秘牌面,揭示命运的多维真相',
10
+ icon: Sparkles,
11
+ path: '/tarot',
12
+ color: 'from-ancient/20 to-mystic/20',
13
+ symbol: '✦',
14
+ },
15
+ {
16
+ title: '紫微斗数',
17
+ description: '东方星命之学,解析人生格局',
18
+ icon: Star,
19
+ path: '/ziwei',
20
+ color: 'from-mystic/20 to-crimson/20',
21
+ symbol: '☆',
22
+ },
23
+ {
24
+ title: '四柱八字',
25
+ description: '天干地支之奥秘,洞察命运起伏',
26
+ icon: Calendar,
27
+ path: '/bazi',
28
+ color: 'from-crimson/20 to-ancient/20',
29
+ symbol: '☯',
30
+ },
31
+ {
32
+ title: '星盘占星',
33
+ description: '西方占星之术,解读行星语言',
34
+ icon: CircleDot,
35
+ path: '/astrology',
36
+ color: 'from-ancient/20 to-crimson/20',
37
+ symbol: '◎',
38
+ },
39
+ ];
40
+
41
+ export default function Home() {
42
+ const [fortune, setFortune] = useState('');
43
+ const [zodiac, setZodiac] = useState('');
44
+ const [moonPhase, setMoonPhase] = useState('');
45
+
46
+ useEffect(() => {
47
+ setFortune(getDailyFortune());
48
+ setZodiac(getCurrentZodiac());
49
+ setMoonPhase(getMoonPhase());
50
+ }, []);
51
+
52
+ return (
53
+ <div className="relative min-h-screen">
54
+ {/* Hero Section */}
55
+ <section className="relative min-h-screen flex items-center justify-center px-4">
56
+ <div className="text-center max-w-4xl mx-auto">
57
+ <div className="mb-8">
58
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-ancient/30 bg-ancient/5 mb-6">
59
+ <Moon className="w-4 h-4 text-ancient" />
60
+ <span className="text-xs text-ancient/80 tracking-widest">探索宇宙的智慧</span>
61
+ <Sun className="w-4 h-4 text-ancient" />
62
+ </div>
63
+ </div>
64
+
65
+ <h1 className="font-display text-5xl md:text-7xl lg:text-8xl text-ancient mb-6 tracking-wider">
66
+ <span className="shimmer-text">玄冥占星</span>
67
+ </h1>
68
+
69
+ <p className="text-stardust/60 text-lg md:text-xl max-w-2xl mx-auto mb-4 leading-relaxed">
70
+ 东西方玄学体系的交汇之地
71
+ </p>
72
+ <p className="text-stardust/40 text-sm md:text-base max-w-xl mx-auto mb-12">
73
+ 塔罗 · 紫微 · 四柱 · 星盘 · 组合占卜
74
+ </p>
75
+
76
+ <div className="flex flex-col sm:flex-row gap-4 justify-center">
77
+ <Link
78
+ to="/tarot"
79
+ className="inline-flex items-center justify-center gap-2 px-8 py-3 border border-ancient/50 text-ancient hover:bg-ancient/10 hover:border-ancient transition-all duration-500 rounded-sm tracking-wider text-sm"
80
+ >
81
+ <Sparkles className="w-4 h-4" />
82
+ 开始占卜
83
+ </Link>
84
+ <Link
85
+ to="/history"
86
+ className="inline-flex items-center justify-center gap-2 px-8 py-3 border border-stardust/20 text-stardust/70 hover:border-stardust/50 hover:text-stardust transition-all duration-500 rounded-sm tracking-wider text-sm"
87
+ >
88
+ 查看记录
89
+ </Link>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Scroll indicator */}
94
+ <div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
95
+ <div className="w-px h-12 bg-gradient-to-b from-ancient/50 to-transparent" />
96
+ </div>
97
+ </section>
98
+
99
+ {/* Daily Fortune */}
100
+ <section className="relative py-16 px-4">
101
+ <div className="max-w-4xl mx-auto">
102
+ <div className="glass-panel p-6 md:p-8 relative overflow-hidden">
103
+ <div className="absolute top-0 right-0 w-32 h-32 bg-ancient/5 rounded-full blur-3xl" />
104
+ <div className="absolute bottom-0 left-0 w-24 h-24 bg-mystic/5 rounded-full blur-3xl" />
105
+
106
+ <div className="relative">
107
+ <div className="flex items-center gap-3 mb-4">
108
+ <Sun className="w-5 h-5 text-ancient" />
109
+ <h2 className="font-display text-xl text-ancient tracking-wider">今日星象</h2>
110
+ </div>
111
+
112
+ <div className="flex flex-wrap gap-4 mb-4 text-sm">
113
+ <span className="px-3 py-1 rounded-full border border-ancient/20 text-ancient/70">
114
+ 太阳星座:{zodiac}
115
+ </span>
116
+ <span className="px-3 py-1 rounded-full border border-mystic/20 text-mystic/70">
117
+ 月相:{moonPhase}
118
+ </span>
119
+ </div>
120
+
121
+ <p className="text-stardust/70 leading-relaxed text-sm md:text-base">
122
+ {fortune}
123
+ </p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </section>
128
+
129
+ {/* Features Grid */}
130
+ <section className="relative py-16 px-4">
131
+ <div className="max-w-6xl mx-auto">
132
+ <div className="text-center mb-12">
133
+ <h2 className="font-display text-3xl text-ancient tracking-wider mb-3">占卜之道</h2>
134
+ <p className="text-stardust/50 text-sm">选择适合你的探索方式</p>
135
+ </div>
136
+
137
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
138
+ {features.map((feature) => (
139
+ <Link
140
+ key={feature.path}
141
+ to={feature.path}
142
+ className="group relative"
143
+ >
144
+ <div className={`absolute inset-0 bg-gradient-to-br ${feature.color} rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-500`} />
145
+ <div className="relative glass-panel p-6 h-full hover:border-ancient/50 transition-all duration-500">
146
+ <div className="flex items-center justify-between mb-4">
147
+ <feature.icon className="w-6 h-6 text-ancient/70 group-hover:text-ancient transition-colors" />
148
+ <span className="text-ancient/30 text-lg group-hover:text-ancient/60 transition-colors">
149
+ {feature.symbol}
150
+ </span>
151
+ </div>
152
+ <h3 className="font-display text-lg text-stardust group-hover:text-ancient transition-colors mb-2">
153
+ {feature.title}
154
+ </h3>
155
+ <p className="text-stardust/50 text-sm leading-relaxed">
156
+ {feature.description}
157
+ </p>
158
+ </div>
159
+ </Link>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ </section>
164
+
165
+ {/* Footer */}
166
+ <footer className="relative py-12 px-4 border-t border-ancient/10">
167
+ <div className="max-w-4xl mx-auto text-center">
168
+ <div className="flex items-center justify-center gap-2 mb-4">
169
+ <Sparkles className="w-4 h-4 text-ancient/40" />
170
+ <span className="font-display text-sm text-ancient/40 tracking-wider">玄冥占星</span>
171
+ </div>
172
+ <p className="text-stardust/30 text-xs">
173
+ 塔罗不是预言,而是镜子 · 命运掌握在你手中
174
+ </p>
175
+ </div>
176
+ </footer>
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,174 @@
1
+ // 首页:Hero + 今日黄历/星运(占位,2.5 步接入数据)+ 九术册页 + 观微三法
2
+ import { useEffect } from 'react';
3
+ import { useLocation, Link } from 'react-router-dom';
4
+ import { ARTS } from '@/data/arts';
5
+ import { dailyAlmanac, dailySky, dailyFortune } from '@core/engine/daily';
6
+ import { currentUser } from '@/utils/userStore';
7
+ import { astrologyCalc } from '@core/engine/astrology';
8
+ import { ZODIAC } from '@core/data/zodiac';
9
+ import { useState } from 'react';
10
+ import SealButton from '@/components/SealButton';
11
+ import HomeSideAnchor from '@/components/HomeSideAnchor';
12
+
13
+ export default function HomePage() {
14
+ const location = useLocation();
15
+ const [now] = useState(() => new Date());
16
+ const almanac = dailyAlmanac(now);
17
+ const sky = dailySky(now);
18
+ // 登录用户 → 按出生档案(日期/时辰/地点)算个人太阳星座 → 个性化运势
19
+ const user = currentUser();
20
+ const fortune = useUserFortune(now, user);
21
+
22
+ // 从导航「九术/法度」菜单进入时,滚动到对应锚点
23
+ useEffect(() => {
24
+ const target = (location.state as { scrollTo?: string } | null)?.scrollTo;
25
+ if (target) {
26
+ requestAnimationFrame(() => {
27
+ document.getElementById(target)?.scrollIntoView({ behavior: 'smooth' });
28
+ });
29
+ // 消费一次即清除,避免返回时重复滚动
30
+ window.history.replaceState({}, '');
31
+ }
32
+ }, [location.state]);
33
+
34
+ return (
35
+ <>
36
+ {/* ============ HERO ============ */}
37
+ <section className="hero" style={{ position: 'relative' }}>
38
+ <div className="mist" aria-hidden="true" />
39
+ <div className="wrap" style={{ position: 'relative', zIndex: 1 }}>
40
+ <p className="hero-kicker fade-up">纸墨玄境 · 玄学九术</p>
41
+ <h1 className="hero-title fade-up delay-1">
42
+ 观<span className="accent">微</span>知著
43
+ </h1>
44
+ <p className="hero-line fade-up delay-2">
45
+ 以 <em>四柱</em> 参命途,以 <em>斗数</em> 观星躔,<br />
46
+ 以 <em>遁甲</em> 演天机,以 <em>卦象</em> 问本心。
47
+ </p>
48
+ <p className="hero-desc fade-up delay-2">
49
+ 集八字、紫微、奇门、梅花、六爻、大六壬、小六壬、星盘、塔罗九术于一堂。<br />
50
+ 静气凝神,焚香默坐,方得片刻清明。
51
+ </p>
52
+ <div className="hero-actions fade-up delay-3">
53
+ <SealButton to="/" onClick={() => document.getElementById('home-arts')?.scrollIntoView({ behavior: 'smooth' })}>
54
+ 入九术之门
55
+ </SealButton>
56
+ <SealButton to="/art/bazi" ghost>先问八字</SealButton>
57
+ </div>
58
+ <ScrollHint />
59
+ <div className="hero-vert" aria-hidden="true">
60
+ <span>静以观微</span>
61
+ <span>虚以纳象</span>
62
+ <span>诚以问占</span>
63
+ </div>
64
+ </div>
65
+ </section>
66
+
67
+ {/* ============ 今日黄历 / 今日星运 ============ */}
68
+ <section className="wrap" id="today" style={{ scrollMarginTop: '88px', paddingBottom: 'var(--sp-6)' }}>
69
+ <div className="today-grid">
70
+ <div className="result-card">
71
+ <h3>今日黄历</h3>
72
+ <div className="result-text">
73
+ <p><strong>{almanac.lunarText}</strong> · {almanac.ganzhi}日 · 生肖{almanac.shengxiao}</p>
74
+ <p>{almanac.jieQi ? `今日节气:<strong>${almanac.jieQi}</strong> · ` : ''}月相 <strong>{almanac.yueXiang}</strong> · 冲{almanac.chong} 煞{almanac.sha}</p>
75
+ <p style={{ marginTop: '.4rem' }}>宜:{almanac.yi.length ? almanac.yi.map(y => <span key={y} className="pill cool">{y}</span>) : '—'}</p>
76
+ <p>忌:{almanac.ji.length ? almanac.ji.map(j => <span key={j} className="pill hot">{j}</span>) : '—'}</p>
77
+ </div>
78
+ </div>
79
+ <div className="result-card">
80
+ <h3>今日星运</h3>
81
+ <div className="result-text">
82
+ <p>太阳落 <strong>{sky.sunSign}</strong> · 月亮落 <strong>{sky.moonSign}</strong> · 月相 {sky.yueXiang}</p>
83
+ <p className="tiny muted" style={{ marginTop: '.3rem', lineHeight: 2 }}>{sky.planets.slice(0, 7).map(pl => `${pl.name}·${pl.sign}`).join(' ')}</p>
84
+ <p style={{ marginTop: '.4rem', color: 'var(--ink)' }}><strong>{fortune.sign}运势:</strong>{fortune.text}</p>
85
+ <p className="tiny muted">{fortune.personalized ? '依馆中生辰档案而示。' : '入馆录生辰档案,星运可依君而示。'}</p>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </section>
90
+
91
+ <HomeSideAnchor />
92
+ {/* ============ 九术册页 ============ */}
93
+ <div id="home-arts" style={{ scrollMarginTop: '88px', paddingTop: 'var(--sp-2)' }}>
94
+ <div className="wrap">
95
+ <div className="section-head fade-up">
96
+ <div className="section-eyebrow">九术总览 · Nine Arts</div>
97
+ <h2 className="section-title">九术册页</h2>
98
+ <p className="section-note">法无高下,术有专攻。择其所应,诚心则灵。</p>
99
+ </div>
100
+ <div className="arts-grid">
101
+ {ARTS.map((a, i) => (
102
+ <Link key={a.id} className="art-card" to={`/art/${a.id}`} role="button" tabIndex={0}>
103
+ <div className="art-num">{a.num} · {a.tag}</div>
104
+ <div className="art-symbol" aria-hidden="true">{a.glyph}</div>
105
+ <div className="art-name">{a.name}</div>
106
+ <div className="art-poem">{a.poem}</div>
107
+ <div className="art-enter">入 观 · {String(i + 1).padStart(2, '0')}</div>
108
+ </Link>
109
+ ))}
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ {/* ============ 观微三法 ============ */}
115
+ <div className="about" id="home-fa" style={{ scrollMarginTop: '76px', marginTop: 'var(--sp-7)', padding: 'var(--sp-7) 0' }}>
116
+ <div className="wrap">
117
+ <div className="section-head">
118
+ <div className="section-eyebrow">占问之道 · Ethos</div>
119
+ <h2 className="section-title">观微三法</h2>
120
+ </div>
121
+ <div className="about-grid">
122
+ <div className="about-item">
123
+ <h3>静</h3>
124
+ <p>占问先定心。宋人焚香默坐,去浮躁而见真。凡问卜,宜独处一室,屏息凝神,一念一事,方得应验。</p>
125
+ </div>
126
+ <div className="about-item">
127
+ <h3>诚</h3>
128
+ <p>《易》曰:「匪我求童蒙,童蒙求我。」心诚则气聚,气聚则象显。同问不三占,三占必有疑,疑则不验。</p>
129
+ </div>
130
+ <div className="about-item">
131
+ <h3>悟</h3>
132
+ <p>术者,渡舟也,非彼岸也。卦象所指,是镜非命。观照自身,反求诸己,方是问道正途。</p>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </>
138
+ );
139
+ }
140
+ // 登录用户:按出生日期算太阳星座(真太阳时校正时辰由档案时辰决定)
141
+ function useUserFortune(now: Date, user: ReturnType<typeof currentUser>) {
142
+ if (!user) return dailyFortune(now);
143
+ try {
144
+ const [y, m, d] = user.profile.birthDate.split('-').map(Number);
145
+ const [bh, bm] = (user.profile.birthTime || '12:00').split(':').map(Number);
146
+ const r = astrologyCalc(y, m, d, bh || 0, bm || 0, user.profile.location?.lng, user.profile.location?.lat);
147
+ const sign = ZODIAC[Math.floor(r.sun / 30)][0] + '座';
148
+ return dailyFortune(now, sign);
149
+ } catch {
150
+ return dailyFortune(now);
151
+ }
152
+ }
153
+ // 首屏滚动引导:仅在首屏(Hero 可见)时显示,下滑渐隐,回顶重现
154
+ function ScrollHint() {
155
+ const [visible, setVisible] = useState(true);
156
+ useEffect(() => {
157
+ const onScroll = () => {
158
+ const hero = document.querySelector('.hero');
159
+ if (!hero) return;
160
+ const rect = hero.getBoundingClientRect();
161
+ // 首屏判定:hero 底部仍在下半屏内
162
+ setVisible(rect.bottom > window.innerHeight * 0.55);
163
+ };
164
+ window.addEventListener('scroll', onScroll, { passive: true });
165
+ onScroll();
166
+ return () => window.removeEventListener('scroll', onScroll);
167
+ }, []);
168
+ return (
169
+ <div className={'scroll-hint inline-hint' + (visible ? '' : ' hidden')} aria-hidden="true">
170
+ <span>下滑 · 观九术</span>
171
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M19 12l-7 7-7-7" /></svg>
172
+ </div>
173
+ );
174
+ }