Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { RuleBasedReport, RuleEvidence, TimeLayerScope } from '../core-engine/types'; | |
| interface AnalysisPanelProps { | |
| report: RuleBasedReport; | |
| activeScope: TimeLayerScope; | |
| onScopeChange: (scope: TimeLayerScope) => void; | |
| onClose: () => void; | |
| } | |
| const AnalysisPanel: React.FC<AnalysisPanelProps> = ({ report, activeScope, onScopeChange, onClose }) => { | |
| const [activeTab, setActiveTab] = useState<'overview' | 'topics' | 'audit'>('overview'); | |
| const currentReport = report.scopedReports[activeScope] || report.globalReport; | |
| return ( | |
| <div className="fixed top-0 right-0 w-[500px] h-full bg-gray-50 shadow-2xl border-l border-gray-200 overflow-y-auto z-50 flex flex-col font-sans"> | |
| {/* 头部 */} | |
| <div className="p-4 bg-indigo-900 text-white flex justify-between items-center sticky top-0 z-10 shadow-md"> | |
| <div> | |
| <h2 className="text-lg font-bold">紫微斗数深度解析报告</h2> | |
| <div className="text-xs text-indigo-200 mt-1 flex space-x-2"> | |
| <span>静态引擎: static-v2.0.0</span> | |
| <span>|</span> | |
| <span>动态引擎: dynamic-v2.0.0</span> | |
| </div> | |
| </div> | |
| <button onClick={onClose} className="text-gray-300 hover:text-white transition-colors bg-white/10 p-1.5 rounded-md"> | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| {/* 时间层切换栏 */} | |
| <div className="bg-indigo-800 p-2 flex space-x-2 overflow-x-auto sticky top-[72px] z-10 shadow-sm hide-scrollbar"> | |
| {report.activeScopes.map(scope => { | |
| const labels: Record<string, string> = { | |
| original: '本命全局', | |
| daXian: '大限(十年)', | |
| liuNian: '流年(当年)', | |
| liuYue: '流月', | |
| liuRi: '流日', | |
| liuShi: '流时' | |
| }; | |
| return ( | |
| <button | |
| key={scope} | |
| onClick={() => onScopeChange(scope)} | |
| className={`whitespace-nowrap px-3 py-1.5 text-xs font-bold rounded-md transition-colors ${ | |
| activeScope === scope | |
| ? 'bg-white text-indigo-900 shadow-sm' | |
| : 'bg-indigo-900/50 text-indigo-100 hover:bg-indigo-800' | |
| }`} | |
| > | |
| {labels[scope]} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {/* Tab 切换 */} | |
| <div className="flex bg-white border-b sticky top-[116px] z-10"> | |
| <button | |
| className={`flex-1 py-3 text-sm font-bold ${activeTab === 'overview' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:bg-gray-50'}`} | |
| onClick={() => setActiveTab('overview')} | |
| > | |
| 综合报告 | |
| </button> | |
| <button | |
| className={`flex-1 py-3 text-sm font-bold ${activeTab === 'topics' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:bg-gray-50'}`} | |
| onClick={() => setActiveTab('topics')} | |
| > | |
| 时间层与专题 | |
| </button> | |
| <button | |
| className={`flex-1 py-3 text-sm font-bold ${activeTab === 'audit' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:bg-gray-50'}`} | |
| onClick={() => setActiveTab('audit')} | |
| > | |
| 规则审计 | |
| </button> | |
| </div> | |
| <div className="p-5 flex-1 overflow-y-auto space-y-6"> | |
| {activeTab === 'overview' && ( | |
| <> | |
| {/* 1. 总览 */} | |
| <section className="bg-white p-5 rounded-xl shadow-sm border border-gray-100"> | |
| <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-100"> | |
| <h3 className="font-bold text-gray-800 text-lg flex items-center"> | |
| <span className="text-indigo-500 mr-2">📊</span> 命局总览 ({activeScope === 'original' ? '本命全局' : '当前时间层'}) | |
| </h3> | |
| <div className="text-right"> | |
| <div className="text-3xl font-black text-indigo-600">{currentReport.synthesis.totalScore}</div> | |
| <div className="text-xs text-gray-400">综合得分 (满分100)</div> | |
| </div> | |
| </div> | |
| <p className="text-gray-700 font-medium mb-2">{currentReport.output.overview.summary}</p> | |
| <ul className="space-y-2 text-sm text-gray-600 mt-4"> | |
| <li className="flex items-start"><span className="text-green-500 mr-2">▲</span> {currentReport.output.overview.strength}</li> | |
| <li className="flex items-start"><span className="text-red-500 mr-2">▼</span> {currentReport.output.overview.risk}</li> | |
| <li className="flex items-start"><span className="text-blue-500 mr-2">★</span> {currentReport.output.overview.focus}</li> | |
| </ul> | |
| </section> | |
| {/* 2. 静态结论 */} | |
| <section className="bg-white p-5 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 text-lg mb-4 flex items-center"> | |
| <span className="text-blue-500 mr-2">🏛️</span> 先天底色 (静态格局) | |
| </h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <h4 className="text-sm font-bold text-gray-700 mb-1">性格特质</h4> | |
| <p className="text-sm text-gray-600 leading-relaxed">{currentReport.output.staticBase.traits.join(' ')}</p> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="bg-green-50 p-3 rounded-lg border border-green-100"> | |
| <h4 className="text-xs font-bold text-green-800 mb-2">先天优势</h4> | |
| <ul className="list-disc pl-4 text-xs text-green-700 space-y-1"> | |
| {currentReport.output.staticBase.pros.map((p, i) => <li key={i}>{p}</li>)} | |
| </ul> | |
| </div> | |
| <div className="bg-red-50 p-3 rounded-lg border border-red-100"> | |
| <h4 className="text-xs font-bold text-red-800 mb-2">潜在隐患</h4> | |
| <ul className="list-disc pl-4 text-xs text-red-700 space-y-1"> | |
| {currentReport.output.staticBase.cons.map((c, i) => <li key={i}>{c}</li>)} | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| {/* 3. 动态结论 */} | |
| <section className="bg-white p-5 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 text-lg mb-4 flex items-center"> | |
| <span className="text-purple-500 mr-2">🌪️</span> 后天推演 (动态四化) | |
| </h3> | |
| <div className="space-y-3 text-sm text-gray-600"> | |
| <div className="bg-purple-50 p-3 rounded-lg border border-purple-100"> | |
| <span className="font-bold text-purple-800 block mb-1">引动宫位:</span> | |
| {currentReport.output.dynamicTrend.triggeredPalaces.length > 0 ? currentReport.output.dynamicTrend.triggeredPalaces.join('、') : '无明显引动'} | |
| </div> | |
| <div> | |
| <span className="font-bold text-gray-800 block mb-1">风险与机遇链条:</span> | |
| <ul className="list-disc pl-5 space-y-1"> | |
| {currentReport.output.dynamicTrend.risksAndOps.length > 0 ? currentReport.output.dynamicTrend.risksAndOps.map((r, i) => <li key={i}>{r}</li>) : <li>无明显动态链</li>} | |
| </ul> | |
| </div> | |
| <div className="text-xs text-gray-400 mt-2 italic">{currentReport.output.dynamicTrend.timing}</div> | |
| </div> | |
| </section> | |
| {/* 4. 天纪处方 */} | |
| <section className="bg-gradient-to-br from-red-50 to-orange-50 p-5 rounded-xl shadow-sm border border-red-200"> | |
| <h3 className="font-bold text-red-800 text-lg mb-4 flex items-center"> | |
| <span className="text-red-600 mr-2">📜</span> 天纪派处方建议 | |
| </h3> | |
| <ul className="space-y-3"> | |
| {currentReport.output.prescriptions.map((adv, i) => ( | |
| <li key={i} className="text-red-900 text-sm leading-relaxed font-medium bg-white/60 p-3 rounded border border-red-100"> | |
| {adv} | |
| </li> | |
| ))} | |
| </ul> | |
| </section> | |
| {/* 5. 能力边界标识 */} | |
| <section className="bg-gray-50 p-4 rounded-xl border border-gray-200 mt-6 text-xs text-gray-500"> | |
| <h4 className="font-bold text-gray-700 mb-2 flex items-center"> | |
| <span className="mr-1">ℹ️</span> 推演层级与能力边界说明 | |
| </h4> | |
| <ul className="space-y-1"> | |
| <li className="flex items-start"><span className="text-green-500 mr-1">✓</span> <strong>已推演:</strong>本命盘静态格局、生年四化、大限流年流月流日流时全时间层引动、天纪派断语字典接入、静动态综合评分、全报告时间层切换。</li> | |
| <li className="flex items-start"><span className="text-orange-400 mr-1">○</span> <strong>待接入:</strong>外部规则知识库检索(已预留 `KnowledgeSource` 接口)、体用链深度推演。</li> | |
| </ul> | |
| </section> | |
| </> | |
| )} | |
| {activeTab === 'topics' && ( | |
| <div className="space-y-6"> | |
| {/* 核心专题展示 */} | |
| <div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 mb-4 flex items-center"> | |
| <span className="text-purple-500 mr-2">🎯</span> 核心人生专题 ({activeScope === 'original' ? '本命盘' : '当前时间层'}) | |
| </h3> | |
| <div className="space-y-4"> | |
| {Object.entries({ | |
| '事业 (官禄)': currentReport.static.topics.career, | |
| '财富 (财帛)': currentReport.static.topics.wealth, | |
| '婚姻 (夫妻)': currentReport.static.topics.marriage, | |
| '健康 (疾厄)': currentReport.static.topics.health, | |
| '资产 (田宅)': currentReport.static.topics.property, | |
| }).map(([title, evidences]) => ( | |
| <div key={title} className="border border-gray-100 rounded-lg p-3"> | |
| <h4 className="font-bold text-sm text-gray-700 mb-2 border-b border-gray-50 pb-1">{title}</h4> | |
| {evidences.length > 0 ? ( | |
| <ul className="space-y-2"> | |
| {evidences.map((ev, idx) => ( | |
| <li key={idx} className="text-xs text-gray-600"> | |
| <span className="font-semibold text-gray-800">{ev.title}: </span> | |
| {ev.description} | |
| </li> | |
| ))} | |
| </ul> | |
| ) : ( | |
| <div className="text-xs text-gray-400 italic">该专题在本层未见明显吉凶极端组合,属平稳态。</div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'audit' && ( | |
| <div className="space-y-6"> | |
| {/* 分项得分雷达图替代文字 */} | |
| <div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 className="font-bold text-gray-800 mb-4">维度得分明细 ({activeScope === 'original' ? '本命全局' : '当前时间层'})</h3> | |
| <div className="grid grid-cols-2 gap-4 text-sm"> | |
| <div className="flex justify-between p-2 bg-gray-50 rounded"><span className="text-gray-600">事业</span><span className="font-bold text-indigo-600">{currentReport.synthesis.dimensionScores.career}</span></div> | |
| <div className="flex justify-between p-2 bg-gray-50 rounded"><span className="text-gray-600">财富</span><span className="font-bold text-indigo-600">{currentReport.synthesis.dimensionScores.wealth}</span></div> | |
| <div className="flex justify-between p-2 bg-gray-50 rounded"><span className="text-gray-600">婚姻</span><span className="font-bold text-indigo-600">{currentReport.synthesis.dimensionScores.marriage}</span></div> | |
| <div className="flex justify-between p-2 bg-gray-50 rounded"><span className="text-gray-600">健康</span><span className="font-bold text-indigo-600">{currentReport.synthesis.dimensionScores.health}</span></div> | |
| <div className="flex justify-between p-2 bg-gray-50 rounded col-span-2"><span className="text-gray-600">置信度评估</span><span className={`font-bold ${currentReport.synthesis.confidence === 'High' ? 'text-green-600' : currentReport.synthesis.confidence === 'Low' ? 'text-red-600' : 'text-yellow-600'}`}>{currentReport.synthesis.confidence}</span></div> | |
| </div> | |
| </div> | |
| {/* 规则审计列表 */} | |
| <div> | |
| <h3 className="font-bold text-gray-800 mb-3 pl-1">触发规则证据链 (Audit Log)</h3> | |
| <div className="space-y-3"> | |
| {currentReport.allEvidences.map((ev, i) => ( | |
| <RuleCard key={i} evidence={ev} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const RuleCard: React.FC<{ evidence: RuleEvidence }> = ({ evidence }) => { | |
| const [expanded, setExpanded] = useState(false); | |
| const isPositive = evidence.scoreImpact > 0; | |
| const isNegative = evidence.scoreImpact < 0; | |
| return ( | |
| <div className="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden text-sm"> | |
| <div | |
| className="p-3 cursor-pointer hover:bg-gray-50 flex justify-between items-center" | |
| onClick={() => setExpanded(!expanded)} | |
| > | |
| <div className="flex items-center space-x-2"> | |
| <span className={`w-2 h-2 rounded-full ${isPositive ? 'bg-green-500' : isNegative ? 'bg-red-500' : 'bg-gray-400'}`}></span> | |
| <span className="font-bold text-gray-800">{evidence.title}</span> | |
| <span className="text-xs text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded border">{evidence.type}</span> | |
| </div> | |
| <div className="flex items-center space-x-3"> | |
| <span className={`font-bold ${isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-gray-500'}`}> | |
| {evidence.scoreImpact > 0 ? '+' : ''}{evidence.scoreImpact} | |
| </span> | |
| <svg className={`w-4 h-4 text-gray-400 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| {expanded && ( | |
| <div className="p-4 bg-gray-50 border-t border-gray-100 space-y-3 text-xs text-gray-600"> | |
| <div><strong className="text-gray-700 block mb-1">结论描述:</strong> {evidence.description}</div> | |
| {evidence.boardEvidence.length > 0 && ( | |
| <div> | |
| <strong className="text-gray-700 block mb-1">盘面证据:</strong> | |
| <ul className="list-disc pl-4 text-gray-500">{evidence.boardEvidence.map((b, i) => <li key={i}>{b}</li>)}</ul> | |
| </div> | |
| )} | |
| {evidence.conflicts && evidence.conflicts.length > 0 && ( | |
| <div> | |
| <strong className="text-red-700 block mb-1">冲突制化项:</strong> | |
| <ul className="list-disc pl-4 text-red-600">{evidence.conflicts.map((c, i) => <li key={i}>{c}</li>)}</ul> | |
| </div> | |
| )} | |
| <div className="flex justify-between items-center pt-2 border-t border-gray-200 mt-2"> | |
| <span className="text-gray-400 font-mono">Rule ID: {evidence.ruleId}</span> | |
| <div className="flex space-x-1"> | |
| {evidence.tags.map((tag, i) => ( | |
| <span key={i} className="bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded text-[10px]">{tag}</span> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default AnalysisPanel; | |