guanwei 1.1.4 → 1.2.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/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/assets/index-CQjUFsty.js +102 -0
- package/dist/assets/index-CQjUFsty.js.map +1 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/src/data/db.json +315 -3728
- package/server/src/data/guanwei.db +0 -0
- package/server/src/services/duanyu.ts +67 -0
- package/server/src/services/promptBuilder.ts +5 -0
- package/shared/core/data/duanyu.ts +15 -14
- package/shared/core/engine/bazi.ts +3 -1
- package/shared/core/engine/calendar.ts +10 -1
- package/shared/core/engine/liuren.ts +218 -6
- package/shared/core/engine/qimen.ts +30 -8
- package/shared/core/types.ts +3 -0
- package/src/components/arts/LiurenArt.tsx +2 -1
- package/src/pages/DemoPage.tsx +80 -22
- package/src/pages/ModulePage.tsx +18 -0
- package/dist/assets/index-DmdGzBy9.js +0 -102
- package/dist/assets/index-DmdGzBy9.js.map +0 -1
package/src/pages/DemoPage.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 演示模式(Demo Mode):无需后端 / 无需 API Key 的完整体验页
|
|
2
2
|
// 九术排盘用 shared/core 本地引擎在浏览器计算,AI 报告加载内置示例(静态 JSON)——GitHub Pages 上可直接体验
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Link } from 'react-router-dom';
|
|
5
5
|
import { baziCalc } from '@core/engine/bazi';
|
|
6
6
|
import { ziweiCalc } from '@core/engine/ziwei';
|
|
@@ -31,22 +31,16 @@ import Disclaimer from '@/components/Disclaimer';
|
|
|
31
31
|
const DEMO_BIRTH = { y: 1988, m: 6, d: 15, hourIndex: 6, time: '12:00', gender: '女' as const, location: { lng: 116.4, lat: 39.9, province: '北京市', city: '北京市', district: '东城区' } };
|
|
32
32
|
|
|
33
33
|
// 九术本地排盘(与后端 divine.ts 同一引擎、同一输入口径)
|
|
34
|
-
const ARTS: { id: string; name: string; glyph: string
|
|
35
|
-
{ id: 'bazi', name: '四柱八字', glyph: '命'
|
|
36
|
-
{ id: 'ziwei', name: '紫微斗数', glyph: '星',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{ id: '
|
|
41
|
-
{ id: '
|
|
42
|
-
{ id: '
|
|
43
|
-
{ id: '
|
|
44
|
-
{ id: 'liuren', name: '大六壬', glyph: '课', calc: () => liurenCalc(new Date()) },
|
|
45
|
-
{ id: 'xiaoliuren', name: '小六壬', glyph: '掌', calc: () => xiaoliurenCalc('time', new Date().getMonth() + 1, new Date().getDate(), new Date().getHours() % 12, 3, 5, 7) },
|
|
46
|
-
{ id: 'tarot', name: '塔罗', glyph: '镜', calc: () => {
|
|
47
|
-
const spread = allTarotSpreads().find((x: any) => x.id === 'three') || allTarotSpreads()[0];
|
|
48
|
-
return { spread, cards: tarotDraw(3) };
|
|
49
|
-
} },
|
|
34
|
+
const ARTS: { id: string; name: string; glyph: string }[] = [
|
|
35
|
+
{ id: 'bazi', name: '四柱八字', glyph: '命' },
|
|
36
|
+
{ id: 'ziwei', name: '紫微斗数', glyph: '星' },
|
|
37
|
+
{ id: 'astrology', name: '古典星盘', glyph: '穹' },
|
|
38
|
+
{ id: 'qimen', name: '奇门遁甲', glyph: '遁' },
|
|
39
|
+
{ id: 'meihua', name: '梅花易数', glyph: '梅' },
|
|
40
|
+
{ id: 'liuyao', name: '六爻', glyph: '爻' },
|
|
41
|
+
{ id: 'liuren', name: '大六壬', glyph: '课' },
|
|
42
|
+
{ id: 'xiaoliuren', name: '小六壬', glyph: '掌' },
|
|
43
|
+
{ id: 'tarot', name: '塔罗', glyph: '镜' },
|
|
50
44
|
];
|
|
51
45
|
const RESULT_VIEWS: Record<string, (data: unknown) => React.ReactElement> = {
|
|
52
46
|
bazi: d => <BaziResult data={d as any} />,
|
|
@@ -66,7 +60,50 @@ export default function DemoPage() {
|
|
|
66
60
|
const [showRaw, setShowRaw] = useState(false);
|
|
67
61
|
const [artId, setArtId] = useState('bazi');
|
|
68
62
|
const [localResult, setLocalResult] = useState<unknown>(null);
|
|
69
|
-
|
|
63
|
+
// ─── 演示输入(#2):日期/时刻/报数,替换固定示例档案 ───
|
|
64
|
+
const [y, setY] = useState(DEMO_BIRTH.y);
|
|
65
|
+
const [m, setM] = useState(DEMO_BIRTH.m);
|
|
66
|
+
const [d, setD] = useState(DEMO_BIRTH.d);
|
|
67
|
+
const [hour, setHour] = useState(12);
|
|
68
|
+
const [n1, setN1] = useState(3);
|
|
69
|
+
const [n2, setN2] = useState(5);
|
|
70
|
+
const [n3, setN3] = useState(7);
|
|
71
|
+
|
|
72
|
+
const birth = { ...DEMO_BIRTH, y, m, d, hourIndex: Math.floor((hour % 24) / 2), time: (hour < 10 ? '0' + hour : '' + hour) + ':00' };
|
|
73
|
+
|
|
74
|
+
// 按术别用当前输入起例(命盘类用输入日期;占问类用报数/当前时刻)
|
|
75
|
+
const buildCalc = (id: string): (() => unknown) => {
|
|
76
|
+
switch (id) {
|
|
77
|
+
case 'bazi': return () => baziCalc(birth);
|
|
78
|
+
case 'ziwei': return () => {
|
|
79
|
+
const lunar = Solar.fromYmd(birth.y, birth.m, birth.d).getLunar();
|
|
80
|
+
return ziweiCalc({ ganzhi: lunar.getYearInGanZhi(), month: Math.abs(lunar.getMonth()), day: lunar.getDay(), hour: birth.hourIndex, time: birth.time, location: birth.location, gender: birth.gender, birthYear: birth.y, solarDate: [birth.y, birth.m, birth.d] });
|
|
81
|
+
};
|
|
82
|
+
case 'astrology': return () => astrologyCalc(birth.y, birth.m, birth.d, hour, 0, birth.location.lng, birth.location.lat);
|
|
83
|
+
case 'qimen': return () => qimenCalc({ datetime: new Date(y, m - 1, d, hour, 0) });
|
|
84
|
+
case 'meihua': return () => meihuaCalc({ mode: 'number', n1, n2, n3, now: new Date() } as any);
|
|
85
|
+
case 'liuyao': return () => liuyaoCalc(undefined, { y, m, d });
|
|
86
|
+
case 'liuren': return () => liurenCalc(new Date(y, m - 1, d, hour, 0));
|
|
87
|
+
case 'xiaoliuren': return () => xiaoliurenCalc('time', m, d, Math.floor((hour % 24) / 2), n1, n2, n3);
|
|
88
|
+
case 'tarot': return () => {
|
|
89
|
+
const spread = allTarotSpreads().find((x: any) => x.id === 'three') || allTarotSpreads()[0];
|
|
90
|
+
return { spread, cards: tarotDraw(3) };
|
|
91
|
+
};
|
|
92
|
+
default: return () => null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// 切换术数时清空上一术的排盘结果(否则旧数据传给新术 Result 组件会崩溃)
|
|
97
|
+
const prevArtRef = useRef(artId);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (prevArtRef.current !== artId) {
|
|
100
|
+
prevArtRef.current = artId;
|
|
101
|
+
setPhase('idle');
|
|
102
|
+
setReport(null);
|
|
103
|
+
setLocalResult(null);
|
|
104
|
+
setShowRaw(false);
|
|
105
|
+
}
|
|
106
|
+
}, [artId]);
|
|
70
107
|
|
|
71
108
|
const reset = () => {
|
|
72
109
|
setPhase('idle');
|
|
@@ -82,7 +119,7 @@ export default function DemoPage() {
|
|
|
82
119
|
// 本地引擎排盘(纯浏览器计算,无需后端)
|
|
83
120
|
setTimeout(() => {
|
|
84
121
|
try {
|
|
85
|
-
setLocalResult(
|
|
122
|
+
setLocalResult(buildCalc(artId)());
|
|
86
123
|
} catch (e) {
|
|
87
124
|
console.error('本地排盘失败', e);
|
|
88
125
|
}
|
|
@@ -109,9 +146,9 @@ export default function DemoPage() {
|
|
|
109
146
|
<div className="wrap" style={{ marginTop: 'var(--sp-4)' }}>
|
|
110
147
|
{phase === 'idle' && (
|
|
111
148
|
<div className="result-card" style={{ padding: 'var(--sp-5)' }}>
|
|
112
|
-
<p style={{ fontSize: '1.05rem', marginBottom: 'var(--sp-2)' }}
|
|
113
|
-
<p className="tiny muted" style={{ marginBottom: 'var(--sp-2)' }}
|
|
114
|
-
<p className="tiny muted" style={{ marginBottom: 'var(--sp-3)' }}>三步即玩:① 选术数 → ②
|
|
149
|
+
<p style={{ fontSize: '1.05rem', marginBottom: 'var(--sp-2)' }}>选一种术数,填好(或直接用默认)日期时刻,点一下就能看到排盘结果;八字还会附带一份完整的 AI 解读示例。</p>
|
|
150
|
+
<p className="tiny muted" style={{ marginBottom: 'var(--sp-2)' }}>命盘类(八字/紫微/星盘)用下面日期时刻排盘;占问类(奇门/六壬/小六壬)用该时刻起例;梅花可用报数起卦</p>
|
|
151
|
+
<p className="tiny muted" style={{ marginBottom: 'var(--sp-3)' }}>三步即玩:① 选术数 → ② 填日期(可选)→ ③ 点「开始体验」看结果</p>
|
|
115
152
|
<div className="art-picker" style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem', marginBottom: 'var(--sp-4)' }}>
|
|
116
153
|
{ARTS.map(a => (
|
|
117
154
|
<button key={a.id} onClick={() => setArtId(a.id)}
|
|
@@ -121,6 +158,27 @@ export default function DemoPage() {
|
|
|
121
158
|
</button>
|
|
122
159
|
))}
|
|
123
160
|
</div>
|
|
161
|
+
{/* 演示输入表单:日期/时刻/报数 */}
|
|
162
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem', alignItems: 'center', marginBottom: 'var(--sp-4)', padding: 'var(--sp-3)', border: '1px dashed var(--line-soft)', borderRadius: 'var(--r-sm)' }}>
|
|
163
|
+
<label className="tiny muted">日期
|
|
164
|
+
<input className="input-line" type="number" style={{ width: 70, marginLeft: '.4rem' }} value={y} min={1900} max={2100} onChange={e => setY(Number(e.target.value) || 1988)} />
|
|
165
|
+
<span className="tiny muted" style={{ margin: '0 .2rem' }}>/</span>
|
|
166
|
+
<input className="input-line" type="number" style={{ width: 46 }} value={m} min={1} max={12} onChange={e => setM(Number(e.target.value) || 6)} />
|
|
167
|
+
<span className="tiny muted" style={{ margin: '0 .2rem' }}>/</span>
|
|
168
|
+
<input className="input-line" type="number" style={{ width: 46 }} value={d} min={1} max={31} onChange={e => setD(Number(e.target.value) || 15)} />
|
|
169
|
+
</label>
|
|
170
|
+
<label className="tiny muted">时刻
|
|
171
|
+
<input className="input-line" type="number" style={{ width: 56, marginLeft: '.4rem' }} value={hour} min={0} max={23} onChange={e => setHour(Math.max(0, Math.min(23, Number(e.target.value) || 12)))} />
|
|
172
|
+
<span className="tiny muted" style={{ marginLeft: '.3rem' }}>时(24 小时制)</span>
|
|
173
|
+
</label>
|
|
174
|
+
{artId === 'meihua' && (
|
|
175
|
+
<label className="tiny muted">报数
|
|
176
|
+
<input className="input-line" type="number" style={{ width: 46, marginLeft: '.4rem' }} value={n1} min={1} max={9} onChange={e => setN1(Number(e.target.value) || 3)} />
|
|
177
|
+
<input className="input-line" type="number" style={{ width: 46, marginLeft: '.3rem' }} value={n2} min={1} max={9} onChange={e => setN2(Number(e.target.value) || 5)} />
|
|
178
|
+
<input className="input-line" type="number" style={{ width: 46, marginLeft: '.3rem' }} value={n3} min={1} max={9} onChange={e => setN3(Number(e.target.value) || 7)} />
|
|
179
|
+
</label>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
124
182
|
<button className="btn-seal" style={{ fontSize: '1rem', padding: '.7rem 2.2rem' }} onClick={start}>开 始 体 验</button>
|
|
125
183
|
</div>
|
|
126
184
|
)}
|
package/src/pages/ModulePage.tsx
CHANGED
|
@@ -129,6 +129,24 @@ export default function ModulePage() {
|
|
|
129
129
|
const divineIdRef = useRef<string | null>(null);
|
|
130
130
|
divineIdRef.current = divineId;
|
|
131
131
|
|
|
132
|
+
// ─── 术数切换(/art/qimen → /art/ziwei)必须全量重置 ───
|
|
133
|
+
// 若不重置:上一术的 status='done' + result 会传给新术的 Result 组件,
|
|
134
|
+
// 新术访问旧数据结构中不存在的字段 → TypeError → ErrorBoundary 兜底显示「此页一时未应机」
|
|
135
|
+
const prevArtRef = useRef(artId);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (prevArtRef.current !== artId) {
|
|
138
|
+
prevArtRef.current = artId;
|
|
139
|
+
ai.reset(); // 清 AI 解读 + 在途流式请求作废(reqId 失效)
|
|
140
|
+
reset(); // 清排盘 status/result/divineId
|
|
141
|
+
setFit(null);
|
|
142
|
+
profileRef.current = null;
|
|
143
|
+
questionRef.current = '';
|
|
144
|
+
inputsRef.current = null;
|
|
145
|
+
profileIdRef.current = 'main';
|
|
146
|
+
savedRef.current = null;
|
|
147
|
+
}
|
|
148
|
+
}, [artId, ai, reset]);
|
|
149
|
+
|
|
132
150
|
// 起占成功 → 自动存档(StrictMode 防重复:以 result 引用去重)
|
|
133
151
|
const savedRef = useRef<unknown>(null);
|
|
134
152
|
useEffect(() => {
|