218 lines
9.9 KiB
TypeScript
218 lines
9.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import type { Order } from '../data/orders';
|
|
import type { Theme } from '../context/ThemeContext';
|
|
import { darkTokens, lightTokens } from '../context/ThemeContext';
|
|
import DataSourceToggle from './DataSourceToggle';
|
|
|
|
interface CostHeaderProps {
|
|
theme?: Theme;
|
|
onToggleTheme?: () => void;
|
|
orders: Order[];
|
|
}
|
|
|
|
export default function CostHeader({ theme = 'dark', onToggleTheme, orders }: CostHeaderProps) {
|
|
const navigate = useNavigate();
|
|
const tokens = theme === 'dark' ? darkTokens : lightTokens;
|
|
const isLight = theme === 'light';
|
|
const [time, setTime] = useState(new Date());
|
|
useEffect(() => {
|
|
const t = setInterval(() => setTime(new Date()), 1000);
|
|
return () => clearInterval(t);
|
|
}, []);
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
const timeStr = `${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}`;
|
|
const dateStr = `${time.getFullYear()}-${pad(time.getMonth() + 1)}-${pad(time.getDate())}`;
|
|
|
|
const totalOrders = orders.length;
|
|
const closedOrders = orders.filter(o => o.status === 'closed').length;
|
|
const dangerOrders = orders.filter(o => o.riskLevel === 'danger').length;
|
|
const warningOrders = orders.filter(o => o.riskLevel === 'warning').length;
|
|
|
|
// 计算所有已结单订单的平均净利率(管理成本口径)
|
|
const closedList = orders.filter(o => o.status === 'closed');
|
|
const avgNetMargin = closedList.length > 0
|
|
? closedList.reduce((sum, o) => sum + (o.rows.find(r => r.type === '管理成本')?.netMargin ?? 0), 0) / closedList.length
|
|
: 0;
|
|
|
|
const stats = [
|
|
{ label: '在管订单', value: String(totalOrders), color: tokens.accent },
|
|
{ label: '已结单', value: String(closedOrders), color: tokens.success },
|
|
{ label: '成本预警', value: String(warningOrders), color: tokens.warning },
|
|
{ label: '亏损风险', value: String(dangerOrders), color: tokens.danger },
|
|
{ label: '平均净利率', value: `${(avgNetMargin * 100).toFixed(1)}%`, color: avgNetMargin >= 0.05 ? tokens.success : avgNetMargin >= 0 ? tokens.warning : tokens.danger },
|
|
];
|
|
|
|
return (
|
|
<header style={{
|
|
height: 88, flexShrink: 0,
|
|
display: 'flex', alignItems: 'center',
|
|
padding: '0 24px',
|
|
background: 'transparent',
|
|
position: 'relative', zIndex: 10, overflow: 'visible',
|
|
}}>
|
|
{/* 顶部扫光线 */}
|
|
<div style={{
|
|
position: 'absolute', top: 0, left: 0, right: 0, height: 1,
|
|
background: tokens.headerScanLine,
|
|
boxShadow: `0 0 12px ${tokens.accentGlow}`, zIndex: 2,
|
|
}} />
|
|
|
|
{/* 底部弧形 */}
|
|
<svg style={{
|
|
position: 'absolute', bottom: -26, left: 0,
|
|
width: '100%', height: 28,
|
|
pointerEvents: 'none', zIndex: 0, overflow: 'visible',
|
|
}} viewBox="0 0 1000 28" preserveAspectRatio="none">
|
|
<path d="M 0,0 C 250,28 750,28 1000,0" fill="none" stroke={tokens.svgStroke} strokeWidth="1" vectorEffect="non-scaling-stroke" />
|
|
<path d="M 0,0 C 250,28 750,28 1000,0" fill="none" stroke={tokens.svgDot.replace('0.9', '0.1')} strokeWidth="6" vectorEffect="non-scaling-stroke" />
|
|
<circle cx="0" cy="0" r="2.5" fill={tokens.svgDot.replace('0.9', '0.6')} />
|
|
<circle cx="1000" cy="0" r="2.5" fill={tokens.svgDot.replace('0.9', '0.6')} />
|
|
</svg>
|
|
|
|
{/* Logo + 标题 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flex: 1, position: 'relative', zIndex: 1 }}>
|
|
<div style={{
|
|
height: 50, borderRadius: 8,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
background: isLight ? '#ffffff' : 'rgba(235,242,255,0.95)',
|
|
padding: '0 10px',
|
|
boxShadow: '0 2px 10px rgba(0,0,0,0.15)',
|
|
}}>
|
|
<img
|
|
src="/regal-rexnord-logo.png"
|
|
alt="Regal Rexnord"
|
|
style={{ height: '92%', objectFit: 'contain' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<h1 style={{
|
|
color: tokens.text, fontSize: 19, fontWeight: 700,
|
|
letterSpacing: '0.05em', margin: 0,
|
|
textShadow: tokens.titleGlow,
|
|
}}>
|
|
订单成本枢纽平台
|
|
</h1>
|
|
<span style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
padding: '2px 8px', borderRadius: 10,
|
|
background: tokens.accentBg, border: `1px solid ${tokens.accent}55`,
|
|
fontSize: 12, color: tokens.accent, fontWeight: 600,
|
|
}}>
|
|
<span style={{
|
|
width: 5, height: 5, borderRadius: '50%',
|
|
backgroundColor: tokens.success,
|
|
boxShadow: `0 0 8px ${tokens.success}`,
|
|
animation: 'space-pulse 2s ease-in-out infinite',
|
|
}} />
|
|
LIVE
|
|
</span>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* 分隔线 */}
|
|
<div style={{ width: 1, height: 44, background: `linear-gradient(to bottom, transparent, ${tokens.accent}33, transparent)`, flexShrink: 0, marginRight: 24, position: 'relative', zIndex: 1 }} />
|
|
|
|
{/* 统计指标 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1, justifyContent: 'center', position: 'relative', zIndex: 1, marginTop: 24 }}>
|
|
{stats.map((item, idx) => (
|
|
<div key={item.label} style={{ display: 'flex', alignItems: 'center' }}>
|
|
<div style={{
|
|
textAlign: 'center', padding: '6px 16px', borderRadius: 10,
|
|
background: tokens.kpiCardBg, border: `1px solid ${tokens.kpiCardBorder}`,
|
|
backdropFilter: 'blur(8px)',
|
|
}}>
|
|
<div style={{ color: item.color, fontSize: 24, fontWeight: 800, lineHeight: 1, textShadow: isLight ? 'none' : `0 0 12px ${item.color}80` }}>
|
|
{item.value}
|
|
</div>
|
|
<div style={{ color: isLight ? tokens.textSub : tokens.textMuted, fontSize: 13, marginTop: 4, letterSpacing: '0.04em', fontWeight: 600 }}>
|
|
{item.label}
|
|
</div>
|
|
</div>
|
|
{idx < stats.length - 1 && <div style={{ width: 1, height: 36, background: `${tokens.accent}18`, margin: '0 3px' }} />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 右分隔线 */}
|
|
<div style={{ width: 1, height: 44, background: `linear-gradient(to bottom, transparent, ${tokens.accent}33, transparent)`, flexShrink: 0, marginLeft: 24, position: 'relative', zIndex: 1 }} />
|
|
|
|
{/* 时间 + 切换按钮 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flex: 1, justifyContent: 'flex-end', position: 'relative', zIndex: 1 }}>
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
style={{
|
|
padding: '6px 12px', borderRadius: 8, cursor: 'pointer',
|
|
border: `1px solid ${tokens.accent}55`,
|
|
background: tokens.accentBg,
|
|
color: tokens.accent, fontSize: 12, fontWeight: 600,
|
|
transition: 'all 0.18s ease',
|
|
}}
|
|
onMouseEnter={e => { e.currentTarget.style.background = `${tokens.accent}18`; e.currentTarget.style.borderColor = tokens.accent; }}
|
|
onMouseLeave={e => { e.currentTarget.style.background = tokens.accentBg; e.currentTarget.style.borderColor = `${tokens.accent}55`; }}
|
|
>
|
|
← 返回平台
|
|
</button>
|
|
<div style={{ textAlign: 'right' }}>
|
|
<div style={{ color: tokens.accent, fontSize: 20, fontWeight: 700, fontVariantNumeric: 'tabular-nums', lineHeight: 1, textShadow: isLight ? 'none' : `0 0 16px ${tokens.accentGlow}`, letterSpacing: '0.05em' }}>
|
|
{timeStr}
|
|
</div>
|
|
<div style={{ color: tokens.textMuted, fontSize: 11, marginTop: 2, letterSpacing: '0.04em' }}>
|
|
{dateStr}
|
|
</div>
|
|
<div style={{ color: `${tokens.accent}88`, fontSize: 11, marginTop: 4, letterSpacing: '0.06em' }}>
|
|
Powered by EIOS
|
|
</div>
|
|
</div>
|
|
|
|
{/* 切换风格按钮 */}
|
|
{onToggleTheme && (
|
|
<button
|
|
onClick={onToggleTheme}
|
|
title={theme === 'dark' ? '切换浅色风格' : '切换深色风格'}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: 5,
|
|
padding: '5px 12px', borderRadius: 20,
|
|
border: `1px solid ${tokens.accent}55`,
|
|
background: tokens.accentBg,
|
|
color: tokens.accent,
|
|
fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
transition: 'all 0.2s ease',
|
|
backdropFilter: 'blur(6px)',
|
|
flexShrink: 0,
|
|
}}
|
|
onMouseEnter={e => {
|
|
e.currentTarget.style.background = `${tokens.accent}18`;
|
|
e.currentTarget.style.borderColor = tokens.accent;
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.background = tokens.accentBg;
|
|
e.currentTarget.style.borderColor = `${tokens.accent}55`;
|
|
}}
|
|
>
|
|
{theme === 'dark' ? (
|
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
|
<circle cx="6.5" cy="6.5" r="3" stroke="currentColor" strokeWidth="1.3"/>
|
|
<path d="M6.5 1 L6.5 2.2 M6.5 10.8 L6.5 12 M1 6.5 L2.2 6.5 M10.8 6.5 L12 6.5 M2.7 2.7 L3.6 3.6 M9.4 9.4 L10.3 10.3 M10.3 2.7 L9.4 3.6 M3.6 9.4 L2.7 10.3" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
|
</svg>
|
|
) : (
|
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
|
<path d="M10.5 7.5 C9.8 9.8 7.6 11.5 5 11.5 C2.2 11.5 0 9.3 0 6.5 C0 3.9 1.7 1.7 4 1 C3.2 2 2.8 3.2 2.8 4.5 C2.8 7.5 5.3 10 8.3 10 C9.1 10 9.9 9.8 10.5 9.4 Z" fill="currentColor" opacity="0.8"/>
|
|
</svg>
|
|
)}
|
|
{theme === 'dark' ? '浅色' : '深色'}
|
|
</button>
|
|
)}
|
|
|
|
{/* 数据源切换 */}
|
|
<DataSourceToggle />
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|