/** * 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 { await initEngine(); chartData = await computeChart({ year, month, day, hour, minute, lat, lon, tz }); // 保存出生数据供生时校正使用 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); // 绑定术语 Tooltip(延迟确保所有异步渲染完成) setTimeout(() => bindTerms(document.querySelector('#page-chart')), 200); setTimeout(() => bindTerms(document.querySelector('#page-chart')), 800); } // ============================================================================ // 星盘中心摘要构建 // ============================================================================ 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 ? `` : ''}
`; }).join(''); let lordReading = ''; if (h.lordHouse && meaning?.lordInHouse) lordReading = meaning.lordInHouse[h.lordHouse] || ''; card.innerHTML = `
${houseLabel(h.house)}${signName(h.sign)} ${h.sign}
${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 = ``; } card.innerHTML = `
${y.name}
${y.name_cn}${y.category ? ` [${y.category}]` : ''}
${y.combination}
${y.effects||''}
${y.strength} ${detail ? `${t('yoga.expand')}` : ''}
${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 = ``; for (const p of overlay.all) { const changeClass = p.sign_changed ? 'style="color:var(--red);font-weight:600"' : ''; html += ``; } html += '
行星Transit星座度数本命宫位逆行星宿本命星座换座
${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') : ''}
'; 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 += `
${dt.desc}
${detail}
`; } } // 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 }); } }); }); }