/**
* 生时校正 UI 渲染层 v2.0
* 引擎逻辑在 rectification-engine.js
*/
import { SIGNS, PLANET_CN, SIGN_LORDS } from './jyotish-engine.js';
import {
EVENT_CATEGORIES, VARGA_SENSITIVITY, runRectification,
getHouseLord, fmtTime, dateToJD
} from './rectification-engine.js';
import { t, getLang, signName, planetName } from './i18n.js';
function fmtOffset(m) { return m === 0 ? t('rect.baseline') : `${m > 0 ? '+' : ''}${m}min`; }
let rectEvents = [];
export function renderRectificationTab(container) {
const lang = getLang();
container.innerHTML = `
${t('rect.config')}
${t('rect.sensitivity.info')}
| ${t('rect.varga.col')} | ${t('rect.time.window')} |
${Object.entries(VARGA_SENSITIVITY).map(([k, v]) =>
`| ${k} (${lang === 'en' ? v.en : v.cn}) | ~${v.min} ${t('rect.min.unit')} |
`
).join('')}
${t('rect.events')} 0 ${t('rect.event.count')}
${t('rect.events.hint')}
`;
bindEvents(container);
}
function bindEvents(container) {
const q = s => container.querySelector(s);
q('#rect-add-btn').addEventListener('click', () => {
const dEl = q('#rect-event-date'), cEl = q('#rect-event-cat'), descEl = q('#rect-event-desc');
if (!dEl.value) { dEl.focus(); return; }
rectEvents.push({ date: dEl.value, category: cEl.value, desc: descEl.value || EVENT_CATEGORIES[cEl.value].cn });
dEl.value = ''; descEl.value = '';
renderEventList(container);
});
q('#rect-run-btn').addEventListener('click', function () { handleRun(container, this); });
}
function renderEventList(container) {
const listEl = container.querySelector('#rect-event-list');
const countEl = container.querySelector('#rect-event-count');
const lang = getLang();
countEl.textContent = `${rectEvents.length} ${t('rect.event.count')}`;
if (!rectEvents.length) { listEl.innerHTML = `${t('rect.no.events')}
`; return; }
listEl.innerHTML = rectEvents.map((evt, i) => {
const cat = EVENT_CATEGORIES[evt.category];
return `
${cat.icon}
${evt.date}
${lang === 'en' ? cat.en : cat.cn} (${cat.varga})
${evt.desc}
`;
}).join('');
listEl.querySelectorAll('.rect-evt-del').forEach(b => {
b.addEventListener('click', () => { rectEvents.splice(+b.dataset.idx, 1); renderEventList(container); });
});
}
async function handleRun(container, runBtn) {
if (!rectEvents.length) { alert(t('rect.alert.no.events')); return; }
if (!window.__jyotishBirth) { alert(t('rect.alert.no.chart')); return; }
const q = s => container.querySelector(s);
const progressEl = q('#rect-progress'), fillEl = q('#rect-progress-fill');
const textEl = q('#rect-progress-text'), resultsEl = q('#rect-results');
const rangeMin = +q('#rect-range').value, stepMin = +q('#rect-step').value;
progressEl.classList.remove('hidden'); resultsEl.classList.add('hidden');
runBtn.querySelector('.btn-text').classList.add('hidden');
runBtn.querySelector('.btn-loading').classList.remove('hidden'); runBtn.disabled = true;
try {
const result = await runRectification(window.__jyotishBirth, rectEvents, {
rangeMin, stepMin,
onProgress(cur, total) {
const pct = Math.round(cur / total * 100);
fillEl.style.width = pct + '%';
textEl.textContent = `${t('rect.calculating')} ${cur}/${total} (${pct}%)`;
},
});
if (result.error) { textEl.textContent = result.error; return; }
renderResults(container, result);
} catch (e) { textEl.textContent = `Error: ${e.message}`; console.error('[Rect]', e);
} finally {
runBtn.querySelector('.btn-text').classList.remove('hidden');
runBtn.querySelector('.btn-loading').classList.add('hidden');
runBtn.disabled = false;
setTimeout(() => progressEl.classList.add('hidden'), 2000);
}
}
function renderResults(container, result) {
const el = container.querySelector('#rect-results'); el.classList.remove('hidden');
const lang = getLang();
const { bestMatch: bm, confidence: conf, baseChartInfo: base, results: all } = result;
const confClr = { '高': '#22c55e', '中': '#f59e0b', '低': '#ef4444', '不确定': '#9ca3af' };
// 分盘变化
const vcHtml = bm.vargaChanges.length > 0
? bm.vargaChanges.map(c => `| ${c.varga} | ${signName(c.from)} | ${signName(c.to)} |
`).join('')
: `| ${t('rect.no.change')} |
`;
// 评分条
const weights = { dasha:'40%', varga:'35%', house:'15%', nak:'10%' };
const scoreBars = Object.keys(weights).map(k => {
const s = bm.scores[k];
return `
${t('rect.scoring.'+k)} (${weights[k]})
${s.pct}% `;
}).join('');
// 事件详情
const evtRows = bm.eventScores.map(es => {
const cat = EVENT_CATEGORIES[es.event.category];
const rel = getRelevanceText(es.dasha, es.event.category, bm.ascSign);
return `
| ${cat.icon} ${lang==='en'?cat.en:cat.cn} (${cat.varga}) |
${es.event.date} |
${es.dasha ? planetName(es.dasha.mahadasha) : '—'} |
${es.dasha?.antardasha ? planetName(es.dasha.antardasha) : '—'} |
${es.dashaScore.toFixed(1)} |
${es.vargaScore.toFixed(1)} |
${rel} |
`;
}).join('');
el.innerHTML = `
${t('rect.result')}
${t('rect.original.time')}${base.time}
${t('rect.rec.time')}${bm.time} (${fmtOffset(bm.offsetMin)})
${t('rect.confidence')}${conf.level} (${conf.bestPct}%)
${conf.recommendation.map(r => `
${r}
`).join('')}
${t('rect.scoring.title')}
${scoreBars}
${t('rect.total.score')} ${bm.totalScore}%
${t('rect.varga.changes')}
| ${t('rect.varga.col')} | ${t('rect.from.col')} | ${t('rect.to.col')} |
${vcHtml}
${t('rect.top.candidates')}
| # | ${t('rect.time')} | ${t('rect.offset')} | ${t('rect.asc')} | D9 | D10 | ${t('rect.score')} | ${t('rect.match')} |
${all.slice(0,10).map((r,i) => {
const d9=r.vargaLagnas?.D9?.sign||r.ascSign, d10=r.vargaLagnas?.D10?.sign||r.ascSign;
const d9chg=r.vargaChanges?.some(v=>v.varga==='D9')?' ⚠️':'';
const d10chg=r.vargaChanges?.some(v=>v.varga==='D10')?' ⚠️':'';
return `
| ${i+1} | ${r.time} | ${fmtOffset(r.offsetMin)} |
${signName(r.ascSign)} | ${signName(d9)}${d9chg} | ${signName(d10)}${d10chg} |
${r.totalScore}% |
|
`;
}).join('')}
${t('rect.event.detail')}
| ${t('rect.event.col')} | ${t('rect.date.col')} | ${t('rect.maha.col')} | ${t('rect.antar.col')} | ${t('rect.score.dasha')} | ${t('rect.score.varga')} | ${t('rect.rel.col')} |
${evtRows}
`;
el.querySelectorAll('.rect-top-results tr[data-offset]').forEach(tr => {
tr.style.cursor = 'pointer';
tr.addEventListener('click', () => {
const off = parseFloat(tr.dataset.offset);
const r = all.find(x => x.offsetMin === off);
if (r) showOffsetDetail(el, r, base);
});
});
}
function getRelevanceText(di, category, ascSign) {
if (!di) return '—';
const cat = EVENT_CATEGORIES[category]; if (!cat) return '—';
const ai = SIGNS.indexOf(ascSign), hl = cat.houses.map(h => getHouseLord(ai, h));
const mdR = cat.planets.includes(di.mahadasha) || hl.includes(di.mahadasha);
const adR = di.antardasha && (cat.planets.includes(di.antardasha) || hl.includes(di.antardasha));
if (mdR && adR) return t('rect.strong.match');
if (mdR) return t('rect.maha.match');
if (adR) return t('rect.antar.match');
return t('rect.no.match');
}
function showOffsetDetail(container, r, base) {
let det = container.querySelector('.rect-offset-detail');
if (!det) { det = document.createElement('div'); det.className = 'rect-offset-detail card'; container.appendChild(det); }
const vlHtml = Object.entries(r.vargaLagnas || {}).map(([k, v]) =>
`${k}: ${v ? signName(v.sign) : '—'}`
).join('');
const hcHtml = r.houseChanges.length > 0
? r.houseChanges.map(c => `${planetName(c.planet)}: H${c.from}→H${c.to}`).join('')
: t('rect.no.change');
det.innerHTML = `
${t('rect.offset.detail').replace('{0}',fmtOffset(r.offsetMin)).replace('{1}',r.time)}
${t('rect.asc.label')}${signName(r.ascSign)} ${r.ascDeg?.toFixed(2)}°
${t('rect.moon.nak')}${r.moonNak?.nakName||'—'} Pada ${r.moonNak?.pada||'?'}
${t('rect.total.score')}${r.totalScore}%
${t('rect.varga.changes')}
${vlHtml}
${t('rect.house.changes')}
${hcHtml}
`;
det.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
export function initRectification() { rectEvents = []; }