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,86 @@
1
+ // 奇门遁甲:起局面板 + 九宫盘渲染
2
+ import { useState } from 'react';
3
+ import { qimenCalc } from '@core/engine/qimen';
4
+ import type { QimenResult } from '@core/types';
5
+ import { ResultCard } from '@/components/ResultCard';
6
+ import SongSelect from '@/components/SongSelect';
7
+
8
+ const USES = ['择时出行', '谋事求财', '讼事纠纷', '考试功名', '婚恋姻缘', '疾病求医', '其他'];
9
+ const QM_POS: Record<number, [number, number]> = { 1: [0, 2], 8: [1, 2], 3: [2, 2], 4: [2, 1], 5: [1, 1], 9: [2, 0], 2: [0, 0], 7: [1, 0], 6: [0, 1] };
10
+ const QM_NAME: Record<number, string> = { 1: '坎', 2: '坤', 3: '震', 4: '巽', 5: '中', 6: '乾', 7: '兑', 8: '艮', 9: '离' };
11
+
12
+ function nowLocal() {
13
+ const d = new Date();
14
+ const p = (n: number) => String(n).padStart(2, '0');
15
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
16
+ }
17
+
18
+ interface PanelProps { onDivine: (inputs: unknown, profile?: unknown, question?: string) => void; }
19
+
20
+ export function QimenPanel({ onDivine }: PanelProps) {
21
+ const [dt, setDt] = useState(nowLocal());
22
+ const [use, setUse] = useState('择时出行');
23
+ const [question, setQuestion] = useState('');
24
+ const go = () => onDivine({ datetime: dt }, undefined, question.trim());
25
+ return (
26
+ <>
27
+ <div className="field"><label htmlFor="qm-datetime">用事时刻(公历)</label>
28
+ <input className="input-line" type="datetime-local" id="qm-datetime" value={dt} onChange={e => setDt(e.target.value)} /></div>
29
+ <div className="field"><label htmlFor="qm-use">所问之属</label>
30
+ <SongSelect id="qm-use" value={use} options={USES.map(u => ({ value: u, label: u }))} onChange={setUse} />
31
+ </div>
32
+ <div className="field"><label htmlFor="q-input">所问之事</label>
33
+ <input className="input-line" id="q-input" placeholder="默念所问,书之于此…" maxLength={60} value={question} onChange={e => setQuestion(e.target.value)} />
34
+ </div>
35
+ <button className="btn-divine" onClick={go}>起 局<span className="small">定时 · 定局 · 布三奇六仪</span></button>
36
+ <p className="hint" style={{ marginTop: '.8rem' }}>节气取精确万年历定阴阳遁,三候取局,九宫布盘。</p>
37
+ </>
38
+ );
39
+ }
40
+
41
+ export function QimenResultView({ data }: { data: QimenResult }) {
42
+ const r = data;
43
+ return (
44
+ <>
45
+ <ResultCard title={`${r.jqName} · ${r.yin ? '阴遁' : '阳遁'}${r.ju}局 · ${r.dayGZ}日 ${r.hourGZ}时`}>
46
+ <p>旬首 <strong>{r.xunshouName}</strong>({r.xunShou}),值符 <strong>{r.zfStar}</strong>,值使 <strong>{r.zsMen}</strong>。</p>
47
+ <p>三奇六仪以 {r.yin ? '逆' : '顺'}布于九宫,{r.yin ? '阴遁主静,宜守' : '阳遁主动,宜进'}。用神所临之宫,即事之机枢。</p>
48
+ </ResultCard>
49
+ <div className="result-card">
50
+ <h3>九宫盘</h3>
51
+ <div className="qimen-grid" style={{ marginTop: '1rem' }}>
52
+ {[1, 8, 3, 4, 9, 2, 7, 6, 5].map(p => {
53
+ const cell = r.pan[p];
54
+ const [c, rw] = QM_POS[p];
55
+ return (
56
+ <div className="qm-cell" key={p} style={{ gridColumn: c + 1, gridRow: rw + 1 }}>
57
+ <div className="qm-name">{QM_NAME[p]}{p === 5 ? ' · 中' : ''}</div>
58
+ <div className="qm-items">{p === 5 ? '中五寄坤' : `${cell.yi} · ${cell.men}门`}</div>
59
+ <div className="qm-star">{p === 5 ? '天禽' : cell.star}</div>
60
+ </div>
61
+ );
62
+ })}
63
+ </div>
64
+ </div>
65
+ <ResultCard title="用事之示">
66
+ <p>九宫之中,值符 {r.zfStar} 所在之{QM_NAME[r.zfPalace]}宫为发端;{r.zsMen}门为使,主行事之出入。择时用事,宜察 {r.yin ? '开、休、生' : '景、惊、伤'} 之吉凶(简示)。</p>
67
+ <p className="mt-2"><strong>用神参详:</strong>{qimenAdvice(r)}</p>
68
+ </ResultCard>
69
+ </>
70
+ );
71
+ }
72
+ // 奇门用神参详(值符宫/值使门/门星组合,现代语言)
73
+ function qimenAdvice(r: any): string {
74
+ const dirMap: Record<number, string> = { 1: '北', 2: '西南', 3: '东', 4: '东南', 5: '中', 6: '西北', 7: '西', 8: '东北', 9: '南' };
75
+ const parts: string[] = [];
76
+ parts.push('值符' + r.zfStar + '落' + QM_NAME[r.zfPalace] + '宫(' + dirMap[r.zfPalace] + '方),为当下气场之枢纽,谋事可借' + dirMap[r.zfPalace] + '方之势。');
77
+ const door: Record<string, string> = {
78
+ 开: '开门吉门,主动通达,宜开创进取', 休: '休门吉门,主修养安顿,宜休整待机',
79
+ 生: '生门吉门,主生机财源,宜经营求财', 伤: '伤门凶门,主损伤争斗,宜避其锋',
80
+ 杜: '杜门平门,主闭塞隐藏,宜守不宜攻', 景: '景门平门,主文书声名,宜宣传谋名',
81
+ 死: '死门凶门,主停滞死寂,宜止不宜行', 惊: '惊门凶门,主惊恐口舌,宜慎言防变',
82
+ };
83
+ parts.push('值使' + r.zsMen + '门,' + (door[r.zsMen] || '主行事出入') + '。');
84
+ parts.push(r.yin ? '阴遁之局,气机内收,宜守成蓄势,谋定后动。' : '阳遁之局,气机外放,宜主动作为,顺势而进。');
85
+ return parts.join('');
86
+ }
@@ -0,0 +1,110 @@
1
+ // 塔罗:问镜面板(牌阵选择)+ 扇形翻牌(未翻开不示牌名)+ 牌阵之示
2
+ import { useState } from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ import { tarotDraw, SUIT } from '@core/engine/tarot';
5
+ import { allTarotSpreads, type TarotSpread } from '@core/data/tarotSpreads';
6
+ import type { TarotCardData } from '@core/types';
7
+ import { ResultCard } from '@/components/ResultCard';
8
+ import SongSelect from '@/components/SongSelect';
9
+
10
+ interface PanelProps { onDivine: (inputs: unknown, profile?: unknown, question?: string) => void; }
11
+
12
+ export function TarotPanel({ onDivine }: PanelProps) {
13
+ const [q, setQ] = useState('');
14
+ const [qType, setQType] = useState('other');
15
+ const spreads = allTarotSpreads();
16
+ const [spreadId, setSpreadId] = useState('three');
17
+ const spread = spreads.find(s => s.id === spreadId) || spreads[0];
18
+ const go = () => onDivine({ spread: spread.id, n: spread.positions.length }, undefined, q.trim());
19
+ return (
20
+ <>
21
+ <p className="small muted" style={{ marginTop: '.6rem', lineHeight: 2 }}>静思所问,心中默念。洗牌之后,凭直觉取牌。</p>
22
+ <div className="field"><label htmlFor="tarot-qtype">所问之属</label>
23
+ <SongSelect id="tarot-qtype" value={qType} options={[{ value: 'love', label: '情感姻缘' }, { value: 'career', label: '事业学业' }, { value: 'wealth', label: '财运投资' }, { value: 'health', label: '健康' }, { value: 'travel', label: '出行方位' }, { value: 'lost', label: '寻物' }, { value: 'decision', label: '抉择决策' }, { value: 'other', label: '其他' }]} onChange={setQType} />
24
+ </div>
25
+ <div className="field"><label htmlFor="q-input">所问之事</label>
26
+ <input className="input-line" id="q-input" placeholder="默念所问,书之于此…" maxLength={60} value={q} onChange={e => setQ(e.target.value)} /></div>
27
+ <div className="field"><label htmlFor="tarot-spread">牌阵</label>
28
+ <SongSelect id="tarot-spread" value={spreadId} options={spreads.map(s => ({ value: s.id, label: s.name + ' · ' + s.positions.length + '张' }))} onChange={setSpreadId} />
29
+ <span className="hint">{spread.description}</span>
30
+ </div>
31
+ <button className="btn-divine" onClick={go}>洗 牌 抽 牌<span className="small">静心默问 · 凭心取牌</span></button>
32
+ <div className="mt-2" style={{ textAlign: 'center' }}>
33
+ <Link className="tiny muted" to="/spread-editor" style={{ letterSpacing: '.15em' }}>自 创 牌 阵 ›</Link>
34
+ </div>
35
+ </>
36
+ );
37
+ }
38
+
39
+ interface DrawResult {
40
+ spread: TarotSpread;
41
+ cards: TarotCardData[];
42
+ }
43
+
44
+ export function TarotResult({ data }: { data: DrawResult }) {
45
+ const { spread, cards } = data;
46
+ const [revealed, setRevealed] = useState<boolean[]>(cards.map(() => false));
47
+ const flip = (i: number) => setRevealed(prev => prev.map((v, j) => j === i ? true : v));
48
+ const allUp = revealed.every(Boolean);
49
+ return (
50
+ <>
51
+ <ResultCard title={`洗牌 · 抽牌 · 翻牌 · ${spread.name}`}>
52
+ <p>{spread.description} —— 凭心取 {cards.length} 张,点击牌面翻启。</p>
53
+ </ResultCard>
54
+ <div className="tarot-stage">
55
+ <div className="tarot-fan" style={{ flexWrap: cards.length > 5 ? 'wrap' : 'nowrap', maxWidth: cards.length > 5 ? 520 : undefined, gap: cards.length > 5 ? '1.2rem' : '2px' }}>
56
+ {cards.map((c, i) => (
57
+ <div className="tarot-slot" key={i}>
58
+ <button
59
+ className={"tarot-card " + (revealed[i] ? 'revealed picked' : 'face-down')}
60
+ onClick={() => flip(i)}
61
+ aria-label={"翻牌 " + c.name}
62
+ >
63
+ <div className="card-inner">
64
+ <span className="en">{c.en}</span>
65
+ <span className="sigil">{c.major ? (c.num ?? '') : (c.suit ? c.suit[0] : '')}</span>
66
+ <span className="cn">{c.name}</span>
67
+ <span className="en">{c.reversed ? '逆位' : '正位'}</span>
68
+ </div>
69
+ </button>
70
+ <span className="slot-label">{spread.positions[i]?.name || ('位' + (i + 1))}</span>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ </div>
75
+ <ResultCard title="牌阵之示">
76
+ {cards.map((c, i) => {
77
+ const pos = spread.positions[i];
78
+ // 未翻开:只示位置,不示牌名
79
+ if (!revealed[i]) {
80
+ return <p key={i} className="muted"><strong>{pos?.name || ('位' + (i + 1))}</strong> —— <span className="tiny">牌未翻启,静候君手</span></p>;
81
+ }
82
+ return (
83
+ <p key={i}>
84
+ <strong>{pos?.name || ('位' + (i + 1))}</strong> · {c.name}({c.reversed ? '逆位' : '正位'}):{c.reversed ? c.rev : c.up}{c.suit ? ` · ${SUIT[c.suit].e} · ${SUIT[c.suit].mean}` : ''}
85
+ </p>
86
+ );
87
+ })}
88
+ {allUp && (
89
+ <>
90
+ <p className="mt-2"><strong>全阵参详:</strong>{tarotSummary(cards, spread)}</p>
91
+ <p>塔罗非断命之器,乃照心之镜。观照自身,反求诸己。</p>
92
+ </>
93
+ )}
94
+ </ResultCard>
95
+ </>
96
+ );
97
+ }
98
+ // 塔罗全阵参详(正逆位比例 + 元素侧重,现代语言)
99
+ function tarotSummary(cards: any[], spread: any): string {
100
+ const rev = cards.filter(c => c.reversed).length;
101
+ const majors = cards.filter(c => c.major).length;
102
+ const parts: string[] = [];
103
+ if (majors === cards.length) parts.push('全为大阿卡纳,此事于你人生分量颇重,非寻常小事。');
104
+ else if (majors > 0) parts.push('大阿卡纳' + majors + '张,此事有' + (majors > cards.length / 2 ? '较深' : '一定') + '的命途意味,宜郑重观之。');
105
+ else parts.push('皆小阿卡纳,此事多系日常实务,脚踏实地即可。');
106
+ if (rev === 0) parts.push('诸牌皆正,能量顺畅外放,时机可进。');
107
+ else if (rev <= cards.length / 2) parts.push('逆位' + rev + '张,有' + (rev === 1 ? '一处' : '几处') + '内耗或阻滞,宜先理顺再行。');
108
+ else parts.push('逆位居多,此事内在功课为重,先安己心,外境自转。');
109
+ return parts.join('');
110
+ }
@@ -0,0 +1,93 @@
1
+ // 小六壬:掐指占时面板 + 掌诀/推演轨迹渲染
2
+ import { useState } from 'react';
3
+ import { xiaoliurenCalc } from '@core/engine/xiaoliuren';
4
+ import { XLR_ORDER, XLR } from '@core/data/xiaoliuren';
5
+ import { mod } from '@core/data/ganzhi';
6
+ import type { XiaoliurenResult } from '@core/types';
7
+ import { ResultCard } from '@/components/ResultCard';
8
+ import SongSelect from '@/components/SongSelect';
9
+
10
+ interface PanelProps { onDivine: (inputs: unknown, profile?: unknown, question?: string) => void; }
11
+
12
+ export function XiaoliurenPanel({ onDivine }: PanelProps) {
13
+ const [mode, setMode] = useState<'time' | 'num'>('time');
14
+ const [m, setM] = useState(1);
15
+ const [d, setD] = useState(1);
16
+ const [h, setH] = useState(1);
17
+ const [n1, setN1] = useState(3);
18
+ const [n2, setN2] = useState(5);
19
+ const [n3, setN3] = useState(7);
20
+ const [q, setQ] = useState('');
21
+ const [qType, setQType] = useState('other');
22
+ const go = () => onDivine({ mode, m, d, h, n1, n2, n3 }, undefined, q.trim());
23
+ return (
24
+ <>
25
+ <div className="field"><label htmlFor="xlr-mode">取数之法</label>
26
+ <SongSelect id="xlr-mode" value={mode} options={[{ value: 'time', label: '以当下农历月、日、时占' }, { value: 'num', label: '随取三数占(1–12)' }]} onChange={v => setMode(v as 'time' | 'num')} />
27
+ </div>
28
+ {mode === 'time' ? (
29
+ <div className="field-row">
30
+ <div className="field"><label htmlFor="xlr-m">月</label>
31
+ <SongSelect id="xlr-m" value={String(m)} options={Array.from({ length: 12 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) }))} onChange={v => setM(Number(v))} />
32
+ </div>
33
+ <div className="field"><label htmlFor="xlr-d">日</label>
34
+ <SongSelect id="xlr-d" value={String(d)} options={Array.from({ length: 30 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) }))} onChange={v => setD(Number(v))} />
35
+ </div>
36
+ </div>
37
+ ) : (
38
+ <div className="field-row">
39
+ <div className="field"><label htmlFor="xlr-n1">一数</label><input className="input-line" type="number" id="xlr-n1" min={1} max={12} value={n1} onChange={e => setN1(Number(e.target.value))} /></div>
40
+ <div className="field"><label htmlFor="xlr-n2">二数</label><input className="input-line" type="number" id="xlr-n2" min={1} max={12} value={n2} onChange={e => setN2(Number(e.target.value))} /></div>
41
+ </div>
42
+ )}
43
+ <div className="field"><label htmlFor="xlr-h">时(时辰序)</label>
44
+ <SongSelect id="xlr-h" value={String(h)} options={['子(1)', '丑(2)', '寅(3)', '卯(4)', '辰(5)', '巳(6)', '午(7)', '未(8)', '申(9)', '酉(10)', '戌(11)', '亥(12)'].map((t, i) => ({ value: String(i + 1), label: t }))} onChange={v => setH(Number(v))} />
45
+ </div>
46
+ {mode === 'num' && (
47
+ <div className="field"><label htmlFor="xlr-n3">三数</label><input className="input-line" type="number" id="xlr-n3" min={1} max={12} value={n3} onChange={e => setN3(Number(e.target.value))} /></div>
48
+ )}
49
+ <div className="field"><label htmlFor="xlr-qtype">所问之属</label>
50
+ <SongSelect id="xlr-qtype" value={qType} options={[{ value: 'love', label: '情感姻缘' }, { value: 'career', label: '事业学业' }, { value: 'wealth', label: '财运投资' }, { value: 'health', label: '健康' }, { value: 'travel', label: '出行方位' }, { value: 'lost', label: '寻物' }, { value: 'decision', label: '抉择决策' }, { value: 'other', label: '其他' }]} onChange={setQType} />
51
+ </div>
52
+ <div className="field"><label htmlFor="q-input">所问之事</label>
53
+ <input className="input-line" id="q-input" placeholder="默念所问,书之于此…" maxLength={60} value={q} onChange={e => setQ(e.target.value)} />
54
+ </div>
55
+ <button className="btn-divine" onClick={go}>掐 指 一 算<span className="small">大安起月 · 月上起日 · 日上起时</span></button>
56
+ </>
57
+ );
58
+ }
59
+
60
+ export function XiaoliurenResult({ data }: { data: XiaoliurenResult }) {
61
+ const r = data;
62
+ const jiClass = r.detail.ji === '吉' ? 'tag-cool' : 'tag-hot';
63
+ const step1 = mod(r.a - 1, 6);
64
+ const step2 = mod(step1 + (r.b - 1), 6);
65
+ return (
66
+ <>
67
+ <ResultCard title={`掌诀 · ${r.name}`}
68
+ cells={[
69
+ { k: '吉凶', v: r.detail.ji, big: true, cool: r.detail.ji === '吉', hot: r.detail.ji !== '吉' },
70
+ { k: '五行', v: r.detail.wx },
71
+ { k: '主数', v: r.detail.num },
72
+ { k: '方位', v: r.detail.dir },
73
+ ]}>
74
+ <p><span className={jiClass}><strong>{r.name}</strong></span> · {r.detail.poem}</p>
75
+ <p>「{r.detail.text}」</p>
76
+ </ResultCard>
77
+ <ResultCard title="推演轨迹">
78
+ <p>{r.a} 月 {r.b} 日 {r.c} 时占:大安起月 → 月{r.a}落 <strong>{XLR_ORDER[step1]}</strong> → 日{r.b}落 <strong>{XLR_ORDER[step2]}</strong> → 时{r.c}终落 <strong className={jiClass}>{r.name}</strong>。</p>
79
+ <p className="mt-2"><strong>断语参详:</strong>{xlrAdvice(r)}</p>
80
+ </ResultCard>
81
+ </>
82
+ );
83
+ }
84
+ // 小六壬断语参详(结合吉凶/五行/方位,现代语言)
85
+ function xlrAdvice(r: any): string {
86
+ const d = r.detail;
87
+ const parts: string[] = [];
88
+ parts.push('此占得' + r.name + ',为' + (d.ji === '吉' ? '吉占' : '凶占') + '。');
89
+ if (d.ji === '吉') parts.push('主数' + d.num + '、方位' + d.dir + ',谋事于' + d.dir + '向或' + d.num.replace(/、/g, '、') + '之数日时,多有应合。');
90
+ else parts.push('此占宜静不宜动,避' + d.dir + '向之事,慎口舌之争;事缓则圆,过此一时再图。');
91
+ parts.push('五行属' + d.wx + ',' + (d.wx === '火' ? '火性急,事可速决' : d.wx === '水' ? '水性流,事有迁延' : d.wx === '木' ? '木性生,事有转机' : d.wx === '金' ? '金性刚,事须决断' : '土性厚,事在积累') + '。');
92
+ return parts.join('');
93
+ }
@@ -0,0 +1,296 @@
1
+ // 紫微斗数:输入面板(干支年/农历月日时 + 地点经度校正)+ 紫微盘渲染
2
+ import { useState } from 'react';
3
+ import { ziweiCalc } from '@core/engine/ziwei';
4
+ import { ZW_STARS, ZW_STAR_MEAN, PALACE_NAMES } from '@core/data/ziwei';
5
+ import { interpretZiwei } from '@core/engine/ziweiInterpret';
6
+ import { ganZhiIndex, GAN, ZHI, NAYIN } from '@core/data/ganzhi';
7
+ import { timeToHourIndex, hourIndexLabel } from '@/data/shichen';
8
+ import TimeShichenInput from '@/components/TimeShichenInput';
9
+ import type { ZiweiResult, GeoLocation } from '@core/types';
10
+ import type { UserProfile } from '@/utils/userStore';
11
+ import { Solar } from 'lunar-typescript';
12
+ import DateInput from '@/components/DateInput';
13
+ import ProfilePicker from '@/components/ProfilePicker';
14
+ import { currentUser } from '@/utils/userStore';
15
+ import LocationPicker from '@/components/LocationPicker';
16
+ import SongSelect from '@/components/SongSelect';
17
+ import { ResultCard } from '@/components/ResultCard';
18
+ import { isChinaDSTDate } from '@/utils/dst';
19
+ import { ziweiTone } from '@/utils/panTone';
20
+ import LifeEventsInput, { type LifeEvent } from '@/components/LifeEventsInput';
21
+
22
+ const DIZHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
23
+ const mod = (a: number, n: number) => ((a % n) + n) % n;
24
+
25
+ // 干支年选项(1950-2010)
26
+ function ganzhiOptions() {
27
+ const out: { gz: string; year: number }[] = [];
28
+ for (let y = 1950; y <= 2010; y++) {
29
+ const i = ((y - 4) % 60 + 60) % 60;
30
+ out.push({ gz: GAN[i % 10] + ZHI[i % 12], year: y });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ interface PanelProps { onDivine: (inputs: unknown, profile?: UserProfile, question?: string, profileId?: string) => void; }
36
+
37
+ export function ZiweiPanel({ onDivine }: PanelProps) {
38
+ const p = currentUser()?.profile;
39
+ const [date, setDate] = useState(p?.birthDate || '1990-06-15');
40
+ const [birthTime, setBirthTime] = useState(p?.birthTime || '00:00');
41
+ const [hourUnknown, setHourUnknown] = useState(p?.birthTimeUnknown || false);
42
+ const [gender, setGender] = useState<'男' | '女'>(p?.gender || '男');
43
+ const [loc, setLoc] = useState<GeoLocation | null>(p?.location || null);
44
+ const [question, setQuestion] = useState('');
45
+ const [profileSrc, setProfileSrc] = useState<{ id: string; name: string } | null>(null);
46
+ const [lifeEvents, setLifeEvents] = useState<LifeEvent[]>(p?.lifeEvents || []);
47
+ const dst = isChinaDSTDate(date);
48
+
49
+ const [formMsg, setFormMsg] = useState('');
50
+ const go = () => {
51
+ const [yy, mm, dd] = date.split('-').map(Number);
52
+ if (!yy || !mm || !dd) { setFormMsg('⚠ 请先填写有效的出生日期'); return; }
53
+ if (new Date(yy, mm - 1, dd).getMonth() !== mm - 1) { setFormMsg('⚠ 出生日期不合法,请检查'); return; }
54
+ setFormMsg('');
55
+ // 公历 → 农历(万年历),紫微依农历排盘
56
+ let lm = 1, ld = 1, gz = '甲子', birthYear = yy;
57
+ try {
58
+ const lunar = Solar.fromYmd(yy, mm, dd).getLunar();
59
+ const rawMonth = lunar.getMonth();
60
+ ld = lunar.getDay();
61
+ // 闰月(lunar 返回负数):按 iztro fixLeap 规则,后半月算下月
62
+ lm = Math.abs(rawMonth) + (rawMonth < 0 && ld > 15 ? 1 : 0);
63
+ gz = lunar.getYearInGanZhi();
64
+ birthYear = lunar.getYear();
65
+ } catch { /* 非法日期回退默认 */ }
66
+ // 时辰未知:紫微以午时示例排盘(仅命局五行参考),档案标记未知供 AI 解读约束
67
+ const hourIdx = hourUnknown ? 6 : timeToHourIndex(birthTime);
68
+ const profile: UserProfile = { birthDate: date, birthTime: hourUnknown ? '' : birthTime, birthHourIndex: hourIdx, birthTimeUnknown: hourUnknown || undefined, gender, location: loc, lifeEvents: lifeEvents.length ? lifeEvents : undefined };
69
+ onDivine(
70
+ { ganzhi: gz, month: lm, day: ld, hour: hourIdx, time: hourUnknown ? undefined : birthTime, location: loc ?? undefined, gender, birthYear: yy, solarDate: [yy, mm, dd] },
71
+ profile,
72
+ question.trim(),
73
+ profileSrc?.id || 'main'
74
+ );
75
+ };
76
+
77
+ return (
78
+ <>
79
+ <ProfilePicker onPick={(p, src) => { setDate(p.birthDate); setBirthTime(p.birthTime || '00:00'); setHourUnknown(!!p.birthTimeUnknown); setGender(p.gender); setLoc(p.location); if (src) setProfileSrc(src); }} />
80
+ <div className="field"><label htmlFor="zw-date">出生日期(公历 / 农历,紫微依农历排盘)</label>
81
+ <DateInput id="zw-date" value={date} onChange={setDate} />
82
+ </div>
83
+ {dst && <p className="tag-hot" style={{ marginTop: '.3rem' }}>⚠ 该日期处于中国夏令时期间(1986-1991 年 4-9 月,钟表拨快 1 小时)——请按出生时钟表时间填写,排盘会自动回拨校正。</p>}
84
+
85
+ <div className="field"><label htmlFor="zw-gender">性别(定大限顺逆)</label>
86
+ <SongSelect id="zw-gender" value={gender} options={[{ value: '男', label: '男' }, { value: '女', label: '女' }]} onChange={v => setGender(v as '男' | '女')} />
87
+ </div>
88
+ <div className="field"><label htmlFor="zw-time">出生时刻</label>
89
+ {hourUnknown
90
+ ? <p className="hint" style={{ padding: '.45rem 0' }}>时辰未录——紫微暂以午时示例排盘(仅观命局五行),AI 解读不涉命宫/身宫/大限细节。</p>
91
+ : <TimeShichenInput id="zw-time" value={birthTime} onChange={setBirthTime} />}
92
+ </div>
93
+ <div className="field">
94
+ <label style={{ display: 'flex', alignItems: 'center', gap: '.45rem', cursor: 'pointer', fontSize: '.9rem', fontWeight: 400 }}>
95
+ <input type="checkbox" checked={hourUnknown} onChange={e => setHourUnknown(e.target.checked)} />
96
+ 时辰未知(未录)
97
+ </label>
98
+ </div>
99
+ {formMsg && <p className="tag-hot" style={{ marginTop: '.3rem' }}>{formMsg}</p>}
100
+ <div className="field">
101
+ <label>出生地点(时辰经度校正)</label>
102
+ <LocationPicker value={loc} onChange={setLoc} previewHourIndex={hourUnknown ? 6 : timeToHourIndex(birthTime)} />
103
+ {!loc && <p className="hint" style={{ marginTop: '.3rem' }}>未选地点:将按北京时间直接排盘(不做真太阳时校正)</p>}
104
+ </div>
105
+ <div className="field"><label htmlFor="q-input">所问之事(可选)</label>
106
+ <input className="input-line" id="q-input" placeholder="可书所问,AI 报告将由此而发…" maxLength={60} value={question} onChange={e => setQuestion(e.target.value)} />
107
+ </div>
108
+ <LifeEventsInput value={lifeEvents} onChange={setLifeEvents} />
109
+ <button className="btn-divine" onClick={go}>布 盘<span className="small">安命宫 · 定紫微 · 布主星</span></button>
110
+ <p className="hint" style={{ marginTop: '.8rem' }}>演示级简盘:十四主星入十二宫,辅曜从略。时辰未录时以午时示例布盘,命宫/身宫仅供参考。</p>
111
+ </>
112
+ );
113
+ }
114
+
115
+ // 4×4 网格布局(十二地支顺时针环布 + 中央中宫)
116
+ const GRID_POS: [string, number, number][] = [
117
+ ['寅', 0, 3], ['卯', 0, 2], ['辰', 0, 1], ['巳', 0, 0],
118
+ ['午', 1, 0], ['未', 2, 0], ['申', 3, 0], ['酉', 3, 1],
119
+ ['戌', 3, 2], ['亥', 3, 3], ['子', 2, 3], ['丑', 1, 3],
120
+ ];
121
+
122
+ export function ZiweiResult({ data }: { data: ZiweiResult }) {
123
+ const r = data;
124
+ const cellStars: Record<number, string[]> = {};
125
+ Object.keys(r.zwStars).forEach(s => {
126
+ const pos = r.zwStars[s];
127
+ if (!cellStars[pos]) cellStars[pos] = [];
128
+ cellStars[pos].push(s);
129
+ });
130
+ const cellFu: Record<number, string[]> = {};
131
+ Object.keys(r.fuStars || {}).forEach(s => {
132
+ const pos = r.fuStars[s];
133
+ if (!cellFu[pos]) cellFu[pos] = [];
134
+ cellFu[pos].push(s);
135
+ });
136
+ const sihuaMark: Record<number, string[]> = {};
137
+ (Object.keys(r.sihuaPos || {}) as Array<'lu' | 'quan' | 'ke' | 'ji'>).forEach(k => {
138
+ const pos = r.sihuaPos![k];
139
+ if (pos !== undefined) {
140
+ if (!sihuaMark[pos]) sihuaMark[pos] = [];
141
+ sihuaMark[pos].push(k === 'lu' ? '禄' : k === 'quan' ? '权' : k === 'ke' ? '科' : '忌');
142
+ }
143
+ });
144
+ const cells = GRID_POS.map(([zhi, c, rw]) => {
145
+ const pos = DIZHI.indexOf(zhi);
146
+ const stars = cellStars[pos] || [];
147
+ const fus = cellFu[pos] || [];
148
+ const marks = sihuaMark[pos] || [];
149
+ const palIdx = Object.keys(r.palaces).find(k => r.palaces[Number(k)] === pos);
150
+ const palName = palIdx !== undefined ? PALACE_NAMES[Number(palIdx)] : '';
151
+ const isMing = r.ming === pos;
152
+ const isShen = r.shen === pos;
153
+ return { zhi, c, rw, stars, fus, marks, palName, isMing, isShen, pos };
154
+ });
155
+ return (
156
+ <>
157
+ <ResultCard title={"命盘 · " + r.juName + ' · ' + r.nayin}>
158
+ <p>命宫安于 <strong>{DIZHI[r.ming]}</strong>{r.ming === r.zwPos ? '(紫微同宫)' : ''},紫微星落 <strong>{DIZHI[r.zwPos]}</strong> 宫。时辰经度校正:{hourIndexLabel(r.correctedHour)}。</p>
159
+ </ResultCard>
160
+ <div className="result-card stagger">
161
+ <h3>星曜布列</h3>
162
+ <div className="ziwei-grid" style={{ marginTop: '1rem' }}>
163
+ <div className="zw-cell center" style={{ gridColumn: '2/4', gridRow: '2/4' }}>
164
+ <div className="zw-name">观微</div><div className="tiny muted">紫微简盘</div><div className="tiny muted">{r.juName}</div>
165
+ </div>
166
+ {cells.sort((a, b) => a.rw === b.rw ? a.c - b.c : a.rw - b.rw).map(c => (
167
+ <div className={"zw-cell" + (c.isMing ? ' ming-cell' : '')} key={c.zhi} style={{ gridColumn: c.c + 1, gridRow: c.rw + 1 }}>
168
+ <div className="zw-name">
169
+ <span>{c.zhi} · {c.palName || '—'}</span>
170
+ {c.marks.map((m, i) => <span key={i} className="zw-extra" style={{ color: m === '忌' ? 'var(--cinnabar)' : 'var(--celadon-deep)' }}>{m}</span>)}
171
+ {c.isMing ? <span className="zw-extra" style={{ color: 'var(--cinnabar)' }}>命</span> : null}
172
+ {c.isShen ? <span className="zw-extra" style={{ color: 'var(--celadon-deep)' }}>身</span> : null}
173
+ </div>
174
+ <div className="zw-stars">{c.stars.map(s => {
175
+ const hot = ['紫微', '七杀', '破军', '贪狼', '廉贞'].includes(s);
176
+ const cool = ['天机', '天同', '天相', '天梁', '天府'].includes(s);
177
+ const b = (r.brightness || {})[s] || '';
178
+ return <span key={s} className={"star-name " + (hot ? 'star-hot' : cool ? 'star-cool' : '')}>{s}<em className="tiny" style={{ fontStyle: 'normal', opacity: .65 }}>{b}</em></span>;
179
+ })}{c.stars.length === 0 && <span className="muted">—</span>}</div>
180
+ {c.fus.length > 0 && <div className="tiny muted" style={{ marginTop: '.15rem', lineHeight: 1.5 }}>{c.fus.join('·')}</div>}
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </div>
185
+ <ResultCard title="命宫参详">
186
+ <p>{ziweiTone(r).mingNote}</p>
187
+ <p className="tiny muted">简盘仅列十四主星,辅曜从略;此参详为现代语言之概览。</p>
188
+ </ResultCard>
189
+ <ResultCard title="命盘基础">
190
+ <p>五行局 <strong>{r.juName}</strong>({r.nayin}),起运 <strong>{r.startAge} 岁</strong>,大限{' '}
191
+ <strong>{r.forward ? '顺行' : '逆行'}</strong>({r.nominalAge} 虚岁,行至第 {((r.curDayunIdx ?? 0) + 1)} 大限)。</p>
192
+ <p>命宫在<strong>{DIZHI[r.ming]}</strong>,身宫在<strong>{DIZHI[r.shen]}</strong>{r.shen === r.ming ? '(与命宫同宫)' : ''};紫微星落<strong>{DIZHI[r.zwPos]}</strong>宫。</p>
193
+ <p>生年四化:{r.sihua.lu}化禄、{r.sihua.quan}化权、{r.sihua.ke}化科、{r.sihua.ji}化忌({r.sihua.lu}化禄主财缘流通,化忌之星须防其暗伤)。</p>
194
+ <p className="tiny muted">大限起运岁数按五行局数计;四化为斗数断流年吉凶之枢。</p>
195
+ </ResultCard>
196
+ <ResultCard title="格局">
197
+ {r.geju.length > 0 ? (
198
+ <>
199
+ <p>{r.geju.map((g, i) => (
200
+ <span key={i}><strong className={g.ji === '吉' ? 'tag-cool' : g.ji === '凶' ? 'tag-hot' : ''}>{g.name}</strong>{g.ji === '吉' ? '(吉格)' : g.ji === '凶' ? '(凶格)' : '(平格)'} </span>
201
+ ))}</p>
202
+ {r.geju.map((g, i) => (
203
+ <p key={i} style={{ marginTop: '.5rem' }}><strong>{g.name}</strong>:{g.desc}<span className="tiny muted">({g.why})</span></p>
204
+ ))}
205
+ </>
206
+ ) : <p>本命未会明显格局,以主星庙陷与四化合参论之。</p>}
207
+ <p className="tiny muted" style={{ marginTop: '.5rem' }}>格局为星曜组合之定式:成格者一生趋向鲜明,破格者(煞忌冲破)则吉格亦减力。</p>
208
+ </ResultCard>
209
+ {r.dayun && r.dayun.length > 0 && (
210
+ <div className="result-card stagger">
211
+ <h3>大限行运</h3>
212
+ <div className="result-grid" style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(140px,1fr))' }}>
213
+ {r.dayun.map((d, i) => {
214
+ const palIdx = Object.keys(r.palaces).find(k => r.palaces[Number(k)] === d.palaceIdx);
215
+ const palName = palIdx !== undefined ? PALACE_NAMES[Number(palIdx)] : '';
216
+ const stars = Object.keys(r.zwStars).filter(s => r.zwStars[s] === d.palaceIdx);
217
+ const isCur = i === r.curDayunIdx;
218
+ return (
219
+ <div className="result-cell" key={i} style={isCur ? { borderColor: 'var(--cinnabar)', boxShadow: '0 0 0 1px rgba(165,64,45,.2)' } : undefined}>
220
+ <div className="k">{d.start}–{d.end} 岁{isCur ? ' · 今' : ''}</div>
221
+ <div className="v" style={isCur ? { color: 'var(--cinnabar)' } : undefined}>{palName}</div>
222
+ <div className="tiny muted">{stars.join('、') || '主星未临'}</div>
223
+ <div className="tiny muted">{dayunTone(stars)}</div>
224
+ </div>
225
+ );
226
+ })}
227
+ </div>
228
+ </div>
229
+ )}
230
+ <ResultCard title="流年之示">
231
+ <p>{r.nominalAge} 虚岁,流年命宫落 <strong>{r.liunianPalaceName}</strong>({DIZHI[r.liunianIdx ?? 0]}),流年主星:<strong>{r.liunianStars?.join('、') || '主星未临'}</strong>。</p>
232
+ <div className="mt-2">
233
+ {[1, 2, 3].map(offset => {
234
+ const pos = mod(r.ming + (r.nominalAge ?? 34) - 1 + offset, 12);
235
+ const stars = Object.keys(r.zwStars).filter(s => r.zwStars[s] === pos);
236
+ const pname = PALACE_NAMES[Number(Object.keys(r.palaces).find(k => r.palaces[Number(k)] === pos)) || 0];
237
+ return <p key={offset} className="tiny" style={{ lineHeight: 2 }}>来年{offset === 1 ? '' : offset + '年'} · {pname}宫({DIZHI[pos]})· 主星{stars.join('、') || '未临'} —— {futureTone(stars)}</p>;
238
+ })}
239
+ </div>
240
+ <p className="tiny muted">流年为当年太岁行运之要,主星吉凶与宫位庙陷须合参;简盘仅供参考。</p>
241
+ </ResultCard>
242
+ {interpretZiwei(r).map((ins, i) => {
243
+ if (ins.title === '十二宫要览') {
244
+ // 结构化排版:每宫一卡(宫名/地支/主星/主题)
245
+ const palaces = Object.keys(r.palaces).map(k => ({
246
+ name: PALACE_NAMES[Number(k)],
247
+ pos: r.palaces[Number(k)],
248
+ stars: Object.keys(r.zwStars).filter(s => r.zwStars[s] === r.palaces[Number(k)]),
249
+ }));
250
+ return (
251
+ <div className="result-card" key={i}>
252
+ <h3>十二宫概览</h3>
253
+ <div className="zw-palace-grid">
254
+ {palaces.map(p => (
255
+ <div key={p.name} className={'zw-palace-cell' + (p.pos === r.ming ? ' is-ming' : '') + (p.stars.length ? '' : ' empty')}>
256
+ <div className="zp-head"><span className="zp-name">{p.name}</span><span className="zp-zhi">{DIZHI[p.pos]}</span>{p.pos === r.ming && <span className="zp-ming">命</span>}</div>
257
+ <div className="zp-stars">{p.stars.length ? p.stars.join(' ') : '主星未临'}</div>
258
+ <div className="zp-theme">{palaceThemeOf(p.name)}</div>
259
+ </div>
260
+ ))}
261
+ </div>
262
+ <div className="result-text mt-3">
263
+ {ins.content.split('\n\n').map((para, j) => <p key={j}>{para}</p>)}
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+ return (<ResultCard key={i} title={ins.title}><p>{ins.content}</p></ResultCard>);
269
+ })}
270
+ </>
271
+ );
272
+ }
273
+ // 宫位主题(渲染层用)
274
+ function palaceThemeOf(name: string): string {
275
+ const map: Record<string, string> = {
276
+ 命宫: '根基底色', 兄弟: '手足同侪', 夫妻: '姻缘伴侣', 子女: '子息晚辈',
277
+ 财帛: '求财理财', 疾厄: '体魄健康', 迁移: '外出环境', 仆役: '部属朋友',
278
+ 官禄: '事业名位', 田宅: '家宅置业', 福德: '福分心境', 父母: '荫庇长辈',
279
+ };
280
+ return map[name] || '';
281
+ }
282
+ // 大限/流年吉凶语(按主星吉凶性,现代语言)
283
+ const GOOD = ['紫微', '天同', '天府', '天相', '天梁', '太阴', '太阳', '武曲'];
284
+ const BAD = ['七杀', '破军', '贪狼', '廉贞', '巨门'];
285
+ function dayunTone(stars: string[]): string {
286
+ if (!stars.length) return '平淡之运';
287
+ if (stars.every(s => GOOD.includes(s))) return '顺遂之运';
288
+ if (stars.some(s => BAD.includes(s)) && !stars.some(s => GOOD.includes(s))) return '多动之运';
289
+ return '起伏之运';
290
+ }
291
+ function futureTone(stars: string[]): string {
292
+ if (!stars.length) return '宜守成';
293
+ if (stars.every(s => GOOD.includes(s))) return '机缘可观,可谋进取';
294
+ if (stars.some(s => BAD.includes(s))) return '变动较多,宜稳中求进';
295
+ return '平稳过渡';
296
+ }