- 新增6个HTML可视化仪表盘组件用于数据展示 * 人效产值损耗三维模型仪表盘 * 指标趋势分析与拐点预警仪表盘 * 一页式决策简报仪表盘 * 订单延迟预警分析仪表盘 * 供应链风险预警仪表盘 * 工单执行进度与异常节点仪表盘 - 添加VSCode工作区配置文件 - 更新businessQueries.json业务查询配置 - 优化api_client.py API客户端实现 - 更新pyproject.toml项目依赖版本 - 重组SQL查询文件结构 - 删除v2版本冗余文档配置 - 添加v2版本技能清单文档 - 更新日志文件记录
544 lines
35 KiB
HTML
544 lines
35 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: '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>
|