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.
- package/LICENSE +21 -0
- package/README.en.md +202 -0
- package/README.md +247 -0
- package/dist/assets/index-CsOD5rSo.js +102 -0
- package/dist/assets/index-CsOD5rSo.js.map +1 -0
- package/dist/assets/index-DQx62TUr.css +1 -0
- package/dist/favicon.svg +12 -0
- package/dist/index.html +15 -0
- package/eslint.config.js +28 -0
- package/index.html +25 -0
- package/package.json +96 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +12 -0
- package/scripts/guanwei +245 -0
- package/scripts/install.sh +79 -0
- package/scripts/release.sh +42 -0
- package/scripts/setup.bat +36 -0
- package/scripts/setup.sh +123 -0
- package/server/src/data/db.json +12059 -0
- package/server/src/data/guanwei.db +0 -0
- package/server/src/data/lifeContext.ts +423 -0
- package/server/src/data/spreads.ts +255 -0
- package/server/src/data/tarotCards.ts +2353 -0
- package/server/src/index.ts +71 -0
- package/server/src/routes/ai.ts +376 -0
- package/server/src/routes/divine.ts +140 -0
- package/server/src/routes/hour.ts +40 -0
- package/server/src/routes/tarot.ts +129 -0
- package/server/src/routes/users.ts +140 -0
- package/server/src/services/chartBrief.ts +131 -0
- package/server/src/services/divineStore.ts +188 -0
- package/server/src/services/hourInference.ts +150 -0
- package/server/src/services/llmProvider.ts +181 -0
- package/server/src/services/promptBuilder.ts +327 -0
- package/server/src/services/relativesCheck.ts +139 -0
- package/server/src/services/sixRelatives.ts +70 -0
- package/server/src/services/skills.ts +116 -0
- package/server/src/types/index.ts +104 -0
- package/server/src/utils/semanticAnalyzer.ts +555 -0
- package/server/src/utils/tarotEngine.ts +849 -0
- package/server/tsconfig.json +18 -0
- package/shared/core/data/classics.ts +276 -0
- package/shared/core/data/courses.ts +211 -0
- package/shared/core/data/duanyu.ts +148 -0
- package/shared/core/data/ganzhi.ts +53 -0
- package/shared/core/data/gua64.ts +93 -0
- package/shared/core/data/liuren.ts +18 -0
- package/shared/core/data/liuyao.ts +73 -0
- package/shared/core/data/qimen.ts +27 -0
- package/shared/core/data/region-full.ts +3 -0
- package/shared/core/data/region.ts +310 -0
- package/shared/core/data/tarotSpreads.ts +78 -0
- package/shared/core/data/xiaoliuren.ts +18 -0
- package/shared/core/data/ziwei.ts +97 -0
- package/shared/core/data/zodiac.ts +20 -0
- package/shared/core/engine/astrology.ts +169 -0
- package/shared/core/engine/bazi.ts +306 -0
- package/shared/core/engine/calendar.ts +127 -0
- package/shared/core/engine/daily.ts +133 -0
- package/shared/core/engine/liuren.ts +86 -0
- package/shared/core/engine/liuyao.ts +93 -0
- package/shared/core/engine/meihua.ts +87 -0
- package/shared/core/engine/qimen.ts +78 -0
- package/shared/core/engine/questionFit.ts +94 -0
- package/shared/core/engine/tarot.ts +69 -0
- package/shared/core/engine/trueSolarTime.ts +86 -0
- package/shared/core/engine/xiaoliuren.ts +19 -0
- package/shared/core/engine/ziwei.ts +217 -0
- package/shared/core/engine/ziweiInterpret.ts +161 -0
- package/shared/core/types.ts +206 -0
- package/src/App.tsx +69 -0
- package/src/arts/registry.ts +31 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Backdrop.tsx +27 -0
- package/src/components/BambooArt.tsx +41 -0
- package/src/components/CalendarInput.tsx +131 -0
- package/src/components/DateInput.tsx +103 -0
- package/src/components/Disclaimer.tsx +12 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/components/HomeSideAnchor.tsx +62 -0
- package/src/components/InkVein.tsx +10 -0
- package/src/components/LifeEventsInput.tsx +47 -0
- package/src/components/LocationPicker.tsx +117 -0
- package/src/components/Motes.tsx +65 -0
- package/src/components/Navigation.tsx +80 -0
- package/src/components/ProfileForm.tsx +40 -0
- package/src/components/ProfilePicker.tsx +40 -0
- package/src/components/QuestionFields.tsx +47 -0
- package/src/components/ReportView.tsx +211 -0
- package/src/components/ResultCard.tsx +40 -0
- package/src/components/SealButton.tsx +18 -0
- package/src/components/SiteFooter.tsx +17 -0
- package/src/components/SiteNav.tsx +103 -0
- package/src/components/SongDialog.tsx +64 -0
- package/src/components/SongSearchSelect.tsx +81 -0
- package/src/components/SongSelect.tsx +61 -0
- package/src/components/StarField.tsx +119 -0
- package/src/components/TarotCard.tsx +129 -0
- package/src/components/TimeShichenInput.tsx +29 -0
- package/src/components/arts/AstrologyArt.tsx +141 -0
- package/src/components/arts/BaziArt.tsx +269 -0
- package/src/components/arts/LiurenArt.tsx +87 -0
- package/src/components/arts/LiuyaoArt.tsx +111 -0
- package/src/components/arts/MeihuaArt.tsx +105 -0
- package/src/components/arts/QimenArt.tsx +86 -0
- package/src/components/arts/TarotArt.tsx +110 -0
- package/src/components/arts/XiaoliurenArt.tsx +93 -0
- package/src/components/arts/ZiweiArt.tsx +296 -0
- package/src/data/arts.ts +84 -0
- package/src/data/lifeContext.ts +423 -0
- package/src/data/sample-report.json +146 -0
- package/src/data/shichen.ts +21 -0
- package/src/data/spreads.ts +255 -0
- package/src/data/tarotCards.ts +2353 -0
- package/src/hooks/useAIInterpret.ts +76 -0
- package/src/hooks/useDivine.ts +39 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/AboutPage.tsx +25 -0
- package/src/pages/AcademyPage.tsx +128 -0
- package/src/pages/AstrologyPage.tsx +33 -0
- package/src/pages/AuthPage.tsx +263 -0
- package/src/pages/BaZiPage.tsx +33 -0
- package/src/pages/ClassicsPage.tsx +120 -0
- package/src/pages/DemoPage.tsx +161 -0
- package/src/pages/HistoryPage.tsx +133 -0
- package/src/pages/Home.tsx +179 -0
- package/src/pages/HomePage.tsx +174 -0
- package/src/pages/ModulePage.tsx +320 -0
- package/src/pages/SpreadEditor.tsx +230 -0
- package/src/pages/SpreadEditorPage.tsx +105 -0
- package/src/pages/SpreadLibrary.tsx +199 -0
- package/src/pages/SpreadLibraryPage.tsx +17 -0
- package/src/pages/TarotPage.tsx +738 -0
- package/src/pages/ZiWeiPage.tsx +33 -0
- package/src/services/api.ts +342 -0
- package/src/stores/useDivinationStore.ts +22 -0
- package/src/styles/song.css +1066 -0
- package/src/styles/tokens.css +60 -0
- package/src/types/index.ts +104 -0
- package/src/utils/astrologyCalc.ts +61 -0
- package/src/utils/comboEngine.ts +134 -0
- package/src/utils/dst.ts +24 -0
- package/src/utils/panTone.ts +206 -0
- package/src/utils/recordStore.ts +60 -0
- package/src/utils/semanticAnalyzer.ts +555 -0
- package/src/utils/spreadStorage.ts +32 -0
- package/src/utils/storage.ts +27 -0
- package/src/utils/tarotEngine.ts +849 -0
- package/src/utils/userStore.ts +234 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +52 -0
- package/tsconfig.json +40 -0
- package/vite.config.ts +34 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// 四柱八字:输入面板(含出生地点→真太阳时)+ 结果渲染(四柱/五行/十神)
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { baziCalc } from '@core/engine/bazi';
|
|
4
|
+
import { SHISHEN_MAP, WUXING } from '@core/data/ganzhi';
|
|
5
|
+
import type { BaziResult, GeoLocation } from '@core/types';
|
|
6
|
+
import type { UserProfile } from '@/utils/userStore';
|
|
7
|
+
import LocationPicker from '@/components/LocationPicker';
|
|
8
|
+
import { currentUser } from '@/utils/userStore';
|
|
9
|
+
import SongSelect from '@/components/SongSelect';
|
|
10
|
+
import DateInput from '@/components/DateInput';
|
|
11
|
+
import ProfilePicker from '@/components/ProfilePicker';
|
|
12
|
+
import { timeToHourIndex } from '@/data/shichen';
|
|
13
|
+
import TimeShichenInput from '@/components/TimeShichenInput';
|
|
14
|
+
import { ResultCard } from '@/components/ResultCard';
|
|
15
|
+
import { isChinaDSTDate } from '@/utils/dst';
|
|
16
|
+
import { apiHourInfer, type HourInferResult } from '@/services/api';
|
|
17
|
+
import { baziTone } from '@/utils/panTone';
|
|
18
|
+
import LifeEventsInput, { type LifeEvent } from '@/components/LifeEventsInput';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
interface PanelProps { onDivine: (inputs: unknown, profile?: UserProfile, question?: string, profileId?: string) => void; }
|
|
22
|
+
|
|
23
|
+
export function BaziPanel({ onDivine }: PanelProps) {
|
|
24
|
+
const user = currentUser();
|
|
25
|
+
const p = user?.profile;
|
|
26
|
+
const [date, setDate] = useState(p?.birthDate || '1990-06-15');
|
|
27
|
+
const [birthTime, setBirthTime] = useState(p?.birthTime || '12:00');
|
|
28
|
+
const [hourUnknown, setHourUnknown] = useState(p?.birthTimeUnknown || false);
|
|
29
|
+
const [gender, setGender] = useState<'男' | '女'>(p?.gender || '男');
|
|
30
|
+
const [loc, setLoc] = useState<GeoLocation | null>(p?.location || null);
|
|
31
|
+
const [question, setQuestion] = useState('');
|
|
32
|
+
const [profileSrc, setProfileSrc] = useState<{ id: string; name: string } | null>(null);
|
|
33
|
+
const [lifeEvents, setLifeEvents] = useState<LifeEvent[]>(p?.lifeEvents || []);
|
|
34
|
+
// 时辰推演(时辰未知时)
|
|
35
|
+
const [inferOpen, setInferOpen] = useState(false);
|
|
36
|
+
const [eventsText, setEventsText] = useState('');
|
|
37
|
+
const [inferring, setInferring] = useState(false);
|
|
38
|
+
const [inferResult, setInferResult] = useState<HourInferResult | null>(null);
|
|
39
|
+
const [inferErr, setInferErr] = useState('');
|
|
40
|
+
const dst = isChinaDSTDate(date);
|
|
41
|
+
|
|
42
|
+
const filled = p && date === p.birthDate && birthTime === p.birthTime && gender === p.gender && JSON.stringify(loc) === JSON.stringify(p.location) && !hourUnknown;
|
|
43
|
+
const [formMsg, setFormMsg] = useState('');
|
|
44
|
+
const go = () => {
|
|
45
|
+
const [y, m, d] = date.split('-').map(Number);
|
|
46
|
+
if (!y || !m || !d) { setFormMsg('⚠ 请先填写有效的出生日期'); return; }
|
|
47
|
+
if (new Date(y, m - 1, d).getMonth() !== m - 1) { setFormMsg('⚠ 出生日期不合法,请检查'); return; }
|
|
48
|
+
setFormMsg('');
|
|
49
|
+
const hourIdx = hourUnknown ? -1 : timeToHourIndex(birthTime);
|
|
50
|
+
onDivine(
|
|
51
|
+
hourUnknown
|
|
52
|
+
? { y, m, d, hourIndex: -1, gender, location: loc ?? undefined }
|
|
53
|
+
: { y, m, d, hourIndex: hourIdx, time: birthTime, gender, location: loc ?? undefined },
|
|
54
|
+
{ birthDate: date, birthTime: hourUnknown ? '' : birthTime, birthHourIndex: hourIdx, birthTimeUnknown: hourUnknown || undefined, gender, location: loc, lifeEvents: lifeEvents.length ? lifeEvents : undefined },
|
|
55
|
+
question.trim(),
|
|
56
|
+
profileSrc?.id || 'main'
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const runInfer = async () => {
|
|
61
|
+
const [y, m, d] = date.split('-').map(Number);
|
|
62
|
+
const events = eventsText.split('\n').map(l => l.trim()).filter(Boolean)
|
|
63
|
+
.map(l => {
|
|
64
|
+
const m2 = /^(\d{4})\s*(.*)$/.exec(l);
|
|
65
|
+
return m2 ? { year: Number(m2[1]), text: (m2[2] || '关键事件').slice(0, 40) } : null;
|
|
66
|
+
}).filter(Boolean) as { year: number; text: string }[];
|
|
67
|
+
if (!events.length) { setInferErr('请按「年份 事件」格式填写,每行一条'); return; }
|
|
68
|
+
setInferring(true); setInferErr(''); setInferResult(null);
|
|
69
|
+
try {
|
|
70
|
+
const r = await apiHourInfer({ y, m, d, gender, location: loc, events });
|
|
71
|
+
setInferResult(r);
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
setInferErr(e?.message || '推演未应机');
|
|
74
|
+
} finally { setInferring(false); }
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const applyHour = (hi: number) => {
|
|
78
|
+
setHourUnknown(false);
|
|
79
|
+
const mid = String(hi * 2 + 1).padStart(2, '0') + ':00'; // 时辰中点(如申时 16:00)
|
|
80
|
+
setBirthTime(mid);
|
|
81
|
+
setInferOpen(false);
|
|
82
|
+
setInferResult(null);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<ProfilePicker onPick={(p, src) => { setDate(p.birthDate); setBirthTime(p.birthTime || '12:00'); setHourUnknown(!!p.birthTimeUnknown); setGender(p.gender); setLoc(p.location); if (src) setProfileSrc(src); }} />
|
|
88
|
+
{user && filled && <p className="hint" style={{ color: 'var(--celadon-deep)', marginTop: 'var(--sp-2)' }}>已依主档案预填(可改,改后以所书为准)</p>}
|
|
89
|
+
<div className="field"><label htmlFor="bz-date">出生日期(公历 / 农历)</label>
|
|
90
|
+
<DateInput id="bz-date" value={date} onChange={setDate} /></div>
|
|
91
|
+
{dst && <p className="tag-hot" style={{ marginTop: '.3rem' }}>⚠ 该日期处于中国夏令时期间(1986-1991 年 4-9 月,钟表拨快 1 小时)——请按出生时钟表时间填写,排盘会自动回拨校正。</p>}
|
|
92
|
+
<div className="field-row">
|
|
93
|
+
<div className="field"><label htmlFor="bz-time">出生时刻</label>
|
|
94
|
+
{hourUnknown
|
|
95
|
+
? <p className="hint" style={{ padding: '.45rem 0' }}>时辰未录——排盘不排时柱,AI 解读仅依年月日三柱,不涉时柱/子女宫/晚年。</p>
|
|
96
|
+
: <TimeShichenInput id="bz-time" value={birthTime} onChange={setBirthTime} />}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="field"><label htmlFor="bz-gender">性别</label>
|
|
99
|
+
<SongSelect id="bz-gender" value={gender} options={[{ value: '男', label: '男' }, { value: '女', label: '女' }]} onChange={v => setGender(v as '男' | '女')} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="field"><label>时辰未知</label>
|
|
103
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: '.45rem', padding: '.45rem 0', cursor: 'pointer', fontSize: '.9rem' }}>
|
|
104
|
+
<input type="checkbox" checked={hourUnknown} onChange={e => setHourUnknown(e.target.checked)} />
|
|
105
|
+
未录(不排时柱)
|
|
106
|
+
</label>
|
|
107
|
+
</div>
|
|
108
|
+
{hourUnknown && (
|
|
109
|
+
<div className="hour-infer" style={{ marginTop: '.4rem' }}>
|
|
110
|
+
<button type="button" className="btn-seal btn-ghost" style={{ fontSize: '.82rem', padding: '.4rem 1rem' }} onClick={() => setInferOpen(v => !v)}>
|
|
111
|
+
{inferOpen ? '收 起' : '🔍 依人生经历推演时辰'}
|
|
112
|
+
</button>
|
|
113
|
+
{inferOpen && (
|
|
114
|
+
<div style={{ marginTop: '.6rem' }}>
|
|
115
|
+
<p className="tiny muted" style={{ marginBottom: '.3rem' }}>每行一条:「年份 关键事件」(3-8 条为宜,越关键越好)</p>
|
|
116
|
+
<textarea className="input-line" rows={5} value={eventsText} onChange={e => setEventsText(e.target.value)}
|
|
117
|
+
placeholder={'2004 家庭变故\n2014 恋爱\n2016 换城市工作\n2020 换工作\n2022 结婚\n2024 健康问题'} />
|
|
118
|
+
<div className="btn-row" style={{ marginTop: '.4rem' }}>
|
|
119
|
+
<button className="btn-seal" disabled={inferring} onClick={runInfer}>{inferring ? '推演中…' : '开 始 推 演'}</button>
|
|
120
|
+
</div>
|
|
121
|
+
{inferErr && <p className="tag-hot" style={{ marginTop: '.4rem' }}>{inferErr}</p>}
|
|
122
|
+
{inferResult && (
|
|
123
|
+
<div className="infer-result" style={{ marginTop: '.6rem' }}>
|
|
124
|
+
<p className="tiny muted">{inferResult.chart.yearGZ} 年 / {inferResult.chart.monthGZ} 月 / {inferResult.chart.dayGZ} 日——各时辰候选得分(含时柱与流年应象):</p>
|
|
125
|
+
{inferResult.candidates.slice(0, 6).map((c, i) => (
|
|
126
|
+
<div key={c.hourIndex} onClick={() => applyHour(c.hourIndex)} style={{
|
|
127
|
+
cursor: 'pointer', border: '1px solid ' + (i === 0 ? 'var(--celadon-deep)' : 'var(--line-soft)'),
|
|
128
|
+
borderRadius: 'var(--r-sm)', padding: '.45rem .6rem', marginTop: '.35rem', background: i === 0 ? 'rgba(94,122,78,.08)' : 'rgba(251,249,243,.5)',
|
|
129
|
+
}}>
|
|
130
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
131
|
+
<strong>{c.shichen}时 · {c.hourGZ} {i === 0 ? <span className="tag-cool">最吻合</span> : null}</strong>
|
|
132
|
+
<span className="tiny muted">得分 {c.score}</span>
|
|
133
|
+
</div>
|
|
134
|
+
{c.hits.slice(0, 2).map((h, j) => <div key={j} className="tiny muted" style={{ marginTop: '.15rem' }}>{h.year}:{h.reason}</div>)}
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
<p className="tiny muted" style={{ marginTop: '.4rem' }}>点击候选时辰即回填并取消「未知」;推演仅区分时柱细节,大运序列不受时辰影响。</p>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
{formMsg && <p className="tag-hot" style={{ marginTop: '.3rem' }}>{formMsg}</p>}
|
|
145
|
+
<div className="field">
|
|
146
|
+
<label>出生地点(真太阳时校正)</label>
|
|
147
|
+
<LocationPicker value={loc} onChange={setLoc} previewHourIndex={hourUnknown ? -1 : timeToHourIndex(birthTime)} />
|
|
148
|
+
{!loc && <p className="hint" style={{ marginTop: '.3rem' }}>未选地点:将按北京时间直接排盘(不做真太阳时校正,时辰边界可能偏差约 15-80 分钟)</p>}
|
|
149
|
+
</div>
|
|
150
|
+
<div className="field"><label htmlFor="q-input">所问之事(可选)</label>
|
|
151
|
+
<input className="input-line" id="q-input" placeholder="可书所问,AI 报告将由此而发…" maxLength={60} value={question} onChange={e => setQuestion(e.target.value)} />
|
|
152
|
+
</div>
|
|
153
|
+
<LifeEventsInput value={lifeEvents} onChange={setLifeEvents} />
|
|
154
|
+
<button className="btn-divine" onClick={go}>排 盘<span className="small">布四柱 · 观五行 · 推十神</span></button>
|
|
155
|
+
<p className="hint" style={{ marginTop: '.8rem' }}>节气取精确万年历;时柱以出生地经度校正真太阳时;时辰未录时仅排年月日三柱。</p>
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const WX_NAMES: Record<string, string> = { 木: '木', 火: '火', 土: '土', 金: '金', 水: '水' };
|
|
161
|
+
// 干支 → 五行名(用于四柱盘着色与显示)
|
|
162
|
+
const wxOf = (gz: string) => WX_NAMES[WUXING[gz[0]]] + WX_NAMES[WUXING[gz[1]]];
|
|
163
|
+
const WX_COLOR: Record<string, string> = { 木: '#5E7A4E', 火: '#A5402D', 土: '#7A6A4A', 金: '#8C7A4E', 水: '#3E5F7A' };
|
|
164
|
+
const HOURS_CN = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
|
165
|
+
|
|
166
|
+
export function BaziResult({ data }: { data: BaziResult }) {
|
|
167
|
+
const r = data;
|
|
168
|
+
const tone = baziTone(r);
|
|
169
|
+
const hourKnown = r.hourGZ !== '未知';
|
|
170
|
+
const pillars = [
|
|
171
|
+
{ name: '年柱', gz: r.yearGZ, wx: wxOf(r.yearGZ), ss: r.shishen[0].name, scope: '祖上 · 幼年', isDay: false },
|
|
172
|
+
{ name: '月柱', gz: r.monthGZ, wx: wxOf(r.monthGZ), ss: r.shishen[1].name, scope: '父母 · 青年', isDay: false },
|
|
173
|
+
{ name: '日柱', gz: r.dayGZ, wx: wxOf(r.dayGZ), ss: '日主', scope: '自身 · 中年', isDay: true },
|
|
174
|
+
hourKnown
|
|
175
|
+
? { name: '时柱', gz: r.hourGZ, wx: wxOf(r.hourGZ), ss: r.shishen[3].name, scope: '子女 · 暮年', isDay: false }
|
|
176
|
+
: { name: '时柱', gz: '未知', wx: '未录', ss: '未知', scope: '子女 · 暮年', isDay: false },
|
|
177
|
+
];
|
|
178
|
+
const wxOrder = ['木', '火', '土', '金', '水'];
|
|
179
|
+
const dayWx = r.dayGanWx;
|
|
180
|
+
const sheng = wxOrder[(wxOrder.indexOf(dayWx) + 4) % 5];
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<div className="result-card stagger">
|
|
184
|
+
<h3>四柱命盘 · {r.dayGan}{dayWx}日主</h3>
|
|
185
|
+
<div className="bazi-pan" style={{ marginTop: 'var(--sp-3)' }}>
|
|
186
|
+
{pillars.map(p => (
|
|
187
|
+
<div key={p.name} className={"bz-pillar" + (p.isDay ? ' day-pillar' : '')}>
|
|
188
|
+
<span className="p-zhu">{p.scope}</span>
|
|
189
|
+
<span className="p-name">{p.name} · {p.ss}</span>
|
|
190
|
+
<span className="p-gan" style={{ color: WX_COLOR[WUXING[p.gz[0]]] }}>{p.gz[0]}</span>
|
|
191
|
+
<span className="p-zhi" style={{ color: WX_COLOR[WUXING[p.gz[1]]] }}>{p.gz[1]}</span>
|
|
192
|
+
<span className="p-wx">{p.wx}</span>
|
|
193
|
+
<span className="p-nayin">{r.nayin}</span>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
<p style={{ marginTop: 'var(--sp-3)' }}><strong>{tone.headline}</strong></p>
|
|
198
|
+
{r.trueSolar && <p className="tag-cool">真太阳时校正:{HOURS_CN[r.correctedHourIndex]}时(依出生地经度 {r.trueSolar.localMeanHours.toFixed(2)}h → {r.trueSolar.trueSolarHours.toFixed(2)}h)</p>}
|
|
199
|
+
</div>
|
|
200
|
+
<ResultCard title="五行流转"
|
|
201
|
+
cells={wxOrder.map(k => ({ k: WX_NAMES[k], v: '●'.repeat(Math.max(r.wxCount[k], 0)) + (r.wxCount[k] === 0 ? '空' : '') + ' ×' + r.wxCount[k] }))}>
|
|
202
|
+
<p><strong>日主:{r.dayGan}{dayWx}</strong> 生扶({dayWx}、{sheng})共 {r.support} 数,克泄耗共 {r.drain} 数 → <span className={r.support >= r.drain ? 'tag-cool' : 'tag-hot'}><strong>{r.strength}</strong></span>。</p>
|
|
203
|
+
<p>{tone.wuxingNote}</p>
|
|
204
|
+
</ResultCard>
|
|
205
|
+
<ResultCard title="占断要旨">
|
|
206
|
+
<p>{tone.overview}</p>
|
|
207
|
+
{tone.character.map((c, i) => <p key={i} style={{ marginTop: '.45rem' }}>◇ {c}</p>)}
|
|
208
|
+
<p style={{ marginTop: '.45rem' }}>{tone.dayunNote}</p>
|
|
209
|
+
<p className="tiny muted" style={{ marginTop: '.5rem' }}>{tone.nayinNote}</p>
|
|
210
|
+
<p className="tiny muted" style={{ marginTop: '.4rem' }}>此要旨由盘面数据生成,为现代语言之概览,细察仍以全局生克为凭。</p>
|
|
211
|
+
</ResultCard>
|
|
212
|
+
<ResultCard title="十神六亲">
|
|
213
|
+
<p>{r.shishen.filter(s => s.name !== '比肩' || true).slice(0, 4).map((s, i) => <span key={i}><strong>{['年干', '月干', '日干', '时干'][i]}</strong>为{s.name}({SHISHEN_MAP[s.name]}日主之{s.name}) </span>)}</p>
|
|
214
|
+
<p>十神之义:比劫主自我与同侪,食伤主才情与输出,财星主所得与经营,官杀主约束与担当,印星主滋养与学识。</p>
|
|
215
|
+
</ResultCard>
|
|
216
|
+
<ResultCard title="地支藏干 · 人元十神">
|
|
217
|
+
<p className="tiny muted" style={{ marginBottom: 'var(--sp-2)' }}>地支非孤物,各藏人元(本气/中气/余气),十神之根尽在此中——用神是否有根、财官是否有力,皆观藏干。</p>
|
|
218
|
+
<div style={{ display: 'flex', gap: '.8rem', flexWrap: 'wrap', marginTop: 'var(--sp-2)' }}>
|
|
219
|
+
{r.canggan.map((cg, i) => (
|
|
220
|
+
<div key={i} style={{ border: '1px solid var(--line-soft)', borderRadius: 'var(--r-sm)', padding: '.6rem .8rem', background: 'rgba(251,249,243,.5)' }}>
|
|
221
|
+
<div className="tiny muted" style={{ letterSpacing: '.2em' }}>{['年支', '月支', '日支', '时支'][i]} · {cg.zhi}</div>
|
|
222
|
+
<div style={{ marginTop: '.3rem', fontSize: '.95rem' }}>
|
|
223
|
+
{cg.gans.map((g, j) => (
|
|
224
|
+
<span key={j} style={{ display: 'block', color: WX_COLOR[g.wx] }}>{g.gan} <span style={{ color: 'var(--ink-soft)' }}>{g.shishen}({g.qi})</span></span>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</ResultCard>
|
|
231
|
+
<ResultCard title="用神 · 喜忌">
|
|
232
|
+
<p><strong>日主{r.strength}</strong>(旺衰分 {r.strengthDetail.score.toFixed(1)}:{r.strengthDetail.reasons.join(';')})</p>
|
|
233
|
+
<p style={{ marginTop: '.6rem' }}>主用神:<strong style={{ color: WX_COLOR[r.yongshen.wx] }}>{r.yongshen.wx}({r.yongshen.shishen})</strong> 调候:{r.yongshen.tiaohou}</p>
|
|
234
|
+
<p style={{ marginTop: '.6rem' }}>喜 神:<span className="tag-cool">{r.yongshen.xi.join('、')}</span> 忌 神:<span className="tag-hot">{r.yongshen.ji.join('、')}</span></p>
|
|
235
|
+
<p className="tiny muted" style={{ marginTop: '.6rem' }}>{r.yongshen.reason}</p>
|
|
236
|
+
<p className="tiny muted" style={{ marginTop: '.4rem' }}>用神为全局取运之枢:行运逢喜则顺,逢忌则慎——此为 AI 解读「就盘论命」的核心依据。</p>
|
|
237
|
+
</ResultCard>
|
|
238
|
+
<ResultCard title="大运 · 流年">
|
|
239
|
+
<p><strong>{r.qiYun.detail}</strong>,大运{ r.dayun[0]?.forward ? '顺行' : '逆行' }(阳男阴女顺、阴男阳女逆)。</p>
|
|
240
|
+
<div style={{ display: 'flex', gap: '.5rem', flexWrap: 'wrap', marginTop: 'var(--sp-2)' }}>
|
|
241
|
+
{r.dayun.slice(0, 8).map((d, i) => (
|
|
242
|
+
<div key={i} style={{ border: '1px solid var(--line-soft)', borderRadius: 'var(--r-sm)', padding: '.5rem .7rem', minWidth: '5.6rem', background: 'rgba(251,249,243,.5)' }}>
|
|
243
|
+
<div style={{ fontWeight: 600, fontSize: '1.05rem' }}>{d.gz}</div>
|
|
244
|
+
<div className="tiny" style={{ color: WX_COLOR[WUXING[d.gz[0]]] }}>{d.ganShishen}</div>
|
|
245
|
+
<div className="tiny muted">{d.startAge} 岁 · {d.startYear}-{d.endYear}</div>
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
<p style={{ marginTop: '.8rem' }}>流年:<strong>{r.liunian.year} 年 {r.liunian.gz}</strong>({r.liunian.ganShishen} · 支藏{r.liunian.zhiShishen})——大运为十年之纲,流年为一年之目,合参断吉凶。</p>
|
|
250
|
+
</ResultCard>
|
|
251
|
+
<ResultCard title="神煞 · 胎元命身">
|
|
252
|
+
<p>神煞:{r.shensha.length > 0
|
|
253
|
+
? r.shensha.map((s, i) => <span key={i}><strong className={s.type === '吉' ? 'tag-cool' : s.type === '凶' ? 'tag-hot' : ''}>{s.name}</strong>({s.zhi}) </span>)
|
|
254
|
+
: '四柱无显著神煞,以五行生克为主论之。'}</p>
|
|
255
|
+
<p style={{ marginTop: '.6rem' }}>胎元 <strong>{r.taiyuan}</strong>(受气之始) 命宫 <strong>{r.minggong}</strong>(立命之基) 身宫 <strong>{r.shengong}</strong>(行事之依)</p>
|
|
256
|
+
<p className="tiny muted" style={{ marginTop: '.4rem' }}>神煞取日干与年支/日支三合:贵人禄神主助力,羊刃主刚猛,驿马主奔波,桃花主情缘。</p>
|
|
257
|
+
</ResultCard>
|
|
258
|
+
</>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
// 十神格局点睛
|
|
262
|
+
function shishenFocus(r: any): string {
|
|
263
|
+
const names = r.shishen.map((s: any) => s.name);
|
|
264
|
+
if (names.includes('正官') || names.includes('七杀')) return '官杀透干,主担当与约束,成事在于守正。';
|
|
265
|
+
if (names.includes('正财') || names.includes('偏财')) return '财星显露,主经营与所得,宜务实聚财。';
|
|
266
|
+
if (names.includes('正印') || names.includes('偏印')) return '印星为用,主学识与荫庇,宜向学深造。';
|
|
267
|
+
if (names.includes('食神') || names.includes('伤官')) return '食伤泄秀,主才情与输出,宜展露所长。';
|
|
268
|
+
return '比劫同气,主自立与同侪,宜广结善缘。';
|
|
269
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// 大六壬:起课面板 + 天地盘/四课三传渲染
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { liurenCalc, jiangName } from '@core/engine/liuren';
|
|
4
|
+
import { ZHI } from '@core/data/ganzhi';
|
|
5
|
+
import { LR_JIANG_SYMBOL, LR_JIANG_WX } from '@core/data/liuren';
|
|
6
|
+
import type { LiurenResult } from '@core/types';
|
|
7
|
+
import { ResultCard } from '@/components/ResultCard';
|
|
8
|
+
import SongSelect from '@/components/SongSelect';
|
|
9
|
+
|
|
10
|
+
const USES = ['问出行', '问财利', '问讼事', '问姻缘', '问疾病', '问失物', '问家宅', '其他'];
|
|
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 LiurenPanel({ 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="lr-datetime">占时(公历)</label>
|
|
28
|
+
<input className="input-line" type="datetime-local" id="lr-datetime" value={dt} onChange={e => setDt(e.target.value)} /></div>
|
|
29
|
+
<div className="field"><label htmlFor="lr-q">所占之属</label>
|
|
30
|
+
<SongSelect id="lr-q" 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 LiurenResult({ data }: { data: LiurenResult }) {
|
|
42
|
+
const r = data;
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<ResultCard title={`${r.jqName} · ${jiangName(r.jiang)}(${r.jiang})加 ${r.hourGZ[1]}时`}>
|
|
46
|
+
<p>日干 <strong>{r.dayGZ}</strong>,占时 <strong>{r.hourGZ}</strong>({r.hourGZ[1]}时)。月将 <strong>{jiangName(r.jiang)}</strong>。</p>
|
|
47
|
+
</ResultCard>
|
|
48
|
+
<div className="result-card">
|
|
49
|
+
<h3>天地盘</h3>
|
|
50
|
+
<div className="liuren-stage mt-2">
|
|
51
|
+
{ZHI.map((z, i) => {
|
|
52
|
+
const up = r.tianpan[i];
|
|
53
|
+
return (
|
|
54
|
+
<div className="liuren-item" key={z}>
|
|
55
|
+
<div className="k">{z}宫({LR_JIANG_SYMBOL[z]})</div>
|
|
56
|
+
<div className="v">{up}{LR_JIANG_SYMBOL[up]}</div>
|
|
57
|
+
<div className="tiny muted">{LR_JIANG_WX[up]} · {jiangName(up)}</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<ResultCard title="四课三传"
|
|
64
|
+
cells={[
|
|
65
|
+
{ k: '一课 · 干上', v: r.ke1, big: true },
|
|
66
|
+
{ k: '二课 · 干阴', v: r.ke2, big: true },
|
|
67
|
+
{ k: '三课 · 支上', v: r.ke3, big: true },
|
|
68
|
+
{ k: '四课 · 支阴', v: r.ke4, big: true },
|
|
69
|
+
]}>
|
|
70
|
+
<p>三传(简式):<strong className="tag-hot">初传 {r.chuan1}</strong> → <strong className="tag-cool">中传 {r.chuan2}</strong> → <strong>末传 {r.chuan3}</strong>。</p>
|
|
71
|
+
<p>初传为事发之端,中传为事进之中,末传为事成之归。干为外、支为内,四课之内外,即人事之表里。</p>
|
|
72
|
+
<p className="mt-2"><strong>三传参详:</strong>{liurenAdvice(r)}</p>
|
|
73
|
+
</ResultCard>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
// 三传参详(结合五行/月将,现代语言)
|
|
78
|
+
function liurenAdvice(r: any): string {
|
|
79
|
+
const wxMap: Record<string, string> = { 亥: '水', 子: '水', 寅: '木', 卯: '木', 巳: '火', 午: '火', 申: '金', 酉: '金', 辰: '土', 戌: '土', 丑: '土', 未: '土' };
|
|
80
|
+
const parts: string[] = [];
|
|
81
|
+
parts.push('初传' + r.chuan1 + '(' + (wxMap[r.chuan1] || '') + ')为事之始,' + (['水'].includes(wxMap[r.chuan1] || '') ? '水主流动,事有周折' : wxMap[r.chuan1] === '金' ? '金主决断,事起于刚' : wxMap[r.chuan1] === '火' ? '火主急速,事发于明' : wxMap[r.chuan1] === '木' ? '木主生发,事起于新' : '土主迟重,事起于稳') + '。');
|
|
82
|
+
if (r.chuan1 === r.chuan3) parts.push('初末同神,事体回环,首尾相应,宜守初心。');
|
|
83
|
+
else if (r.chuan2 === r.chuan3) parts.push('中末同神,事中已定,后续之势多在把握。');
|
|
84
|
+
else parts.push('三传递进,事势渐明,宜循序而行。');
|
|
85
|
+
parts.push('月将' + r.jiang + '加时,天地盘已定,观其生克以断吉凶。');
|
|
86
|
+
return parts.join('');
|
|
87
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// 六爻:铜钱摇卦面板 + 六爻/卦象渲染
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { liuyaoCalc } from '@core/engine/liuyao';
|
|
4
|
+
import type { LiuyaoResult } from '@core/types';
|
|
5
|
+
import { ResultCard } from '@/components/ResultCard';
|
|
6
|
+
import SongSelect from '@/components/SongSelect';
|
|
7
|
+
|
|
8
|
+
interface PanelProps { onDivine: (inputs: unknown, profile?: unknown, question?: string) => void; }
|
|
9
|
+
|
|
10
|
+
export function LiuyaoPanel({ onDivine }: PanelProps) {
|
|
11
|
+
const [q, setQ] = useState('');
|
|
12
|
+
const [qType, setQType] = useState('other');
|
|
13
|
+
const go = () => onDivine({}, undefined, q.trim());
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<p className="small muted" style={{ marginTop: '.6rem', lineHeight: 2 }}>默念所问之事,静心片刻。以三枚铜钱掷之,共六次,自下而上成卦。</p>
|
|
17
|
+
<div className="coins" aria-hidden="true" style={{ padding: 'var(--sp-3) 0' }}>
|
|
18
|
+
<div className="coin"><span className="face">宋<br />通宝</span></div>
|
|
19
|
+
<div className="coin"><span className="face">宋<br />通宝</span></div>
|
|
20
|
+
<div className="coin"><span className="face">宋<br />通宝</span></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="field"><label htmlFor="ly-qtype">所问之属</label>
|
|
23
|
+
<SongSelect id="ly-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
|
+
<button className="btn-divine" onClick={go}>掷 钱 成 卦<span className="small">六掷成卦 · 观变断象</span></button>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const YAO_LABELS = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
|
|
33
|
+
|
|
34
|
+
export function LiuyaoResult({ data }: { data: LiuyaoResult }) {
|
|
35
|
+
const r = data;
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<ResultCard title={r.dongYao.length ? `${r.dongYao.length} 爻发动` : '六爻安静'}>
|
|
39
|
+
<div style={{ maxWidth: 420, margin: '0 auto' }}>
|
|
40
|
+
{r.yao.map((v, i) => {
|
|
41
|
+
const nm = r.names[i].nm;
|
|
42
|
+
const moving = nm === '老阴' || nm === '老阳';
|
|
43
|
+
return (
|
|
44
|
+
<div className="yao-row" key={i}>
|
|
45
|
+
<span className="yao-label">{YAO_LABELS[i]}</span>
|
|
46
|
+
<span className="yao-glyph">
|
|
47
|
+
{v ? <span className="yang" style={{ display: 'block', width: '2.6rem', height: '.55rem', background: 'var(--ink)', borderRadius: '1px' }} />
|
|
48
|
+
: <span className="yin" style={{ display: 'block', width: '2.6rem', height: '.55rem', borderTop: '1.5px solid var(--ink)', borderBottom: '1.5px solid var(--ink)' }} />}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="yao-move">{moving ? `${nm}${nm === '老阳' ? '○' : '×'} 变` : nm}{moving ? <span className="dot" /> : null}</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
</ResultCard>
|
|
56
|
+
|
|
57
|
+
{r.najia && (
|
|
58
|
+
<ResultCard title={"纳甲装卦 · " + r.najia.gong + "宫(" + r.najia.dayGZ + "日 · " + r.najia.monthZhi + "月建)"}>
|
|
59
|
+
<div style={{ maxWidth: 420, margin: '0 auto' }}>
|
|
60
|
+
{r.najia.lines.map((l, i) => (
|
|
61
|
+
<div className="yao-row" key={i} style={{ fontSize: '.88rem' }}>
|
|
62
|
+
<span className="yao-label">{['初', '二', '三', '四', '五', '上'][i]}爻{l.isShi ? ' 世' : l.isYing ? ' 应' : ''}</span>
|
|
63
|
+
<span className="yao-glyph" style={{ width: '2.6rem', textAlign: 'center' }}>
|
|
64
|
+
<strong>{l.gz}</strong>
|
|
65
|
+
</span>
|
|
66
|
+
<span className="yao-move">
|
|
67
|
+
<span className="tag-cool">{l.liuqin}</span> {l.shen}{l.kong ? ' · 旬空' : ''}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
<p className="tiny muted" style={{ marginTop: '.6rem' }}>
|
|
73
|
+
世爻为占事之我,应爻为对方之事:世爻{r.najia.shiLiQin}。月破地支:{r.najia.yuePo.join('、')};旬空:{r.najia.xunKong.join('、')}。
|
|
74
|
+
六亲以{r.najia.gong}宫五行为体——取用神之法:问财取妻财、问官取官鬼、问父母长辈取父母、问子女小辈取子孙、问同侪取兄弟。
|
|
75
|
+
</p>
|
|
76
|
+
</ResultCard>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<div className="result-grid">
|
|
80
|
+
<div className="result-cell">
|
|
81
|
+
<div className="k">本卦</div>
|
|
82
|
+
<div className="v big">{r.benGua.name}</div>
|
|
83
|
+
<div className="tiny muted mt-1">{r.benGua.xiang}</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="result-cell">
|
|
86
|
+
<div className="k">变卦</div>
|
|
87
|
+
<div className={"v big " + (r.dongYao.length ? 'hot' : '')}>{r.bianGua.name}</div>
|
|
88
|
+
<div className="tiny muted mt-1">{r.dongYao.length ? '动爻之变,事之趋向' : '无事不变'}</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<ResultCard title={`卦辞 · ${r.benGua.name}`} className="mt-3">
|
|
92
|
+
<p>「{r.benGua.ci}」</p>
|
|
93
|
+
<p>{r.benGua.xiang}。{r.dongYao.length ? `今有 ${r.dongYao.map(i => '第' + i + '爻').join('、')} 发动,${r.bianGua.name}为事之归趋;动而变者,其势在「${r.bianGua.xiang}」。` : '六爻安静,事依现局而行,宜守其常。'}</p>
|
|
94
|
+
<p className="mt-2"><strong>爻位参详:</strong>{yaoDetail(r.dongYao)}</p>
|
|
95
|
+
</ResultCard>
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
// 爻位参详(初爻至上爻的分位语义,现代语言)
|
|
100
|
+
function yaoDetail(dongYao: number[]): string {
|
|
101
|
+
if (dongYao.length === 0) return '六爻皆静,事在当下,宜守成观察,不轻动。';
|
|
102
|
+
const map: Record<number, string> = {
|
|
103
|
+
1: '初爻发动——事发于微,根基初动,宜察其始。',
|
|
104
|
+
2: '二爻发动——事涉自身与内宅,宜安内而后攘外。',
|
|
105
|
+
3: '三爻发动——事逢转折关口,多劳多思,宜慎择。',
|
|
106
|
+
4: '四爻发动——事近上层或外局,进退之间,宜度势。',
|
|
107
|
+
5: '五爻发动——事居中枢,主事者之决断,宜当机立断。',
|
|
108
|
+
6: '上爻发动——事已至末,宜知止知足,防盛极而衰。',
|
|
109
|
+
};
|
|
110
|
+
return dongYao.map(i => map[i] || '').join('');
|
|
111
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// 梅花易数:起卦面板(时间/报数)+ 卦象渲染(本卦/互卦/变卦 + 体用)
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { meihuaCalc } from '@core/engine/meihua';
|
|
4
|
+
import { BAGUA } from '@core/data/gua64';
|
|
5
|
+
import type { MeihuaResult } from '@core/types';
|
|
6
|
+
import { ResultCard } from '@/components/ResultCard';
|
|
7
|
+
import SongSelect from '@/components/SongSelect';
|
|
8
|
+
|
|
9
|
+
interface PanelProps { onDivine: (inputs: unknown, profile?: unknown, question?: string) => void; }
|
|
10
|
+
|
|
11
|
+
export function MeihuaPanel({ onDivine }: PanelProps) {
|
|
12
|
+
const [mode, setMode] = useState<'time' | 'num'>('time');
|
|
13
|
+
const [n1, setN1] = useState(3);
|
|
14
|
+
const [n2, setN2] = useState(7);
|
|
15
|
+
const [n3, setN3] = useState(5);
|
|
16
|
+
const [q, setQ] = useState('');
|
|
17
|
+
const [qType, setQType] = useState('other');
|
|
18
|
+
const go = () => onDivine({ mode, n1, n2, n3, now: new Date().toISOString() }, undefined, q.trim());
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<div className="field"><label htmlFor="mh-mode">起卦之法</label>
|
|
22
|
+
<SongSelect id="mh-mode" value={mode} options={[{ value: 'time', label: '时间起卦(以当下年月日时)' }, { value: 'num', label: '报数起卦(默念所问,取三数)' }]} onChange={v => setMode(v as 'time' | 'num')} />
|
|
23
|
+
</div>
|
|
24
|
+
{mode === 'num' ? (
|
|
25
|
+
<div className="field-row">
|
|
26
|
+
<div className="field"><label htmlFor="mh-n1">一数</label><input className="input-line" type="number" id="mh-n1" min={1} max={999} value={n1} onChange={e => setN1(Number(e.target.value))} /></div>
|
|
27
|
+
<div className="field"><label htmlFor="mh-n2">二数</label><input className="input-line" type="number" id="mh-n2" min={1} max={999} value={n2} onChange={e => setN2(Number(e.target.value))} /></div>
|
|
28
|
+
</div>
|
|
29
|
+
) : null}
|
|
30
|
+
<div className="field"><label htmlFor="mh-n3">三数(动爻)</label>
|
|
31
|
+
<input className="input-line" type="number" id="mh-n3" min={1} max={999} value={n3} onChange={e => setN3(Number(e.target.value))} /></div>
|
|
32
|
+
<div className="field"><label htmlFor="mh-qtype">所问之属</label>
|
|
33
|
+
<SongSelect id="mh-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} />
|
|
34
|
+
</div>
|
|
35
|
+
<div className="field"><label htmlFor="q-input">所问之事</label>
|
|
36
|
+
<input className="input-line" id="q-input" placeholder="默念所问,书之于此…" maxLength={60} value={q} onChange={e => setQ(e.target.value)} /></div>
|
|
37
|
+
<button className="btn-divine" onClick={go}>起 卦<span className="small">定本卦 · 推互变 · 辨体用</span></button>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function YaoGlyph({ yaos }: { yaos: number[] }) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="yao-glyph" style={{ display: 'flex', flexDirection: 'column', gap: '3px', alignItems: 'center' }}>
|
|
45
|
+
{yaos.map((v, i) => v ? <span key={i} className="yang" style={{ display: 'block', width: '2.6rem', height: '.55rem', background: 'var(--ink)', borderRadius: '1px' }} />
|
|
46
|
+
: <span key={i} className="yin" style={{ display: 'block', width: '2.6rem', height: '.55rem', borderTop: '1.5px solid var(--ink)', borderBottom: '1.5px solid var(--ink)' }} />)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function MeihuaResult({ data }: { data: MeihuaResult }) {
|
|
52
|
+
const r = data;
|
|
53
|
+
const guaName = (g: number | undefined) => (g !== undefined && BAGUA[g] ? BAGUA[g].name : '—');
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<ResultCard title={`起卦 · ${r.benGua.name}`}
|
|
57
|
+
cells={[
|
|
58
|
+
{ k: '上卦 · ' + guaName(r.upper), v: BAGUA[r.upper].symbol, big: true },
|
|
59
|
+
{ k: '下卦 · ' + guaName(r.lower), v: BAGUA[r.lower].symbol, big: true },
|
|
60
|
+
{ k: '动爻', v: '第 ' + r.move + ' 爻', big: true, hot: true },
|
|
61
|
+
]}>
|
|
62
|
+
<p>本卦 <strong>{r.benGua.name}</strong>:{r.benGua.xiang}。动而观变,{r.bianGua.name}为演进之端。</p>
|
|
63
|
+
</ResultCard>
|
|
64
|
+
<div className="result-card">
|
|
65
|
+
<h3>本卦 · 互卦 · 变卦</h3>
|
|
66
|
+
<div className="result-grid" style={{ marginTop: '1rem' }}>
|
|
67
|
+
<div className="result-cell">
|
|
68
|
+
<div className="k">本卦 · {r.benGua.name}</div>
|
|
69
|
+
<YaoGlyph yaos={r.benYao} />
|
|
70
|
+
<div className="tiny muted mt-1">{r.benGua.ci}</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="result-cell">
|
|
73
|
+
<div className="k">互卦 · {r.huGua ? r.huGua.name : '—'}</div>
|
|
74
|
+
<div className="tiny muted mt-1">中四爻交互之象</div>
|
|
75
|
+
<div className="tiny muted mt-1">{r.huGua ? r.huGua.xiang : ''}</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="result-cell">
|
|
78
|
+
<div className="k">变卦 · {r.bianGua.name}</div>
|
|
79
|
+
<div className="tiny muted mt-1">动爻变后之象</div>
|
|
80
|
+
<div className="tiny muted mt-1">{r.bianGua.xiang}</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<ResultCard title="体用生克">
|
|
85
|
+
<p>体卦为 <strong>{guaName(r.tiGua)}{BAGUA[r.tiGua].symbol}</strong>({r.tiWx},主事主自身),用卦为 <strong>{guaName(r.yongGua)}{BAGUA[r.yongGua].symbol}</strong>({r.yongWx},主所问之事)。</p>
|
|
86
|
+
<p>体用之间:<span className={r.shengke.includes('吉') ? 'tag-cool' : r.shengke.includes('凶') ? 'tag-hot' : ''}><strong>{r.shengke}</strong></span>。{r.shengke.includes('生') ? '生者如风送帆,事有助力;' : r.shengke.includes('克') ? '克者如舟行逆水,宜缓宜守;' : '比和者势均,事在人为。'}</p>
|
|
87
|
+
</ResultCard>
|
|
88
|
+
<ResultCard title={`卦辞指引 · ${r.benGua.name}`}>
|
|
89
|
+
<p>「{r.benGua.ci}」</p>
|
|
90
|
+
<p>{r.benGua.xiang}</p>
|
|
91
|
+
<p className="mt-2"><strong>占断参详:</strong>{meihuaAdvice(r)}</p>
|
|
92
|
+
</ResultCard>
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
// 梅花占断参详(结合动爻/体用/变卦,现代语言)
|
|
97
|
+
function meihuaAdvice(r: any): string {
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
parts.push('此卦动于第' + r.move + '爻,' + (r.move <= 3 ? '动在下卦,所问之事多系于内、关于自身之念' : '动在上卦,所问之事多系于外、关于彼方之势') + '。');
|
|
100
|
+
if (r.shengke.includes('吉')) parts.push('体用之间' + r.shengke + ',事有助力,宜顺势推进,但不可恃势而骄。');
|
|
101
|
+
else if (r.shengke.includes('凶')) parts.push('体用之间' + r.shengke + ',事有阻力,宜缓图慎行,先安己而后谋事。');
|
|
102
|
+
else parts.push('体用比和,' + r.shengke + ',事在人为,成败系于一心之诚。');
|
|
103
|
+
if (r.bianGua) parts.push('变卦' + r.bianGua.name + '为事之归趋,其象「' + r.bianGua.xiang + '」,可作收束之鉴。');
|
|
104
|
+
return parts.join('');
|
|
105
|
+
}
|