dsh-metaphysics 0.59.1 → 0.60.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.
- package/CHANGELOG.md +58 -1
- package/client.almanac.js +61 -60
- package/client.bazi.js +112 -111
- package/client.chenggu.js +61 -60
- package/client.convert.js +60 -59
- package/client.daliuren.js +116 -115
- package/client.fengshui.js +61 -60
- package/client.hepan.js +108 -107
- package/client.js +352 -290
- package/client.liuyao.js +113 -112
- package/client.mbti.js +63 -62
- package/client.name.js +61 -60
- package/client.numerology.js +63 -62
- package/client.qimen.js +116 -115
- package/client.star.js +66 -65
- package/client.tarot.js +116 -115
- package/client.trend.js +112 -111
- package/client.xiaoliuren.js +77 -76
- package/client.zeri.js +61 -60
- package/client.ziwei.js +110 -109
- package/engine/calendar.js +8 -2
- package/engine/dst.js +28 -1
- package/engine/handle.js +6 -2
- package/engine/index.js +85 -13
- package/engine/lunar.js +10 -11
- package/engine/random.js +9 -1
- package/engine/rectify.js +24 -3
- package/engine/trend.js +29 -20
- package/engine/zeri.js +25 -9
- package/engine/ziwei.js +5 -2
- package/index.js +29 -12
- package/package.json +4 -3
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +722 -78
|
@@ -240,9 +240,16 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
240
240
|
header{flex-direction:column;align-items:stretch;gap:10px;margin-bottom:12px}
|
|
241
241
|
header>*{min-width:0}
|
|
242
242
|
header{gap:10px;padding-bottom:12px}
|
|
243
|
-
.brand{font-size:19px;letter-spacing:.1em}
|
|
243
|
+
.brand{font-size:19px;letter-spacing:.1em;min-width:0}
|
|
244
|
+
/* 标题与「分组/平铺」同一排:换行会给窄屏多顶出一整行工具栏 */
|
|
245
|
+
.brand-row{flex-wrap:nowrap}
|
|
246
|
+
.brand{flex:1 1 auto;min-width:0}
|
|
247
|
+
.view-switch{flex:none}
|
|
248
|
+
.view-switch .seg button{white-space:nowrap}
|
|
244
249
|
.brand .seal{width:34px;height:34px;border-radius:9px;font-size:17px}
|
|
245
|
-
|
|
250
|
+
/* 一行副标题 + 省略号:窄屏下让它换行会把「分组/平铺」顶到下一排,整块头重脚轻 */
|
|
251
|
+
.brand-txt{min-width:0;overflow:hidden}
|
|
252
|
+
.brand-txt small{font-size:11.5px;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
246
253
|
/* 分组卡片:每组先露 4 个,其余折起来;否则三组铺完,表单要滚过一整屏才见到 */
|
|
247
254
|
.view-switch{gap:6px}
|
|
248
255
|
.view-switch .hint{display:none}
|
|
@@ -347,7 +354,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
347
354
|
<div class="wrap">
|
|
348
355
|
<header>
|
|
349
356
|
<div class="brand-row">
|
|
350
|
-
<div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.
|
|
357
|
+
<div class="brand"><span class="seal" aria-hidden="true">命</span><span class="brand-txt">命理排盘<span class="ver">v0.60.0</span><small>八字 · 紫微 · 六爻 · 梅花 · 六壬 · 奇门 · 合盘 · 塔罗 全部在本机计算</small></span></div>
|
|
351
358
|
<div class="view-switch">
|
|
352
359
|
<span class="hint">熟练了可以换回平铺</span>
|
|
353
360
|
<span class="seg" role="group" aria-label="首屏样式">
|
|
@@ -379,6 +386,10 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
379
386
|
</div>
|
|
380
387
|
</header>
|
|
381
388
|
|
|
389
|
+
<!-- 我的命盘(本机档案):卡片列表 + 存入条,渲染与存储都取自 panel-kit,
|
|
390
|
+
样式在 PP.CSS 里;放在入口之上——回来第一件事通常是接着排上次那个人。 -->
|
|
391
|
+
<div id="profwrap"></div>
|
|
392
|
+
|
|
382
393
|
<!-- 首屏分组:同一批入口按「想干什么」重排,点击走 web-ui 的 setTab;平铺视图下整块收起 -->
|
|
383
394
|
<section class="start" id="start" aria-label="选择要算的事">
|
|
384
395
|
<div class="recent" id="recent" hidden>
|
|
@@ -634,7 +645,7 @@ h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--li
|
|
|
634
645
|
<h3>对拍:把典籍当期望值</h3>
|
|
635
646
|
<ul><li>六爻:京房八宫卦序与世应,64 卦全表;旬空六旬表</li><li>六壬:日干寄宫、中气换将;紫微:纳音定五行局、紫微天府相照</li><li>卦例:《增删卜易》9 条 +《卜筮正宗》128 条(93 成对 + 35 静卦),核本变卦 / 动爻数 / 卦宫 / 缺爻 / 旬空</li><li>奇门:20 个时辰 × 9 宫,核四柱 / 局数 / 值符 / 值使 / 星门神 / 天盘干</li></ul>
|
|
636
647
|
<h3>工程验证</h3>
|
|
637
|
-
<ul><li>测试:
|
|
648
|
+
<ul><li>测试:147 个测试文件 / 7139 条断言;七道闸(回归 · 断言基线 · 测试可失败自检 · HTML 注入面 · 工程约定 · 产物宿主形态 · 随机扫射)</li><li>可复现:seed / asOf 显式;同一输入同一输出</li><li>口径:晚子时、真太阳时、奇门三流派、闰月归月都可切换,并有字段级差异对照</li></ul>
|
|
638
649
|
<h3>已知边界(不猜)</h3>
|
|
639
650
|
<ul><li>六爻应期(具体哪天应验)不做;卦例只录动爻个数,不录爻位</li><li>财富/健康只给结构判断与档位,不给金额区间、不作诊断</li><li>紫微飞星已做「单一支化逐宫追到成环」与「三方四正 16 条按落宫归堆」,但都未叠大限流年四化、不折吉凶;八字交运日已能算到日/时,但未与第三方排盘案例批量对拍</li></ul>
|
|
640
651
|
<p class="note">内容用于传统文化学习与自我探索,不构成医疗、投资、法律或人事决策依据。</p>
|
|
@@ -1600,13 +1611,19 @@ function termInstantJd(year, index) {
|
|
|
1600
1611
|
function jdToParts(jdLocal) {
|
|
1601
1612
|
const jdn = Math.floor(jdLocal + 0.5);
|
|
1602
1613
|
const frac = jdLocal + 0.5 - jdn;
|
|
1603
|
-
|
|
1614
|
+
// 按「整分钟向下取」而不是四舍五入:23:59:45 一进位就成了 1440 分,
|
|
1615
|
+
// 旧写法把小时取模、日序不动,于是这一瞬时塌回「当日 00:00」——
|
|
1616
|
+
// 日柱退一天、partsToJd 往返回来差将近一天(晚子时换日与真太阳时跨午夜正好踩在刀口上)。
|
|
1617
|
+
// 向下取后 hour/minute 与 jdn 永远自洽:拆出来的那一天,就是这一刻所在的那一天。
|
|
1618
|
+
// 但向下取会被浮点噪声咬住:JD 里的 13:00 常算成 779.99999 分,直接 floor 就是 12:59,
|
|
1619
|
+
// 未时变成午时。先归到最近的整秒(输入精度到分,误差远小于一秒),再向下取整分。
|
|
1620
|
+
const totalMinutes = Math.min(1439, Math.floor(Math.round(frac * 86400) / 60));
|
|
1604
1621
|
const { year, month, day } = fromJDN(jdn);
|
|
1605
1622
|
return {
|
|
1606
1623
|
year,
|
|
1607
1624
|
month,
|
|
1608
1625
|
day,
|
|
1609
|
-
hour: Math.floor(totalMinutes / 60)
|
|
1626
|
+
hour: Math.floor(totalMinutes / 60),
|
|
1610
1627
|
minute: totalMinutes % 60,
|
|
1611
1628
|
};
|
|
1612
1629
|
}
|
|
@@ -1733,7 +1750,7 @@ return { STEMS, BRANCHES, TERM_NAMES, termLongitude, toJDN, fromJDN, sexagenaryI
|
|
|
1733
1750
|
|
|
1734
1751
|
__mods.lunar = (function () {
|
|
1735
1752
|
// 农历(定朔):朔望月计算 + 冬至所在月为十一月 + 无中气置闰
|
|
1736
|
-
const { toJDN, jdToParts, termInstantJd, deltaTSeconds, localOffsetFor
|
|
1753
|
+
const { toJDN, jdToParts, termInstantJd, deltaTSeconds, localOffsetFor } = (__mods.calendar || {});
|
|
1737
1754
|
|
|
1738
1755
|
const SYNODIC = 29.530588861;
|
|
1739
1756
|
const NEW_MOON_EPOCH = 2451550.09766;
|
|
@@ -1795,12 +1812,11 @@ function dayNumberOf(jd) {
|
|
|
1795
1812
|
return Math.floor(jd + 0.5);
|
|
1796
1813
|
}
|
|
1797
1814
|
|
|
1798
|
-
//
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1815
|
+
// 时区切换常量与换算统一在 calendar.js:termInstantJd 与 newMoonJd 都已经落在
|
|
1816
|
+
// 「当时的本地时间轴」上(1929 前北京地方平时,之后东八区),两边直接比日序就行。
|
|
1817
|
+
// 以前这里再套一次 localOffset 把节气拉回本地轴,等于对同一个量移了两次时:
|
|
1818
|
+
// 净偏移约 −12 分,节气时刻一旦落在午夜前后十几分钟内就少算一天,
|
|
1819
|
+
// 中气归属错一个月,置闰跟着整月错位(1917 报闰三、真历表闰二;1922 报闰六、真历表闰五)。
|
|
1804
1820
|
|
|
1805
1821
|
function kNear(jdDay) {
|
|
1806
1822
|
return Math.round((jdDay + 0.5 - NEW_MOON_EPOCH) / SYNODIC);
|
|
@@ -1810,7 +1826,7 @@ function kNear(jdDay) {
|
|
|
1810
1826
|
function hasZhongQi(year, startDay, endDay) {
|
|
1811
1827
|
for (let y = year - 1; y <= year + 1; y++) {
|
|
1812
1828
|
for (let i = 1; i < 24; i += 2) {
|
|
1813
|
-
const jdLocal =
|
|
1829
|
+
const jdLocal = termInstantJd(y, i);
|
|
1814
1830
|
const d = dayNumberOf(jdLocal);
|
|
1815
1831
|
if (d >= startDay && d < endDay) return { index: i, jd: jdLocal };
|
|
1816
1832
|
}
|
|
@@ -1825,9 +1841,9 @@ function hasZhongQi(year, startDay, endDay) {
|
|
|
1825
1841
|
function solarToLunar(year, month, day) {
|
|
1826
1842
|
const targetDay = toJDN(year, month, day);
|
|
1827
1843
|
|
|
1828
|
-
const wsDayThisYear = dayNumberOf(
|
|
1844
|
+
const wsDayThisYear = dayNumberOf(termInstantJd(year, 23));
|
|
1829
1845
|
const wsYear = targetDay < wsDayThisYear ? year - 1 : year;
|
|
1830
|
-
const wsDay = dayNumberOf(
|
|
1846
|
+
const wsDay = dayNumberOf(termInstantJd(wsYear, 23));
|
|
1831
1847
|
|
|
1832
1848
|
// 十一月朔:日序不晚于冬至的最后一个朔日
|
|
1833
1849
|
let k = kNear(wsDay);
|
|
@@ -1837,7 +1853,7 @@ function solarToLunar(year, month, day) {
|
|
|
1837
1853
|
// ── 置闰前提(国标口径)──
|
|
1838
1854
|
// 只有「两个冬至月之间有 13 个朔望月」才需要置闰,且取其中第一个无中气的月。
|
|
1839
1855
|
// 若只有 12 个月,即使某月无中气也不闰(1985 正月、2053 正月就是这种反例)。
|
|
1840
|
-
const nextWsDay = dayNumberOf(
|
|
1856
|
+
const nextWsDay = dayNumberOf(termInstantJd(wsYear + 1, 23));
|
|
1841
1857
|
let kNext = k;
|
|
1842
1858
|
while (dayNumberOf(newMoonJd(kNext + 1)) <= nextWsDay) kNext++;
|
|
1843
1859
|
const cycleMonths = kNext - k;
|
|
@@ -2112,7 +2128,15 @@ __mods.random = (function () {
|
|
|
2112
2128
|
|
|
2113
2129
|
/** mulberry32:32 位状态,分布够用,实现只有几行 */
|
|
2114
2130
|
function mulberry32(seed) {
|
|
2115
|
-
|
|
2131
|
+
const n = Number(seed);
|
|
2132
|
+
// 状态只有 32 位,但不能只截低 32 位(`>>> 0`):相差 2^32 的种子会跑出同一串数。
|
|
2133
|
+
// 拿毫秒时间戳当种子时,隔 49.7 天的两次摇卦会得到完全一样的卦——用户换了种子却看不出区别,
|
|
2134
|
+
// 「可复现」就退成了「可猜测」。所以把高位与小数位各折一份进状态;
|
|
2135
|
+
// 整数种子(0…2^31)的低 32 位仍原样参与,小种子不会莫名其妙换一卦。
|
|
2136
|
+
const lo = n >>> 0;
|
|
2137
|
+
const hi = Math.floor(Math.abs(n) / 4294967296) >>> 0;
|
|
2138
|
+
const frac = Math.round((n - Math.trunc(n)) * 4294967296) >>> 0;
|
|
2139
|
+
let a = (lo ^ Math.imul(hi + 1, 0x85ebca6b) ^ frac) >>> 0;
|
|
2116
2140
|
if (!a) a = 0x9e3779b9;
|
|
2117
2141
|
return function next() {
|
|
2118
2142
|
a = (a + 0x6d2b79f5) >>> 0;
|
|
@@ -2181,6 +2205,26 @@ function inDstSpan(t) {
|
|
|
2181
2205
|
return true;
|
|
2182
2206
|
}
|
|
2183
2207
|
|
|
2208
|
+
/**
|
|
2209
|
+
* 换表当天绕不开的两个钟面区间:
|
|
2210
|
+
* - 起算日 02:00–02:59:钟表从 02:00 直接拨到 03:00,这个读数在当年根本不存在;
|
|
2211
|
+
* - 终止日 01:00–01:59:钟表往回拨一小时,同一读数会走两遍,对应两个真实时刻。
|
|
2212
|
+
* 两者都不是「算错了」,而是输入本身有歧义,需要告诉调用方我们按哪种读法折的。
|
|
2213
|
+
* @param {{year:number,month:number,day:number,hour:number,minute?:number}} t
|
|
2214
|
+
* @returns {'gap'|'twice'|null}
|
|
2215
|
+
*/
|
|
2216
|
+
function dstAmbiguity(t) {
|
|
2217
|
+
const span = DST_SPANS[t.year];
|
|
2218
|
+
if (!span) return null;
|
|
2219
|
+
const [sy, sm, sd] = span[0].split('-').map(Number);
|
|
2220
|
+
const [ey, em, ed] = span[1].split('-').map(Number);
|
|
2221
|
+
const n = ORD(t.year, t.month, t.day);
|
|
2222
|
+
const minutes = t.hour * 60 + (t.minute || 0);
|
|
2223
|
+
if (n === ORD(sy, sm, sd) && minutes >= 120 && minutes < 180) return 'gap';
|
|
2224
|
+
if (n === ORD(ey, em, ed) && minutes >= 60 && minutes < 120) return 'twice';
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2184
2228
|
/**
|
|
2185
2229
|
* 夏令时校正:钟面时间减 1 小时(跨日正确回退)。
|
|
2186
2230
|
* @param {{year:number,month:number,day:number,hour:number,minute?:number}} t
|
|
@@ -2200,10 +2244,17 @@ function applyDst(t) {
|
|
|
2200
2244
|
day = daysInMonth(year, month);
|
|
2201
2245
|
}
|
|
2202
2246
|
}
|
|
2247
|
+
const amb = dstAmbiguity(t);
|
|
2203
2248
|
return {
|
|
2204
2249
|
year, month, day, hour, minute,
|
|
2205
2250
|
applied: true,
|
|
2206
|
-
note: `检出自 ${DST_SPANS[t.year][0]} 至 ${DST_SPANS[t.year][1]} 的中国夏令时:出生钟面时间已减 1
|
|
2251
|
+
note: `检出自 ${DST_SPANS[t.year][0]} 至 ${DST_SPANS[t.year][1]} 的中国夏令时:出生钟面时间已减 1 小时校正`
|
|
2252
|
+
+ (amb === 'gap'
|
|
2253
|
+
? ';这一天 02:00–03:00 钟表直接跳过,所给钟面时间在当年并不存在——这里按「拨快后的读数」回退一小时,'
|
|
2254
|
+
+ '若登记的本是标准时间,请传 dst:false 重排。'
|
|
2255
|
+
: amb === 'twice'
|
|
2256
|
+
? ';这一天 01:00–02:00 因回拨走了两遍,同一钟面时间对应两个真实时刻——这里按夏令时的那一遍(回退一小时)取。'
|
|
2257
|
+
: ''),
|
|
2207
2258
|
};
|
|
2208
2259
|
}
|
|
2209
2260
|
|
|
@@ -2214,7 +2265,7 @@ function withDstNote(notes, dst) {
|
|
|
2214
2265
|
return out;
|
|
2215
2266
|
}
|
|
2216
2267
|
|
|
2217
|
-
return { DST_SPANS, inDstSpan, applyDst, withDstNote };
|
|
2268
|
+
return { DST_SPANS, inDstSpan, dstAmbiguity, applyDst, withDstNote };
|
|
2218
2269
|
})();
|
|
2219
2270
|
|
|
2220
2271
|
__mods.shensha = (function () {
|
|
@@ -7570,8 +7621,11 @@ function buildZiwei(input) {
|
|
|
7570
7621
|
};
|
|
7571
7622
|
})();
|
|
7572
7623
|
|
|
7573
|
-
//
|
|
7574
|
-
|
|
7624
|
+
// 流年(基准年起 12 年):基准年优先用调用方传来的 asOf,缺省才落本机当年。
|
|
7625
|
+
// 直接读挂钟的话,同一组参数(同一个句柄)跨年会排出不同的流年表,
|
|
7626
|
+
// 「同参数同盘」的承诺就只在当年成立。
|
|
7627
|
+
const asOfForLiunian = asOfParts(input.asOf);
|
|
7628
|
+
const thisYear = (asOfForLiunian && asOfForLiunian.year) || new Date().getFullYear();
|
|
7575
7629
|
const liunian = [];
|
|
7576
7630
|
for (let y = thisYear; y < thisYear + 12; y++) {
|
|
7577
7631
|
const gz = ganzhiName((((y - 4) % 60) + 60) % 60);
|
|
@@ -9486,21 +9540,37 @@ function tenGodOfZhi(dayStem, branch) {
|
|
|
9486
9540
|
|
|
9487
9541
|
const yearGanzhi = (year) => ganzhiName(((year - 4) % 60 + 60) % 60);
|
|
9488
9542
|
|
|
9489
|
-
/**
|
|
9490
|
-
|
|
9491
|
-
|
|
9543
|
+
/**
|
|
9544
|
+
* 大运分段,按「交运那一年」切,年龄落在与 points[].age 同一条虚岁轴上。
|
|
9545
|
+
*
|
|
9546
|
+
* 不能直接用 dayun.startAge:那是周岁(三日用折一年从出生日算起),
|
|
9547
|
+
* 而 K 线一年一格、`age` 就是虚岁(= 年份 − 出生年 + 1,与八字流年表同一口径)。
|
|
9548
|
+
* 两套轴混在一起会让整条线提前一年:1990-05-12 男命 1998-08 交壬午,
|
|
9549
|
+
* 旧写法把 1997(还没起运)划进壬午,2007(仍在壬午)却标成癸未。
|
|
9550
|
+
* 交运日期本来就是同一份数据的精确表达,所以按它的年份起算;
|
|
9551
|
+
* 引擎只排八步,八步之外按十年一格推年份,干支仍按六十甲子顺/逆推。
|
|
9552
|
+
*/
|
|
9553
|
+
function buildSegments(list, direction, birthYear, toAge) {
|
|
9554
|
+
const toYear = birthYear + toAge - 1;
|
|
9492
9555
|
const step = direction === '逆行' ? -1 : 1;
|
|
9556
|
+
const years = [];
|
|
9557
|
+
for (const d of list) years.push({ ganZhi: d.ganzhi, startYear: Number(String(d.fromDate).slice(0, 4)) });
|
|
9493
9558
|
let guard = 0;
|
|
9494
|
-
while (
|
|
9495
|
-
const last =
|
|
9496
|
-
//
|
|
9497
|
-
const lastIdx = sexagenaryIndex(STEMS.indexOf(last.
|
|
9498
|
-
const
|
|
9499
|
-
|
|
9500
|
-
out.push({ step: last.step + 1, startAge: last.startAge + 10, ganzhi: g.name, stem: g.stem, branch: g.branch, stemTenGod: last.stemTenGod });
|
|
9559
|
+
while (years.length && years[years.length - 1].startYear <= toYear && guard < 24) {
|
|
9560
|
+
const last = years[years.length - 1];
|
|
9561
|
+
// ganZhi 是「甲辰」这样的名字,不是六十甲子序号:先换回序号再顺/逆推一步
|
|
9562
|
+
const lastIdx = sexagenaryIndex(STEMS.indexOf(last.ganZhi[0]), BRANCHES.indexOf(last.ganZhi[1]));
|
|
9563
|
+
const g = ganzhiName((lastIdx + step + 60) % 60);
|
|
9564
|
+
years.push({ ganZhi: g.name, startYear: last.startYear + 10 });
|
|
9501
9565
|
guard++;
|
|
9502
9566
|
}
|
|
9503
|
-
return
|
|
9567
|
+
return years
|
|
9568
|
+
.map((s, i) => ({
|
|
9569
|
+
ganZhi: s.ganZhi,
|
|
9570
|
+
startAge: s.startYear - birthYear + 1,
|
|
9571
|
+
endAge: (years[i + 1] ? years[i + 1].startYear : s.startYear + 10) - birthYear,
|
|
9572
|
+
}))
|
|
9573
|
+
.filter((s) => s.startAge <= toAge);
|
|
9504
9574
|
}
|
|
9505
9575
|
|
|
9506
9576
|
/**
|
|
@@ -9536,16 +9606,9 @@ function buildTrend(input) {
|
|
|
9536
9606
|
const scoreOf = (god) => table[god] ?? 0;
|
|
9537
9607
|
const boundary = LEVEL_CUTS.some((c) => Math.abs(r.strength.score - c) <= BOUNDARY_EPS);
|
|
9538
9608
|
|
|
9539
|
-
const dayun = extendDayun(r.dayun.list, r.dayun.direction, toAge);
|
|
9540
|
-
const segments = dayun
|
|
9541
|
-
.map((d, i) => ({
|
|
9542
|
-
ganZhi: d.ganzhi,
|
|
9543
|
-
startAge: d.startAge,
|
|
9544
|
-
endAge: (dayun[i + 1] ? dayun[i + 1].startAge : d.startAge + 10) - 1,
|
|
9545
|
-
}))
|
|
9546
|
-
.filter((s) => s.startAge <= toAge);
|
|
9547
|
-
|
|
9548
9609
|
const birthYear = Number(String(r.solarTime.civilText).slice(0, 4));
|
|
9610
|
+
const segments = buildSegments(r.dayun.list, r.dayun.direction, birthYear, toAge);
|
|
9611
|
+
|
|
9549
9612
|
const points = [];
|
|
9550
9613
|
let prev = 50;
|
|
9551
9614
|
for (let age = 1; age <= toAge; age++) {
|
|
@@ -13855,6 +13918,11 @@ __mods.zeri = (function () {
|
|
|
13855
13918
|
// 口径透明:这是排序引导,不是吉凶断语;未收录逐日神煞,冲煞按日支对生肖。
|
|
13856
13919
|
const { fromJDN, toJDN } = (__mods.calendar || {});
|
|
13857
13920
|
const { buildAlmanac, SHENGXIAO } = (__mods.almanac || {});
|
|
13921
|
+
const { fail } = (__mods.fail || {});
|
|
13922
|
+
|
|
13923
|
+
// 择日的入参错误一律带 E_INPUT:裸 new Error 到工具层只剩 [E_INTERNAL],
|
|
13924
|
+
// 模型拿到的是一句没有指向性的文案,改不了参数。
|
|
13925
|
+
const bad = (message) => fail('E_INPUT', message);
|
|
13858
13926
|
|
|
13859
13927
|
/** 支持的事项:直接宜词 + 关联宜词(黄历措辞)。 */
|
|
13860
13928
|
const TASKS = {
|
|
@@ -13957,39 +14025,50 @@ const ymd = (p) => `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day
|
|
|
13957
14025
|
*/
|
|
13958
14026
|
function buildZeri(input) {
|
|
13959
14027
|
const task = input.task;
|
|
13960
|
-
if (!TASKS[task]) throw
|
|
14028
|
+
if (!TASKS[task]) throw bad(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
|
|
13961
14029
|
const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
|
|
13962
14030
|
const shengXiao = shengXiaoRaw || null;
|
|
13963
|
-
if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw
|
|
14031
|
+
if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw bad(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
|
|
13964
14032
|
|
|
13965
14033
|
// 候选日模式:只比用户给的这几天,不再按区间扫
|
|
13966
14034
|
let candidates = null;
|
|
13967
14035
|
if (input.candidates !== undefined && input.candidates !== null) {
|
|
13968
14036
|
if (!Array.isArray(input.candidates)) {
|
|
13969
|
-
throw
|
|
14037
|
+
throw bad('candidates 须为日期数组,例如 ["2026-12-01","2026-12-08","2026-12-15"]');
|
|
13970
14038
|
}
|
|
13971
|
-
if (input.candidates.length < 2) throw
|
|
13972
|
-
if (input.candidates.length > 12) throw
|
|
14039
|
+
if (input.candidates.length < 2) throw bad('candidates 至少给 2 个候选日;只想看一天请用 startDate + days: 1');
|
|
14040
|
+
if (input.candidates.length > 12) throw bad('candidates 最多 12 个候选日;更多请用 startDate + days 扫区间');
|
|
13973
14041
|
const seen = new Set();
|
|
13974
14042
|
candidates = [];
|
|
13975
14043
|
for (const raw of input.candidates) {
|
|
13976
14044
|
const p = parseYmd(raw);
|
|
13977
|
-
if (!p) throw
|
|
14045
|
+
if (!p) throw bad(`候选日不是真实存在的公历日期:「${raw}」,请用 2026-12-01 这种格式`);
|
|
13978
14046
|
const key = ymd(p);
|
|
13979
14047
|
if (seen.has(key)) continue;
|
|
13980
14048
|
seen.add(key);
|
|
13981
14049
|
candidates.push(p);
|
|
13982
14050
|
}
|
|
13983
|
-
if (candidates.length < 2) throw
|
|
14051
|
+
if (candidates.length < 2) throw bad('候选日去重后不足 2 天;至少要两个不同的日子才谈得上比较');
|
|
13984
14052
|
}
|
|
13985
14053
|
|
|
13986
14054
|
let start = null;
|
|
13987
14055
|
let days = 0;
|
|
13988
14056
|
if (!candidates) {
|
|
13989
14057
|
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
|
|
13990
|
-
if (!m) throw
|
|
14058
|
+
if (!m) throw bad('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
|
|
13991
14059
|
start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
|
|
13992
|
-
days
|
|
14060
|
+
// days 以前只认 Number.isInteger,于是 "45"(模型很爱给字符串)与 45.5 都静默变成 30 天:
|
|
14061
|
+
// 用户要的区间被悄悄改短,而结果里只看得到实际扫了几天,看不出被改过。
|
|
14062
|
+
// 现在:能规范成整数的(含数字串)就规范,范围外或非数字一律报错,不猜。
|
|
14063
|
+
const rawDays = input.days;
|
|
14064
|
+
if (rawDays === undefined || rawDays === null || rawDays === '') {
|
|
14065
|
+
days = 30;
|
|
14066
|
+
} else {
|
|
14067
|
+
const n = Number(rawDays);
|
|
14068
|
+
if (!Number.isInteger(n)) throw bad(`择日的 days 要是 1–60 的整数,收到「${rawDays}」`);
|
|
14069
|
+
if (n < 1 || n > 60) throw bad(`择日的 days 要在 1–60 之间(${n} 超出范围);更长请用分段多次查询`);
|
|
14070
|
+
days = n;
|
|
14071
|
+
}
|
|
13993
14072
|
}
|
|
13994
14073
|
|
|
13995
14074
|
const spec = TASKS[task];
|
|
@@ -18841,6 +18920,8 @@ const { buildBazi } = (__mods.bazi || {});
|
|
|
18841
18920
|
|
|
18842
18921
|
const KINDS = ['career', 'wealth', 'love', 'move', 'health', 'other'];
|
|
18843
18922
|
const HOURS = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23];
|
|
18923
|
+
/** 事件条数上限:对账用不到几百条,超限是输入异常而不是需求 */
|
|
18924
|
+
const MAX_EVENTS = 200;
|
|
18844
18925
|
|
|
18845
18926
|
const TEN_GOD = (l) => l.stemTenGod || '';
|
|
18846
18927
|
const has = (l, re) => re.test((l.relations || []).join(''));
|
|
@@ -18899,10 +18980,29 @@ function rectifyBirth(opts) {
|
|
|
18899
18980
|
e.code = 'E_INPUT';
|
|
18900
18981
|
throw e;
|
|
18901
18982
|
}
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18983
|
+
// 事件只用来对账,几十条已足够。不设上限的后果很具体:20 万条会原样回显成 2MB 结果,
|
|
18984
|
+
// 并且下面的取最小值(Math.min(...arr) 逐元素入参)直接把栈打爆。
|
|
18985
|
+
if (clean.length > MAX_EVENTS) {
|
|
18986
|
+
const e = new Error(`生时校正的事件最多 ${MAX_EVENTS} 条,收到 ${clean.length} 条——只报最近几十年里确实发生过的事即可`);
|
|
18987
|
+
e.code = 'E_INPUT';
|
|
18988
|
+
throw e;
|
|
18989
|
+
}
|
|
18990
|
+
if (clean.some((x) => x.year < 1600 || x.year > 2400)) {
|
|
18991
|
+
const e = new Error('事件年份要落在 1600–2400 之间(与排盘支持的年份一致)');
|
|
18992
|
+
e.code = 'E_INPUT';
|
|
18993
|
+
throw e;
|
|
18994
|
+
}
|
|
18995
|
+
const nowYear = clean.reduce((m, x) => (x.year < m ? x.year : m), clean[0].year);
|
|
18996
|
+
// 候选时辰同样要限:一天只有 12 个时辰,给了 1000 个就是白排 1000 张盘
|
|
18997
|
+
const wanted = Array.isArray(opts.hours) && opts.hours.length
|
|
18998
|
+
? [...new Set(opts.hours.filter((h) => Number.isInteger(h) && h >= 0 && h <= 23))]
|
|
18905
18999
|
: HOURS;
|
|
19000
|
+
if (!wanted.length) {
|
|
19001
|
+
const e = new Error('候选时辰里没有有效的小时(0–23 的整数)');
|
|
19002
|
+
e.code = 'E_INPUT';
|
|
19003
|
+
throw e;
|
|
19004
|
+
}
|
|
19005
|
+
const hours = wanted.slice(0, 24);
|
|
18906
19006
|
|
|
18907
19007
|
const candidates = hours.map((hour) => {
|
|
18908
19008
|
const chart = buildBazi({
|
|
@@ -19656,7 +19756,7 @@ const PREFIX = 'mc1.';
|
|
|
19656
19756
|
* 反过来,seed / asOf / direction / 各流派开关都必须留下——少了它们就复现不出同一张盘。
|
|
19657
19757
|
*/
|
|
19658
19758
|
const SKIP = new Set([
|
|
19659
|
-
'handle', 'trace', 'shadow', 'shadowVersion', 'diff', 'withInterpret', 'cross',
|
|
19759
|
+
'handle', 'trace', 'shadow', 'shadowVersion', 'diff', 'withInterpret', 'detail', 'cross',
|
|
19660
19760
|
'verifyPast', 'rectify', 'events', 'ask', 'followup', 'question', 'geocode', 'caliber',
|
|
19661
19761
|
// 一句话改参是把原话翻成补丁:补丁已经落在其余参数里,原话本身不进句柄,
|
|
19662
19762
|
// 否则同一张盘换个说法就换一个句柄,回放时还会再套一遍补丁。
|
|
@@ -19804,6 +19904,9 @@ function decodeHandle(handle) {
|
|
|
19804
19904
|
return payload;
|
|
19805
19905
|
}
|
|
19806
19906
|
|
|
19907
|
+
/** 不能作为增量键的名字:`out['__proto__'] = v` 走的是 setter,会把 out 的原型换掉 */
|
|
19908
|
+
const KEY_BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
|
|
19909
|
+
|
|
19807
19910
|
/**
|
|
19808
19911
|
* 把 handle 与增量参数并起来。没有 handle 时原样返回(深一层的浅拷贝,避免改调用方对象)。
|
|
19809
19912
|
* 增量优先:{ handle, asOf: 2030 } 只覆盖 asOf,其余沿用原参数。
|
|
@@ -19816,7 +19919,8 @@ function resolveHandle(args = {}) {
|
|
|
19816
19919
|
// 显式的 undefined 当作「没传」:否则 { handle, datetime: undefined } 会把句柄里的日期抹掉
|
|
19817
19920
|
const out = { ...base };
|
|
19818
19921
|
for (const [k, v] of Object.entries(rest)) {
|
|
19819
|
-
if (v
|
|
19922
|
+
if (v === undefined || KEY_BLOCKED.has(k)) continue;
|
|
19923
|
+
out[k] = v;
|
|
19820
19924
|
}
|
|
19821
19925
|
return out;
|
|
19822
19926
|
}
|
|
@@ -21281,13 +21385,17 @@ function ziweiSchoolArgs(args) {
|
|
|
21281
21385
|
return out;
|
|
21282
21386
|
}
|
|
21283
21387
|
|
|
21284
|
-
/** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD'
|
|
21388
|
+
/** 解析 'YYYY-MM-DDTHH:mm' / 'YYYY-MM-DD HH:mm' / 'YYYY-MM-DD'
|
|
21389
|
+
* 可以带秒(用户常直接粘 ISO 串),但引擎的民用时间口径到分钟:秒只校验、不参与换算。 */
|
|
21285
21390
|
function parseDateTime(text) {
|
|
21286
21391
|
if (typeof text !== 'string' || !text.trim()) {
|
|
21287
21392
|
throw fail('E_INPUT', '缺少 datetime(公历日期时间),例如 1990-05-12T14:30');
|
|
21288
21393
|
}
|
|
21289
21394
|
const m = text.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
|
|
21290
21395
|
if (!m) throw fail('E_INPUT', `无法解析 datetime:「${text}」,请用 1990-05-12T14:30 这种格式`);
|
|
21396
|
+
// 秒位以前只捕获不校验:\d{1,2} 放过 0–99,于是「T03:04:99」被静默折成 03:04。
|
|
21397
|
+
// 与 1990-02-30 同一类问题——非法输入悄悄变成一个合法时刻,盘面照样排出,没人知道。
|
|
21398
|
+
if (m[6] !== undefined && Number(m[6]) > 59) throw fail('E_INPUT', `秒超出范围:${m[6]}`);
|
|
21291
21399
|
const out = {
|
|
21292
21400
|
year: Number(m[1]), month: Number(m[2]), day: Number(m[3]),
|
|
21293
21401
|
hour: Number(m[4] ?? 0), minute: Number(m[5] ?? 0),
|
|
@@ -21722,7 +21830,7 @@ function jsonSafe(value) {
|
|
|
21722
21830
|
// ── 影子盘:同一组归一化参数 + 同一版本 → 同一个短 ID ──────────────
|
|
21723
21831
|
// 只收影响盘面的参数:地址换成有效经度后不再把原文算进去,问事/方向按体系保留。
|
|
21724
21832
|
const SHADOW_IGNORE = new Set([
|
|
21725
|
-
'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'cross', 'verifyPast', 'rectify', 'events',
|
|
21833
|
+
'shadow', 'shadowVersion', 'diff', 'trace', 'traceError', 'withInterpret', 'detail', 'cross', 'verifyPast', 'rectify', 'events',
|
|
21726
21834
|
'geocode', 'ask', 'followup', 'caliber', 'seed', 'asOf', 'handle', 'revise',
|
|
21727
21835
|
]);
|
|
21728
21836
|
|
|
@@ -21961,6 +22069,8 @@ function compute(input = {}) {
|
|
|
21961
22069
|
args = { ...args };
|
|
21962
22070
|
delete args.partner2;
|
|
21963
22071
|
}
|
|
22072
|
+
// 经度整形 + 守卫(非有限 / 超范围 → E_RANGE;数字串 → 数字)
|
|
22073
|
+
args = normalizeLongitudes(args);
|
|
21964
22074
|
const r = jsonSafe(computeRaw(args));
|
|
21965
22075
|
// 「所问之事」以前只有六爻和塔罗会落到结果里,梅花/六壬/奇门/小六壬都丢掉了:
|
|
21966
22076
|
// 于是第一屏想说「你问的是……」却拿不到那句话。这里统一补齐,已有的值不动。
|
|
@@ -22009,11 +22119,15 @@ ${r.summary}`;
|
|
|
22009
22119
|
${r.summary}`;
|
|
22010
22120
|
}
|
|
22011
22121
|
// 显式要求时才附加解析模块,避免默认输出过长;「所问之事」与「询问方向」一并传进去
|
|
22122
|
+
const tier = detailOf(args.detail);
|
|
22012
22123
|
if (args.withInterpret) {
|
|
22013
22124
|
r.interpret = interpret(r, {
|
|
22014
22125
|
question: typeof args.question === 'string' ? args.question : '',
|
|
22015
22126
|
direction: typeof args.direction === 'string' ? args.direction : 'general',
|
|
22016
22127
|
});
|
|
22128
|
+
// 档位只改「回给模型的文本」,不改解析对象:面板拿到的仍是完整的 interpret。
|
|
22129
|
+
// 键名不用 detail / tier:合盘结果里 r.detail 是详批对象、流年行里 r.tier 是档位,都会撞。
|
|
22130
|
+
if (tier === 'lean') r.interpretTier = 'lean';
|
|
22017
22131
|
}
|
|
22018
22132
|
// 八字 × 紫微交叉验证:同一份出生信息再排另一套盘,并列两边对同一件事的说法
|
|
22019
22133
|
// 交叉验证要再排另一套盘:时辰未知时紫微排不出来,这条支路直接跳过,
|
|
@@ -22109,9 +22223,13 @@ ${r.summary}`;
|
|
|
22109
22223
|
return r;
|
|
22110
22224
|
}
|
|
22111
22225
|
|
|
22112
|
-
/** 解析与详解的紧凑文本(给模型看,不塞整块 JSON)
|
|
22113
|
-
|
|
22226
|
+
/** 解析与详解的紧凑文本(给模型看,不塞整块 JSON)
|
|
22227
|
+
* detail:'full'(默认,与历史输出逐字一致)或 'lean'(只给结论、你问的那条与三张表,
|
|
22228
|
+
* 长论述与「规则·出处」折成一行指针)。省的是回给模型的文本,不改盘面、不改解析对象本身。 */
|
|
22229
|
+
function interpretText(i, detail) {
|
|
22114
22230
|
if (!i) return '';
|
|
22231
|
+
const lean = detail === 'lean';
|
|
22232
|
+
const LONG_SECTION = 300;
|
|
22115
22233
|
const out = [`【${i.title || '解析与详解'}】${i.ask && i.ask.echo ? i.ask.echo : ''}`];
|
|
22116
22234
|
// 直答放在最前:先回答「你问的事」,再说依据,避免读者读到最后才知道结论;
|
|
22117
22235
|
// 没填问题时就不该喊「答所问」——改为「先看结论」,否则是一句假话
|
|
@@ -22124,16 +22242,27 @@ function interpretText(i) {
|
|
|
22124
22242
|
}
|
|
22125
22243
|
// 解析生成失败时不能只返回一个空壳:盘面数据仍然可用,但要让调用方知道"这段没生成出来"
|
|
22126
22244
|
if (i.error) out.push(`⚠ 解析生成失败:${i.error}(上方盘面数据本身可用)`);
|
|
22127
|
-
|
|
22245
|
+
const allDims = i.dimensions || [];
|
|
22246
|
+
// 问了方向就只给那一条:其余四条在 full 档里,模型要展开随时可以再排一次
|
|
22247
|
+
const dims = lean && allDims.some((d) => d && d.focused) ? allDims.filter((d) => d && d.focused) : allDims;
|
|
22248
|
+
for (const d of dims) {
|
|
22128
22249
|
const evidence = Array.isArray(d.evidence) && d.evidence.length ? `\n 依据:${d.evidence.join(';')}${d.confidence ? ` 置信度:${d.confidence}` : ''}` : '';
|
|
22129
22250
|
const plain = d.plain ? `(${d.plain})` : '';
|
|
22130
22251
|
// 知识条目:「为什么这样断」+出处+可信度标注,是结论与依据之外的第三层
|
|
22131
|
-
const know =
|
|
22132
|
-
|
|
22133
|
-
.
|
|
22252
|
+
const know = lean
|
|
22253
|
+
? ((d.knowledge || []).length ? `\n 规则 ${d.knowledge.length} 条未展开(detail:"full" 给出原文与出处)` : '')
|
|
22254
|
+
: (d.knowledge || [])
|
|
22255
|
+
.map((k) => `\n 规则·${k.gist}〔${k.statusLabel}〕${k.detail} 出处:${k.source}${k.note ? ` 注:${k.note}` : ''}`)
|
|
22256
|
+
.join('');
|
|
22134
22257
|
out.push(`${d.label}:${plain}${d.text}${d.advice ? ` 建议:${d.advice}` : ''}${evidence}${know}`);
|
|
22135
22258
|
}
|
|
22136
|
-
|
|
22259
|
+
const secs = i.sections || [];
|
|
22260
|
+
const longSecs = lean ? secs.filter((s) => String(s.text || '').length > LONG_SECTION) : [];
|
|
22261
|
+
for (const s of secs) {
|
|
22262
|
+
if (lean && String(s.text || '').length > LONG_SECTION) continue;
|
|
22263
|
+
out.push(`${s.label}:${s.text}`);
|
|
22264
|
+
}
|
|
22265
|
+
if (longSecs.length) out.push(`(另有 ${longSecs.length} 节长论述未展开:${longSecs.map((s) => s.label).join('、')}。需要原文与出处时传 detail:"full")`);
|
|
22137
22266
|
// 三张表:大运 / 流年 / 触发事件(纯文本对齐,工具输出也能直接读)
|
|
22138
22267
|
for (const t of i.tables || []) {
|
|
22139
22268
|
if (!Array.isArray(t.rows) || !t.rows.length) continue;
|
|
@@ -22157,7 +22286,7 @@ function interpretText(i) {
|
|
|
22157
22286
|
|
|
22158
22287
|
/** 对照时不参与计算的参数:它们只影响解析/附加模块,不影响盘面 */
|
|
22159
22288
|
// shadow / trace 是结果的派生展示,不是盘面口径;带进来会让影子盘回显把每组开关都算成「有变化」
|
|
22160
|
-
const CALIBER_IGNORE_ARGS = ['cross', 'verifyPast', 'withInterpret', 'question', 'direction', 'ask', 'geocode', 'rectify', 'events', 'shadow', 'trace'];
|
|
22289
|
+
const CALIBER_IGNORE_ARGS = ['cross', 'verifyPast', 'withInterpret', 'detail', 'question', 'direction', 'ask', 'geocode', 'rectify', 'events', 'shadow', 'trace'];
|
|
22161
22290
|
|
|
22162
22291
|
/**
|
|
22163
22292
|
* 口径对照:同一组输入,只把某一个口径开关换成它的另一档,逐字段列出变化。
|
|
@@ -22225,15 +22354,62 @@ const JU_METHODS = ['chaibu', 'chaoshen', 'zhirun'];
|
|
|
22225
22354
|
const TAROT_COUNTS = [1, 3, 5];
|
|
22226
22355
|
const LATE_ZI_MODES = ['same-day', 'next-day'];
|
|
22227
22356
|
|
|
22357
|
+
/**
|
|
22358
|
+
* 经度守卫:入口只判 `typeof === 'number'`,于是 JSON 里的 1e999(= Infinity)
|
|
22359
|
+
* 会一路走到真太阳时里炸成裸 TypeError,181 这类超范围值则静默排出另一张盘。
|
|
22360
|
+
* 两者都比报错坏:一个把堆栈式文案交给模型,一个把错结果当成对结果说出去。
|
|
22361
|
+
*/
|
|
22362
|
+
function checkLongitude(value, who = '') {
|
|
22363
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
22364
|
+
const n = Number(value);
|
|
22365
|
+
const tag = who ? `${who}的` : '';
|
|
22366
|
+
if (!Number.isFinite(n)) throw fail('E_RANGE', `${tag}经度不是有限数值:收到「${value}」。经度写十进制数字即可,例如北京 116.4`);
|
|
22367
|
+
if (n < -180 || n > 180) throw fail('E_RANGE', `${tag}经度要在 −180 到 180 之间(东经为正),收到 ${n}`);
|
|
22368
|
+
return n;
|
|
22369
|
+
}
|
|
22370
|
+
|
|
22371
|
+
/**
|
|
22372
|
+
* 把主方 / 对方 / 第三方的经度统一成数字。
|
|
22373
|
+
* 下游按 `typeof === 'number'` 取值,所以 "86.0"(模型常给带引号的数)会被静默丢掉,
|
|
22374
|
+
* 少一次真太阳时修正而结果里毫无痕迹——这里先整形,再交给校验过的值。
|
|
22375
|
+
*/
|
|
22376
|
+
function normalizeLongitudes(args) {
|
|
22377
|
+
const self = checkLongitude(args.longitude, '');
|
|
22378
|
+
const p = args.partner && typeof args.partner === 'object' ? checkLongitude(args.partner.longitude, '对方') : undefined;
|
|
22379
|
+
const p2 = args.partner2 && typeof args.partner2 === 'object' ? checkLongitude(args.partner2.longitude, '第三方') : undefined;
|
|
22380
|
+
const dirty = (self !== undefined && args.longitude !== self)
|
|
22381
|
+
|| (p !== undefined && args.partner.longitude !== p)
|
|
22382
|
+
|| (p2 !== undefined && args.partner2.longitude !== p2);
|
|
22383
|
+
if (!dirty) return args;
|
|
22384
|
+
const out = { ...args };
|
|
22385
|
+
if (self !== undefined) out.longitude = self;
|
|
22386
|
+
if (p !== undefined) out.partner = { ...out.partner, longitude: p };
|
|
22387
|
+
if (p2 !== undefined) out.partner2 = { ...out.partner2, longitude: p2 };
|
|
22388
|
+
return out;
|
|
22389
|
+
}
|
|
22390
|
+
|
|
22391
|
+
/** 输出档位:缺省与 'full' 都是历史全文;'lean' 只回结论与你问的那一条,省模型上下文。
|
|
22392
|
+
* 其它值以前会被当成「缺省」静默放过,写了 detail:'brief' 的人以为自己省了钱,其实没有。 */
|
|
22393
|
+
function detailOf(value) {
|
|
22394
|
+
if (value === undefined || value === null || value === '' || value === 'full') return 'full';
|
|
22395
|
+
if (value === 'lean') return 'lean';
|
|
22396
|
+
throw fail('E_INPUT', `detail 只能是 'full' 或 'lean':收到「${String(value).slice(0, 24)}」`);
|
|
22397
|
+
}
|
|
22398
|
+
|
|
22228
22399
|
/** asOf 只接受能解析成真实日期的字符串;非法值以前会静默回退到「此刻」 */
|
|
22229
22400
|
function checkAsOf(asOf) {
|
|
22230
22401
|
if (asOf === undefined || asOf === null || asOf === '') return undefined;
|
|
22231
22402
|
const parts = asOfParts(asOf);
|
|
22232
22403
|
if (!parts) throw fail('E_ASOF', `无法解析 asOf:「${asOf}」,请用 2026-10-22T16:00 这种格式`);
|
|
22233
|
-
const { year, month, day } = parts;
|
|
22404
|
+
const { year, month, day, hour, minute } = parts;
|
|
22234
22405
|
if (!(month >= 1 && month <= 12) || !(day >= 1 && day <= daysInMonth(year, month))) {
|
|
22235
22406
|
throw fail('E_ASOF', `asOf 指向的日期不存在:${asOf}`);
|
|
22236
22407
|
}
|
|
22408
|
+
// 时分以前不校验:asOf: '2026-06-01T25:99' 会被接受并原样回显,
|
|
22409
|
+
// 而小时又按 mod 24 折回当日子时——流日/流时静默错位,比报错更坏。
|
|
22410
|
+
// 与 parseDateTime 同一把尺:给了就必须在范围内,缺省才由下游补默认值。
|
|
22411
|
+
if (hour !== undefined && !(hour >= 0 && hour <= 23)) throw fail('E_ASOF', `asOf 的小时要在 0–23 之间:${asOf}`);
|
|
22412
|
+
if (minute !== undefined && !(minute >= 0 && minute <= 59)) throw fail('E_ASOF', `asOf 的分钟要在 0–59 之间:${asOf}`);
|
|
22237
22413
|
return parts;
|
|
22238
22414
|
}
|
|
22239
22415
|
|
|
@@ -22279,7 +22455,7 @@ function computeRaw(args = {}) {
|
|
|
22279
22455
|
// 流月/流日/流时的基准时刻:默认本机此刻,可用 asOf 指定以便复现
|
|
22280
22456
|
asOf,
|
|
22281
22457
|
});
|
|
22282
|
-
return { ...result, precision: dt.precision, summary: ziweiText(result) };
|
|
22458
|
+
return { ...result, ...(asOf ? { asOf } : {}), precision: dt.precision, summary: ziweiText(result) };
|
|
22283
22459
|
}
|
|
22284
22460
|
if (system === 'tarot') {
|
|
22285
22461
|
const count = args.count === undefined || args.count === null || args.count === '' ? 3 : Number(args.count);
|
|
@@ -22938,7 +23114,7 @@ function renderText(result) {
|
|
|
22938
23114
|
+ result.trace.map((t, i) => `${i + 1}. ${t.field}|${t.result}\n 规则:${t.rule} 出处:${t.source}`).join('\n'));
|
|
22939
23115
|
}
|
|
22940
23116
|
if (result.interpret) {
|
|
22941
|
-
const extra = interpretText(result.interpret);
|
|
23117
|
+
const extra = interpretText(result.interpret, result.interpretTier);
|
|
22942
23118
|
if (extra) parts.push(extra);
|
|
22943
23119
|
}
|
|
22944
23120
|
if (result.cross) parts.push(`【八字 × 紫微 交叉验证】\n${crossText(result.cross)}`);
|
|
@@ -23007,7 +23183,7 @@ return { interpret, DIRECTIONS, DIRECTION_DEFAULT, BRIGHTNESS, flowOf, geocodeAd
|
|
|
23007
23183
|
})();
|
|
23008
23184
|
|
|
23009
23185
|
window.Engine = __mods.index;
|
|
23010
|
-
window.__MP_VER = "0.
|
|
23186
|
+
window.__MP_VER = "0.60.0";
|
|
23011
23187
|
</script>
|
|
23012
23188
|
<script>
|
|
23013
23189
|
// 两个 UI 共用的表单规则:宿主面板(src/app.js)与独立页(src/web-ui.js)。
|
|
@@ -23119,7 +23295,7 @@ window.__MP_VER = "0.59.1";
|
|
|
23119
23295
|
// 共用渲染层:紫微/八字/六爻/梅花/六壬/塔罗 -> HTML 字符串。面板与独立页面共用同一份。
|
|
23120
23296
|
(function () {
|
|
23121
23297
|
const CSS = `
|
|
23122
|
-
.pp,.mp-it,.mp-al,.mp-cg,.mp-cl,.mp-cli,.mp-kline,.mp-mb,.mp-mb-paper,.mp-nm,.mp-quick,.mp-cdiff,.mp-shadow,.mp-trace-card,.mp-trace-layer{--ink:#1b1b1b;--ink2:#3f3a33;--muted:#6f6a60;--label:#8a8378;--faint:#a9a296;
|
|
23298
|
+
.pp,.mp-it,.mp-al,.mp-cg,.mp-cl,.mp-cli,.mp-kline,.mp-mb,.mp-mb-paper,.mp-nm,.mp-quick,.mp-cdiff,.mp-shadow,.mp-trace-card,.mp-trace-layer,.mp-prof{--ink:#1b1b1b;--ink2:#3f3a33;--muted:#6f6a60;--label:#8a8378;--faint:#a9a296;
|
|
23123
23299
|
--line:#c8c1b2;--dimline:#b9b2a4;--hair:#e9e2d3;
|
|
23124
23300
|
/* 底色略沉作「桌面」,--card 跳到近白:原先 bg 与 card 只差 1/255,卡片感等于零。
|
|
23125
23301
|
同一色相里塞 9 个几乎相等的浅色,谁都浮不起来。 */
|
|
@@ -23140,7 +23316,7 @@ window.__MP_VER = "0.59.1";
|
|
|
23140
23316
|
/* 深色:只换变量,不动任何一条选择器。用 @media screen 限定,
|
|
23141
23317
|
打印永远走浅色 —— 深色主题下直接打印会得到一整页耗墨的深底 PDF。 */
|
|
23142
23318
|
@media screen and (prefers-color-scheme:dark){
|
|
23143
|
-
.pp,.mp-it,.mp-al,.mp-cg,.mp-cl,.mp-cli,.mp-kline,.mp-mb,.mp-mb-paper,.mp-nm,.mp-quick,.mp-cdiff,.mp-shadow,.mp-trace-card,.mp-trace-layer{--ink:#eae5d9;--ink2:#d5cfc2;--muted:#aca598;--label:#a49c8e;--faint:#928b7e;
|
|
23319
|
+
.pp,.mp-it,.mp-al,.mp-cg,.mp-cl,.mp-cli,.mp-kline,.mp-mb,.mp-mb-paper,.mp-nm,.mp-quick,.mp-cdiff,.mp-shadow,.mp-trace-card,.mp-trace-layer,.mp-prof{--ink:#eae5d9;--ink2:#d5cfc2;--muted:#aca598;--label:#a49c8e;--faint:#928b7e;
|
|
23144
23320
|
--line:#514a3e;--dimline:#5f5849;--hair:#2f2b23;
|
|
23145
23321
|
/* 与浅色同理:底更沉、卡片更亮,让层级在深底上也能立起来 */
|
|
23146
23322
|
--bg:#12110c;--card:#211f18;--band:#282419;--hover:#2e2a1f;--hover2:#383322;
|
|
@@ -24414,6 +24590,44 @@ window.__MP_VER = "0.59.1";
|
|
|
24414
24590
|
.mp-hy-row{grid-template-columns:12px 30px minmax(56px,1fr) 42px 38px 28px;gap:4px;font-size:11.5px}
|
|
24415
24591
|
.mp-hy-row.hy-plain{grid-template-columns:28px minmax(56px,1fr);gap:6px}
|
|
24416
24592
|
}
|
|
24593
|
+
/* ── 我的命盘(本机档案)──
|
|
24594
|
+
照市面排盘 App 的「命盘列表」做:一张卡一个人,点卡直接出盘,
|
|
24595
|
+
改名/置顶/删除收在卡片右上角,hover 或键盘聚焦才出现——平时不占视觉。 */
|
|
24596
|
+
.mp-prof{margin:0 0 14px;font-family:system-ui,-apple-system,"Microsoft YaHei",sans-serif}
|
|
24597
|
+
.mp-prof-h{display:flex;align-items:center;gap:8px;margin-bottom:7px}
|
|
24598
|
+
.mp-prof-h b{font-size:13.5px;color:var(--ink);letter-spacing:.02em}
|
|
24599
|
+
.mp-prof-h .mp-prof-sub{font-size:11.5px;color:var(--muted)}
|
|
24600
|
+
.mp-prof-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(164px,1fr));gap:8px}
|
|
24601
|
+
.mp-prof-card{position:relative;border:1px solid var(--line);border-radius:12px;background:var(--card);transition:border-color .12s,box-shadow .12s}
|
|
24602
|
+
.mp-prof-card:hover,.mp-prof-card:focus-within{border-color:var(--red);box-shadow:0 1px 2px rgba(35,28,18,.06)}
|
|
24603
|
+
.mp-prof-card.pinned{border-color:var(--quan)}
|
|
24604
|
+
.mp-prof-use{display:flex;flex-direction:column;gap:2px;width:100%;text-align:left;background:transparent;border:none;
|
|
24605
|
+
padding:9px 10px;cursor:pointer;color:inherit;font:inherit;line-height:1.4}
|
|
24606
|
+
.mp-prof-nm{display:flex;align-items:center;gap:5px;font-size:13.5px;font-weight:600;color:var(--ink);
|
|
24607
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:52px}
|
|
24608
|
+
.mp-prof-nm .sx{font-size:11px;font-weight:400;color:var(--muted)}
|
|
24609
|
+
.mp-prof-sys{align-self:flex-start;font-size:10.5px;line-height:16px;padding:0 6px;border-radius:8px;
|
|
24610
|
+
background:var(--tri);color:var(--quan);border:1px solid color-mix(in srgb,var(--quan) 28%,transparent)}
|
|
24611
|
+
.mp-prof-bd{font-size:11.5px;color:var(--ink2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
24612
|
+
.mp-prof-at{font-size:11px;color:var(--faint)}
|
|
24613
|
+
.mp-prof-ops{position:absolute;top:6px;right:6px;display:none;gap:4px}
|
|
24614
|
+
.mp-prof-card:hover .mp-prof-ops,.mp-prof-card:focus-within .mp-prof-ops{display:flex}
|
|
24615
|
+
.mp-prof-ops button{font:inherit;font-size:10.5px;line-height:18px;padding:0 6px;border-radius:6px;cursor:pointer;
|
|
24616
|
+
border:1px solid var(--line);background:var(--card);color:var(--muted)}
|
|
24617
|
+
.mp-prof-ops button:hover{color:var(--red);border-color:var(--red)}
|
|
24618
|
+
.mp-prof-ops button.on{color:var(--quan);border-color:var(--quan)}
|
|
24619
|
+
/* 触屏没有 hover:改名/置顶/删除若只靠悬停出现,手机上永远点不到,所以常驻 */
|
|
24620
|
+
@media (hover:none){.mp-prof-ops{display:flex}.mp-prof-nm{padding-right:0}}
|
|
24621
|
+
.mp-prof-empty{margin:0;padding:10px 12px;border:1px dashed var(--line);border-radius:10px;
|
|
24622
|
+
font-size:12px;color:var(--muted);background:var(--band)}
|
|
24623
|
+
.mp-prof-bar{display:flex;align-items:center;gap:8px;margin-top:9px;flex-wrap:wrap}
|
|
24624
|
+
.mp-prof-bar input{font:inherit;font-size:12.5px;padding:6px 9px;border-radius:9px;border:1px solid var(--line);
|
|
24625
|
+
background:var(--card);color:var(--ink);min-width:0;flex:1 1 150px;max-width:220px}
|
|
24626
|
+
.mp-prof-bar button{font:inherit;font-size:12.5px;padding:6px 12px;border-radius:9px;cursor:pointer;
|
|
24627
|
+
border:1px solid var(--red);background:var(--red);color:var(--on-accent)}
|
|
24628
|
+
.mp-prof-bar button.ghost{background:transparent;color:var(--muted);border-color:var(--line)}
|
|
24629
|
+
.mp-prof-bar .mp-prof-tip{font-size:11.5px;color:var(--muted)}
|
|
24630
|
+
@media (max-width:360px){.mp-prof-list{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}}
|
|
24417
24631
|
`;
|
|
24418
24632
|
const pp = (window.__PP = window.__PP || {});
|
|
24419
24633
|
pp.CSS = CSS;
|
|
@@ -24435,7 +24649,9 @@ window.__MP_VER = "0.59.1";
|
|
|
24435
24649
|
};
|
|
24436
24650
|
const POS_NAME = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
|
|
24437
24651
|
|
|
24438
|
-
|
|
24652
|
+
// 单引号也转:本仓拼接属性一律用双引号,但「少一个字符就破功」的写法不该赌下一位
|
|
24653
|
+
// 贡献者守不守约定(text-node 之外的任何位置都可能被 ' 截断)。
|
|
24654
|
+
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
24439
24655
|
|
|
24440
24656
|
/**
|
|
24441
24657
|
* 单爻线:阳一整条,阴两段;动爻描红。
|
|
@@ -26154,6 +26370,286 @@ window.__MP_VER = "0.59.1";
|
|
|
26154
26370
|
return diff;
|
|
26155
26371
|
};
|
|
26156
26372
|
|
|
26373
|
+
// ── 我的命盘(本机档案)────────────────────────────────────────
|
|
26374
|
+
// 存的是「输入」而不是结果:换台机器或清掉站点数据就没有,也不含出生时间以外的身份信息。
|
|
26375
|
+
// 面板与单页共用这一份实现与这一份渲染,两边看到的是同一批档案。
|
|
26376
|
+
const PROF_KEY = 'dsh-metaphysics/profiles-v1';
|
|
26377
|
+
const PROF_MAX = 24;
|
|
26378
|
+
const PROF_LABEL_MAX = 16;
|
|
26379
|
+
const PROF_ADDR_MAX = 40;
|
|
26380
|
+
const PROF_NOTE_MAX = 60;
|
|
26381
|
+
const PROF_KINDS = ['bazi', 'ziwei', 'liuyao', 'tarot', 'meihua', 'daliuren', 'hepan', 'qimen',
|
|
26382
|
+
'almanac', 'zeri', 'chenggu', 'xiaoliuren', 'star', 'numerology', 'mbti', 'trend', 'fengshui', 'name', 'convert'];
|
|
26383
|
+
pp.PROF_KEY = PROF_KEY;
|
|
26384
|
+
pp.PROF_MAX = PROF_MAX;
|
|
26385
|
+
pp.PROF_KINDS = PROF_KINDS;
|
|
26386
|
+
|
|
26387
|
+
// 档案卡上的体系短名。面板与单页各自的 tab 标签更长(带说明),这里只要两三个字
|
|
26388
|
+
const PROF_KIND_LABEL = {
|
|
26389
|
+
bazi: '八字', ziwei: '紫微', liuyao: '六爻', tarot: '塔罗', meihua: '梅花', daliuren: '六壬',
|
|
26390
|
+
hepan: '合盘', qimen: '奇门', almanac: '黄历', zeri: '择日', chenggu: '称骨', xiaoliuren: '小六壬',
|
|
26391
|
+
star: '星座', numerology: '生命数字', mbti: '人格', trend: '人生K线', fengshui: '风水', name: '起名', convert: '农历',
|
|
26392
|
+
};
|
|
26393
|
+
pp.profKindLabel = (k) => PROF_KIND_LABEL[k] || String(k || '');
|
|
26394
|
+
|
|
26395
|
+
const isStr = (v) => typeof v === 'string';
|
|
26396
|
+
|
|
26397
|
+
/**
|
|
26398
|
+
* 档案来自 localStorage,等于一份用户可以自己改的外部输入:
|
|
26399
|
+
* 逐字段验一遍再放进列表,坏条目整条丢掉(不是把 undefined 渲染进卡片)。
|
|
26400
|
+
*/
|
|
26401
|
+
function profClean(raw) {
|
|
26402
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
26403
|
+
if (!isStr(raw.id) || !raw.id || raw.id.length > 40) return null;
|
|
26404
|
+
if (!isStr(raw.kind) || PROF_KINDS.indexOf(raw.kind) < 0) return null;
|
|
26405
|
+
// 日期要真过一遍日历:'2020-13-45' 形如合法,但会让引擎拿一个不存在的日子排出张怪盘
|
|
26406
|
+
const dm = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(raw.date || ''));
|
|
26407
|
+
if (!dm) return null;
|
|
26408
|
+
const y = Number(dm[1]);
|
|
26409
|
+
const mo = Number(dm[2]);
|
|
26410
|
+
const dd = Number(dm[3]);
|
|
26411
|
+
if (mo < 1 || mo > 12 || dd < 1 || dd > new Date(y, mo, 0).getDate()) return null;
|
|
26412
|
+
const time = isStr(raw.time) && /^([01]?\d|2[0-3]):[0-5]\d$/.test(raw.time) ? raw.time : '';
|
|
26413
|
+
return {
|
|
26414
|
+
id: raw.id,
|
|
26415
|
+
kind: raw.kind,
|
|
26416
|
+
label: isStr(raw.label) ? raw.label.slice(0, PROF_LABEL_MAX) : '',
|
|
26417
|
+
date: raw.date,
|
|
26418
|
+
time: raw.hourUnknown === true ? '' : time,
|
|
26419
|
+
hourUnknown: raw.hourUnknown === true,
|
|
26420
|
+
gender: raw.gender === 'male' || raw.gender === 'female' ? raw.gender : '',
|
|
26421
|
+
address: isStr(raw.address) ? raw.address.slice(0, PROF_ADDR_MAX) : '',
|
|
26422
|
+
note: isStr(raw.note) ? raw.note.slice(0, PROF_NOTE_MAX) : '',
|
|
26423
|
+
pinned: raw.pinned === true,
|
|
26424
|
+
createdAt: Number(raw.createdAt) || 0,
|
|
26425
|
+
lastUsedAt: Number(raw.lastUsedAt) || 0,
|
|
26426
|
+
uses: Number(raw.uses) || 0,
|
|
26427
|
+
};
|
|
26428
|
+
}
|
|
26429
|
+
|
|
26430
|
+
function profReadRaw() {
|
|
26431
|
+
try {
|
|
26432
|
+
const raw = window.localStorage ? window.localStorage.getItem(PROF_KEY) : null;
|
|
26433
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
26434
|
+
return Array.isArray(list) ? list : [];
|
|
26435
|
+
} catch (e) { return []; }
|
|
26436
|
+
}
|
|
26437
|
+
|
|
26438
|
+
/** 置顶在前,其余按最近排过;同一秒内按创建时间兜底,保证两次渲染顺序一致 */
|
|
26439
|
+
function profSort(list) {
|
|
26440
|
+
return list.slice().sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)
|
|
26441
|
+
|| (b.lastUsedAt || b.createdAt || 0) - (a.lastUsedAt || a.createdAt || 0)
|
|
26442
|
+
|| (b.createdAt || 0) - (a.createdAt || 0));
|
|
26443
|
+
}
|
|
26444
|
+
|
|
26445
|
+
pp.profilesLoad = function () {
|
|
26446
|
+
const out = [];
|
|
26447
|
+
const seen = Object.create(null);
|
|
26448
|
+
for (const raw of profReadRaw()) {
|
|
26449
|
+
const p = profClean(raw);
|
|
26450
|
+
// 同 id 只留第一条:手改过的存储里出现重复 id 时,后一条会把前一条悄悄顶掉
|
|
26451
|
+
if (p && !seen[p.id]) { seen[p.id] = 1; out.push(p); }
|
|
26452
|
+
}
|
|
26453
|
+
return profSort(out);
|
|
26454
|
+
};
|
|
26455
|
+
|
|
26456
|
+
pp.profilesSave = function (list) {
|
|
26457
|
+
const arr = profSort(Array.isArray(list) ? list.slice(0, PROF_MAX) : []);
|
|
26458
|
+
try {
|
|
26459
|
+
// 没有 localStorage 就等于没写进去,不能报成功(卡片只活这一次会话)
|
|
26460
|
+
if (!window.localStorage) return false;
|
|
26461
|
+
window.localStorage.setItem(PROF_KEY, JSON.stringify(arr));
|
|
26462
|
+
return true;
|
|
26463
|
+
} catch (e) { return false; }
|
|
26464
|
+
};
|
|
26465
|
+
|
|
26466
|
+
// 同毫秒新建两张卡时靠这个自增序列分开;只在本页生命周期内有效
|
|
26467
|
+
let profSeq = 0;
|
|
26468
|
+
pp.profilesNewId = function () {
|
|
26469
|
+
const taken = Object.create(null);
|
|
26470
|
+
for (const p of pp.profilesLoad()) taken[p.id] = 1;
|
|
26471
|
+
let id;
|
|
26472
|
+
do { id = `p${Date.now().toString(36)}${(profSeq++).toString(36)}`; } while (taken[id]);
|
|
26473
|
+
return id;
|
|
26474
|
+
};
|
|
26475
|
+
|
|
26476
|
+
/** 新增或按 id 覆盖,返回存好后的完整列表;超出上限时丢最久没排的那张 */
|
|
26477
|
+
pp.profilesPut = function (entry) {
|
|
26478
|
+
const clean = profClean(Object.assign({
|
|
26479
|
+
id: pp.profilesNewId(), createdAt: Date.now(), lastUsedAt: Date.now(),
|
|
26480
|
+
}, entry));
|
|
26481
|
+
if (!clean) return { ok: false, list: pp.profilesLoad() };
|
|
26482
|
+
const list = pp.profilesLoad();
|
|
26483
|
+
const i = list.findIndex((x) => x.id === clean.id);
|
|
26484
|
+
if (i >= 0) {
|
|
26485
|
+
// 更新只换出生信息与称呼:排过几次、上次什么时候排的、置没置顶归存储管,
|
|
26486
|
+
// 不能因为「改个称呼」就把这张卡的历史和位置抹平
|
|
26487
|
+
const prev = list[i];
|
|
26488
|
+
clean.createdAt = prev.createdAt || clean.createdAt;
|
|
26489
|
+
clean.lastUsedAt = prev.lastUsedAt;
|
|
26490
|
+
clean.uses = prev.uses;
|
|
26491
|
+
clean.pinned = prev.pinned;
|
|
26492
|
+
list.splice(i, 1, clean);
|
|
26493
|
+
} else list.push(clean);
|
|
26494
|
+
if (list.length > PROF_MAX) {
|
|
26495
|
+
const byUse = list.slice().sort((a, b) => (a.lastUsedAt || a.createdAt) - (b.lastUsedAt || b.createdAt));
|
|
26496
|
+
const drop = byUse.find((x) => x.id !== clean.id) || byUse[0];
|
|
26497
|
+
const at = list.indexOf(drop);
|
|
26498
|
+
if (at >= 0) list.splice(at, 1);
|
|
26499
|
+
}
|
|
26500
|
+
// 写不进去就别当存成功:卡片只活在本次会话里,下次进来就没了,回执必须是失败
|
|
26501
|
+
if (!pp.profilesSave(list)) return { ok: false, list: pp.profilesLoad() };
|
|
26502
|
+
return { ok: true, list: profSort(list), item: clean };
|
|
26503
|
+
};
|
|
26504
|
+
|
|
26505
|
+
pp.profilesRemove = function (id) {
|
|
26506
|
+
const list = pp.profilesLoad().filter((x) => x.id !== id);
|
|
26507
|
+
// 写不进去就把存储里的真实状态还回去:否则卡片从界面消失、下次进来又复活
|
|
26508
|
+
if (!pp.profilesSave(list)) return pp.profilesLoad();
|
|
26509
|
+
return list;
|
|
26510
|
+
};
|
|
26511
|
+
|
|
26512
|
+
pp.profilesPin = function (id) {
|
|
26513
|
+
const list = pp.profilesLoad().map((x) => (x.id === id ? Object.assign({}, x, { pinned: !x.pinned }) : x));
|
|
26514
|
+
if (!pp.profilesSave(list)) return pp.profilesLoad();
|
|
26515
|
+
return profSort(list);
|
|
26516
|
+
};
|
|
26517
|
+
|
|
26518
|
+
/** 排过一次就记一笔:档案卡按「最近排过」排序靠它 */
|
|
26519
|
+
pp.profilesTouch = function (id) {
|
|
26520
|
+
let hit = false;
|
|
26521
|
+
const list = pp.profilesLoad().map((x) => {
|
|
26522
|
+
if (x.id !== id) return x;
|
|
26523
|
+
hit = true;
|
|
26524
|
+
return Object.assign({}, x, { lastUsedAt: Date.now(), uses: (x.uses || 0) + 1 });
|
|
26525
|
+
});
|
|
26526
|
+
if (!hit) return list;
|
|
26527
|
+
if (!pp.profilesSave(list)) return pp.profilesLoad();
|
|
26528
|
+
return profSort(list);
|
|
26529
|
+
};
|
|
26530
|
+
|
|
26531
|
+
const DAY_MS = 86400000;
|
|
26532
|
+
function profAgo(ts) {
|
|
26533
|
+
const n = Number(ts) || 0;
|
|
26534
|
+
if (!n) return '未排过';
|
|
26535
|
+
const d = Math.floor((Date.now() - n) / DAY_MS);
|
|
26536
|
+
if (d <= 0) return '今天排过';
|
|
26537
|
+
if (d === 1) return '昨天排过';
|
|
26538
|
+
if (d < 30) return `${d} 天前排过`;
|
|
26539
|
+
const dt = new Date(n);
|
|
26540
|
+
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')} 排过`;
|
|
26541
|
+
}
|
|
26542
|
+
pp.profilesAgo = profAgo;
|
|
26543
|
+
|
|
26544
|
+
const PROF_GENDER = { male: '♂', female: '♀' };
|
|
26545
|
+
|
|
26546
|
+
/**
|
|
26547
|
+
* 档案列表的 HTML。交互全靠 data-mp-prof + data-id 事件委托,
|
|
26548
|
+
* 所以面板(React 容器)与单页(原生 DOM)用同一份标记就能接上。
|
|
26549
|
+
* opts: { editingId, armedDel, bar }
|
|
26550
|
+
* bar:档案条(称呼输入 + 存入)。必须走这个参数塞进 section 里,不能由调用方拼在外面——
|
|
26551
|
+
* 配色变量挂在 .mp-prof 上,拼在外面就等于拿不到 --red/--on-accent,深色下按钮糊成一片。
|
|
26552
|
+
*/
|
|
26553
|
+
pp.profilesHTML = function (list, opts) {
|
|
26554
|
+
const o = opts || {};
|
|
26555
|
+
const rows = (Array.isArray(list) ? list : []).map((raw) => {
|
|
26556
|
+
const p = profClean(raw);
|
|
26557
|
+
if (!p) return '';
|
|
26558
|
+
const birth = [p.date, p.hourUnknown ? '时辰未知' : p.time, p.address].filter(Boolean).join(' ');
|
|
26559
|
+
const editing = o.editingId && p.id === o.editingId;
|
|
26560
|
+
const armed = o.armedDel && p.id === o.armedDel;
|
|
26561
|
+
return `<div class="mp-prof-card${p.pinned ? ' pinned' : ''}${editing ? ' editing' : ''}" data-id="${esc(p.id)}">
|
|
26562
|
+
<button type="button" class="mp-prof-use" data-mp-prof="use" data-id="${esc(p.id)}"
|
|
26563
|
+
title="${esc(`用「${p.label || p.kind}」的出生信息直接排盘`)}">
|
|
26564
|
+
<span class="mp-prof-nm">${esc(p.label || '未命名')}${p.gender ? `<i class="sx">${PROF_GENDER[p.gender] || ''}</i>` : ''}</span>
|
|
26565
|
+
<span class="mp-prof-sys">${esc(pp.profKindLabel ? pp.profKindLabel(p.kind) : p.kind)}</span>
|
|
26566
|
+
<span class="mp-prof-bd">${esc(birth)}</span>
|
|
26567
|
+
<span class="mp-prof-at">${esc(profAgo(p.lastUsedAt || p.createdAt))}${p.uses ? ` · ${p.uses} 次` : ''}${p.pinned ? ' · 置顶' : ''}</span>
|
|
26568
|
+
</button>
|
|
26569
|
+
<span class="mp-prof-ops">
|
|
26570
|
+
<button type="button" data-mp-prof="pin" data-id="${esc(p.id)}" class="${p.pinned ? 'on' : ''}"
|
|
26571
|
+
aria-pressed="${p.pinned ? 'true' : 'false'}" title="置顶这张档案">${p.pinned ? '已置顶' : '置顶'}</button>
|
|
26572
|
+
<button type="button" data-mp-prof="edit" data-id="${esc(p.id)}" title="把这张档案读回输入框,改完再存">改名</button>
|
|
26573
|
+
<button type="button" data-mp-prof="del" data-id="${esc(p.id)}" class="${armed ? 'on' : ''}"
|
|
26574
|
+
title="${armed ? '再点一次就从本机删除这张档案' : '从本机删除这张档案(点两次确认)'}">${armed ? '确认删除' : '删除'}</button>
|
|
26575
|
+
</span>
|
|
26576
|
+
</div>`;
|
|
26577
|
+
}).join('');
|
|
26578
|
+
const body = rows
|
|
26579
|
+
? `<div class="mp-prof-list">${rows}</div>`
|
|
26580
|
+
: `<p class="mp-prof-empty">还没有存过命盘。填好出生信息,给这张盘起个称呼,点「存为我的命盘」——下次进来一点就出盘,不必重报。</p>`;
|
|
26581
|
+
return `<section class="mp-prof" aria-label="我的命盘">
|
|
26582
|
+
<div class="mp-prof-h"><b>我的命盘</b><span class="mp-prof-sub">只存本机 · 不上传${list && list.length ? ` · ${list.length}/${PROF_MAX}` : ''}</span></div>
|
|
26583
|
+
${body}
|
|
26584
|
+
${o.bar || ''}
|
|
26585
|
+
</section>`;
|
|
26586
|
+
};
|
|
26587
|
+
|
|
26588
|
+
/**
|
|
26589
|
+
* 档案条(称呼输入 + 存 / 取消)。单页用这份;面板那份是 React 容器,
|
|
26590
|
+
* 把输入框塞进 innerHTML 会每敲一个字整块重绘、光标被踢走,
|
|
26591
|
+
* 所以面板自己渲染同样这几个控件。
|
|
26592
|
+
*/
|
|
26593
|
+
pp.profilesBarHTML = function (opts) {
|
|
26594
|
+
const o = opts || {};
|
|
26595
|
+
return `<div class="mp-prof-bar">
|
|
26596
|
+
<input class="mp-prof-name" type="text" maxlength="${PROF_LABEL_MAX}" placeholder="称呼,如「我」「妈妈」" aria-label="档案称呼" value="${esc(o.name || '')}">
|
|
26597
|
+
<button type="button" data-mp-prof="put"${o.canAdd === false ? ' disabled' : ''}>${esc(o.editingId ? '更新这张档案' : '存为我的命盘')}</button>
|
|
26598
|
+
${o.editingId ? '<button type="button" class="ghost" data-mp-prof="cancel">取消改名</button>' : ''}
|
|
26599
|
+
<span class="mp-prof-tip">${esc(o.tip || (o.editingId ? '改完出生信息或称呼,点「更新这张档案」覆盖原档。' : '存的是出生时间与出生地这些输入,不是结果。'))}</span>
|
|
26600
|
+
</div>`;
|
|
26601
|
+
};
|
|
26602
|
+
|
|
26603
|
+
/**
|
|
26604
|
+
* 事件委托:容器上点一次,返回 {action, id} 或 null。
|
|
26605
|
+
* 两边(React / 原生)都靠它,避免各写一份 closest 判断。
|
|
26606
|
+
*/
|
|
26607
|
+
pp.profEvent = function (target) {
|
|
26608
|
+
let el = target && target.closest ? target.closest('[data-mp-prof]') : null;
|
|
26609
|
+
if (!el) return null;
|
|
26610
|
+
const action = el.getAttribute('data-mp-prof');
|
|
26611
|
+
if (el.tagName === 'BUTTON' && el.disabled) return null;
|
|
26612
|
+
return { action, id: el.getAttribute('data-id') || '', name: el };
|
|
26613
|
+
};
|
|
26614
|
+
|
|
26615
|
+
/** 从当前输入拼一条档案(称呼取自档案输入框,缺省用体系名) */
|
|
26616
|
+
// 「时辰未知」只有八字与人生K线成立(紫微没有时支整盘不成立),勾选状态在体系之间
|
|
26617
|
+
// 是会残留的,所以在这里按体系收口,免得存出一张「紫微 · 时辰未知」的矛盾档案。
|
|
26618
|
+
const PROF_HU = ['bazi', 'trend'];
|
|
26619
|
+
// 这些体系没有时刻就排不出「这张盘」(出生类与起卦类);其余只需日期或不需时间
|
|
26620
|
+
const PROF_NEEDS_TIME = ['bazi', 'ziwei', 'liuyao', 'meihua', 'daliuren', 'qimen', 'hepan',
|
|
26621
|
+
'chenggu', 'trend', 'fengshui', 'name', 'xiaoliuren'];
|
|
26622
|
+
/**
|
|
26623
|
+
* 这张档案能不能直接排:手改存储 / 旧版本可能留下「有日期没时刻」的卡。
|
|
26624
|
+
* 直接排会拿当前表单里的时刻顶上,等于排成另一个人,所以两处接线都先问这一句。
|
|
26625
|
+
*/
|
|
26626
|
+
pp.profUsable = function (p) {
|
|
26627
|
+
if (!p || typeof p !== 'object') return false;
|
|
26628
|
+
if (p.hourUnknown === true || p.time) return true;
|
|
26629
|
+
return PROF_NEEDS_TIME.indexOf(String(p.kind || '')) < 0;
|
|
26630
|
+
};
|
|
26631
|
+
pp.profFromForm = function (form, label, id) {
|
|
26632
|
+
const src = form || {};
|
|
26633
|
+
const hu = src.hourUnknown === true && PROF_HU.indexOf(String(src.kind || '')) >= 0;
|
|
26634
|
+
// 没时刻就排不出「这张盘」:存下去只会变成一张点开时静默沿用上一次时刻的卡。
|
|
26635
|
+
// 时辰未知是显式选择,不算缺时刻。
|
|
26636
|
+
if (!hu && !/^([01]?\d|2[0-3]):[0-5]\d$/.test(String(src.time || '').trim())) return null;
|
|
26637
|
+
return profClean(Object.assign({
|
|
26638
|
+
id: id || pp.profilesNewId(),
|
|
26639
|
+
kind: String(src.kind || ''),
|
|
26640
|
+
label: String(label || '').trim().slice(0, PROF_LABEL_MAX) || (pp.profKindLabel ? pp.profKindLabel(src.kind) : String(src.kind || '')),
|
|
26641
|
+
date: String(src.date || ''),
|
|
26642
|
+
time: String(src.time || ''),
|
|
26643
|
+
hourUnknown: hu,
|
|
26644
|
+
gender: String(src.gender || ''),
|
|
26645
|
+
address: String(src.address || ''),
|
|
26646
|
+
pinned: false,
|
|
26647
|
+
createdAt: Date.now(),
|
|
26648
|
+
lastUsedAt: 0,
|
|
26649
|
+
uses: 0,
|
|
26650
|
+
}));
|
|
26651
|
+
};
|
|
26652
|
+
|
|
26157
26653
|
/**
|
|
26158
26654
|
* 推导链:入口卡 + 右侧抽屉。数据来自 Engine.compute({ trace: true }) 的 r.trace。
|
|
26159
26655
|
* 每层只读展示 {field, rule, inputs, result, source},不重算、不改盘。
|
|
@@ -26597,6 +27093,9 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
26597
27093
|
const TYPE_CLASS = { 主星: 'main', 吉星: 'good', 煞星: 'bad', 辅星: 'aux', 杂曜: 'aux' };
|
|
26598
27094
|
const MUT_CLASS = { 禄: 'm-lu', 权: 'm-quan', 科: 'm-ke', 忌: 'm-ji' };
|
|
26599
27095
|
let tab = 'ziwei';
|
|
27096
|
+
// 排盘代次:每次点「排盘」自增。地址解析要等网络,回来时若代次已变,说明用户换过 tab
|
|
27097
|
+
// 或又点了一次,这一次作废(之后所有 el() 读到的都是新 tab 的控件,接着拼会排出错盘)
|
|
27098
|
+
let runSeq = 0;
|
|
26600
27099
|
let mhMode = 'time';
|
|
26601
27100
|
let xlrMode = 'time';
|
|
26602
27101
|
let fsMode = 'natal';
|
|
@@ -26680,6 +27179,122 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
26680
27179
|
if (next === 'mbti') mbEnter(); else run();
|
|
26681
27180
|
}
|
|
26682
27181
|
|
|
27182
|
+
// ── 我的命盘(本机档案)────────────────────────────────────────
|
|
27183
|
+
// 存的是「输入」而不是结果,点卡回填 + 直接出盘。存储、清洗、卡片渲染都取自
|
|
27184
|
+
// panel-kit 那一份实现:宿主面板里存的档案,在这个单页里同样看得见、同样能一点出盘。
|
|
27185
|
+
const PROF_TABS = ['bazi', 'ziwei', 'trend', 'chenggu'];
|
|
27186
|
+
let profEditing = '';
|
|
27187
|
+
let profArmed = '';
|
|
27188
|
+
let profTip = '';
|
|
27189
|
+
// 称呼框是裸 DOM,重渲染会整块换掉:把用户敲的字存在这里,才不会因为点别的按钮被抹掉
|
|
27190
|
+
let profName = '';
|
|
27191
|
+
|
|
27192
|
+
function renderProfiles() {
|
|
27193
|
+
const box = el('profwrap');
|
|
27194
|
+
const PP = window.__PP;
|
|
27195
|
+
if (!box || !PP || !PP.profilesHTML) return;
|
|
27196
|
+
const list = PP.profilesLoad();
|
|
27197
|
+
// 档案描述的是「一个人」,占事与合盘类没有可复用的出生信息,不给存入条
|
|
27198
|
+
const bar = PROF_TABS.indexOf(tab) >= 0 && PP.profilesBarHTML
|
|
27199
|
+
? PP.profilesBarHTML({ editingId: profEditing, name: profName, tip: profTip }) : '';
|
|
27200
|
+
box.innerHTML = PP.profilesHTML(list, { editingId: profEditing, armedDel: profArmed, bar });
|
|
27201
|
+
}
|
|
27202
|
+
|
|
27203
|
+
/** 回填出生信息:顺序与深链还原一致(先 setTab 再填,setTab 会按体系改可见性)。返回是否真排了盘 */
|
|
27204
|
+
function profApply(p) {
|
|
27205
|
+
// 外部改坏 / 旧版本留下的「有日期没时刻」卡:别拿当前表单里的时刻顶上,那会排成另一个人
|
|
27206
|
+
if (!window.__PP.profUsable(p)) {
|
|
27207
|
+
profTip = '这张档案没有出生时刻,先补上时刻再存一次。';
|
|
27208
|
+
renderProfiles();
|
|
27209
|
+
return false;
|
|
27210
|
+
}
|
|
27211
|
+
profName = '';
|
|
27212
|
+
setTab(p.kind);
|
|
27213
|
+
recordRecent(p.kind);
|
|
27214
|
+
const puts = [['date', p.date], ['time', p.hourUnknown ? '' : p.time], ['gender', p.gender], ['addr', p.address]];
|
|
27215
|
+
for (const pair of puts) {
|
|
27216
|
+
const node = el(pair[0]);
|
|
27217
|
+
if (node && pair[1]) node.value = pair[1];
|
|
27218
|
+
}
|
|
27219
|
+
const hu = el('hourunknown');
|
|
27220
|
+
if (hu) hu.checked = !!p.hourUnknown;
|
|
27221
|
+
// 把这次回填当成「用户填的」:否则切走再切回出生类体系时会被上一次的自动值顶掉
|
|
27222
|
+
onManualEdit();
|
|
27223
|
+
run();
|
|
27224
|
+
return true;
|
|
27225
|
+
}
|
|
27226
|
+
|
|
27227
|
+
function profReadForm() {
|
|
27228
|
+
const PP = window.__PP;
|
|
27229
|
+
const v = (id) => { const n = el(id); return n ? n.value : ''; };
|
|
27230
|
+
const hu = el('hourunknown');
|
|
27231
|
+
const box = el('profwrap');
|
|
27232
|
+
const nameEl = box ? box.querySelector('.mp-prof-name') : null;
|
|
27233
|
+
return PP.profFromForm({
|
|
27234
|
+
kind: tab, date: v('date'), time: v('time'), gender: v('gender'), address: v('addr'),
|
|
27235
|
+
hourUnknown: !!(hu && hu.checked),
|
|
27236
|
+
}, nameEl ? nameEl.value : '', profEditing || undefined);
|
|
27237
|
+
}
|
|
27238
|
+
|
|
27239
|
+
function onProfClick(e) {
|
|
27240
|
+
const PP = window.__PP;
|
|
27241
|
+
if (!PP || !PP.profEvent) return;
|
|
27242
|
+
const ev = PP.profEvent(e.target);
|
|
27243
|
+
if (!ev) return;
|
|
27244
|
+
const of = (id) => PP.profilesLoad().find((x) => x.id === id);
|
|
27245
|
+
if (ev.action === 'use') {
|
|
27246
|
+
const p = of(ev.id);
|
|
27247
|
+
if (!p) return;
|
|
27248
|
+
// 没排出来就不算「排过一次」:否则缺时刻的坏卡会顶着「1 次」跳到列表最前
|
|
27249
|
+
profTip = '';
|
|
27250
|
+
if (!profApply(p)) { renderProfiles(); return; }
|
|
27251
|
+
PP.profilesTouch(p.id);
|
|
27252
|
+
renderProfiles();
|
|
27253
|
+
return;
|
|
27254
|
+
}
|
|
27255
|
+
if (ev.action === 'edit') {
|
|
27256
|
+
const p = of(ev.id);
|
|
27257
|
+
if (!p) return;
|
|
27258
|
+
profEditing = p.id;
|
|
27259
|
+
profName = p.label;
|
|
27260
|
+
profTip = '';
|
|
27261
|
+
renderProfiles();
|
|
27262
|
+
// 改名要连着能改出生信息:把档案读回输入框,用户调完再点「更新这张档案」
|
|
27263
|
+
const puts = [['date', p.date], ['time', p.hourUnknown ? '' : p.time], ['gender', p.gender], ['addr', p.address]];
|
|
27264
|
+
for (const pair of puts) {
|
|
27265
|
+
const node = el(pair[0]);
|
|
27266
|
+
if (node && pair[1]) node.value = pair[1];
|
|
27267
|
+
}
|
|
27268
|
+
const hu = el('hourunknown');
|
|
27269
|
+
if (hu) hu.checked = !!p.hourUnknown;
|
|
27270
|
+
return;
|
|
27271
|
+
}
|
|
27272
|
+
if (ev.action === 'cancel') { profEditing = ''; profName = ''; profTip = ''; renderProfiles(); return; }
|
|
27273
|
+
if (ev.action === 'pin') { PP.profilesPin(ev.id); profTip = ''; renderProfiles(); return; }
|
|
27274
|
+
if (ev.action === 'del') {
|
|
27275
|
+
// 删除点两次才算:第一次只把按钮变成「确认删除」,误触不会丢档案
|
|
27276
|
+
if (profArmed !== ev.id) { profArmed = ev.id; profTip = ''; renderProfiles(); return; }
|
|
27277
|
+
PP.profilesRemove(ev.id);
|
|
27278
|
+
if (profEditing === ev.id) { profEditing = ''; profName = ''; }
|
|
27279
|
+
profArmed = '';
|
|
27280
|
+
profTip = '已从本机删除这张档案卡(不影响已经排出来的盘)。';
|
|
27281
|
+
renderProfiles();
|
|
27282
|
+
return;
|
|
27283
|
+
}
|
|
27284
|
+
if (ev.action === 'put') {
|
|
27285
|
+
const wasEditing = profEditing;
|
|
27286
|
+
const entry = profReadForm();
|
|
27287
|
+
if (!entry) { profTip = '先填好出生日期(和时刻),再存档案。'; renderProfiles(); return; }
|
|
27288
|
+
const res = PP.profilesPut(entry);
|
|
27289
|
+
if (!res.ok) { profTip = '档案没能存进本机(存储被禁用或已满),排盘不受影响。'; renderProfiles(); return; }
|
|
27290
|
+
profEditing = '';
|
|
27291
|
+
profArmed = '';
|
|
27292
|
+
profName = '';
|
|
27293
|
+
profTip = `${wasEditing ? '已更新档案' : '已存入我的命盘'}「${res.item.label}」。`;
|
|
27294
|
+
renderProfiles();
|
|
27295
|
+
}
|
|
27296
|
+
}
|
|
27297
|
+
|
|
26683
27298
|
/** 分组 / 平铺:只切显示,19 个 tab 与分组卡片两套入口始终可用 */
|
|
26684
27299
|
function applyView(v) {
|
|
26685
27300
|
const flat = v === 'flat';
|
|
@@ -26896,9 +27511,14 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
26896
27511
|
};
|
|
26897
27512
|
function bridgePackages() {
|
|
26898
27513
|
if (!last || !last.r || !window.Engine || !window.Engine.askPackages) return null;
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
27514
|
+
// 追问框就在按钮旁边,用户刚写下的那句必须进包;没写追问时才退回「所问之事」。
|
|
27515
|
+
// 只读 #question 的话,命盘类根本没有这个输入框,复制出去的「所问」永远是默认开场句。
|
|
27516
|
+
const follow = el('askq') ? el('askq').value.trim() : '';
|
|
27517
|
+
const main = el('question') ? el('question').value.trim() : '';
|
|
27518
|
+
try {
|
|
27519
|
+
// interpret 一并交出去:面板已算过「直答 / 说人话」,别的模型开场就该沿用同一口径
|
|
27520
|
+
return window.Engine.askPackages(last.r, { question: follow || main, interpret: last.it || null, base: pageBase() });
|
|
27521
|
+
} catch (e) { return null; }
|
|
26902
27522
|
}
|
|
26903
27523
|
|
|
26904
27524
|
// ── C3 深链:打开带 #mp= 的链接时还原同一张盘 ──────────────────
|
|
@@ -26981,6 +27601,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
26981
27601
|
/** 用缓存的卦象重新渲染:解读按当前的问题与方向重出,卦面本身不动 */
|
|
26982
27602
|
function paint() {
|
|
26983
27603
|
if (!last) return;
|
|
27604
|
+
// 重绘会换掉整个 #out:先记下追问框里的字,否则切方向 / 刷新推导链时用户刚打的问题会凭空消失
|
|
27605
|
+
const askBefore = el('askq') ? el('askq').value : '';
|
|
26984
27606
|
let it = null;
|
|
26985
27607
|
try {
|
|
26986
27608
|
it = window.Engine.interpret ? window.Engine.interpret(last.r, askArgs()) : (last.r.interpret || null);
|
|
@@ -27000,6 +27622,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27000
27622
|
+ (last.tab === 'bazi' ? `<div id="vbox">${verifyHTML()}</div><div id="rectbox">${rectifyHTML()}</div>` : '')
|
|
27001
27623
|
+ exportBar();
|
|
27002
27624
|
last.it = it;
|
|
27625
|
+
const askNow = el('askq');
|
|
27626
|
+
if (askNow && askBefore) askNow.value = askBefore;
|
|
27003
27627
|
// 耗时行跟盘走:方向切换等重绘会把 innerHTML 整个换掉,不在这里补一次就会丢
|
|
27004
27628
|
if (last.perf) {
|
|
27005
27629
|
const perfHtml = window.__PP.perfHTML(last.perf);
|
|
@@ -27015,7 +27639,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27015
27639
|
return `<div class="askbox no-print">
|
|
27016
27640
|
<input id="askq" maxlength="120" placeholder="追问:如 2027 年事业怎么样(回车提交)">
|
|
27017
27641
|
<button type="button" data-act="ask">追问</button>
|
|
27018
|
-
<button type="button" data-act="copyask" title="
|
|
27642
|
+
<button type="button" data-act="copyask" title="把这张盘和上面输入的问题整理成一段提问,粘到任意 AI 对话窗都能用">复制提问</button>
|
|
27019
27643
|
<span class="hint">只回答盘面能支持的部分;找不到会说找不到</span>
|
|
27020
27644
|
</div><div id="fout"></div>`;
|
|
27021
27645
|
}
|
|
@@ -27151,6 +27775,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27151
27775
|
|
|
27152
27776
|
async function run() {
|
|
27153
27777
|
const runStart = mpNow();
|
|
27778
|
+
const myRun = ++runSeq;
|
|
27779
|
+
const startTab = tab;
|
|
27154
27780
|
const date = el('date').value;
|
|
27155
27781
|
// 勾了「时辰未知」就不读钟点:读出来的只是占位值,用它排出来的时柱是编的
|
|
27156
27782
|
const hourUnknown = !!(el('hourunknown') && el('hourunknown').checked) && (tab === 'bazi' || tab === 'trend');
|
|
@@ -27185,6 +27811,8 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27185
27811
|
const g = await window.Engine.geocodeAddress(addr, { online: el('online').checked });
|
|
27186
27812
|
if (g && typeof g.lon === 'number') { args.longitude = g.lon; args.geo = g; }
|
|
27187
27813
|
} catch (e) { /* 退回内置表 */ }
|
|
27814
|
+
// 换过 tab 或又点了一次:这一次作废,让最新那次去排(下面读的全是新 tab 的控件)
|
|
27815
|
+
if (myRun !== runSeq || startTab !== tab) return;
|
|
27188
27816
|
}
|
|
27189
27817
|
}
|
|
27190
27818
|
if (tab === 'hepan') {
|
|
@@ -27467,6 +28095,7 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27467
28095
|
applyDateTime(k); // 起卦类 → 当前时刻;命盘类 → 示例默认;用户改过则保留
|
|
27468
28096
|
ctxKind = k;
|
|
27469
28097
|
renderTip();
|
|
28098
|
+
renderProfiles(); // 档案条只在命盘类体系出现,切 tab 要跟着换
|
|
27470
28099
|
}
|
|
27471
28100
|
|
|
27472
28101
|
function syncMhMode() {
|
|
@@ -27695,6 +28324,15 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27695
28324
|
});
|
|
27696
28325
|
applyView(storeGet(VIEW_KEY) === 'flat' ? 'flat' : 'group');
|
|
27697
28326
|
renderRecent();
|
|
28327
|
+
const profBox = el('profwrap');
|
|
28328
|
+
if (profBox) {
|
|
28329
|
+
profBox.addEventListener('click', onProfClick);
|
|
28330
|
+
// 只记名字、不重绘:每敲一个字重绘会把光标踢走
|
|
28331
|
+
profBox.addEventListener('input', (e) => {
|
|
28332
|
+
const nm = e.target && e.target.closest ? e.target.closest('.mp-prof-name') : null;
|
|
28333
|
+
if (nm) profName = nm.value;
|
|
28334
|
+
});
|
|
28335
|
+
}
|
|
27698
28336
|
const mbHost = el('mbti-qs');
|
|
27699
28337
|
if (mbHost) {
|
|
27700
28338
|
renderMbti();
|
|
@@ -27829,7 +28467,13 @@ document.head.appendChild(Object.assign(document.createElement('style'), { textC
|
|
|
27829
28467
|
const copyAsk = e.target && e.target.closest ? e.target.closest('[data-act="copyask"]') : null;
|
|
27830
28468
|
if (copyAsk) {
|
|
27831
28469
|
const pkg = bridgePackages();
|
|
27832
|
-
|
|
28470
|
+
// 打包失败时按钮不能毫无反应:用户会以为点了没生效,再点、再粘贴,最后贴进去一段旧内容
|
|
28471
|
+
if (!pkg || !pkg.full) {
|
|
28472
|
+
copyAsk.textContent = '暂无可复制内容';
|
|
28473
|
+
setTimeout(() => { copyAsk.textContent = '复制提问'; }, 1400);
|
|
28474
|
+
return;
|
|
28475
|
+
}
|
|
28476
|
+
copyPlain(copyAsk, pkg.full, '复制提问');
|
|
27833
28477
|
return;
|
|
27834
28478
|
}
|
|
27835
28479
|
onExportClick(e);
|