guanwei 1.1.4 → 1.2.0

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.
@@ -1,9 +1,28 @@
1
- // 奇门遁甲:演示级九宫起局(精确节气定阴阳遁,三候取局)
1
+ // 奇门遁甲:九宫起局(节气定阴阳遁 · 日干支三元定局 · 时干支定旬首)
2
+ // 说明:超神接气/置闰(节气时刻与甲己日不齐时的补闰规则)属专业级范畴,当前按「正授」近似:
3
+ // 以日干支查三元表定局,与交节日正授情形完全一致;超接偏差时可能差一元,后续专业级补齐。
2
4
  import { GAN, ZHI, mod } from '../data/ganzhi';
3
5
  import { QM_SEASONS, QM_STARS, QM_MEN, LUOSHU } from '../data/qimen';
4
6
  import { daysSince, currentJieqiNameExact } from './calendar';
5
7
  import type { QimenInput, QimenResult } from '../types';
6
8
 
9
+
10
+ // 三元定局表(时家奇门标准):行=日干五合组,列=日支组(0=子午卯酉 1=寅申巳亥 2=辰戌丑未)
11
+ // 值 0=上元 1=中元 2=下元。甲子旬前五日上元、次五日中元、甲戌旬前五日下元——已按日干支循环验证自洽。
12
+ const SAN_YUAN: number[][] = [
13
+ [0, 1, 2], // 甲己:子午卯酉上元 寅申巳亥中元 辰戌丑未下元
14
+ [1, 2, 0], // 乙庚:子午卯酉中元 寅申巳亥下元 辰戌丑未上元
15
+ [2, 0, 1], // 丙辛:子午卯酉下元 寅申巳亥上元 辰戌丑未中元
16
+ [0, 1, 2], // 丁壬:同甲己
17
+ [1, 2, 0], // 戊癸:同乙庚
18
+ ];
19
+
20
+ function sanYuanOf(dayGan: string, dayZhi: string): number {
21
+ const ganGroup = ['甲', '己'].includes(dayGan) ? 0 : ['乙', '庚'].includes(dayGan) ? 1 : ['丙', '辛'].includes(dayGan) ? 2 : ['丁', '壬'].includes(dayGan) ? 3 : 4;
22
+ const zhiGroup = ['子', '午', '卯', '酉'].includes(dayZhi) ? 0 : ['寅', '申', '巳', '亥'].includes(dayZhi) ? 1 : 2;
23
+ return SAN_YUAN[ganGroup][zhiGroup];
24
+ }
25
+
7
26
  export function qimenCalc(input: QimenInput): QimenResult {
8
27
  const d = input.datetime instanceof Date ? input.datetime : new Date(input.datetime);
9
28
  const y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
@@ -21,13 +40,16 @@ export function qimenCalc(input: QimenInput): QimenResult {
21
40
  const season = QM_SEASONS.find(s => s.name === jqName);
22
41
  if (!season) throw new Error('节气未找到: ' + jqName);
23
42
  const yin = season.yin;
24
- /* 候(三元):按日数粗略取元 */
25
- const xun = Math.floor(day / 5) % 3;
43
+ /* 三元定局(按日干支,替代旧「按日数粗略取元」) */
44
+ const xun = sanYuanOf(dayGZ[0], dayGZ[1]);
26
45
  const ju = season.ju[xun];
27
- /* 旬首六仪(由时干定) */
28
- const xunshouMap: Record<string, string> = { 戊: '甲子', 己: '甲戌', 庚: '甲申', 辛: '甲午', 壬: '甲辰', 癸: '甲寅' };
29
- const hourGan = hourGZ[0];
30
- const xunShou = ['戊', '己', '庚', '辛', '壬', '癸'].includes(hourGan) ? hourGan : '戊';
46
+ /* 旬首六仪(按时干支真实旬首:时干支序号 → 所在旬 → 六仪) */
47
+ const gzIdx = mod((GAN.indexOf(hourGZ[0] as any) - ZHI.indexOf(hourGZ[1] as any)) * 6 + ZHI.indexOf(hourGZ[1] as any), 60);
48
+ const xunStart = Math.floor(gzIdx / 10) * 10; // 旬首干支序号(甲子=0 甲戌=10 …)
49
+ const xunShouZhi = ZHI[xunStart % 12];
50
+ const xunshouMap: Record<string, string> = { 子: '戊', 戌: '己', 申: '庚', 午: '辛', 辰: '壬', 寅: '癸' };
51
+ const xunShou = xunshouMap[xunShouZhi]; // 旬首六仪(甲子戊 甲戌己 …)
52
+ const xunshouName = '甲' + xunShouZhi;
31
53
  /* 地盘布奇仪:阳遁顺布(1→8→3→4→9→2→7→6→中5),阴遁逆布 */
32
54
  const qiyiOrder = ['戊', '己', '庚', '辛', '壬', '癸', '丁', '丙', '乙'];
33
55
  const yiAt: Record<number, string> = {};
@@ -74,5 +96,5 @@ export function qimenCalc(input: QimenInput): QimenResult {
74
96
  shen[p] = shenSeq[step];
75
97
  });
76
98
  shen[5] = '';
77
- return { yin, ju, jqName, dayGZ, hourGZ, xunShou, xunshouName: xunshouMap[xunShou] || '', zfStar, zsMen, zfPalace, pan, zsPalace, tianYi, shen };
99
+ return { yin, ju, jqName, dayGZ, hourGZ, xunShou, xunshouName, zfStar, zsMen, zfPalace, pan, zsPalace, tianYi, shen };
78
100
  }
