Files
ai-platform/src/modules/knowledge-graph/components/NodeDetailPanel.tsx
T

276 lines
12 KiB
TypeScript

import { X } from 'lucide-react';
import { projectMap, prefixLabels } from '../data/materials';
import type { Material } from '../data/materials';
const lightCategoryColors: Record<string, string> = {
'温控系统': '#0A7AFF',
'气路系统': '#7B6CF7',
'阀件系统': '#F07830',
'结构件': '#28A866',
};
const lightAgingColors: Record<string, string> = {
'60-90': '#28A866',
'90-120': '#E08C00',
'120-150':'#D94040',
};
const darkCategoryColors: Record<string, string> = {
'温控系统': '#38b8ff',
'气路系统': '#a78bfa',
'阀件系统': '#fb923c',
'结构件': '#34d399',
};
const darkAgingColors: Record<string, string> = {
'60-90': '#34d399',
'90-120': '#fbbf24',
'120-150':'#f87171',
};
interface NodeDetailPanelProps {
node: {
id: string;
label: string;
type: string;
color: string;
material?: Material;
count?: number;
agingRange?: string;
} | null;
onClose: () => void;
darkMode?: boolean;
}
function Row({ label, children, darkMode }: { label: string; children: React.ReactNode; darkMode: boolean }) {
return (
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start',
padding: '10px 0',
borderBottom: `1px solid ${darkMode ? 'rgba(0,212,255,0.08)' : '#f2f2f7'}`,
gap: 12,
}}>
<span style={{ fontSize: 13, color: darkMode ? '#4a7fa8' : '#8e8e93', letterSpacing: '-0.01em', flexShrink: 0 }}>{label}</span>
<span style={{ fontSize: 13, fontWeight: 500, color: darkMode ? '#a8d8ff' : '#1c1c1e', letterSpacing: '-0.01em', textAlign: 'right' }}>{children}</span>
</div>
);
}
export default function NodeDetailPanel({ node, onClose, darkMode = false }: NodeDetailPanelProps) {
if (!node) return null;
const catColors = darkMode ? darkCategoryColors : lightCategoryColors;
const agColors = darkMode ? darkAgingColors : lightAgingColors;
const mat = node.material;
const catColor = mat ? (catColors[mat.category] || '#94a3b8') : node.color;
const agColor = mat ? (agColors[mat.aging] || '#94a3b8') : node.color;
const typeLabel: Record<string, string> = {
material: '物料节点', project: '项目节点', category: '分类节点', aging: '账龄节点',
};
// ── 深色主题变量 ──────────────────────────────
const panelBg = darkMode ? 'rgba(0,15,45,0.92)' : '#ffffff';
const panelBorder = darkMode ? '1px solid rgba(0,212,255,0.2)' : '1px solid #e5e5ea';
const panelShadow = darkMode ? '0 0 30px rgba(0,212,255,0.12)' : '0 4px 28px rgba(0,0,0,0.09)';
const headerBorder = darkMode ? 'rgba(0,212,255,0.1)' : '#f2f2f7';
const titleColor = darkMode ? '#e8f4ff' : '#1c1c1e';
const subtitleColor= darkMode ? '#4a7fa8' : '#8e8e93';
const closeBtnBg = darkMode ? 'rgba(0,212,255,0.1)' : '#f2f2f7';
const closeBtnColor= darkMode ? '#00d4ff' : '#6d6d72';
const specBg = darkMode ? 'rgba(0,30,70,0.6)' : '#f9f9fb';
const specBorder = darkMode ? 'rgba(0,212,255,0.12)' : '#e8e8ed';
const specText = darkMode ? '#7aadcc' : '#3a3a3c';
const sectionLabel = darkMode ? '#3a6080' : '#aeaeb2';
const projBg = darkMode ? 'rgba(100,80,200,0.12)' : '#f5f0ff';
const projBorder = darkMode ? 'rgba(167,139,250,0.25)' : 'rgba(123,108,247,0.18)';
const projTitle = darkMode ? '#a78bfa' : '#7B6CF7';
const projDesc = darkMode ? '#4a7fa8' : '#6d6d72';
const projId = darkMode ? '#3a6080' : '#aeaeb2';
const groupBg = darkMode ? 'rgba(0,30,70,0.5)' : '#f9f9fb';
return (
<div style={{
backgroundColor: panelBg,
backdropFilter: darkMode ? 'blur(20px)' : undefined,
WebkitBackdropFilter: darkMode ? 'blur(20px)' : undefined,
borderRadius: 18, height: '100%', overflowY: 'auto',
border: panelBorder,
boxShadow: panelShadow,
display: 'flex', flexDirection: 'column',
}}>
{/* Header */}
<div style={{ padding: '18px 18px 14px', borderBottom: `1px solid ${headerBorder}`, flexShrink: 0 }}>
{/* 深色模式顶部扫光线 */}
{darkMode && (
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, height: 1,
background: 'linear-gradient(90deg, transparent, rgba(0,212,255,0.5) 50%, transparent)',
borderRadius: '18px 18px 0 0',
}} />
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 42, height: 42, borderRadius: 12, flexShrink: 0,
backgroundColor: `${catColor}${darkMode ? '20' : '14'}`,
border: `1.5px solid ${catColor}${darkMode ? '50' : '30'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: darkMode ? `0 0 12px ${catColor}30` : 'none',
}}>
<div style={{
width: 14, height: 14, borderRadius: '50%', backgroundColor: catColor,
boxShadow: darkMode ? `0 0 8px ${catColor}` : 'none',
}} />
</div>
<div>
<p style={{ fontSize: 12, color: subtitleColor, marginBottom: 3, letterSpacing: '-0.01em' }}>
{typeLabel[node.type] || node.type}
</p>
<h3 style={{ fontSize: 16, fontWeight: 700, color: titleColor, letterSpacing: '-0.03em', lineHeight: 1.2, margin: 0 }}>
{node.label}
</h3>
</div>
</div>
<button onClick={onClose} style={{
width: 28, height: 28, borderRadius: 8,
backgroundColor: closeBtnBg, border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
color: closeBtnColor, transition: 'all 0.15s ease',
}}>
<X size={14} />
</button>
</div>
</div>
{/* Body */}
<div style={{ padding: '14px 18px', flex: 1 }}>
{mat ? (
<div>
{/* 技术规格 */}
<div style={{ marginBottom: 16 }}>
<p style={{ fontSize: 11, fontWeight: 600, color: sectionLabel, marginBottom: 8, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
技术规格
</p>
<div style={{ backgroundColor: specBg, borderRadius: 12, padding: '12px 14px', border: `1px solid ${specBorder}` }}>
<p style={{ fontSize: 13, color: specText, lineHeight: 1.8, letterSpacing: '-0.01em', margin: 0 }}>
{mat.spec}
</p>
</div>
</div>
{/* 字段列表 */}
<div style={{ marginBottom: 16 }}>
<Row label="料号" darkMode={darkMode}>
<span style={{ fontFamily: 'SF Mono,Menlo,Monaco,monospace', fontSize: 12, color: darkMode ? '#6aaccc' : undefined }}>
{mat.id}
</span>
</Row>
<Row label="数量" darkMode={darkMode}>
<strong style={{ fontWeight: 700, fontSize: 15, color: darkMode ? '#e8f4ff' : '#1c1c1e' }}>{mat.qty}</strong>
<span style={{ fontSize: 12, color: subtitleColor, marginLeft: 3 }}>件</span>
</Row>
<Row label="物料分类" darkMode={darkMode}>
<span style={{
color: catColor, fontWeight: 600,
textShadow: darkMode ? `0 0 8px ${catColor}60` : 'none',
}}>{mat.category}</span>
</Row>
<Row label="前缀类型" darkMode={darkMode}>
<span style={{ color: darkMode ? '#a8d8ff' : '#1c1c1e' }}>
{prefixLabels[mat.prefix] || mat.prefix}
</span>
</Row>
{/* 账龄行 */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '10px 0',
borderBottom: `1px solid ${darkMode ? 'rgba(0,212,255,0.08)' : '#f2f2f7'}`,
}}>
<span style={{ fontSize: 13, color: darkMode ? '#4a7fa8' : '#8e8e93' }}>账龄</span>
<span style={{
fontSize: 13, fontWeight: 600, padding: '4px 12px', borderRadius: 20,
backgroundColor: `${agColor}${darkMode ? '20' : '12'}`,
color: agColor,
border: `1px solid ${agColor}${darkMode ? '50' : '30'}`,
boxShadow: darkMode ? `0 0 8px ${agColor}30` : 'none',
}}>{mat.aging} 天</span>
</div>
</div>
{/* 关联项目卡片 */}
<div style={{
backgroundColor: projBg, borderRadius: 12,
padding: '12px 14px', border: `1px solid ${projBorder}`,
boxShadow: darkMode ? '0 0 16px rgba(167,139,250,0.08)' : 'none',
}}>
<p style={{ fontSize: 12, color: sectionLabel, marginBottom: 5 }}>关联项目</p>
<p style={{ fontSize: 16, fontWeight: 700, color: projTitle, letterSpacing: '-0.03em', margin: 0,
textShadow: darkMode ? `0 0 12px ${projTitle}50` : 'none',
}}>
{projectMap[mat.projectId]?.name || mat.projectId}
</p>
<p style={{ fontSize: 13, color: projDesc, marginTop: 4 }}>
{projectMap[mat.projectId]?.desc || ''}
</p>
<p style={{ fontSize: 11, fontFamily: 'SF Mono,Menlo,Monaco,monospace', color: projId, marginTop: 6 }}>
{mat.projectId}
</p>
</div>
</div>
) : (
<div>
{/* 分组节点:物料数量 */}
{node.count !== undefined && (
<div style={{
backgroundColor: groupBg, borderRadius: 12,
padding: '18px', border: `1px solid ${specBorder}`,
marginBottom: 14, textAlign: 'center',
boxShadow: darkMode ? `0 0 20px ${catColor}15` : 'none',
}}>
<p style={{ fontSize: 13, color: subtitleColor, marginBottom: 6 }}>关联物料数</p>
<p style={{
fontSize: 40, fontWeight: 800, color: catColor,
letterSpacing: '-0.05em', lineHeight: 1, margin: 0,
textShadow: darkMode ? `0 0 20px ${catColor}70` : 'none',
}}>
{node.count}
</p>
</div>
)}
{node.type === 'category' && (
<p style={{ fontSize: 14, color: darkMode ? '#7aadcc' : '#6d6d72', lineHeight: 1.7, letterSpacing: '-0.01em' }}>
该分类下包含所有归属于{' '}
<strong style={{ color: catColor, textShadow: darkMode ? `0 0 8px ${catColor}60` : 'none' }}>
{node.label}
</strong>{' '}
的采购物料,点击物料节点查看详细规格。
</p>
)}
{node.type === 'aging' && node.agingRange && (
<div style={{
backgroundColor: groupBg, borderRadius: 12,
padding: '14px', border: `1px solid ${specBorder}`,
}}>
<p style={{ fontSize: 13, color: subtitleColor, marginBottom: 8 }}>账龄区间</p>
<span style={{
fontSize: 14, fontWeight: 600, padding: '5px 14px', borderRadius: 20,
backgroundColor: `${agColors[node.agingRange] || '#94a3b8'}${darkMode ? '20' : '12'}`,
color: agColors[node.agingRange] || '#94a3b8',
border: `1px solid ${agColors[node.agingRange] || '#94a3b8'}${darkMode ? '50' : '30'}`,
boxShadow: darkMode ? `0 0 8px ${agColors[node.agingRange] || '#94a3b8'}30` : 'none',
}}>
{node.agingRange} 天
</span>
<p style={{ fontSize: 13, color: darkMode ? '#7aadcc' : '#6d6d72', marginTop: 12, lineHeight: 1.7 }}>
该账龄段物料需关注资金占用,建议优先推进采购收货。
</p>
</div>
)}
</div>
)}
</div>
</div>
);
}