Files
lzwcai-mcp-server-package/lzwcai_mcpskills_mfg_data_agent/html/EfficiencyOutputLossDashboard.html
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

544 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; background: #0a0f1a; color: #e2e8f0; min-height: 100vh; }
/* 动画 */
@keyframes glow { 0%, 100% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); } 50% { box-shadow: 0 0 40px rgba(99, 102, 241, 0.5); } }
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes gradientFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
.animate-glow { animation: glow 3s ease-in-out infinite; }
.animate-float { animation: float 4s ease-in-out infinite; }
.animate-pulse { animation: pulse 2s ease-in-out infinite; }
.animate-slide { animation: slideUp 0.6s ease-out forwards; }
/* 背景 */
.page-bg {
background: linear-gradient(135deg, #0a0f1a 0%, #111827 50%, #0f172a 100%);
background-attachment: fixed;
position: relative;
}
.page-bg::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(236, 72, 153, 0.06) 0%, transparent 50%),
radial-gradient(ellipse at 50% 50%, rgba(6, 182, 212, 0.05) 0%, transparent 60%);
pointer-events: none;
}
/* 卡片 */
.card {
background: linear-gradient(145deg, rgba(30, 41, 59, 0.8), rgba(15, 23, 42, 0.9));
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 16px;
backdrop-filter: blur(20px);
transition: all 0.3s ease;
}
.card:hover { border-color: rgba(99, 102, 241, 0.4); transform: translateY(-2px); }
/* 渐变文字 */
.gradient-text {
background: linear-gradient(135deg, #818cf8 0%, #c084fc 50%, #f472b6 100%);
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
/* 滚动条 */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(30, 41, 59, 0.5); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: rgba(99, 102, 241, 0.5); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(99, 102, 241, 0.7); }
/* 表格 */
.data-table { width: 100%; border-collapse: separate; border-spacing: 0; }
.data-table th { padding: 12px 16px; text-align: left; font-size: 11px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; background: rgba(30, 41, 59, 0.5); border-bottom: 1px solid rgba(99, 102, 241, 0.1); }
.data-table td { padding: 14px 16px; font-size: 13px; border-bottom: 1px solid rgba(51, 65, 85, 0.3); transition: background 0.2s; }
.data-table tr:hover td { background: rgba(99, 102, 241, 0.05); }
/* 标签 */
.tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.tag-green { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
.tag-yellow { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
.tag-red { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
.tag-blue { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
/* 进度条 */
.progress-bar { height: 6px; background: rgba(51, 65, 85, 0.5); border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #6366f1, #a855f7); transition: width 0.5s ease; }
/* 分页 */
.pagination { display: flex; align-items: center; gap: 8px; }
.page-btn { padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; border: 1px solid rgba(99, 102, 241, 0.3); background: transparent; color: #94a3b8; cursor: pointer; transition: all 0.2s; }
.page-btn:hover:not(:disabled) { background: rgba(99, 102, 241, 0.2); color: #e2e8f0; }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.page-btn.active { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; border-color: transparent; }
</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 warningCfg = {
GREEN: { label: '正常', class: 'tag-green', icon: '●' },
YELLOW: { label: '关注', class: 'tag-yellow', icon: '●' },
RED: { label: '预警', class: 'tag-red', icon: '●' }
};
const lossCfg = {
LOW: { label: '低', class: 'tag-green' },
MEDIUM: { label: '中', class: 'tag-yellow' },
HIGH: { label: '高', class: 'tag-red' }
};
// 空状态
const Empty = ({ icon, text }) => html`
<div style=${{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '40px 20px', color: '#64748b' }}>
<div style=${{ fontSize: '32px', marginBottom: '12px', opacity: 0.5 }}>${icon}</div>
<div style=${{ fontSize: '13px' }}>${text}</div>
</div>
`;
// 格式化
const fmt = (v, d = 2) => v == null || isNaN(v) ? '-' : Number(v).toLocaleString('zh-CN', { minimumFractionDigits: d, maximumFractionDigits: d });
const fmtPct = v => v == null || isNaN(v) ? '-' : Number(v).toFixed(1) + '%';
const fmtMoney = v => v == null || isNaN(v) ? '-' : '¥' + Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
function App() {
const [data, setData] = useState(defaultData);
const [error, setError] = useState(null);
const [workerPage, setWorkerPage] = useState(1);
const [productPage, setProductPage] = useState(1);
const [deptFilter, setDeptFilter] = useState('all');
const pageSize = 8;
const chartRef1 = useRef(null);
const chartRef2 = useRef(null);
const chartRef3 = useRef(null);
const chart1 = useRef(null);
const chart2 = useRef(null);
const chart3 = useRef(null);
const validate = d => d && typeof d === 'object' && Array.isArray(d.data);
// 处理数据
const processed = useMemo(() => {
if (!data.data || data.data.length < 5) return null;
const [depts = [], workers = [], trends = [], products = [], orders = []] = data.data;
const stats = {
workers: depts.reduce((s, d) => s + parseInt(d.worker_count || 0), 0),
output: depts.reduce((s, d) => s + (d.total_output_qty || 0), 0),
hours: depts.reduce((s, d) => s + (d.total_work_hours || 0), 0),
value: depts.reduce((s, d) => s + (d.estimated_output_value || 0), 0),
efficiency: depts.length ? depts.reduce((s, d) => s + (d.efficiency_index || 0), 0) / depts.length : 0,
performance: depts.length ? depts.reduce((s, d) => s + (d.performance_score || 0), 0) / depts.length : 0,
defect: depts.length ? depts.reduce((s, d) => s + (d.defect_rate || 0), 0) / depts.length : 0,
completion: depts.length ? depts.reduce((s, d) => s + (d.plan_completion_rate || 0), 0) / depts.length : 0
};
const warnings = { GREEN: 0, YELLOW: 0, RED: 0 };
depts.forEach(d => { const l = d.warning_level || 'GREEN'; if (warnings[l] !== undefined) warnings[l]++; });
const deptList = [...new Set(depts.map(d => d.department))];
return { depts, workers, trends, products, orders, stats, warnings, deptList };
}, [data]);
// 筛选员工
const filteredWorkers = useMemo(() => {
if (!processed) return [];
return deptFilter === 'all' ? processed.workers : processed.workers.filter(w => w.department === deptFilter);
}, [processed, deptFilter]);
const workerPages = Math.ceil(filteredWorkers.length / pageSize);
const pagedWorkers = filteredWorkers.slice((workerPage - 1) * pageSize, workerPage * pageSize);
const productPages = processed ? Math.ceil(processed.products.length / pageSize) : 0;
const pagedProducts = processed ? processed.products.slice((productPage - 1) * pageSize, productPage * pageSize) : [];
// 图表渲染
useEffect(() => {
if (!processed || !window.G2) return;
[chart1, chart2, chart3].forEach(c => { if (c.current) { c.current.destroy(); c.current = null; } });
// 部门效率柱状图
if (chartRef1.current && processed.depts.length > 0) {
const d = processed.depts.map(x => ({ name: x.department.slice(0, 4), efficiency: x.efficiency_index || 0, performance: x.performance_score || 0 }));
chart1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 180 });
chart1.current.theme({ type: 'classicDark' });
chart1.current.interval().data(d).encode('x', 'name').encode('y', 'efficiency').encode('color', 'name')
.scale('color', { range: ['#6366f1', '#8b5cf6', '#a855f7', '#c084fc', '#d946ef', '#ec4899'] })
.style('radius', 4).axis('x', { labelFontSize: 10, labelFill: '#94a3b8' }).axis('y', { title: false, labelFill: '#64748b' })
.legend(false).tooltip({ items: [{ channel: 'y', name: '效率', valueFormatter: v => v.toFixed(1) }] });
chart1.current.render();
}
// 产值分布环形图
if (chartRef2.current && processed.depts.length > 0) {
const d = processed.depts.filter(x => x.estimated_output_value > 0).map(x => ({ name: x.department, value: x.estimated_output_value }));
chart2.current = new G2.Chart({ container: chartRef2.current, autoFit: true, height: 180 });
chart2.current.theme({ type: 'classicDark' });
chart2.current.coordinate({ type: 'theta', innerRadius: 0.6 });
chart2.current.interval().data(d).transform({ type: 'stackY' }).encode('y', 'value').encode('color', 'name')
.scale('color', { range: ['#6366f1', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b'] })
.style('stroke', '#1e293b').style('lineWidth', 2)
.label({ text: d => d.name.slice(0, 3), position: 'outside', fontSize: 10, fill: '#94a3b8' })
.legend(false).tooltip({ items: [{ channel: 'y', name: '产值', valueFormatter: v => '¥' + (v/10000).toFixed(1) + '万' }] });
chart2.current.render();
}
// 月度趋势折线图
if (chartRef3.current && processed.trends.length > 0) {
const d = processed.trends.map(x => ({ month: x.month, dept: x.department.slice(0, 3), output: x.total_output || 0 }));
chart3.current = new G2.Chart({ container: chartRef3.current, autoFit: true, height: 180 });
chart3.current.theme({ type: 'classicDark' });
chart3.current.line().data(d).encode('x', 'month').encode('y', 'output').encode('color', 'dept')
.scale('color', { range: ['#6366f1', '#8b5cf6', '#06b6d4', '#10b981'] })
.style('lineWidth', 2).axis('x', { labelFontSize: 9, labelFill: '#64748b' }).axis('y', { title: false, labelFill: '#64748b' })
.legend('color', { position: 'top', itemLabelFill: '#94a3b8', itemMarkerSize: 6 });
chart3.current.point().data(d).encode('x', 'month').encode('y', 'output').encode('color', 'dept').style('r', 3);
chart3.current.render();
}
return () => { [chart1, chart2, chart3].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()
});
}, []);
// 分页组件
const Pager = ({ page, total, onChange }) => total <= 1 ? null : html`
<div class="pagination">
<button class="page-btn" disabled=${page === 1} onClick=${() => onChange(page - 1)}></button>
${Array.from({ length: Math.min(5, total) }, (_, i) => {
let p = i + 1;
if (total > 5) { if (page <= 3) p = i + 1; else if (page >= total - 2) p = total - 4 + i; else p = page - 2 + i; }
return html`<button key=${p} class="page-btn ${page === p ? 'active' : ''}" onClick=${() => onChange(p)}>${p}</button>`;
})}
<button class="page-btn" disabled=${page === total} onClick=${() => onChange(page + 1)}></button>
</div>
`;
if (error) return html`
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: '20px' }}>
<div class="card" style=${{ padding: '40px', textAlign: 'center', maxWidth: '360px' }}>
<div style=${{ fontSize: '48px', marginBottom: '16px' }}></div>
<div style=${{ fontSize: '18px', fontWeight: 600, color: '#f87171', marginBottom: '8px' }}>数据异常</div>
<div style=${{ fontSize: '13px', color: '#64748b' }}>${error}</div>
</div>
</div>`;
if (!processed) return html`
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', padding: '20px' }}>
<div class="card animate-glow" style=${{ padding: '40px', textAlign: 'center' }}>
<div class="animate-float" style=${{ fontSize: '48px', marginBottom: '16px' }}>📊</div>
<div class="gradient-text" style=${{ fontSize: '18px', fontWeight: 600 }}>加载中...</div>
</div>
</div>`;
const { depts, stats, warnings, deptList, orders } = processed;
return html`
<div style=${{ padding: '20px', maxWidth: '1600px', margin: '0 auto' }}>
<!-- 顶部标题栏 -->
<div class="card animate-slide" style=${{ padding: '20px 28px', marginBottom: '20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '16px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div class="animate-glow" style=${{ width: '52px', height: '52px', borderRadius: '14px', background: 'linear-gradient(135deg, #6366f1, #a855f7)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '24px' }}>📊</div>
<div>
<h1 class="gradient-text" style=${{ fontSize: '22px', fontWeight: 700, margin: 0 }}>${config.title}</h1>
<p style=${{ fontSize: '12px', color: '#64748b', marginTop: '4px' }}>人效分析 · 产值统计 · 损耗监控 · 实时数据</p>
</div>
</div>
<div style=${{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
${warnings.RED > 0 && html`<span class="tag tag-red animate-pulse"><span>●</span> ${warnings.RED} 预警</span>`}
${warnings.YELLOW > 0 && html`<span class="tag tag-yellow"><span>●</span> ${warnings.YELLOW} 关注</span>`}
<span class="tag tag-green"><span>●</span> ${warnings.GREEN} 正常</span>
</div>
</div>
<!-- 核心指标 -->
<div style=${{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '16px', marginBottom: '20px' }}>
${[
{ v: stats.workers, l: '总人数', i: '👥', c: '#6366f1' },
{ v: stats.output.toLocaleString(), l: '总产出', i: '📦', c: '#8b5cf6' },
{ v: fmt(stats.hours, 1) + 'h', l: '总工时', i: '⏱️', c: '#06b6d4' },
{ v: '¥' + (stats.value / 10000).toFixed(1) + '万', l: '总产值', i: '💰', c: '#10b981' },
{ v: fmt(stats.efficiency, 1), l: '平均效率', i: '⚡', c: '#f59e0b' },
{ v: fmt(stats.performance, 1), l: '平均绩效', i: '🎯', c: '#ec4899' },
{ v: fmtPct(stats.completion), l: '完成率', i: '✅', c: '#14b8a6' },
{ v: fmtPct(stats.defect), l: '不良率', i: '📉', c: stats.defect > 1 ? '#ef4444' : '#64748b', alert: stats.defect > 1 }
].map((m, i) => html`
<div key=${i} class="card animate-slide" style=${{ padding: '20px', animationDelay: i * 60 + 'ms', border: m.alert ? '1px solid rgba(239, 68, 68, 0.4)' : undefined }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
<span style=${{ fontSize: '22px' }}>${m.i}</span>
<div style=${{ width: '8px', height: '8px', borderRadius: '50%', background: m.c, boxShadow: '0 0 10px ' + m.c }}></div>
</div>
<div style=${{ fontSize: '24px', fontWeight: 700, color: m.alert ? '#f87171' : '#f1f5f9' }}>${m.v}</div>
<div style=${{ fontSize: '12px', color: '#64748b', marginTop: '4px' }}>${m.l}</div>
</div>
`)}
</div>
<!-- 图表区域 -->
<div style=${{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginBottom: '20px' }}>
<div class="card" style=${{ padding: '20px' }}>
<div style=${{ fontSize: '14px', fontWeight: 600, color: '#e2e8f0', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#6366f1' }}></span>
部门效率对比
</div>
${depts.length > 0 ? html`<div ref=${chartRef1}></div>` : html`<${Empty} icon="📊" text="暂无数据" />`}
</div>
<div class="card" style=${{ padding: '20px' }}>
<div style=${{ fontSize: '14px', fontWeight: 600, color: '#e2e8f0', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#8b5cf6' }}></span>
产值分布
</div>
${depts.length > 0 ? html`<div ref=${chartRef2}></div>` : html`<${Empty} icon="💰" text="暂无数据" />`}
</div>
<div class="card" style=${{ padding: '20px' }}>
<div style=${{ fontSize: '14px', fontWeight: 600, color: '#e2e8f0', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#06b6d4' }}></span>
月度趋势
</div>
${processed.trends.length > 0 ? html`<div ref=${chartRef3}></div>` : html`<${Empty} icon="📈" text="暂无数据" />`}
</div>
</div>
<!-- 部门汇总表 -->
<div class="card" style=${{ marginBottom: '20px', overflow: 'hidden' }}>
<div style=${{ padding: '16px 20px', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#6366f1' }}></span>
<span style=${{ fontSize: '14px', fontWeight: 600 }}>部门人效-产值-损耗汇总</span>
<span class="tag tag-blue" style=${{ marginLeft: 'auto' }}>${depts.length} 部门</span>
</div>
${depts.length > 0 ? html`
<div style=${{ overflowX: 'auto' }}>
<table class="data-table">
<thead>
<tr>
<th>部门</th><th style=${{ textAlign: 'center' }}>人数</th><th style=${{ textAlign: 'center' }}>工时</th>
<th style=${{ textAlign: 'center' }}>产出</th><th style=${{ textAlign: 'center' }}>人均</th><th style=${{ textAlign: 'center' }}>时均</th>
<th style=${{ textAlign: 'center' }}>效率</th><th>完成率</th><th style=${{ textAlign: 'right' }}>产值</th>
<th style=${{ textAlign: 'center' }}>不良率</th><th style=${{ textAlign: 'center' }}>绩效</th><th style=${{ textAlign: 'center' }}>状态</th>
</tr>
</thead>
<tbody>
${depts.map((d, i) => {
const w = warningCfg[d.warning_level] || warningCfg.GREEN;
return html`
<tr key=${i}>
<td style=${{ fontWeight: 600, color: '#f1f5f9' }}>${d.department}</td>
<td style=${{ textAlign: 'center' }}>${d.worker_count}</td>
<td style=${{ textAlign: 'center' }}>${fmt(d.total_work_hours, 1)}h</td>
<td style=${{ textAlign: 'center', fontWeight: 600, color: '#a5b4fc' }}>${d.total_output_qty?.toLocaleString() || 0}</td>
<td style=${{ textAlign: 'center' }}>${fmt(d.output_per_worker, 1)}</td>
<td style=${{ textAlign: 'center' }}>${fmt(d.output_per_hour, 1)}</td>
<td style=${{ textAlign: 'center' }}>
<span class="tag ${d.efficiency_index >= 80 ? 'tag-green' : d.efficiency_index >= 50 ? 'tag-yellow' : 'tag-red'}">${fmt(d.efficiency_index, 1)}</span>
</td>
<td>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div class="progress-bar" style=${{ flex: 1, maxWidth: '80px' }}>
<div class="progress-fill" style=${{ width: Math.min(d.plan_completion_rate || 0, 100) + '%' }}></div>
</div>
<span style=${{ fontSize: '12px', color: '#94a3b8' }}>${fmtPct(d.plan_completion_rate)}</span>
</div>
</td>
<td style=${{ textAlign: 'right', fontWeight: 600, color: '#34d399' }}>${fmtMoney(d.estimated_output_value)}</td>
<td style=${{ textAlign: 'center' }}>
<span class="tag ${(d.defect_rate || 0) === 0 ? 'tag-green' : d.defect_rate < 1 ? 'tag-yellow' : 'tag-red'}">${fmtPct(d.defect_rate)}</span>
</td>
<td style=${{ textAlign: 'center', fontWeight: 600, color: '#a5b4fc' }}>${fmt(d.performance_score, 1)}</td>
<td style=${{ textAlign: 'center' }}><span class="tag ${w.class}">${w.icon} ${w.label}</span></td>
</tr>
`;
})}
</tbody>
</table>
</div>
` : html`<${Empty} icon="🏢" text="暂无部门数据" />`}
</div>
<!-- 下方两栏:员工明细 + 产品质量 -->
<div style=${{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<!-- 员工产出明细 -->
<div class="card" style=${{ overflow: 'hidden' }}>
<div style=${{ padding: '16px 20px', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#f59e0b' }}></span>
<span style=${{ fontSize: '14px', fontWeight: 600 }}>员工产出明细</span>
</div>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<select value=${deptFilter} onChange=${e => { setDeptFilter(e.target.value); setWorkerPage(1); }}
style=${{ padding: '6px 12px', fontSize: '12px', borderRadius: '8px', border: '1px solid rgba(99, 102, 241, 0.3)', background: 'rgba(30, 41, 59, 0.8)', color: '#e2e8f0', cursor: 'pointer' }}>
<option value="all">全部部门</option>
${deptList.map(d => html`<option key=${d} value=${d}>${d}</option>`)}
</select>
<span class="tag tag-blue">${filteredWorkers.length} 人</span>
</div>
</div>
${filteredWorkers.length > 0 ? html`
<div style=${{ overflowX: 'auto', maxHeight: '360px', overflowY: 'auto' }}>
<table class="data-table">
<thead><tr><th>姓名</th><th>部门</th><th style=${{ textAlign: 'center' }}>工单</th><th style=${{ textAlign: 'center' }}>工时</th><th style=${{ textAlign: 'center' }}>产出</th><th style=${{ textAlign: 'center' }}>时均</th><th style=${{ textAlign: 'center' }}>排名</th></tr></thead>
<tbody>
${pagedWorkers.map((w, i) => html`
<tr key=${i}>
<td style=${{ fontWeight: 600, color: '#f1f5f9' }}>${w.worker_name}</td>
<td><span class="tag tag-blue">${w.department}</span></td>
<td style=${{ textAlign: 'center' }}>${w.work_order_count}</td>
<td style=${{ textAlign: 'center' }}>${fmt(w.total_work_hours, 2)}h</td>
<td style=${{ textAlign: 'center', fontWeight: 600, color: '#a5b4fc' }}>${w.total_output}</td>
<td style=${{ textAlign: 'center' }}>
<span class="tag ${w.output_per_hour >= 80 ? 'tag-green' : w.output_per_hour >= 40 ? 'tag-yellow' : 'tag-red'}">${fmt(w.output_per_hour, 1)}</span>
</td>
<td style=${{ textAlign: 'center' }}>
<span style=${{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '26px', height: '26px', borderRadius: '50%', fontSize: '11px', fontWeight: 700, background: parseInt(w.dept_output_rank) <= 3 ? 'linear-gradient(135deg, #f59e0b, #f97316)' : 'rgba(51, 65, 85, 0.5)', color: parseInt(w.dept_output_rank) <= 3 ? '#fff' : '#94a3b8' }}>${w.dept_output_rank}</span>
</td>
</tr>
`)}
</tbody>
</table>
</div>
<div style=${{ padding: '12px 20px', borderTop: '1px solid rgba(99, 102, 241, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style=${{ fontSize: '12px', color: '#64748b' }}>共 ${filteredWorkers.length} 人,第 ${workerPage}/${workerPages || 1} 页</span>
<${Pager} page=${workerPage} total=${workerPages} onChange=${setWorkerPage} />
</div>
` : html`<${Empty} icon="👤" text="暂无员工数据" />`}
</div>
<!-- 产品质量与损耗 -->
<div class="card" style=${{ overflow: 'hidden' }}>
<div style=${{ padding: '16px 20px', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style=${{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#10b981' }}></span>
<span style=${{ fontSize: '14px', fontWeight: 600 }}>产品质量与损耗</span>
</div>
<span class="tag tag-green">${processed.products.length} 产品</span>
</div>
${processed.products.length > 0 ? html`
<div style=${{ overflowX: 'auto', maxHeight: '360px', overflowY: 'auto' }}>
<table class="data-table">
<thead><tr><th>产品</th><th>编码</th><th>部门</th><th style=${{ textAlign: 'center' }}>合格</th><th style=${{ textAlign: 'center' }}>不良</th><th style=${{ textAlign: 'center' }}>不良率</th><th style=${{ textAlign: 'center' }}>损耗</th></tr></thead>
<tbody>
${pagedProducts.map((p, i) => {
const lc = lossCfg[p.loss_level] || lossCfg.LOW;
return html`
<tr key=${i}>
<td style=${{ fontWeight: 600, color: '#f1f5f9', maxWidth: '160px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>${p.product_name}</td>
<td style=${{ fontFamily: 'monospace', fontSize: '11px', color: '#64748b' }}>${p.product_code}</td>
<td><span class="tag tag-blue">${p.department}</span></td>
<td style=${{ textAlign: 'center', color: '#34d399', fontWeight: 600 }}>${p.pass_qty?.toLocaleString() || 0}</td>
<td style=${{ textAlign: 'center', color: (p.fail_qty || 0) > 0 ? '#f87171' : '#64748b' }}>${p.fail_qty || 0}</td>
<td style=${{ textAlign: 'center' }}>
<span class="tag ${p.defect_rate == null || p.defect_rate === 0 ? 'tag-green' : p.defect_rate < 1 ? 'tag-yellow' : 'tag-red'}">${p.defect_rate == null ? '-' : fmtPct(p.defect_rate)}</span>
</td>
<td style=${{ textAlign: 'center' }}><span class="tag ${lc.class}">${lc.label}</span></td>
</tr>
`;
})}
</tbody>
</table>
</div>
<div style=${{ padding: '12px 20px', borderTop: '1px solid rgba(99, 102, 241, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style=${{ fontSize: '12px', color: '#64748b' }}>共 ${processed.products.length} 产品,第 ${productPage}/${productPages || 1} 页</span>
<${Pager} page=${productPage} total=${productPages} onChange=${setProductPage} />
</div>
` : html`<${Empty} icon="🔍" text="暂无产品数据" />`}
</div>
</div>
<!-- 订单状态 -->
${orders.length > 0 && html`
<div class="card" style=${{ marginTop: '16px', padding: '20px' }}>
<div style=${{ fontSize: '14px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style=${{ width: '6px', height: '6px', borderRadius: '50%', background: '#a855f7' }}></span>
订单状态概览
<span class="tag tag-blue" style=${{ marginLeft: 'auto' }}>${orders.length} 条</span>
</div>
<div style=${{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '12px' }}>
${orders.map((o, i) => html`
<div key=${i} style=${{ padding: '14px 16px', borderRadius: '10px', background: 'rgba(30, 41, 59, 0.5)', border: '1px solid rgba(99, 102, 241, 0.15)' }}>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '10px' }}>
<span style=${{ fontSize: '13px', fontWeight: 600, color: '#e2e8f0' }}>${o.department}</span>
<span class="tag ${o.status === 'CLOSED' ? 'tag-green' : o.status === 'STARTED' ? 'tag-blue' : 'tag-yellow'}">
${o.status === 'CLOSED' ? '已完成' : o.status === 'STARTED' ? '进行中' : '待处理'}
</span>
</div>
<div style=${{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '12px', color: '#94a3b8' }}>
<span>${o.order_count} 单</span>
<span style=${{ fontWeight: 600, color: '#a5b4fc' }}>${fmtPct(o.completion_rate)}</span>
</div>
<div class="progress-bar" style=${{ marginTop: '8px' }}>
<div class="progress-fill" style=${{ width: Math.min(o.completion_rate || 0, 100) + '%' }}></div>
</div>
</div>
`)}
</div>
</div>
`}
<!-- 底部 -->
<div style=${{ textAlign: 'center', padding: '24px 0 8px', color: '#475569', fontSize: '12px' }}>
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '8px 20px', borderRadius: '20px', background: 'rgba(30, 41, 59, 0.5)', border: '1px solid rgba(99, 102, 241, 0.1)' }}>
📊 人效-产值-损耗三维模型 · 实时监控
</span>
</div>
</div>
`;
}
ReactDOM.render(React.createElement(App), document.getElementById('root'));
}
</script>
</body>
</html>