@@ -160,6 +160,9 @@ export interface LiurenResult {
160
160
  chuanJiang: { chuan: string; jiang: string }[]; // 三传所乘天将
161
161
  keti: string; // 课体(伏吟/反吟/常课)
162
162
  ketiNote: string; // 课体断语
163
+ // ─── 九宗门起三传(2026-08-23)───
164
+ chuanMethod: string; // 起传法门(元首/始入/重审/知一/涉害/蒿矢/弹射/昴星/别责/八专/返吟/伏吟)
165
+ chuanNote: string; // 起传断语
163
166
  }
164
167
 
165
168
  // 小六壬输出
@@ -67,7 +67,8 @@ export function LiurenResult({ data }: { data: LiurenResult }) {
67
67
  { k: '三课 · 支上', v: r.ke3, big: true },
68
68
  { k: '四课 · 支阴', v: r.ke4, big: true },
69
69
  ]}>
70
- <p>三传(简式):<strong className="tag-hot">初传 {r.chuan1}</strong> → <strong className="tag-cool">中传 {r.chuan2}</strong> → <strong>末传 {r.chuan3}</strong>。</p>
70
+ <p>三传({r.chuanMethod || '起传'}):<strong className="tag-hot">初传 {r.chuan1}</strong> → <strong className="tag-cool">中传 {r.chuan2}</strong> → <strong>末传 {r.chuan3}</strong>。</p>
71
+ {r.chuanNote && <p className="tiny muted">{r.chuanNote}</p>}
71
72
  <p>初传为事发之端,中传为事进之中,末传为事成之归。干为外、支为内,四课之内外,即人事之表里。</p>
72
73
  <p className="mt-2"><strong>三传参详:</strong>{liurenAdvice(r)}</p>
73
74
  </ResultCard>
