/**
* Jyotish App — 主入口 v3.0
* 整合: 排盘、宫位、相位、Yoga、Karaka、分盘、Ashtakavarga、Shadbala、Dasha三级、Transit
*/
import {
initEngine, computeChart, detectYogas, detectExtendedYogas,
computeAspects, computeHouseAnalysis, searchCities,
SIGNS, SIGNS_CN, PLANET_CN, PLANET_SYMBOLS, SIGN_LORDS,
} from './jyotish-engine.js';
import {
computeCombustion, computeKaraka, computeArudha, computeAllVargas,
computeAshtakavarga, computeShadbala, computeTithiYoga, computeDashaWithPratyantar,
} from './jyotish-advanced.js';
import { renderSouthIndianChart, renderNorthIndianChart, setChartStyle, getChartStyle } from './chart-renderer.js';
import {
HOUSE_MEANINGS, PLANET_IN_HOUSE, NAKSHATRA_DATA,
YOGA_DEFINITIONS, ASPECT_DESC, HOUSE_GROUPS, TRANSIT_EFFECTS,
DASHA_THEMES, ASCENDANT_TABLE, PLANET_DIGNITY, NEECHA_BHANGA_RULES,
} from './interpretation.js';
import { YOGA_EXTENDED_A } from './yoga-extended.js';
import { YOGA_EXTENDED_B } from './yoga-extended-b.js';
import { YOGA_DETAILS_A } from './yoga-details-a.js';
import { YOGA_DETAILS_B } from './yoga-details-b.js';
import { PIH_PART1 } from './planet-house-details-a.js';
import { PIH_PART2 } from './planet-house-details-b.js';
import { PIH_PART3 } from './planet-house-details-c.js';
const YOGA_DETAILS = { ...YOGA_DETAILS_A, ...YOGA_DETAILS_B };
const PLANET_HOUSE_DETAIL = { ...PIH_PART1, ...PIH_PART2, ...PIH_PART3 };
import {
computeTransit, computeTransitOverlay, computeDoubleTransit, checkSadeSati,
computeTransitAVScore, computeTransitHouseImpact,
computeDoubleTransitPAC, computeTransitLL7L, computePlanetaryCongregation, computeVivahSaham,
} from './transit.js';
import {
renderTithiInfo, renderArudhaQuick, renderKaraka, renderVargas, renderArudha,
renderAshtakavarga, renderShadbala, renderDasha3Level, updatePlanetsTable,
} from './renderers.js';
import {
computePACDARES, computeHouseInfluence, detectVargottama, computeFrequency,
computeTriangle, computeRamanHouseScore, computeTrikonaKendra,
} from './analysis-deep.js';
import {
renderFunctionalNature, renderPACDARES, renderHouseInfluence, renderVargottama,
renderFrequency, renderTriangle, renderRamanGrades, renderTrikonaKendra,
} from './analysis-renderers.js';
import {
computeBhavaBala, computeVimsopaka, computeVaiseshikamsas,
computePlanetActivity, computePlanetAge, computePlanetAlertness, computePlanetMood,
computeAVPinda, computeAllExtraDasas,
} from './jyotish-extended.js';
import {
renderBhavaBala, renderVimsopakaVaiseshikamsa, renderPlanetStates, renderAVPinda, renderExtraDasas,
} from './renderers.js';
import { exportJSON, exportSVG, exportPNG } from './export.js';
import { computeArgala } from './argala.js';
import { computeTajika } from './tajika.js';
import { computeNakshatraAdvanced, computeCharaDasha, computeKarakamsha } from './jyotish-advanced.js';
import { computeValidation, computeAudit, computeActionableContext } from './jyotish-export-modules.js';
import { initTooltip, bindTerms } from './glossary.js';
import { initAIChat, aiChatSetChartData } from './ai-chat.js';
import { initAuth } from './auth.js';
import { initSubscription } from './subscription.js';
import { renderRectificationTab, initRectification } from './rectification.js';
import { t, getLang, initI18N, onLangChange, signName, planetName, statusName, houseLabel, houseAreaName, yearsLabel } from './i18n.js';
// 合并所有 Yoga 定义
const ALL_YOGA_DEFS = [...YOGA_DEFINITIONS, ...YOGA_EXTENDED_A, ...YOGA_EXTENDED_B];
const $ = id => document.getElementById(id);
// 全局状态
let chartData = null;
let _lastChartSummary = null;
// ============================================================================
// Tab 切换
// ============================================================================
document.addEventListener('DOMContentLoaded', () => {
const tabs = $('section-tabs');
if (!tabs) return;
tabs.addEventListener('click', e => {
const btn = e.target.closest('.tab-btn');
if (!btn) return;
tabs.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
const panel = $(`tab-${btn.dataset.tab}`);
if (panel) {
panel.classList.add('active');
// Tab 切换后重新绑定术语
setTimeout(() => bindTerms(panel), 100);
setTimeout(() => bindTerms(panel), 500);
}
// 自动将当前 Tab 滚入视区
btn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
});
// Tab 栏滚动提示箭头
const hint = document.createElement('div');
hint.className = 'tab-scroll-hint';
hint.innerHTML = '›';
hint.title = 'Scroll →';
tabs.parentNode.style.position = 'relative';
tabs.parentNode.appendChild(hint);
function checkScrollHint() {
const atEnd = tabs.scrollLeft + tabs.clientWidth >= tabs.scrollWidth - 10;
hint.classList.toggle('visible', !atEnd && tabs.scrollWidth > tabs.clientWidth);
}
tabs.addEventListener('scroll', checkScrollHint);
// 全局可调用,切换页面后触发
window.__checkTabScrollHint = checkScrollHint;
window.addEventListener('resize', checkScrollHint);
hint.addEventListener('click', () => {
tabs.scrollBy({ left: 200, behavior: 'smooth' });
});
});
// ============================================================================
// 城市搜索
// ============================================================================
let debounceTimer = null;
function setupCitySearch() {
const cityInput = $('birth-city'), suggestions = $('city-suggestions');
const latInput = $('birth-lat'), lonInput = $('birth-lon'), tzSelect = $('birth-tz');
cityInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const q = cityInput.value.trim();
if (q.length < 1) { suggestions.classList.add('hidden'); return; }
const results = searchCities(q);
if (results.length === 0) { suggestions.classList.add('hidden'); return; }
suggestions.innerHTML = results.map(c => {
const label = c.en ? `${c.name} (${c.en})` : c.name;
return `
${label} ${c.lat.toFixed(1)}°, ${c.lon.toFixed(1)}°
`;
}).join('');
suggestions.classList.remove('hidden');
suggestions.querySelectorAll('.suggestion-item').forEach(el => {
el.addEventListener('click', () => {
const text = el.textContent.trim();
cityInput.value = text.split(/\s+\(/)[0]; // 取中文名部分
latInput.value = el.dataset.lat; lonInput.value = el.dataset.lon; tzSelect.value = el.dataset.tz;
suggestions.classList.add('hidden');
});
});
}, 200);
});
document.addEventListener('click', e => {
if (!cityInput.contains(e.target) && !suggestions.contains(e.target)) suggestions.classList.add('hidden');
});
}
// ============================================================================
// 日期下拉框初始化
// ============================================================================
function initDateSelects() {
const yearEl = $('birth-year'), monthEl = $('birth-month'), dayEl = $('birth-day');
if (!yearEl) return;
const thisYear = new Date().getFullYear();
for (let y = thisYear; y >= 1940; y--) {
const opt = document.createElement('option');
opt.value = y; opt.textContent = y;
yearEl.appendChild(opt);
}
yearEl.value = '1997';
for (let m = 1; m <= 12; m++) {
const opt = document.createElement('option');
opt.value = m; opt.textContent = m;
monthEl.appendChild(opt);
}
function updateDays() {
const y = parseInt(yearEl.value) || 2000;
const m = parseInt(monthEl.value) || 1;
const maxDay = new Date(y, m, 0).getDate();
const curDay = parseInt(dayEl.value) || 0;
dayEl.innerHTML = '';
const ph = document.createElement('option');
ph.value = ''; ph.disabled = true; ph.textContent = t('date.day');
dayEl.appendChild(ph);
for (let d = 1; d <= maxDay; d++) {
const opt = document.createElement('option');
opt.value = d; opt.textContent = d;
dayEl.appendChild(opt);
}
if (curDay >= 1 && curDay <= maxDay) dayEl.value = curDay;
}
yearEl.addEventListener('change', updateDays);
monthEl.addEventListener('change', updateDays);
updateDays();
// 更新 placeholder option 文本
function updatePlaceholders() {
yearEl.options[0].textContent = t('date.year');
monthEl.options[0].textContent = t('date.month');
if (dayEl.options[0]) dayEl.options[0].textContent = t('date.day');
}
updatePlaceholders();
onLangChange(updatePlaceholders);
}
// ============================================================================
// 表单提交
// ============================================================================
function setupForm() {
const form = $('birth-form'), btn = $('btn-calculate');
const btnText = btn.querySelector('.btn-text'), btnLoading = btn.querySelector('.btn-loading');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const year = parseInt($('birth-year').value);
const month = parseInt($('birth-month').value);
const day = parseInt($('birth-day').value);
const timeVal = $('birth-time').value;
const lat = parseFloat($('birth-lat').value), lon = parseFloat($('birth-lon').value);
const tz = parseFloat($('birth-tz').value);
if (!year || !month || !day || !timeVal) { alert(t('alert.date')); return; }
if (isNaN(lat) || isNaN(lon)) { alert(t('alert.city')); return; }
const [hour, minute] = timeVal.split(':').map(Number);
btnText.classList.add('hidden'); btnLoading.classList.remove('hidden'); btn.disabled = true;
try {
// v6.9.2: API优先, 不可用时回退JS引擎
if (window.JyotishAPI) {
chartData = await window.JyotishAPI.computeAll({ year, month, day, hour, minute, lat, lon, tz });
if (!chartData || !chartData.success) throw new Error(chartData?.error || 'API计算失败');
console.log('[Jyotish] ✅ API —', chartData.dasha_count, 'Dasha,', chartData.yogas?.length || 0, 'Yoga');
} else {
throw new Error('API桥接未加载');
}
} catch (apiErr) {
console.warn('[Jyotish] API不可用:', apiErr.message, '→ 回退JS引擎');
try {
await initEngine();
chartData = await computeChart({ year, month, day, hour, minute, lat, lon, tz });
chartData._fallback = true;
console.log('[Jyotish] ⚠️ JS引擎回退成功');
} catch (jsErr) {
console.error('[Jyotish] JS引擎也失败:', jsErr);
alert('计算失败: ' + (apiErr.message || jsErr.message) + '\n请检查网络或启动API服务器');
btnText.classList.remove('hidden'); btnLoading.classList.add('hidden'); btn.disabled = false;
return;
}
}
try {
// 保存出生数据供生时校正使用
window.__jyotishBirth = { year, month, day, hour, minute, lat, lon, tz };
renderAll();
showPage('chart');
// 将当前星盘数据传给 AI 聊天
aiChatSetChartData(chartData);
} catch (err) {
console.error('[Jyotish] Error:', err);
alert(t('alert.error') + err.message);
} finally {
btnText.classList.remove('hidden'); btnLoading.classList.add('hidden'); btn.disabled = false;
}
});
}
// ============================================================================
// 全部渲染
// ============================================================================
function renderAll() {
if (!chartData) return;
const { ascendant, planets, birth_info } = chartData;
const moonP = planets.Moon;
// 高级计算
computeCombustion(planets);
const karaka = computeKaraka(planets);
const arudha = computeArudha(planets, ascendant.sign);
const allV = computeAllVargas(planets);
const av = computeAshtakavarga(planets, ascendant.sign);
const sb = computeShadbala(planets, ascendant.sign, birth_info, allV);
const ty = computeTithiYoga(planets, birth_info);
let dashaData = null;
if (moonP) dashaData = computeDashaWithPratyantar(moonP.degree, birth_info.date);
// Banner
$('asc-sign').textContent = `${ascendant.sign} · ${signName(ascendant.sign)}`;
$('asc-degree').textContent = `${(ascendant.degree_in_sign != null ? ascendant.degree_in_sign : ascendant.degree).toFixed(2)}° | ${t('asc.lord')}: ${planetName(ascendant.lord)}`;
if (moonP) $('asc-nakshatra').textContent = `${t('asc.nakshatra')}: ${moonP.nakshatra} Pada ${moonP.nakshatra_pada}`;
$('asc-lord-badge').textContent = `${t('asc.lord')}: ${planetName(ascendant.lord)} (${ascendant.lord})`;
// 生时校正引导卡片:显示/绑定
const rectPrompt = $('rect-prompt');
const rectPanel = $('rect-panel');
const rectOverlay = $('rect-overlay');
if (rectPrompt && rectPanel) {
rectPrompt.classList.remove('hidden');
if (!rectPrompt.dataset.bound) {
rectPrompt.dataset.bound = 'true';
$('rect-prompt-btn').addEventListener('click', () => {
rectPanel.classList.remove('hidden');
if (rectOverlay) rectOverlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
initRectification();
renderRectificationTab($('rect-content'));
});
$('rect-panel-close').addEventListener('click', () => {
rectPanel.classList.add('hidden');
if (rectOverlay) rectOverlay.classList.add('hidden');
document.body.style.overflow = '';
});
if (rectOverlay) rectOverlay.addEventListener('click', () => {
rectPanel.classList.add('hidden');
rectOverlay.classList.add('hidden');
document.body.style.overflow = '';
});
}
}
// 提前计算 Yogas(供星盘中心区域和 Yoga Tab 共用)
const baseYogas = detectYogas(planets, ascendant.sign);
let allYogas = [...baseYogas.yogas];
for (const yd of ALL_YOGA_DEFS) {
try {
const combo = yd.check(planets, ascendant.sign);
if (combo) allYogas.push({ id: yd.id, name: yd.name, name_cn: yd.name_cn, combination: combo, effects: yd.effects, strength: yd.strength, negative: yd.negative || false, category: yd.category });
} catch (e) {}
}
// 构建星盘中心摘要
const chartSummary = buildChartSummary(ascendant, moonP, dashaData, karaka, allYogas);
_lastChartSummary = chartSummary;
// 本命盘 Tab
const renderFn = getChartStyle() === 'north' ? renderNorthIndianChart : renderSouthIndianChart;
renderFn($('rasi-chart'), chartData, { summary: chartSummary });
updatePlanetsTable(planets, allV.D9);
renderTithiInfo(ty);
renderArudhaQuick(arudha);
// Karaka Tab
renderKaraka(karaka);
// 宫位分析 Tab
renderHouseAnalysis(planets, ascendant.sign);
// 相位 Tab
renderAspects(planets);
// Yoga Tab(使用预计算结果)
renderYogasWith(allYogas);
// 分盘 Tab
renderVargas(allV, planets, ascendant);
renderArudha(arudha);
// Ashtakavarga Tab
renderAshtakavarga(av, ascendant.sign);
// Shadbala Tab
renderShadbala(sb);
// Dasha Tab
if (dashaData) renderDasha3Level(dashaData);
// Transit Tab(异步)
renderTransit(planets, ascendant.sign, av, ascendant.degree);
// 深度分析 Tab
const pacdares = computePACDARES(planets, ascendant.sign);
const houseInfluence = computeHouseInfluence(planets, ascendant.sign);
const vargottama = detectVargottama(planets);
const frequency = computeFrequency(planets);
const triangle = computeTriangle(planets);
const ramanGrades = computeRamanHouseScore(planets, ascendant.sign);
renderFunctionalNature(ascendant.sign);
renderPACDARES(pacdares);
renderRamanGrades(ramanGrades);
renderHouseInfluence(houseInfluence);
renderVargottama(vargottama);
renderTriangle(triangle);
renderFrequency(frequency);
// 三方四正映射分析
const tkData = computeTrikonaKendra(planets, ascendant.sign);
renderTrikonaKendra(tkData);
// 扩展分析 Tab
const bb = computeBhavaBala(planets, ascendant.sign, sb);
const vim = computeVimsopaka(planets);
const vais = computeVaiseshikamsas(vim);
const activity = computePlanetActivity(planets);
const pAge = computePlanetAge(planets);
const alertness = computePlanetAlertness(planets);
const mood = computePlanetMood(planets);
const pinda = computeAVPinda(av);
const extraDasas = computeAllExtraDasas(planets, ascendant.sign, birth_info.date);
renderBhavaBala(bb);
renderVimsopakaVaiseshikamsa(vim, vais);
renderPlanetStates(activity, pAge, alertness, mood);
renderAVPinda(pinda);
renderExtraDasas(extraDasas);
// 🔥 v6.7.2: API数据渲染 Recovery & KP tabs
renderRemediesTab(chartData);
renderKPTab(chartData);
// v6.9.0: 验证 & 过境 Tab
renderVerifyTab(chartData);
renderTransitCompareTab(chartData);
// 绑定术语 Tooltip(延迟确保所有异步渲染完成)
setTimeout(() => bindTerms(document.querySelector('#page-chart')), 200);
setTimeout(() => bindTerms(document.querySelector('#page-chart')), 800);
}
// ============================================================================
// Remedies Tab 渲染
// ============================================================================
function renderRemediesTab(chartData) {
const sec = document.getElementById('remedies-section');
if (!sec) return;
const ext = chartData._extended;
if (!ext?.remedies) {
sec.innerHTML = '补救建议将在API返回后展示
';
return;
}
const { recommendations, summary } = ext.remedies;
let html = `${summary}
`;
// Gems
const gems = recommendations?.gems || [];
if (gems.length > 0) {
html += '💎 宝石建议
';
for (const g of gems.slice(0, 4)) {
html += `
${g.planet}: ${g.gem}
${g.metal || ''} · ${g.finger || ''} · ${g.day || ''}
`;
}
html += '
';
}
// Mantras
const mantras = recommendations?.mantras || [];
if (mantras.length > 0) {
html += '🕉️ 咒语建议
';
for (const m of mantras.slice(0, 5)) {
html += `
${m.mantra} × ${m.repetitions} — ${m.note || ''}
`;
}
}
// Donations
const donations = recommendations?.donations || [];
if (donations.length > 0) {
html += '🙏 捐赠建议
';
for (const d of donations.slice(0, 3)) {
html += `${d.note || ''}
`;
}
}
// Dosha remedies
const dr = recommendations?.dosha_remedies || [];
if (dr.length > 0) {
html += '⚠️ Dosha专项补救
';
for (const d of dr) {
html += `
${d.dosha}: ${d.note || ''}
`;
}
}
sec.innerHTML = html;
}
// ============================================================================
// KP Tab 渲染
// ============================================================================
function renderKPTab(chartData) {
const sec = document.getElementById('kp-section');
if (!sec) return;
const ext = chartData._extended;
if (!ext?.kp?.houses) {
sec.innerHTML = 'KP分析将在API返回后展示
';
return;
}
let html = '';
// House significators
for (let h = 1; h <= 12; h++) {
const hd = ext.kp.houses[h];
if (!hd) continue;
const sig = hd.significators || {};
html += `
${h}宫 (${hd.sign || ''})
A: ${(sig.A || []).join(', ') || '—'}
B: ${(sig.B || []).join(', ') || '—'}
C: ${(sig.C || []).join(', ') || '—'}
D: ${sig.D || '—'}
`;
}
html += '
';
// KP lords summary
const planets = ext.kp.planets || {};
if (Object.keys(planets).length > 0) {
html += 'KP Sublord 概览
';
html += '| 行星 | Sub Lord | SubSub Lord | A | B |
';
for (const [pn, pd] of Object.entries(planets).slice(0, 9)) {
const kl = pd.kp_lords || {};
const sig = pd.significators || {};
html += `
| ${pn} |
${kl.sub_lord || '—'} |
${kl.sub_sub_lord || '—'} |
${sig.A || '—'} |
${sig.B || '—'} |
`;
}
html += '
';
}
sec.innerHTML = html;
}
// ============================================================================
// 🔍 验证Tab渲染 (v6.9.0)
// ============================================================================
function renderVerifyTab(chartData) {
const pastSec = document.getElementById('past-events-result');
if (pastSec && chartData._extended?.past_events) {
const pe = chartData._extended.past_events;
pastSec.innerHTML = `${pe.summary || '基于Dasha/Transit反推'}
${(pe.events || []).map(e => `
${e.period || ''}: ${e.description || ''} (${e.confidence || ''})
`).join('')}`;
}
const caseSec = document.getElementById('case-match-result');
if (caseSec && chartData._extended?.case_matches) {
const cm = chartData._extended.case_matches;
caseSec.innerHTML = `匹配到${cm.length || 0}个相似案例
${(cm || []).slice(0,5).map(c => `
${c.name || ''}: ${c.match || ''} 吻合度:${c.accuracy || '?'}
`).join('')}`;
}
const fallSec = document.getElementById('fallacy-warn-result');
if (fallSec) {
let warnings = [];
const interps = chartData._extended?.interpretations || {};
for (const [k, v] of Object.entries(interps)) {
if (typeof v === 'string' && (v.includes('毁灭') || v.includes('落陷') || v.includes('必坏'))) {
warnings.push({text: v.slice(0, 60), fix: '需多配置综合判断'});
}
}
if (warnings.length === 0) warnings.push({text: '未检测到常见误区', fix: ''});
fallSec.innerHTML = warnings.map(w => `
⚠️ ${w.text}${w.fix ? ' → '+w.fix : ''}
`).join('');
}
}
// ============================================================================
// 🪐 过境对比 Tab渲染 (v6.9.0)
// ============================================================================
function renderTransitCompareTab(chartData) {
const transitBtn = document.getElementById('btn-run-transit');
if (!transitBtn) return;
transitBtn.onclick = async () => {
const start = document.getElementById('transit-start').value;
const end = document.getElementById('transit-end').value;
if (!start || !end) return;
const rd = document.getElementById('transit-result');
rd.innerHTML = '搜索过境触发点...
';
try {
const base = window.JyotishAPI?.apiBase || '';
const key = window.JyotishAPI?.apiKey || '';
const resp = await fetch(`${base}/api/transit`, {
method: 'POST', headers: {'Content-Type': 'application/json', 'Authorization': `Bearer ${key}`},
body: JSON.stringify({start, end, planets: Object.keys(chartData.planets || {}).slice(0, 9)})
});
const data = await resp.json();
if (data.triggers?.length) {
rd.innerHTML = `发现${data.triggers.length}个触发点
${data.triggers.slice(0, 20).map(t => `
${t.planet || ''} → ${t.event || t.description} · ${t.start_date || t.date}
`).join('')}`;
} else {
rd.innerHTML = '无显著过境触发点
';
}
} catch(e) {
rd.innerHTML = '过境数据需API服务器 (python3 scripts/jyotish_api_server.py)
';
}
};
}
// ============================================================================
// 星盘中心摘要构建
// ============================================================================
function buildChartSummary(ascendant, moonP, dashaData, karaka, allYogas) {
// 当前 Dasha 信息
let dasha = null;
if (dashaData?.current_dasha) {
const md = dashaData.current_dasha;
const ad = md.antardasha?.find(a => a.is_current);
dasha = {
maha: md.lord_cn || md.lord,
antar: ad ? (ad.lord_cn || ad.lord) : '',
period: ad ? `${ad.start} — ${ad.end}` : `${md.start || ''} — ${md.end || ''}`,
};
}
// AK / DK
let karakaSummary = null;
if (karaka) {
const k7 = karaka.karaka7 || {};
karakaSummary = {
AK: k7.AK?.planet_cn || k7.AK?.planet || null,
DK: k7.DK?.planet_cn || k7.DK?.planet || null,
};
}
// 关键 Yogas(按强度排序取前4)
const strengthOrder = { '极强': 5, '强': 4, '中强': 3, '中': 2, '中负': 1, '负面': 0 };
const topYogas = allYogas
.filter(y => !y.negative)
.sort((a, b) => (strengthOrder[b.strength] || 2) - (strengthOrder[a.strength] || 2))
.slice(0, 4)
.map(y => ({ name_cn: y.name_cn, negative: y.negative }));
return {
moonNakshatra: moonP ? `${moonP.nakshatra} P${moonP.nakshatra_pada}` : null,
dasha,
karaka: karakaSummary,
topYogas,
};
}
// ============================================================================
// 宫位分析
// ============================================================================
function renderHouseAnalysis(planets, ascSign) {
const container = $('house-grid');
container.innerHTML = '';
const houses = computeHouseAnalysis(planets, ascSign);
for (const h of houses) {
const meaning = HOUSE_MEANINGS[h.house];
const card = document.createElement('div');
card.className = 'house-card';
const occHTML = h.occupants.map(o => {
const reading = PLANET_IN_HOUSE[o.name]?.[h.house] || '';
const detail = PLANET_HOUSE_DETAIL[o.name]?.[h.house];
return `
${PLANET_SYMBOLS[o.name]||''} ${planetName(o.name)}
${detail ? '
▼' : ''}
${detail ? `
${t('detail.pos')}${detail.p}
${t('detail.neg')}${detail.n}
${t('detail.adv')}${detail.a}
${t('detail.special')}${detail.s}
` : ''}
`;
}).join('');
let lordReading = '';
if (h.lordHouse && meaning?.lordInHouse) lordReading = meaning.lordInHouse[h.lordHouse] || '';
card.innerHTML = `
${t('house.lord')}: ${planetName(h.lord)}(${houseLabel(h.lordHouse||'?')})
${occHTML ? `${occHTML}
` : ''}
${meaning ? `【${meaning.name}】${meaning.themes}` : ''}${lordReading ? `
${t('house.inHouse')}: ${lordReading}` : ''}
`;
// Bind expand/collapse for occupant details
card.querySelectorAll('.occ-item').forEach(item => {
const badge = item.querySelector('.occupant-badge');
const detail = item.querySelector('.occ-detail');
const hint = item.querySelector('.occ-expand-hint');
if (badge && detail) {
badge.style.cursor = 'pointer';
badge.addEventListener('click', () => {
const open = detail.style.display !== 'none';
detail.style.display = open ? 'none' : 'block';
if (hint) hint.textContent = open ? '▼' : '▲';
});
}
});
container.appendChild(card);
}
}
// ============================================================================
// 相位
// ============================================================================
function renderAspects(planets) {
const container = $('aspects-grid');
container.innerHTML = '';
const aspects = computeAspects(planets);
if (aspects.length === 0) { container.innerHTML = `${t('aspect.none')}
`; return; }
for (const a of aspects) {
const friendlyClass = a.friendly === 'friendly' ? 'aspect-friendly' : a.friendly === 'hostile' ? 'aspect-hostile' : '';
const row = document.createElement('div');
row.className = `aspect-row ${friendlyClass}`;
const aspType = a.type === 'opposition' ? t('aspect.opposition') : a.type === 'trine' ? t('aspect.trine') : t('aspect.special');
row.innerHTML = `
${PLANET_SYMBOLS[a.from]||''} ${planetName(a.from)}
→ ${houseLabel(a.toHouse)} →
${PLANET_SYMBOLS[a.to]||''} ${planetName(a.to)}
${aspType}
`;
container.appendChild(row);
}
}
// ============================================================================
// Yoga(扩展版 — 接受预计算结果)
// ============================================================================
function renderYogasWith(allYogas) {
const container = $('yoga-cards'), summary = $('yoga-summary');
container.innerHTML = ''; summary.innerHTML = '';
const positive = allYogas.filter(y => !y.negative).length;
const negative = allYogas.filter(y => y.negative).length;
summary.innerHTML = `${allYogas.length} ${t('yoga.count')}
${positive} ${t('yoga.bene')}
${negative > 0 ? `${negative} ${t('yoga.male')}
` : ''}`;
if (allYogas.length === 0) { container.innerHTML = `${t('yoga.none')}
`; return; }
for (const y of allYogas) {
const card = document.createElement('div');
card.className = 'yoga-card';
if (y.negative) card.style.borderLeft = '3px solid #f87171';
const detail = YOGA_DETAILS[y.id];
let detailHTML = '';
if (detail) {
const eff = detail.e || {};
detailHTML = `
${t('yoga.formation')}${detail.f}
${eff.c ? `
${t('detail.career')}${eff.c}
` : ''}
${eff.w ? `
${t('detail.wealth')}${eff.w}
` : ''}
${eff.m ? `
${t('detail.marriage')}${eff.m}
` : ''}
${eff.h ? `
${t('detail.health')}${eff.h}
` : ''}
${detail.x ? `
${t('yoga.cancel')}${detail.x}
` : ''}
${detail.t?.length ? `
${detail.t.map(tag=>`${tag}`).join('')}
` : ''}
`;
}
card.innerHTML = `
${detailHTML}
`;
if (detail) {
const header = card.querySelector('.yoga-header');
const detailEl = card.querySelector('.yoga-detail');
const hint = card.querySelector('.yoga-expand-hint');
header.addEventListener('click', () => {
const open = detailEl.style.display !== 'none';
detailEl.style.display = open ? 'none' : 'block';
if (hint) hint.textContent = open ? t('yoga.expand') : t('yoga.collapse');
});
}
container.appendChild(card);
}
}
// ============================================================================
// Yoga(原版 — 兼容保留)
// ============================================================================
function renderYogas(planets, ascSign) {
const container = $('yoga-cards'), summary = $('yoga-summary');
container.innerHTML = ''; summary.innerHTML = '';
const baseYogas = detectYogas(planets, ascSign);
let allYogas = [...baseYogas.yogas];
for (const yd of ALL_YOGA_DEFS) {
try {
const combo = yd.check(planets, ascSign);
if (combo) allYogas.push({ name: yd.name, name_cn: yd.name_cn, combination: combo, effects: yd.effects, strength: yd.strength, negative: yd.negative || false });
} catch (e) {}
}
const positive = allYogas.filter(y => !y.negative).length;
const negative = allYogas.filter(y => y.negative).length;
summary.innerHTML = `${allYogas.length} ${t('yoga.count')}
${positive} ${t('yoga.bene')}
${negative > 0 ? `${negative} ${t('yoga.male')}
` : ''}`;
if (allYogas.length === 0) { container.innerHTML = `${t('yoga.none')}
`; return; }
for (const y of allYogas) {
const card = document.createElement('div');
card.className = 'yoga-card';
if (y.negative) card.style.borderLeft = '3px solid #f87171';
card.innerHTML = `
${y.name}
${y.name_cn}${y.category ? ` [${y.category}]` : ''}
${y.combination}
${y.effects||''}
${y.strength}
`;
container.appendChild(card);
}
}
// ============================================================================
// Transit 推运(v2.0 增强)
// ============================================================================
let _lastTransitData = null;
async function renderTransit(natalPlanets, ascSign, avData, ascDegree = 0) {
$('transit-date').textContent = t('transit.computing');
try {
// 读取用户选择的日期(如果有)
const dateInput = $('transit-date-input');
const timeInput = $('transit-time-input');
const dateStr = dateInput?.value || null;
const timeStr = timeInput?.value || null;
const tz = chartData?.birth_info?.tz || 0;
const transit = await computeTransit(dateStr, timeStr, tz);
_lastTransitData = { transit, natalPlanets, ascSign };
$('transit-date').textContent = `${transit.date} ${transit.time} UT · Ayanamsa: ${transit.ayanamsa}°`;
// Transit 星盘渲染
const tChartData = { ascendant: { sign: ascSign }, planets: {} };
for (const [pn, pi] of Object.entries(transit.planets)) {
tChartData.planets[pn] = { ...pi, status: '中性', house: 1 };
}
renderSouthIndianChart($('transit-chart'), tChartData);
// 全行星 Transit 表格
const overlay = computeTransitOverlay(natalPlanets, ascSign, transit.planets);
const tableContainer = $('transit-all-table');
if (tableContainer) {
let html = `
| 行星 | Transit星座 | 度数 | 本命宫位 | 逆行 | 星宿 | 本命星座 | 换座 |
`;
for (const p of overlay.all) {
const changeClass = p.sign_changed ? 'style="color:var(--red);font-weight:600"' : '';
html += `
| ${p.symbol} ${planetName(p.planet)} |
${signName(p.transit_sign)} |
${p.transit_degree_in_sign.toFixed(2)}° |
H${p.transit_house} |
${p.retrograde ? '℞' : ''} |
${p.transit_nakshatra} P${p.transit_nakshatra_pada} |
${p.natal_sign ? signName(p.natal_sign) : '-'} |
${p.sign_changed ? '⚡' + t('col.change') : ''} |
`;
}
html += '
';
tableContainer.innerHTML = html;
}
// 慢星详细分析
const slowContainer = $('transit-overlay');
slowContainer.innerHTML = '';
for (const item of overlay.slow) {
const effect = TRANSIT_EFFECTS[item.planet]?.[item.transit_house] || '';
const cjStr = item.conjunctions.length > 0
? `${t('transit.conj')}: ${item.conjunctions.map(c => `${planetName(c.planet)}(${c.degree_diff}°)`).join(', ')}
` : '';
const aspStr = item.aspects.length > 0
? `${t('transit.asp')}: ${item.aspects.map(a => `${planetName(a.to)}[H${a.to_house}](${a.type})`).join(', ')}
` : '';
const div = document.createElement('div');
div.className = 'transit-overlay-item';
div.innerHTML = `${item.symbol}
${planetName(item.planet)} ${item.retrograde ? '℞' : ''} → ${houseLabel(item.transit_house)}
${signName(item.transit_sign)} ${item.transit_degree_in_sign.toFixed(2)}° ${item.retrograde ? t('ss.retro') : ''}
${effect ? `
${effect}
` : ''}
${cjStr}${aspStr}
`;
slowContainer.appendChild(div);
}
// Double Transit
const doubleTransits = computeDoubleTransit(transit.planets, ascSign);
const dtSection = $('double-transit-section');
dtSection.innerHTML = '';
if (doubleTransits.length > 0) {
dtSection.innerHTML = `${t('dt.title')}
`;
for (const dt of doubleTransits) {
const detail = dt.aspectedBy.map(a => `${planetName(a.planet)}(H${a.from_house}→H${a.offset})`).join(' + ');
dtSection.innerHTML += ``;
}
}
// Sade Sati(增强版)
const ss = checkSadeSati(natalPlanets.Moon?.sign || '', transit.planets);
const ssSection = $('sade-sati-section');
ssSection.innerHTML = '';
if (ss) {
ssSection.innerHTML = `${t('ss.detect')}
${ss.active ? `[!] ${ss.desc}` : (ss.phase === 0 ? `[~] ${ss.desc}` : `[OK] ${t('ss.active')}`)}
${ss.detail ? `${ss.detail}
` : ''}
${t('ss.moon')}: ${signName(ss.natal_moon_sign)} · ${t('ss.saturn')}: ${signName(ss.saturn_sign)} ${ss.saturn_degree_in_sign.toFixed(2)}° ${ss.saturn_retrograde ? t('ss.retro') : ''}
`;
}
// ── 新功能: Double Transit PAC + D9 ──
const dtpac = computeDoubleTransitPAC(transit.planets, natalPlanets, ascSign, ascDegree, 7);
const dtpacSection = $('double-transit-pac-section');
if (dtpacSection) {
dtpacSection.innerHTML = '';
const summaryIcon = dtpac.summary.startsWith('✅') ? '✅' : dtpac.summary.startsWith('⚠️') ? '⚠️' : '❌';
let html = `KN Rao Double Transit PAC + D9
`;
html += `${summaryIcon} ${dtpac.summary.replace(/[✅⚠️❌]/g, '')}
`;
// D1 层
const d1j = Object.keys(dtpac.d1.jupiter);
const d1s = Object.keys(dtpac.d1.saturn);
if (d1j.length || d1s.length) {
html += `D1层 | Jupiter→[${d1j.join(', ')}] Saturn→[${d1s.join(', ')}]
`;
}
// D9 层
const d9j = Object.keys(dtpac.d9.jupiter);
const d9s = Object.keys(dtpac.d9.saturn);
if (d9j.length || d9s.length) {
html += `D9层 | Jupiter→[${d9j.join(', ')}] Saturn→[${d9s.join(', ')}]
`;
}
// Double Transit 命中
if (dtpac.double_transit.length > 0) {
html += '';
for (const dt of dtpac.double_transit) {
html += `
[${dt.layer}] ${dt.target} (${dt.strength})
`;
}
html += '
';
}
dtpacSection.innerHTML = html;
}
// ── 新功能: Transit LL/7L ──
const ll7l = computeTransitLL7L(transit.planets, natalPlanets, ascSign);
const ll7lSection = $('transit-ll7l-section');
if (ll7lSection) {
ll7lSection.innerHTML = '';
let html = `Transit LL/7L 连接
`;
html += `LL: ${planetName(ll7l.lagna_lord)} | 7L: ${planetName(ll7l.seventh_lord)}
`;
if (ll7l.p5.hit) {
html += `P5(98%): ${ll7l.p5.details.map(d => `${d.direction} → ${d.connections.map(c => c.type).join('+')}`).join(' | ')}
`;
} else {
html += `P5(98%): 未触发
`;
}
if (ll7l.p8.hit) {
html += `P8: ${ll7l.p8.details.join(' | ')}
`;
}
if (ll7l.parivartana.hit) {
html += `互换: ${ll7l.parivartana.details.join(' | ')}
`;
}
ll7lSection.innerHTML = html;
}
// ── 新功能: 行星聚集 ──
const cong = computePlanetaryCongregation(natalPlanets, ascSign, transit.planets, 7);
const congSection = $('planetary-congregation-section');
if (congSection) {
congSection.innerHTML = '';
let html = `行星聚集检测
`;
html += `本命 Lagna: [${cong.natal.lagna.map(planetName).join(', ')}] (${cong.natal.lagna.length}) | 7H: [${cong.natal.house_7.map(planetName).join(', ')}] (${cong.natal.house_7.length})
`;
if (cong.transit) {
const t7 = cong.transit[7] || [];
html += `Transit 7H: [${t7.map(planetName).join(', ')}] (${t7.length})
`;
}
html += `${cong.summary}
`;
congSection.innerHTML = html;
}
// ── 新功能: Vivah Saham ──
const vs = computeVivahSaham(natalPlanets, ascDegree, transit.planets);
const vsSection = $('vivah-saham-section');
if (vsSection) {
vsSection.innerHTML = '';
let html = `Vivah Saham 婚姻敏感点
`;
html += `${vs.vivah_saham.sign_cn} ${vs.vivah_saham.degree_in_sign.toFixed(2)}°`;
if (vs.vivah_saham.nakshatra) html += ` | ${vs.vivah_saham.nakshatra} P${vs.vivah_saham.pada}`;
html += '
';
if (vs.transit_activation) {
const ta = vs.transit_activation;
html += `Jupiter PAC: [${ta.jupiter.map(c => c.type).join(', ') || '-'}] | Saturn PAC: [${ta.saturn.map(c => c.type).join(', ') || '-'}]
`;
html += `双星激活: ${ta.double_activation ? '✅ 是' : '❌ 否'}
`;
if (ta.venus_in_saham_sign) html += `Venus 在 Saham 星座 ✅
`;
}
vsSection.innerHTML = html;
}
// Ashtakavarga Transit 评分
if (avData) {
const avScores = computeTransitAVScore(transit.planets, ascSign, avData);
const avSection = $('transit-av-section');
if (avSection && avScores) {
let avHTML = `${t('tav.title')}
`;
for (const s of avScores) {
const qClass = s.transit_quality === 'favorable' ? 'av-fav' : s.transit_quality === 'challenging' ? 'av-chal' : 'av-neu';
avHTML += `
${planetName(s.planet)}
${signName(s.transit_sign)}
SAV: ${s.sav_score ?? '-'}
${s.transit_quality === 'favorable' ? t('quality.fav') : s.transit_quality === 'challenging' ? t('quality.chal') : t('quality.neu')}
`;
}
avHTML += '
';
avSection.innerHTML = avHTML;
}
}
// 宫位影响摘要
const houseImpact = computeTransitHouseImpact(transit.planets, ascSign);
const hiSection = $('transit-house-impact');
if (hiSection) {
let hiHTML = `${t('thi.title')}
`;
for (let h = 1; h <= 12; h++) {
const hi = houseImpact[h];
const total = hi.transiting.length + hi.aspected_by.length;
const intensity = total >= 3 ? 'intense' : total >= 2 ? 'moderate' : 'light';
hiHTML += `
H${h} ${houseAreaName(h)}
${hi.transiting.map(t => `${planetName(t.planet)}${t.retrograde?'℞':''}`).join('')}
${hi.aspected_by.map(a => `${planetName(a.planet)}(${a.aspect_type})`).join('')}
`;
}
hiHTML += '
';
hiSection.innerHTML = hiHTML;
}
} catch (err) {
console.error('[Transit] Error:', err);
$('transit-date').textContent = 'Transit 计算失败: ' + err.message;
}
}
// ============================================================================
// 页面切换 + 初始化
// ============================================================================
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
if (name === 'input') $('page-input').classList.add('active');
else if (name === 'chart') {
$('page-chart').classList.add('active');
// 延迟触发 Tab 滚动提示检查(需等 DOM 渲染完毕)
setTimeout(() => { if (window.__checkTabScrollHint) window.__checkTabScrollHint(); }, 100);
}
}
document.addEventListener('DOMContentLoaded', () => {
// 初始化 i18n
initI18N();
// 语言变化时重新渲染
onLangChange(() => {
if (chartData) renderAll();
});
initDateSelects();
setupCitySearch();
setupForm();
$('btn-back').addEventListener('click', () => showPage('input'));
// 初始化术语 Tooltip
initTooltip();
// 初始化认证系统
initAuth();
// 初始化订阅系统
initSubscription();
// 初始化 AI 聊天
initAIChat();
// Transit 更新按钮
const transitBtn = $('btn-transit-update');
if (transitBtn) {
transitBtn.addEventListener('click', async () => {
if (!chartData) return;
const { planets, ascendant, birth_info } = chartData;
const allV = computeAllVargas(planets);
const av = computeAshtakavarga(planets, ascendant.sign);
await renderTransit(planets, ascendant.sign, av, ascendant.degree);
});
}
// 导出功能
setupExport();
// 星盘风格切换
setupChartStyle();
});
// ============================================================================
// 导出辅助函数
// ============================================================================
function _getBirthNakIdx(planets) {
const moonLon = planets.Moon?.degree || 0;
return Math.floor(moonLon / (360 / 27)) % 27;
}
function _buildAspectsModule(planets) {
const rawAspects = computeAspects(planets);
const aspects = (rawAspects || []).map(a => ({
planet1: a.from, planet2: a.to,
type: a.offset === 7 ? 'opposition' : (a.offset === 5 || a.offset === 9) ? 'trine' : `aspect_${a.offset}`,
aspect_degree: a.offset * 30,
actual_diff: Math.abs((planets[a.from]?.degree || 0) - (planets[a.to]?.degree || 0)),
orb: 0, orb_category: 'standard',
applying: false, strength: 0,
description: `${a.from}→${a.to}(${a.offset}宫相位)`,
friendly: a.friendly,
}));
// Conjunctions
const names = Object.keys(planets).filter(p => !planets[p].error);
const conjunctions = [];
for (let i = 0; i < names.length; i++) {
for (let j = i + 1; j < names.length; j++) {
const d1 = planets[names[i]].degree, d2 = planets[names[j]].degree;
const diff = Math.min(Math.abs(d1 - d2), 360 - Math.abs(d1 - d2));
if (diff < 10) conjunctions.push({ planet1: names[i], planet2: names[j], orb: Math.round(diff * 100) / 100 });
}
}
return { aspects, conjunctions, summary: `${aspects.length} aspects, ${conjunctions.length} conjunctions` };
}
function _buildAVModule(av) {
if (!av) return null;
return {
method: 'Parashara BPHS',
bav: av.bav_raw,
sav: { scores: av.sav },
sav_total: av.sav_total,
sav_by_house: av.sav_by_house,
bav_by_house: av.bav_by_house,
};
}
function _buildJaiminiModule(karaka, charaDasha, karakamsha) {
const k7 = karaka?.karaka7 || {};
const k8 = karaka?.karaka8 || {};
const DOMAIN = {
AK: '灵魂使命、核心自我、人生最高目标', AmK: '事业方向、主要谋士、权力代理',
BK: '兄弟姐妹、勇气、冒险精神', MK: '母亲、家庭根基、情感安全感',
PK: '子女、创造力、学生、智能成果', GK: '竞争对手、疾病、障碍、转化力量',
DK: '配偶特质、伴侣关系、婚姻质量', PiK: '父亲、祖先、遗产',
};
const CN = {
AK: '灵魂星AK', AmK: '事业星AmK', BK: '兄弟星BK', MK: '母亲星MK',
PK: '子女星PK', GK: '障碍星GK', DK: '配偶星DK', PiK: '父亲星PiK',
};
const kt7 = {}, kt8 = {};
let rank = 1;
for (const [k, v] of Object.entries(k7)) {
kt7[k] = { ...v, rank: rank++, domain: DOMAIN[k] || '', cn_name: CN[k] || '' };
}
rank = 1;
for (const [k, v] of Object.entries(k8)) {
kt8[k] = { ...v, rank: rank++, domain: DOMAIN[k] || '', cn_name: CN[k] || '' };
}
return {
chara_karaka_7: {
karaka_table: kt7,
summary: {
AK: k7.AK ? `${k7.AK.planet} (${k7.AK.degree}°)` : '',
DK: k7.DK ? `${k7.DK.planet} (${k7.DK.degree}°)` : '',
AK_domain: DOMAIN.AK, DK_domain: DOMAIN.DK,
},
},
chara_karaka_8: { karaka_table_8: kt8 },
chara_dasha: charaDasha,
karakamsha,
};
}
// ============================================================================
// 导出功能
// ============================================================================
function setupExport() {
const btnExport = $('btn-export');
const menu = $('export-menu');
if (!btnExport || !menu) return;
btnExport.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
document.addEventListener('click', () => menu.classList.add('hidden'));
menu.querySelectorAll('.export-item').forEach(item => {
item.addEventListener('click', () => {
menu.classList.add('hidden');
if (!chartData) { alert(t('alert.chart')); return; }
const format = item.dataset.format;
if (format === 'json') {
const { planets, ascendant, birth_info } = chartData;
// ── 收集全部13模块 ──
const vargas = computeAllVargas(planets);
const karaka = computeKaraka(planets);
const av = computeAshtakavarga(planets, ascendant.sign);
const sb = computeShadbala(planets, ascendant.sign, birth_info, vargas);
const dasha = computeDashaWithPratyantar(planets.Moon?.degree || 0, birth_info.date, new Date().toISOString().split('T')[0]);
const yogas = [...detectYogas(planets, ascendant.sign)];
// Extended yogas
try { yogas.push(...detectExtendedYogas(planets, ascendant.sign)); } catch(e) {}
const aspects = _buildAspectsModule(planets);
const panchanga = computeTithiYoga(planets, birth_info);
const nakshatraAdvanced = computeNakshatraAdvanced(planets, _getBirthNakIdx(planets));
const charaDasha = computeCharaDasha(planets, ascendant.sign);
const karakamsha = computeKarakamsha(planets);
const ascIdx = SIGNS.indexOf(ascendant.sign);
const planetSignIdx = {};
for (const [pn, p] of Object.entries(planets)) { if (!p.error) planetSignIdx[pn] = Math.floor(p.degree / 30); }
const argala = computeArgala(planetSignIdx, ascIdx);
const birthYear = parseInt((birth_info.date || '').split('-')[0]) || 1990;
const birthMonth = parseInt((birth_info.date || '').split('-')[1]) || 1;
const tajika = computeTajika(planets, ascendant.sign, birthYear, birthMonth);
const validation = computeValidation(planets, ascendant, av, dasha);
const audit = computeAudit(planets, ascendant, av, validation);
const actionableCtx = computeActionableContext(planets, ascendant);
const jaimini = _buildJaiminiModule(karaka, charaDasha, karakamsha);
exportJSON(chartData, {
dasha, yogas, vargas, aspects, jaimini,
nakshatraAdvanced, argala, tajika,
shadbala: sb, ashtakavarga: _buildAVModule(av),
panchanga, validation, audit, actionableContext,
});
} else if (format === 'svg') {
const chartEl = $('rasi-chart');
exportSVG(chartEl, `jyotish-${chartData.birth_info?.date || 'chart'}`);
} else if (format === 'png') {
const chartEl = $('rasi-chart');
exportPNG(chartEl, `jyotish-${chartData.birth_info?.date || 'chart'}`);
}
});
});
}
// ============================================================================
// 星盘风格切换
// ============================================================================
function setupChartStyle() {
const btns = document.querySelectorAll('.style-btn');
btns.forEach(btn => {
btn.addEventListener('click', () => {
const style = btn.dataset.style;
setChartStyle(style);
btns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 重新渲染本命盘
if (chartData) {
const fn = style === 'north' ? renderNorthIndianChart : renderSouthIndianChart;
fn($('rasi-chart'), chartData, { summary: _lastChartSummary });
}
});
});
}