Files
yuanzhipeng 3ea772c3be feat(mfg-data-agent): 添加HTML可视化仪表盘和优化项目配置
- 新增6个HTML可视化仪表盘组件用于数据展示
* 人效产值损耗三维模型仪表盘
* 指标趋势分析与拐点预警仪表盘
* 一页式决策简报仪表盘
* 订单延迟预警分析仪表盘
* 供应链风险预警仪表盘
* 工单执行进度与异常节点仪表盘
- 添加VSCode工作区配置文件
- 更新businessQueries.json业务查询配置
- 优化api_client.py API客户端实现
- 更新pyproject.toml项目依赖版本
- 重组SQL查询文件结构
- 删除v2版本冗余文档配置
- 添加v2版本技能清单文档
- 更新日志文件记录
2026-01-14 11:56:43 +08:00

567 lines
37 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一页式决策简报</title>
<script src="/LzwcaiEmbedFrameFile/LzwcaiEmbedFrameV5.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif; min-height: 100vh; }
/* 动画 */
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes glow { 0%, 100% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.4); } 50% { box-shadow: 0 0 40px rgba(99, 102, 241, 0.6); } }
@keyframes slideUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.anim-slide { animation: slideUp 0.5s ease-out forwards; opacity: 0; }
.anim-float { animation: float 6s ease-in-out infinite; }
.anim-glow { animation: glow 3s ease-in-out infinite; }
.anim-pulse { animation: pulse 2s ease-in-out infinite; }
/* 现代渐变背景 */
.page-bg {
background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 25%, #0d1b2a 50%, #1b263b 75%, #0f0f23 100%);
background-size: 400% 400%;
animation: gradientShift 20s ease infinite;
min-height: 100vh;
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
@keyframes gradientShift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
.page-bg::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 80%, rgba(236, 72, 153, 0.1) 0%, transparent 40%),
radial-gradient(circle at 50% 50%, rgba(6, 182, 212, 0.08) 0%, transparent 50%);
pointer-events: none;
}
/* 玻璃卡片 */
.glass {
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 20px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass:hover {
border-color: rgba(255,255,255,0.25);
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}
/* 响应式布局 */
.main-grid { display: grid; grid-template-columns: 1fr 1.5fr 1fr; gap: 20px; }
.metric-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; }
.footer-content { display: flex; align-items: center; gap: 32px; flex-wrap: wrap; }
@media (max-width: 1200px) {
.main-grid { grid-template-columns: 1fr 1fr; }
.metric-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 900px) {
.main-grid { grid-template-columns: 1fr; }
.metric-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
.metric-grid { grid-template-columns: 1fr; }
.footer-content { flex-direction: column; align-items: flex-start; gap: 16px; }
.footer-content .divider-v { display: none; }
}
/* 滚动条美化 */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
</style>
</head>
<body class="page-bg">
<div id="root"></div>
<script>
window.addEventListener('DOMContentLoaded', async function() {
try {
const loader = new LibraryLoader({
enableLog: true, async: false,
libraries: [
{ name: 'react', src: '/LzwcaiEmbedFrameFile/lib/react.production.min.js', check: () => typeof window.React !== 'undefined' },
{ name: 'reactDOM', src: '/LzwcaiEmbedFrameFile/lib/react-dom.production.min.js', check: () => typeof window.ReactDOM !== 'undefined' },
{ name: 'htm', src: '/LzwcaiEmbedFrameFile/lib/htm.js', check: () => typeof window.htm !== 'undefined' },
{ name: 'g2', src: '/LzwcaiEmbedFrameFile/lib/g2@5.2.4.min.js', check: () => typeof window.G2 !== 'undefined' }
]
});
await loader.loadAll();
initApp();
} catch (e) { console.error('加载失败:', e); }
});
function initApp() {
const { useState, useEffect, useMemo, useRef } = React;
const html = htm.bind(React.createElement);
const config = { title: '一页式决策简报' };
const lzwcaiComInitDate = '{{lzwcaiComInitDate}}';
const defaultData = { success: false, data: [[], [], [], [], [], []] };
// 健康状态配置
const healthCfg = {
HEALTHY: { label: '健康', gradient: 'linear-gradient(135deg, #10b981, #059669)', icon: '✓', glow: 'rgba(16, 185, 129, 0.4)' },
WARNING: { label: '关注', gradient: 'linear-gradient(135deg, #f59e0b, #d97706)', icon: '!', glow: 'rgba(245, 158, 11, 0.4)' },
CRITICAL: { label: '预警', gradient: 'linear-gradient(135deg, #ef4444, #dc2626)', icon: '✗', glow: 'rgba(239, 68, 68, 0.4)' }
};
// 指标状态配置
const metricCfg = {
NORMAL: { color: '#10b981', bg: 'rgba(16, 185, 129, 0.15)', border: 'rgba(16, 185, 129, 0.3)' },
EXCEEDED: { color: '#ef4444', bg: 'rgba(239, 68, 68, 0.15)', border: 'rgba(239, 68, 68, 0.3)' }
};
// 格式化函数
const fmt = (v, d = 0) => v == null || isNaN(v) ? '-' : Number(v).toLocaleString('zh-CN', { minimumFractionDigits: d, maximumFractionDigits: d });
const fmtMoney = v => v == null || isNaN(v) ? '-' : '¥' + (Math.abs(v) >= 10000 ? (v / 10000).toFixed(1) + '万' : fmt(v));
const fmtPct = v => v == null || isNaN(v) ? '-' : Number(v).toFixed(1) + '%';
// 空状态组件
const Empty = ({ icon, text }) => html`
<div style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 20px', color: 'rgba(255,255,255,0.4)' }}>
<div style=${{ fontSize: '32px', marginBottom: '12px', opacity: 0.5 }}>${icon}</div>
<div style=${{ fontSize: '13px' }}>${text}</div>
</div>
`;
// 指标卡片组件
const MetricCard = ({ icon, value, label, sub, gradient, delay = 0, alert = false, highlight = false }) => html`
<div class="glass anim-slide" style=${{
padding: '20px',
animationDelay: delay + 'ms',
background: alert ? 'linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1))' :
highlight ? 'linear-gradient(135deg, rgba(16,185,129,0.2), rgba(16,185,129,0.1))' : undefined,
borderColor: alert ? 'rgba(239,68,68,0.3)' : highlight ? 'rgba(16,185,129,0.3)' : undefined
}}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style=${{
width: '48px', height: '48px', borderRadius: '14px',
background: gradient || 'linear-gradient(135deg, #6366f1, #8b5cf6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '22px', boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)'
}}>${icon}</div>
${alert && html`<span class="anim-pulse" style=${{ width: '10px', height: '10px', borderRadius: '50%', background: '#ef4444', boxShadow: '0 0 12px rgba(239,68,68,0.6)' }}></span>`}
</div>
<div style=${{ fontSize: '28px', fontWeight: 700, color: alert ? '#f87171' : highlight ? '#34d399' : '#fff', marginBottom: '4px' }}>${value}</div>
<div style=${{ fontSize: '13px', color: 'rgba(255,255,255,0.7)', fontWeight: 500 }}>${label}</div>
${sub && html`<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.4)', marginTop: '4px' }}>${sub}</div>`}
</div>
`;
function App() {
const [data, setData] = useState(defaultData);
const [error, setError] = useState(null);
const chartRef1 = useRef(null);
const chartRef2 = useRef(null);
const chart1 = useRef(null);
const chart2 = useRef(null);
const validate = d => d && typeof d === 'object' && Array.isArray(d.data);
// 处理数据
const processed = useMemo(() => {
if (!data.data || data.data.length < 6) return null;
const [summary = [], comparison = [], customers = [], production = [], warnings = [], trends = []] = data.data;
const s = summary[0] || {};
return { summary: s, comparison, customers, production, warnings, trends };
}, [data]);
// 图表渲染
useEffect(() => {
if (!processed || !window.G2) return;
[chart1, chart2].forEach(c => { if (c.current) { c.current.destroy(); c.current = null; } });
// 客户贡献环形图
if (chartRef1.current && processed.customers.length > 0) {
const d = processed.customers.slice(0, 5).map(x => ({
name: x.customer_name.length > 5 ? x.customer_name.slice(0, 5) + '..' : x.customer_name,
value: x.total_order_amount || 0
}));
chart1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 200 });
chart1.current.theme({ type: 'classicDark' });
chart1.current.coordinate({ type: 'theta', innerRadius: 0.65 });
chart1.current.interval().data(d).transform({ type: 'stackY' }).encode('y', 'value').encode('color', 'name')
.scale('color', { range: ['#818cf8', '#a78bfa', '#c084fc', '#e879f9', '#f472b6'] })
.style('stroke', 'rgba(15,15,35,0.8)').style('lineWidth', 3)
.label({ text: d => d.name, position: 'outside', fontSize: 10, fill: 'rgba(255,255,255,0.7)', connector: true })
.legend(false).tooltip({ items: [{ channel: 'y', name: '金额', valueFormatter: v => '¥' + (v/10000).toFixed(1) + '万' }] });
chart1.current.render();
}
// 月度趋势面积图
if (chartRef2.current && processed.trends.length > 0) {
const d = processed.trends.slice().reverse().map(x => ({ month: x.month?.slice(5) || '', amount: (x.sales_amount || 0) / 10000 }));
chart2.current = new G2.Chart({ container: chartRef2.current, autoFit: true, height: 180 });
chart2.current.theme({ type: 'classicDark' });
chart2.current.area().data(d).encode('x', 'month').encode('y', 'amount')
.style('fill', 'linear-gradient(90deg, rgba(99,102,241,0.6), rgba(168,85,247,0.6))')
.style('fillOpacity', 0.4)
.axis('x', { labelFontSize: 10, labelFill: 'rgba(255,255,255,0.5)', line: null, tick: null })
.axis('y', { title: false, labelFill: 'rgba(255,255,255,0.4)', labelFormatter: v => v + '万', grid: { stroke: 'rgba(255,255,255,0.05)' } });
chart2.current.line().data(d).encode('x', 'month').encode('y', 'amount')
.style('stroke', 'url(#lineGradient)').style('lineWidth', 3);
chart2.current.point().data(d).encode('x', 'month').encode('y', 'amount')
.style('fill', '#a78bfa').style('r', 5).style('stroke', '#fff').style('lineWidth', 2);
chart2.current.render();
}
return () => { [chart1, chart2].forEach(c => { if (c.current) { c.current.destroy(); c.current = null; } }); };
}, [processed]);
// 初始化
useEffect(() => {
const helper = new ChildPageHelper({
autoRenderStatus: true, enableLog: true,
onReady: async () => {
await helper.autoInitialize(() => {
const d = helper.parseTemplateData(lzwcaiComInitDate, defaultData);
if (d && validate(d)) { setData(d); setError(null); }
});
}
});
helper.expose({
setDataLzwcaiEmbedFrameFn: d => { if (validate(d)) { setData(d); setError(null); return { status: 'success' }; } return { status: 'error' }; },
getDataLzwcaiEmbedFrameFn: () => ({ status: 'success', data }),
captureScreenshotLzwcaiEmbedFrameFn: helper.createScreenshotMethod(),
getRenderStatusLzwcaiEmbedFrameFn: () => helper.getRenderStatus()
});
}, []);
// 错误状态
if (error) return html`
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: '20px' }}>
<div class="glass" style=${{ padding: '48px', textAlign: 'center', maxWidth: '400px' }}>
<div class="anim-float" style=${{ fontSize: '56px', marginBottom: '20px' }}>⚠️</div>
<div style=${{ fontSize: '20px', fontWeight: 700, color: '#f87171', marginBottom: '12px' }}>数据加载异常</div>
<div style=${{ fontSize: '14px', color: 'rgba(255,255,255,0.5)' }}>${error}</div>
</div>
</div>`;
// 加载状态
if (!processed) return html`
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: '20px' }}>
<div class="glass anim-glow" style=${{ padding: '56px', textAlign: 'center' }}>
<div class="anim-float" style=${{ fontSize: '56px', marginBottom: '20px' }}>📊</div>
<div style=${{ fontSize: '20px', fontWeight: 700, background: 'linear-gradient(135deg, #818cf8, #c084fc)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>加载决策数据...</div>
</div>
</div>`;
const { summary: s, comparison, customers, production, warnings, trends } = processed;
const health = healthCfg[s.health_status] || healthCfg.WARNING;
return html`
<div style=${{ padding: '24px', maxWidth: '1500px', margin: '0 auto', position: 'relative', zIndex: 1 }}>
<!-- 顶部标题栏 -->
<header class="glass anim-slide" style=${{ padding: '20px 28px', marginBottom: '24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div class="anim-glow" style=${{
width: '56px', height: '56px', borderRadius: '16px',
background: 'linear-gradient(135deg, #6366f1, #a855f7, #ec4899)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '26px'
}}>📊</div>
<div>
<h1 style=${{ fontSize: '24px', fontWeight: 700, color: '#fff', margin: 0, background: 'linear-gradient(135deg, #fff, rgba(255,255,255,0.8))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>${config.title}</h1>
<p style=${{ fontSize: '12px', color: 'rgba(255,255,255,0.5)', marginTop: '4px' }}>📅 ${s.report_date || '-'} · 订单 · 生产 · 财务 · 售后</p>
</div>
</div>
<div style=${{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style=${{
padding: '10px 20px', borderRadius: '12px',
background: health.gradient,
boxShadow: '0 4px 20px ' + health.glow,
display: 'flex', alignItems: 'center', gap: '8px'
}}>
<span style=${{ fontSize: '16px', fontWeight: 700 }}>${health.icon}</span>
<span style=${{ fontSize: '14px', fontWeight: 600, color: '#fff' }}>经营${health.label}</span>
</div>
</div>
</header>
<!-- 核心指标网格 -->
<div class="metric-grid" style=${{ marginBottom: '24px' }}>
<${MetricCard} icon="📋" value=${s.sales_order_count || 0} label="销售订单" sub=${fmtMoney(s.total_sales_amount)} gradient="linear-gradient(135deg, #6366f1, #8b5cf6)" delay=${0} />
<${MetricCard} icon="🏭" value=${s.work_order_count || 0} label="生产工单" sub=${'完成 ' + (s.completed_work_orders || 0) + ' / 进行 ' + (s.in_progress_work_orders || 0)} gradient="linear-gradient(135deg, #3b82f6, #06b6d4)" delay=${50} />
<${MetricCard} icon="📊" value=${fmtPct(s.production_completion_rate)} label="生产完成率" sub=${(s.completed_qty || 0) + ' / ' + (s.planned_qty || 0)} gradient="linear-gradient(135deg, #10b981, #14b8a6)" delay=${100} highlight=${s.production_completion_rate >= 90} />
<${MetricCard} icon="✅" value=${fmtPct(s.pass_rate)} label="质检合格率" sub=${(s.pass_qty || 0) + ' 件合格'} gradient="linear-gradient(135deg, #22c55e, #10b981)" delay=${150} highlight=${s.pass_rate >= 98} />
<${MetricCard} icon="💰" value=${fmtMoney(s.net_cash_flow)} label="净现金流" sub=${'收入 ' + fmtMoney(s.total_ar_amount)} gradient="linear-gradient(135deg, #f59e0b, #f97316)" delay=${200} highlight=${s.net_cash_flow > 0} />
<${MetricCard} icon="↩️" value=${fmtPct(s.return_rate)} label="退货率" sub=${fmtMoney(s.total_return_amount)} gradient="linear-gradient(135deg, #ef4444, #f97316)" delay=${250} alert=${s.return_rate > 5} />
</div>
<!-- 主内容区 -->
<div class="main-grid" style=${{ marginBottom: '24px' }}>
<!-- 左栏 -->
<div style=${{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<!-- 订单概况 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '300ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>📋</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>订单概况</span>
</div>
<div style=${{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
${[
{ l: '订单总数', v: s.sales_order_count || 0, c: '#818cf8' },
{ l: '订单均价', v: fmtMoney(s.avg_order_amount), c: '#a78bfa' },
{ l: '已付款', v: s.paid_order_count || 0, c: '#34d399' },
{ l: '部分付款', v: s.partial_paid_count || 0, c: '#fbbf24' },
{ l: '未付款', v: s.unpaid_order_count || 0, c: '#f87171' },
{ l: '应收款', v: fmtMoney(s.receivable_amount), c: '#fb923c' }
].map((item, i) => html`
<div key=${i} style=${{ padding: '12px', background: 'rgba(255,255,255,0.05)', borderRadius: '10px', borderLeft: '3px solid ' + item.c }}>
<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.5)', marginBottom: '4px' }}>${item.l}</div>
<div style=${{ fontSize: '16px', fontWeight: 700, color: item.c }}>${item.v}</div>
</div>
`)}
</div>
</div>
<!-- 财务流水 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '350ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #f59e0b, #f97316)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>💰</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>财务流水</span>
</div>
<div style=${{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
${[
{ l: '应收入账', v: fmtMoney(s.total_ar_amount), n: s.ar_receipt_count, c: '#34d399', icon: '📥' },
{ l: '应付支出', v: fmtMoney(s.total_ap_amount), n: s.ap_payment_count, c: '#f87171', icon: '📤' },
{ l: '开票金额', v: fmtMoney(s.total_invoice_amount), n: s.invoice_count, c: '#60a5fa', icon: '🧾' },
{ l: '发货金额', v: fmtMoney(s.total_shipment_amount), n: s.shipment_count, c: '#a78bfa', icon: '🚚' }
].map((item, i) => html`
<div key=${i} style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', background: 'rgba(255,255,255,0.05)', borderRadius: '10px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>${item.icon}</span>
<span style=${{ fontSize: '12px', color: 'rgba(255,255,255,0.7)' }}>${item.l}</span>
</div>
<div style=${{ textAlign: 'right' }}>
<span style=${{ fontSize: '14px', fontWeight: 700, color: item.c }}>${item.v}</span>
<span style=${{ fontSize: '10px', color: 'rgba(255,255,255,0.4)', marginLeft: '6px' }}>${item.n}</span>
</div>
</div>
`)}
</div>
</div>
</div>
<!-- 中栏 -->
<div style=${{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<!-- 客户贡献 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '400ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #ec4899, #f472b6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>👥</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>客户贡献 TOP5</span>
</div>
<span style=${{ padding: '4px 10px', background: 'rgba(236,72,153,0.2)', borderRadius: '20px', fontSize: '11px', color: '#f472b6' }}>${customers.length} 客户</span>
</div>
${customers.length > 0 ? html`
<div style=${{ display: 'flex', gap: '20px' }}>
<div ref=${chartRef1} style=${{ flex: '0 0 200px' }}></div>
<div style=${{ flex: 1, display: 'flex', flexDirection: 'column', gap: '8px' }}>
${customers.slice(0, 5).map((c, i) => html`
<div key=${i} style=${{
display: 'flex', alignItems: 'center', gap: '10px', padding: '10px 12px',
background: i === 0 ? 'linear-gradient(135deg, rgba(129,140,248,0.2), rgba(167,139,250,0.1))' : 'rgba(255,255,255,0.05)',
borderRadius: '10px', border: i === 0 ? '1px solid rgba(129,140,248,0.3)' : '1px solid transparent'
}}>
<span style=${{
width: '24px', height: '24px', borderRadius: '8px',
background: i === 0 ? 'linear-gradient(135deg, #818cf8, #a78bfa)' : 'rgba(255,255,255,0.1)',
color: '#fff', fontSize: '11px', fontWeight: 700,
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>${i + 1}</span>
<div style=${{ flex: 1, minWidth: 0 }}>
<div style=${{ fontSize: '13px', fontWeight: 600, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>${c.customer_name}</div>
<div style=${{ fontSize: '10px', color: 'rgba(255,255,255,0.4)' }}>${c.order_count}单 · 贡献${fmtPct(c.contribution_rate)}</div>
</div>
<span style=${{ fontSize: '14px', fontWeight: 700, color: '#34d399' }}>${fmtMoney(c.total_order_amount)}</span>
</div>
`)}
</div>
</div>
` : html`<${Empty} icon="👥" text="暂无客户数据" />`}
</div>
<!-- 销售趋势 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '450ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #06b6d4, #0ea5e9)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>📈</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>销售趋势</span>
</div>
</div>
${trends.length > 0 ? html`
<div ref=${chartRef2} style=${{ marginBottom: '16px' }}></div>
<div style=${{ display: 'flex', justifyContent: 'space-around', padding: '12px 0', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
${trends.slice(0, 4).map((t, i) => html`
<div key=${i} style=${{ textAlign: 'center' }}>
<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.4)', marginBottom: '4px' }}>${t.month}</div>
<div style=${{ fontSize: '16px', fontWeight: 700, color: '#fff' }}>${t.order_count}单</div>
<div style=${{ fontSize: '12px', color: '#34d399' }}>${fmtMoney(t.sales_amount)}</div>
</div>
`)}
</div>
` : html`<${Empty} icon="📈" text="暂无趋势数据" />`}
</div>
</div>
<!-- 右栏 -->
<div style=${{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<!-- 生产进度 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '500ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #10b981, #14b8a6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>🏭</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>生产进度</span>
</div>
</div>
${production.filter(p => p.work_order_count > 0).length > 0 ? html`
<div style=${{ display: 'flex', flexDirection: 'column', gap: '10px', maxHeight: '220px', overflowY: 'auto' }}>
${production.filter(p => p.work_order_count > 0).map((p, i) => {
const rate = p.completion_rate || 0;
const color = rate >= 100 ? '#34d399' : rate >= 70 ? '#fbbf24' : '#f87171';
return html`
<div key=${i} style=${{ padding: '12px', background: 'rgba(255,255,255,0.05)', borderRadius: '10px' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style=${{ fontSize: '13px', fontWeight: 600, color: '#fff' }}>${p.product_category}</span>
<span style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.5)' }}>${p.work_order_count}单</span>
</div>
<div style=${{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style=${{ flex: 1, height: '8px', background: 'rgba(255,255,255,0.1)', borderRadius: '4px', overflow: 'hidden' }}>
<div style=${{ width: Math.min(rate, 100) + '%', height: '100%', background: 'linear-gradient(90deg, ' + color + ', ' + color + '99)', borderRadius: '4px', transition: 'width 0.6s ease' }}></div>
</div>
<span style=${{ fontSize: '13px', fontWeight: 700, color: color, minWidth: '50px', textAlign: 'right' }}>${fmtPct(rate)}</span>
</div>
<div style=${{ fontSize: '10px', color: 'rgba(255,255,255,0.4)', marginTop: '6px' }}>
完成 ${p.completed_qty || 0} / 计划 ${p.planned_qty || 0} · ${p.worker_count || 0}人 · ${p.total_work_hours || 0}h
</div>
</div>
`;
})}
</div>
` : html`<${Empty} icon="🏭" text="暂无生产数据" />`}
</div>
<!-- 预警指标 -->
<div class="glass anim-slide" style=${{ padding: '20px', animationDelay: '550ms' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style=${{ width: '32px', height: '32px', borderRadius: '10px', background: 'linear-gradient(135deg, #ef4444, #f97316)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px' }}>⚠️</div>
<span style=${{ fontSize: '15px', fontWeight: 600, color: '#fff' }}>预警指标</span>
</div>
${warnings.filter(w => w.status === 'EXCEEDED').length > 0 && html`
<span class="anim-pulse" style=${{ padding: '4px 10px', background: 'rgba(239,68,68,0.2)', borderRadius: '20px', fontSize: '11px', color: '#f87171' }}>
${warnings.filter(w => w.status === 'EXCEEDED').length} 项超标
</span>
`}
</div>
${warnings.length > 0 ? html`
<div style=${{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
${warnings.map((w, i) => {
const cfg = metricCfg[w.status] || metricCfg.NORMAL;
const names = {
'unpaid_order_amount': '未付款金额',
'defect_rate_pct': '不良率',
'production_completion_rate': '生产完成率',
'return_rate_pct': '退货率'
};
return html`
<div key=${i} style=${{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px', background: cfg.bg, borderRadius: '10px',
border: '1px solid ' + cfg.border
}}>
<div>
<div style=${{ fontSize: '13px', fontWeight: 600, color: '#fff' }}>${names[w.metric_name] || w.metric_name}</div>
<div style=${{ fontSize: '10px', color: 'rgba(255,255,255,0.5)', marginTop: '2px' }}>阈值: ${w.threshold}</div>
</div>
<div style=${{ textAlign: 'right' }}>
<div style=${{ fontSize: '18px', fontWeight: 700, color: cfg.color }}>${w.current_value}</div>
<div style=${{ fontSize: '10px', color: cfg.color, fontWeight: 600 }}>${w.status === 'EXCEEDED' ? '超标' : '正常'}</div>
</div>
</div>
`;
})}
</div>
` : html`<${Empty} icon="✅" text="暂无预警" />`}
</div>
</div>
</div>
<!-- 底部汇总栏 -->
<footer class="glass anim-slide" style=${{ padding: '20px 28px', animationDelay: '600ms', background: 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(168,85,247,0.1))' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '20px' }}>
<div class="footer-content">
<div style=${{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style=${{ width: '40px', height: '40px', borderRadius: '12px', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '18px', flexShrink: 0 }}>👥</div>
<div>
<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.5)' }}>人效统计</div>
<div style=${{ fontSize: '14px', color: '#fff' }}>
<span style=${{ fontWeight: 700, color: '#818cf8' }}>${s.active_worker_count || 0}</span> 人 ·
<span style=${{ fontWeight: 700 }}>${fmt(s.total_work_hours, 1)}</span> 工时 ·
人均 <span style=${{ fontWeight: 700, color: '#34d399' }}>${fmt(s.output_per_worker, 1)}</span>
</div>
</div>
</div>
<div class="divider-v" style=${{ width: '1px', height: '36px', background: 'rgba(255,255,255,0.1)' }}></div>
<div style=${{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style=${{ width: '40px', height: '40px', borderRadius: '12px', background: 'linear-gradient(135deg, #10b981, #14b8a6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '18px', flexShrink: 0 }}>✅</div>
<div>
<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.5)' }}>质检统计</div>
<div style=${{ fontSize: '14px', color: '#fff' }}>
<span style=${{ fontWeight: 700 }}>${s.qc_batch_count || 0}</span> 批次 ·
合格 <span style=${{ fontWeight: 700, color: '#34d399' }}>${s.pass_qty || 0}</span> ·
不良 <span style=${{ fontWeight: 700, color: s.fail_qty > 0 ? '#f87171' : 'rgba(255,255,255,0.5)' }}>${s.fail_qty || 0}</span>
</div>
</div>
</div>
<div class="divider-v" style=${{ width: '1px', height: '36px', background: 'rgba(255,255,255,0.1)' }}></div>
<div style=${{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style=${{ width: '40px', height: '40px', borderRadius: '12px', background: 'linear-gradient(135deg, #f59e0b, #f97316)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '18px', flexShrink: 0 }}>📦</div>
<div>
<div style=${{ fontSize: '11px', color: 'rgba(255,255,255,0.5)' }}>采购与退货</div>
<div style=${{ fontSize: '14px', color: '#fff' }}>
采购 <span style=${{ fontWeight: 700 }}>${s.purchase_order_count || 0}</span> 单 ·
退货 <span style=${{ fontWeight: 700, color: s.return_count > 0 ? '#f87171' : 'rgba(255,255,255,0.5)' }}>${s.return_count || 0}</span> 单
</div>
</div>
</div>
</div>
<div style=${{ textAlign: 'right' }}>
<div style=${{ fontSize: '10px', color: 'rgba(255,255,255,0.4)' }}>报告生成时间</div>
<div style=${{ fontSize: '13px', color: '#a78bfa', fontWeight: 500 }}>${new Date().toLocaleString('zh-CN')}</div>
</div>
</div>
</footer>
</div>
`;
}
ReactDOM.render(html`<${App} />`, document.getElementById('root'));
}
</script>
</body>
</html>