@@ -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; calc: () => unknown }[] = [
35
- { id: 'bazi', name: '四柱八字', glyph: '命', calc: () => baziCalc(DEMO_BIRTH) },
36
- { id: 'ziwei', name: '紫微斗数', glyph: '星', calc: () => {
37
- const lunar = Solar.fromYmd(DEMO_BIRTH.y, DEMO_BIRTH.m, DEMO_BIRTH.d).getLunar();
38
- return ziweiCalc({ ganzhi: lunar.getYearInGanZhi(), month: Math.abs(lunar.getMonth()), day: lunar.getDay(), hour: DEMO_BIRTH.hourIndex, time: DEMO_BIRTH.time, location: DEMO_BIRTH.location, gender: DEMO_BIRTH.gender, birthYear: DEMO_BIRTH.y, solarDate: [DEMO_BIRTH.y, DEMO_BIRTH.m, DEMO_BIRTH.d] });
39
- } },
40
- { id: 'astrology', name: '古典星盘', glyph: '穹', calc: () => astrologyCalc(DEMO_BIRTH.y, DEMO_BIRTH.m, DEMO_BIRTH.d, 12, 0, DEMO_BIRTH.location.lng, DEMO_BIRTH.location.lat) },
41
- { id: 'qimen', name: '奇门遁甲', glyph: '遁', calc: () => qimenCalc({ datetime: new Date() }) },
42
- { id: 'meihua', name: '梅花易数', glyph: '梅', calc: () => meihuaCalc({ mode: 'time', n1: 3, n2: 5, n3: 7, now: new Date() }) },
43
- { id: 'liuyao', name: '六爻', glyph: '爻', calc: () => liuyaoCalc(undefined, { y: new Date().getFullYear(), m: new Date().getMonth() + 1, d: new Date().getDate() }) },
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,8 +60,38 @@ 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
- const art = ARTS.find(a => a.id === artId)!;
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);
70
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
+ };
71
95
  const reset = () => {
72
96
  setPhase('idle');
73
97
  setReport(null);
@@ -82,7 +106,7 @@ export default function DemoPage() {
82
106
  // 本地引擎排盘(纯浏览器计算,无需后端)
83
107
  setTimeout(() => {
84
108
  try {
85
- setLocalResult(art.calc());
109
+ setLocalResult(buildCalc(artId)());
86
110
  } catch (e) {
87
111
  console.error('本地排盘失败', e);
88
112
  }
@@ -109,9 +133,9 @@ export default function DemoPage() {
109
133
  <div className="wrap" style={{ marginTop: 'var(--sp-4)' }}>
110
134
  {phase === 'idle' && (
111
135
  <div className="result-card" style={{ padding: 'var(--sp-5)' }}>
112
- <p style={{ fontSize: '1.05rem', marginBottom: 'var(--sp-2)' }}>选一种术数,点一下就能看到排盘结果;八字还会附带一份完整的 AI 解读示例。</p>
113
- <p className="tiny muted" style={{ marginBottom: 'var(--sp-2)' }}>示例档案:1988年6月15日 · 北京 · 女;其余术数按当前时刻起例</p>
114
- <p className="tiny muted" style={{ marginBottom: 'var(--sp-3)' }}>三步即玩:① 选术数 → ② 点「开始体验」→ ③ 看结果</p>
136
+ <p style={{ fontSize: '1.05rem', marginBottom: 'var(--sp-2)' }}>选一种术数,填好(或直接用默认)日期时刻,点一下就能看到排盘结果;八字还会附带一份完整的 AI 解读示例。</p>
137
+ <p className="tiny muted" style={{ marginBottom: 'var(--sp-2)' }}>命盘类(八字/紫微/星盘)用下面日期时刻排盘;占问类(奇门/六壬/小六壬)用该时刻起例;梅花可用报数起卦</p>
138
+ <p className="tiny muted" style={{ marginBottom: 'var(--sp-3)' }}>三步即玩:① 选术数 → ② 填日期(可选)→ ③ 点「开始体验」看结果</p>
115
139
  <div className="art-picker" style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem', marginBottom: 'var(--sp-4)' }}>
116
140
  {ARTS.map(a => (
117
141
  <button key={a.id} onClick={() => setArtId(a.id)}
@@ -121,6 +145,27 @@ export default function DemoPage() {
121
145
  </button>
122
146
  ))}
123
147
  </div>
148
+ {/* 演示输入表单:日期/时刻/报数 */}
149
+ <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)' }}>
150
+ <label className="tiny muted">日期
151
+ <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)} />
152
+ <span className="tiny muted" style={{ margin: '0 .2rem' }}>/</span>
153
+ <input className="input-line" type="number" style={{ width: 46 }} value={m} min={1} max={12} onChange={e => setM(Number(e.target.value) || 6)} />
154
+ <span className="tiny muted" style={{ margin: '0 .2rem' }}>/</span>
155
+ <input className="input-line" type="number" style={{ width: 46 }} value={d} min={1} max={31} onChange={e => setD(Number(e.target.value) || 15)} />
156
+ </label>
157
+ <label className="tiny muted">时刻
158
+ <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)))} />
159
+ <span className="tiny muted" style={{ marginLeft: '.3rem' }}>时(24 小时制)</span>
160
+ </label>
161
+ {artId === 'meihua' && (
162
+ <label className="tiny muted">报数
163
+ <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)} />
164
+ <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)} />
165
+ <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)} />
166
+ </label>
167
+ )}
168
+ </div>
124
169
  <button className="btn-seal" style={{ fontSize: '1rem', padding: '.7rem 2.2rem' }} onClick={start}>开 始 体 验</button>
125
170
  </div>
126
171
  )}