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

792 lines
45 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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; }
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
@keyframes slideIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.animate-slide-in { animation: slideIn 0.5s ease-out forwards; }
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
.animate-scale-in { animation: scaleIn 0.4s ease-out forwards; }
.animate-pulse { animation: pulse 2s ease-in-out infinite; }
.animate-blink { animation: blink 1.5s ease-in-out infinite; }
.animate-bounce { animation: bounce 1s ease-in-out infinite; }
.glass { backdrop-filter: blur(16px); background: rgba(255,255,255,0.85); }
.gradient-text { background: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #8b5cf6 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.page-bg { background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 30%, #f1f5f9 70%, #f8fafc 100%); min-height: 100vh; }
.scrollbar-thin::-webkit-scrollbar { width: 6px; height: 6px; }
.scrollbar-thin::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 3px; }
.scrollbar-thin::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
.scrollbar-thin::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.card-hover { transition: all 0.3s ease; }
.card-hover:hover { transform: translateY(-4px); box-shadow: 0 20px 40px -12px rgba(0,0,0,0.15); }
.trend-arrow { display: inline-flex; align-items: center; justify-content: center; }
.shimmer-bg { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); background-size: 200% 100%; animation: shimmer 2s infinite; }
</style>
<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: 'tailwindcss', src: '/LzwcaiEmbedFrameFile/lib/tailwindcss-3.4.17.js', check: () => typeof window.tailwind !== 'undefined' },
{ name: 'g2', src: '/LzwcaiEmbedFrameFile/lib/g2@5.2.4.min.js', check: () => typeof window.G2 !== 'undefined' }
]
});
await loader.loadAll();
initReactApp();
} catch (error) { console.error('[MetricTrend] 库加载失败:', error); }
});
function initReactApp() {
const { useState, useEffect, useMemo, useRef, useCallback } = React;
const html = htm.bind(React.createElement);
const config = { enableLog: true, title: '指标趋势分析与拐点预警' };
const lzwcaiComInitDate = '{{lzwcaiComInitDate}}';
const defaultData = { success: false, data: [[], [], [], []] };
// 趋势配置
const trendConfig = {
'RISING': { label: '上升', icon: '📈', arrow: '↑', color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200', gradient: 'from-emerald-400 to-teal-500' },
'FALLING': { label: '下降', icon: '📉', arrow: '↓', color: 'text-red-600', bg: 'bg-red-50', border: 'border-red-200', gradient: 'from-red-400 to-rose-500' },
'STABLE': { label: '平稳', icon: '➡️', arrow: '→', color: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-200', gradient: 'from-blue-400 to-indigo-500' },
'NONE': { label: '无数据', icon: '⚪', arrow: '-', color: 'text-slate-500', bg: 'bg-slate-50', border: 'border-slate-200', gradient: 'from-slate-400 to-slate-500' }
};
// 状态配置
const statusConfig = {
'NORMAL': { label: '正常', icon: '✅', color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200' },
'ANOMALY': { label: '异常', icon: '⚠️', color: 'text-red-600', bg: 'bg-red-50', border: 'border-red-200' }
};
// 拐点配置
const turningPointConfig = {
'TURNING_POINT': { label: '拐点', icon: '🔄', color: 'text-amber-600', bg: 'bg-amber-50', border: 'border-amber-200' },
'UPWARD': { label: '向上拐点', icon: '⬆️', color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200' },
'DOWNWARD': { label: '向下拐点', icon: '⬇️', color: 'text-red-600', bg: 'bg-red-50', border: 'border-red-200' },
'NONE': { label: '无拐点', icon: '', color: 'text-slate-500', bg: 'bg-slate-50', border: 'border-slate-200' }
};
// 建议配置
const recommendationConfig = {
'MAINTAIN_CURRENT_MEASURES': { label: '维持当前措施', icon: '✅', color: 'text-emerald-600' },
'INCREASE_MONITORING': { label: '加强监控', icon: '👁️', color: 'text-amber-600' },
'TAKE_ACTION': { label: '采取行动', icon: '🚨', color: 'text-red-600' }
};
// 空状态组件
const EmptyState = ({ icon, title, desc }) => html`
<div class="flex flex-col items-center justify-center py-10 text-center">
<div class="w-14 h-14 bg-gradient-to-br from-slate-100 to-slate-200 rounded-2xl flex items-center justify-center text-2xl mb-3">${icon}</div>
<p class="text-sm font-medium text-slate-500">${title}</p>
<p class="text-xs text-slate-400 mt-1">${desc}</p>
</div>
`;
// 趋势箭头组件
const TrendArrow = ({ trend, size = 'md' }) => {
const cfg = trendConfig[trend] || trendConfig['NONE'];
const sizeClass = size === 'lg' ? 'text-2xl' : size === 'sm' ? 'text-sm' : 'text-lg';
return html`<span class="trend-arrow ${cfg.color} ${sizeClass} font-bold">${cfg.arrow}</span>`;
};
// 指标卡片组件(可交互)
const MetricCard = ({ title, value, unit, trend, status, icon, gradient, onClick, isActive, subValue, subLabel }) => {
const trendCfg = trendConfig[trend] || trendConfig['NONE'];
const statusCfg = statusConfig[status] || statusConfig['NORMAL'];
const isAnomaly = status === 'ANOMALY';
return html`
<div
onClick=${onClick}
class="bg-white rounded-2xl p-4 shadow-md border border-slate-100/80 card-hover cursor-pointer relative overflow-hidden
${isActive ? 'ring-2 ring-indigo-400 ring-offset-2' : ''}
${isAnomaly ? 'ring-2 ring-red-200' : ''}"
>
${isAnomaly && html`<div class="absolute top-0 right-0 w-20 h-20 bg-red-500/10 rounded-bl-full"></div>`}
<div class="flex items-start justify-between mb-3">
<span class="w-10 h-10 bg-gradient-to-br ${gradient} rounded-xl flex items-center justify-center text-lg shadow-md ${isAnomaly ? 'animate-pulse' : ''}">${icon}</span>
<div class="flex items-center gap-1">
<${TrendArrow} trend=${trend} size="sm" />
<span class="text-xs font-medium ${trendCfg.color}">${trendCfg.label}</span>
</div>
</div>
<p class="text-2xl font-black text-slate-800">${value}<span class="text-sm font-normal text-slate-400 ml-1">${unit}</span></p>
<p class="text-xs text-slate-500 mt-1">${title}</p>
${subValue !== undefined && html`
<div class="mt-2 pt-2 border-t border-slate-100 flex items-center justify-between">
<span class="text-xs text-slate-400">${subLabel}</span>
<span class="text-xs font-medium text-slate-600">${subValue}</span>
</div>
`}
${isAnomaly && html`
<div class="mt-2 flex items-center gap-1">
<span class="w-2 h-2 bg-red-500 rounded-full animate-blink"></span>
<span class="text-xs font-medium text-red-600">需要关注</span>
</div>
`}
</div>
`;
};
function ChartApp() {
const [rawData, setRawData] = useState(defaultData);
const [pageHelper, setPageHelper] = useState(null);
const [error, setError] = useState(null);
const [activeMetric, setActiveMetric] = useState('efficiency'); // efficiency, output, defect
const [selectedDate, setSelectedDate] = useState(null);
const [showDetail, setShowDetail] = useState(false);
const chartRef1 = useRef(null);
const chartRef2 = useRef(null);
const chartRef3 = useRef(null);
const chartInstance1 = useRef(null);
const chartInstance2 = useRef(null);
const chartInstance3 = useRef(null);
const validateData = (data) => {
if (!data || typeof data !== 'object') return { valid: false, error: '数据格式错误' };
if (!Array.isArray(data.data)) return { valid: false, error: 'data 字段应为数组' };
return { valid: true };
};
// 处理数据
const processedData = useMemo(() => {
if (!rawData.data || rawData.data.length < 4) return null;
const [dailyMetrics = [], weeklyMetrics = [], summaryMetrics = [], turningPoints = []] = rawData.data;
// 日指标数据处理(按日期排序)
const sortedDaily = [...dailyMetrics].sort((a, b) => new Date(a.metric_date) - new Date(b.metric_date));
// 周指标数据处理
const sortedWeekly = [...weeklyMetrics].sort((a, b) => new Date(a.week_start) - new Date(b.week_start));
// 汇总指标
const summaryMap = {};
summaryMetrics.forEach(m => { summaryMap[m.metric_name] = m; });
// 统计异常和拐点数量
let anomalyCount = 0;
let turningPointCount = 0;
sortedDaily.forEach(d => {
if (d.efficiency_status === 'ANOMALY') anomalyCount++;
if (d.output_status === 'ANOMALY') anomalyCount++;
if (d.defect_status === 'ANOMALY') anomalyCount++;
if (d.efficiency_turning_point === 'TURNING_POINT') turningPointCount++;
if (d.output_turning_point === 'TURNING_POINT') turningPointCount++;
if (d.defect_turning_point === 'TURNING_POINT') turningPointCount++;
});
// 获取最新数据
const latestDaily = sortedDaily.length > 0 ? sortedDaily[sortedDaily.length - 1] : null;
const latestWeekly = sortedWeekly.length > 0 ? sortedWeekly[sortedWeekly.length - 1] : null;
return {
dailyMetrics: sortedDaily,
weeklyMetrics: sortedWeekly,
summaryMetrics: summaryMap,
turningPoints,
anomalyCount,
turningPointCount,
latestDaily,
latestWeekly
};
}, [rawData]);
// 渲染趋势图表
useEffect(() => {
if (!processedData || !window.G2) return;
// 清理旧图表
[chartInstance1, chartInstance2, chartInstance3].forEach(ref => {
if (ref.current) { ref.current.destroy(); ref.current = null; }
});
const { dailyMetrics, weeklyMetrics } = processedData;
// 人效趋势图(折线图 + 移动平均线)
if (chartRef1.current && dailyMetrics.length > 0) {
const efficiencyData = [];
dailyMetrics.forEach(d => {
efficiencyData.push({ date: d.metric_date, value: d.hourly_output, type: '实际人效' });
efficiencyData.push({ date: d.metric_date, value: d.efficiency_ma7, type: '7日均线' });
});
chartInstance1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 220 });
chartInstance1.current.line()
.data(efficiencyData)
.encode('x', 'date').encode('y', 'value').encode('color', 'type')
.scale('color', { range: ['#3b82f6', '#f59e0b'] })
.style('lineWidth', 2)
.axis('x', { labelAutoRotate: true, labelFontSize: 10, title: false })
.axis('y', { title: false, labelFormatter: v => v.toFixed(1) })
.legend('color', { position: 'top', layout: { justifyContent: 'center' } })
.tooltip({ shared: true });
// 标记拐点
const turningPointData = dailyMetrics.filter(d => d.efficiency_turning_point === 'TURNING_POINT');
if (turningPointData.length > 0) {
chartInstance1.current.point()
.data(turningPointData.map(d => ({ date: d.metric_date, value: d.hourly_output })))
.encode('x', 'date').encode('y', 'value')
.style('fill', '#ef4444').style('r', 6)
.tooltip({ items: [{ channel: 'y', name: '拐点', valueFormatter: v => v.toFixed(2) }] });
}
chartInstance1.current.render();
}
// 产量趋势图
if (chartRef2.current && dailyMetrics.length > 0) {
const outputData = [];
dailyMetrics.forEach(d => {
outputData.push({ date: d.metric_date, value: d.daily_output, type: '日产量' });
outputData.push({ date: d.metric_date, value: d.output_ma7, type: '7日均线' });
});
chartInstance2.current = new G2.Chart({ container: chartRef2.current, autoFit: true, height: 220 });
chartInstance2.current.line()
.data(outputData)
.encode('x', 'date').encode('y', 'value').encode('color', 'type')
.scale('color', { range: ['#10b981', '#8b5cf6'] })
.style('lineWidth', 2)
.axis('x', { labelAutoRotate: true, labelFontSize: 10, title: false })
.axis('y', { title: false })
.legend('color', { position: 'top', layout: { justifyContent: 'center' } })
.tooltip({ shared: true });
// 标记拐点
const turningPointData = dailyMetrics.filter(d => d.output_turning_point === 'TURNING_POINT');
if (turningPointData.length > 0) {
chartInstance2.current.point()
.data(turningPointData.map(d => ({ date: d.metric_date, value: d.daily_output })))
.encode('x', 'date').encode('y', 'value')
.style('fill', '#ef4444').style('r', 6);
}
chartInstance2.current.render();
}
// 周环比趋势图(柱状图)
if (chartRef3.current && weeklyMetrics.length > 0) {
const weeklyData = weeklyMetrics.map(w => ({
week: w.week_start,
人效: w.hourly_output,
环比: w.efficiency_wow_pct || 0
}));
chartInstance3.current = new G2.Chart({ container: chartRef3.current, autoFit: true, height: 220 });
chartInstance3.current.interval()
.data(weeklyData)
.encode('x', 'week').encode('y', '人效')
.encode('color', d => d['环比'] >= 0 ? '上升' : '下降')
.scale('color', { range: ['#10b981', '#ef4444'] })
.style('radius', 6)
.axis('x', { labelAutoRotate: true, labelFontSize: 10, title: false })
.axis('y', { title: false })
.legend('color', { position: 'top', layout: { justifyContent: 'center' } })
.tooltip({ items: [
{ channel: 'y', name: '人效', valueFormatter: v => v.toFixed(2) },
{ field: '环比', name: '环比', valueFormatter: v => (v >= 0 ? '+' : '') + v.toFixed(1) + '%' }
] });
chartInstance3.current.render();
}
return () => {
[chartInstance1, chartInstance2, chartInstance3].forEach(ref => {
if (ref.current) { ref.current.destroy(); ref.current = null; }
});
};
}, [processedData]);
useEffect(() => {
const helper = new ChildPageHelper({
autoRenderStatus: true, enableLog: config.enableLog,
onReady: async () => {
await helper.autoInitialize(() => {
const finalInitData = helper.parseTemplateData(lzwcaiComInitDate, defaultData);
if (finalInitData) {
const v = validateData(finalInitData);
v.valid ? (setRawData(finalInitData), setError(null)) : setError(v.error);
}
});
}
});
setPageHelper(helper);
helper.expose({
setDataLzwcaiEmbedFrameFn: (data) => {
const v = validateData(data);
if (v.valid) { setRawData(data); setError(null); return { status: 'success' }; }
setError(v.error); return { status: 'error', message: v.error };
},
getDataLzwcaiEmbedFrameFn: () => ({ status: 'success', data: rawData }),
captureScreenshotLzwcaiEmbedFrameFn: helper.createScreenshotMethod(),
getRenderStatusLzwcaiEmbedFrameFn: () => helper.getRenderStatus()
});
}, []);
const formatPercent = v => v !== null && v !== undefined ? (v >= 0 ? '+' : '') + v.toFixed(1) + '%' : '-';
const formatNumber = v => v !== null && v !== undefined ? v.toFixed(2) : '-';
const getTrend = trend => trendConfig[trend] || trendConfig['NONE'];
const getStatus = status => statusConfig[status] || statusConfig['NORMAL'];
const getTurningPoint = tp => turningPointConfig[tp] || turningPointConfig['NONE'];
if (error) return html`
<div class="page-bg flex items-center justify-center p-6">
<div class="glass rounded-3xl shadow-2xl p-8 max-w-sm text-center border border-red-100">
<div class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-red-400 to-rose-500 rounded-2xl flex items-center justify-center text-3xl shadow-lg">⚠️</div>
<h3 class="text-lg font-bold text-red-700 mb-2">数据异常</h3>
<p class="text-sm text-red-500">${error}</p>
</div>
</div>`;
if (!processedData) return html`
<div class="page-bg flex items-center justify-center p-6">
<div class="glass rounded-3xl shadow-xl p-10 max-w-sm text-center border border-blue-100">
<div class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-2xl flex items-center justify-center text-3xl animate-pulse">📊</div>
<h3 class="text-lg font-bold text-blue-600 mb-2">加载中</h3>
<p class="text-sm text-slate-400">等待指标数据...</p>
</div>
</div>`;
const { dailyMetrics, weeklyMetrics, summaryMetrics, turningPoints, anomalyCount, turningPointCount, latestDaily, latestWeekly } = processedData;
return html`
<div class="page-bg p-4 sm:p-5">
<div class="max-w-7xl mx-auto space-y-4">
<!-- 头部 -->
<div class="glass rounded-2xl p-5 shadow-lg border border-white/60">
<div class="flex items-center justify-between flex-wrap gap-4">
<div class="flex items-center gap-4">
<div class="w-14 h-14 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-500/30">
<span class="text-white text-2xl">📊</span>
</div>
<div>
<h1 class="text-2xl font-bold gradient-text">${config.title}</h1>
<p class="text-xs text-slate-500 mt-0.5">移动平均 · 线性回归 · 拐点预警</p>
</div>
</div>
<div class="flex items-center gap-3 flex-wrap">
${anomalyCount > 0 && html`
<span class="px-4 py-2 bg-red-50 border border-red-200 rounded-xl text-sm font-bold text-red-600 flex items-center gap-2 animate-pulse">
<span class="w-2.5 h-2.5 bg-red-500 rounded-full animate-blink"></span>
${anomalyCount} 个异常
</span>
`}
${turningPointCount > 0 && html`
<span class="px-4 py-2 bg-amber-50 border border-amber-200 rounded-xl text-sm font-bold text-amber-600 flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-amber-500 rounded-full"></span>
${turningPointCount} 个拐点
</span>
`}
</div>
</div>
</div>
<!-- 核心指标卡片(可点击切换) -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
${latestDaily && html`
<${MetricCard}
title="人效(件/小时)"
value=${formatNumber(latestDaily.hourly_output)}
unit=""
trend=${latestDaily.efficiency_trend}
status=${latestDaily.efficiency_status}
icon="⚡"
gradient="from-blue-400 to-indigo-500"
onClick=${() => setActiveMetric('efficiency')}
isActive=${activeMetric === 'efficiency'}
subValue=${formatNumber(latestDaily.efficiency_ma7)}
subLabel="7日均线"
/>
<${MetricCard}
title="日产量"
value=${latestDaily.daily_output}
unit="件"
trend=${latestDaily.output_trend}
status=${latestDaily.output_status}
icon="📦"
gradient="from-emerald-400 to-teal-500"
onClick=${() => setActiveMetric('output')}
isActive=${activeMetric === 'output'}
subValue=${formatNumber(latestDaily.output_ma7)}
subLabel="7日均线"
/>
<${MetricCard}
title="废品率"
value=${(latestDaily.defect_rate * 100).toFixed(2)}
unit="%"
trend=${latestDaily.defect_trend}
status=${latestDaily.defect_status}
icon="🔍"
gradient="from-purple-400 to-pink-500"
onClick=${() => setActiveMetric('defect')}
isActive=${activeMetric === 'defect'}
subValue=${(latestDaily.defect_rate_ma7 * 100).toFixed(2) + '%'}
subLabel="7日均线"
/>
`}
${latestWeekly && html`
<${MetricCard}
title="周环比变化"
value=${formatPercent(latestWeekly.efficiency_wow_pct)}
unit=""
trend=${latestWeekly.efficiency_trend}
status=${latestWeekly.efficiency_wow_pct < -20 ? 'ANOMALY' : 'NORMAL'}
icon="📈"
gradient="from-amber-400 to-orange-500"
subValue=${latestWeekly.worker_count + ' 人'}
subLabel="本周人数"
/>
`}
</div>
<!-- 趋势图表区域 -->
<div class="grid lg:grid-cols-2 gap-4">
<!-- 人效趋势图 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4 card-hover">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-lg flex items-center justify-center text-white text-xs">⚡</span>
人效趋势分析
</h3>
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-blue-500 rounded-full"></span>
<span class="text-xs text-slate-500">实际值</span>
<span class="w-3 h-3 bg-amber-500 rounded-full ml-2"></span>
<span class="text-xs text-slate-500">均线</span>
<span class="w-3 h-3 bg-red-500 rounded-full ml-2"></span>
<span class="text-xs text-slate-500">拐点</span>
</div>
</div>
${dailyMetrics.length > 0
? html`<div ref=${chartRef1} class="w-full"></div>`
: html`<${EmptyState} icon="📈" title="暂无人效数据" desc="等待数据加载" />`
}
</div>
<!-- 产量趋势图 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4 card-hover">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-lg flex items-center justify-center text-white text-xs">📦</span>
产量趋势分析
</h3>
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-emerald-500 rounded-full"></span>
<span class="text-xs text-slate-500">日产量</span>
<span class="w-3 h-3 bg-purple-500 rounded-full ml-2"></span>
<span class="text-xs text-slate-500">均线</span>
</div>
</div>
${dailyMetrics.length > 0
? html`<div ref=${chartRef2} class="w-full"></div>`
: html`<${EmptyState} icon="📦" title="暂无产量数据" desc="等待数据加载" />`
}
</div>
</div>
<!-- 周数据分析 + 拐点预警 -->
<div class="grid lg:grid-cols-3 gap-4">
<!-- 周环比趋势 -->
<div class="lg:col-span-2 bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-amber-400 to-orange-500 rounded-lg flex items-center justify-center text-white text-xs">📊</span>
周人效环比分析
</h3>
<span class="px-2 py-1 bg-amber-50 text-amber-600 rounded-full text-xs font-medium">${weeklyMetrics.length} 周</span>
</div>
${weeklyMetrics.length > 0
? html`<div ref=${chartRef3} class="w-full"></div>`
: html`<${EmptyState} icon="📊" title="暂无周数据" desc="等待数据加载" />`
}
</div>
<!-- 拐点预警列表 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-red-400 to-rose-500 rounded-lg flex items-center justify-center text-white text-xs animate-pulse">🔄</span>
拐点预警
</h3>
<span class="px-2 py-1 bg-red-50 text-red-600 rounded-full text-xs font-medium">${turningPoints.length} 项</span>
</div>
${turningPoints.length > 0 ? html`
<div class="space-y-2 max-h-[280px] overflow-y-auto scrollbar-thin pr-1">
${turningPoints.map((tp, i) => {
const tpCfg = getTurningPoint(tp.turning_type);
const recCfg = recommendationConfig[tp.recommendation] || { label: tp.recommendation, icon: '📋', color: 'text-slate-600' };
return html`
<div key=${i} class="bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl p-3 border border-amber-100 hover:shadow-md transition-all card-hover animate-slide-in" style=${{ animationDelay: i * 60 + 'ms' }}>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-lg">${tpCfg.icon}</span>
<span class="font-bold text-slate-800 text-sm">${tp.metric_date}</span>
</div>
<div class="flex flex-wrap gap-1 mb-2">
<span class="px-2 py-0.5 ${tpCfg.bg} ${tpCfg.border} border rounded text-xs ${tpCfg.color} font-medium">${tpCfg.label}</span>
</div>
<div class="flex items-center gap-2 text-xs text-slate-500">
<span>人效: ${formatNumber(tp.daily_efficiency)}</span>
<span>均线: ${formatNumber(tp.ma7_line)}</span>
</div>
</div>
<div class="text-right shrink-0">
<span class="inline-flex items-center gap-1 px-2 py-1 bg-white/80 rounded-lg text-xs font-medium ${recCfg.color}">
${recCfg.icon} ${recCfg.label}
</span>
</div>
</div>
</div>
`;
})}
</div>
` : html`<${EmptyState} icon="✨" title="暂无拐点" desc="趋势运行平稳" />`}
</div>
</div>
<!-- 汇总指标对比 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-indigo-400 to-purple-500 rounded-lg flex items-center justify-center text-white text-xs">📋</span>
周期对比分析
</h3>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
${[
{ key: 'efficiency_per_hour', label: '人效(件/小时)', icon: '⚡', gradient: 'from-blue-400 to-indigo-500' },
{ key: 'daily_output', label: '日产量', icon: '📦', gradient: 'from-emerald-400 to-teal-500' },
{ key: 'defect_rate_pct', label: '废品率', icon: '🔍', gradient: 'from-purple-400 to-pink-500' }
].map((item, i) => {
const m = summaryMetrics[item.key] || {};
const trendCfg = getTrend(m.trend);
const isWarning = m.warning === 'ANOMALY';
return html`
<div key=${i} class="bg-gradient-to-br from-slate-50 to-white rounded-xl p-4 border border-slate-100 hover:shadow-md transition-all card-hover ${isWarning ? 'ring-2 ring-red-200' : ''}">
<div class="flex items-center gap-2 mb-3">
<span class="w-8 h-8 bg-gradient-to-br ${item.gradient} rounded-lg flex items-center justify-center text-sm shadow-md">${item.icon}</span>
<span class="text-sm font-medium text-slate-700">${item.label}</span>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-slate-400 mb-1">近7日均值</p>
<p class="text-lg font-bold text-slate-800">${m.last_7d_avg !== null ? formatNumber(m.last_7d_avg) : '-'}</p>
</div>
<div>
<p class="text-xs text-slate-400 mb-1">前7日均值</p>
<p class="text-lg font-bold text-slate-600">${m.prev_7d_avg !== null ? formatNumber(m.prev_7d_avg) : '-'}</p>
</div>
</div>
<div class="mt-3 pt-3 border-t border-slate-100 flex items-center justify-between">
<div class="flex items-center gap-2">
<${TrendArrow} trend=${m.trend} size="sm" />
<span class="text-xs font-medium ${trendCfg.color}">${trendCfg.label}</span>
</div>
<span class="text-sm font-bold ${m.change_rate_pct >= 0 ? 'text-emerald-600' : 'text-red-600'}">
${m.change_rate_pct !== null ? formatPercent(m.change_rate_pct) : '-'}
</span>
</div>
${isWarning && html`
<div class="mt-2 flex items-center gap-1">
<span class="w-2 h-2 bg-red-500 rounded-full animate-blink"></span>
<span class="text-xs font-medium text-red-600">需要关注</span>
</div>
`}
</div>
`;
})}
</div>
</div>
<!-- 日指标明细表 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 overflow-hidden">
<div class="px-4 py-3 border-b border-slate-100 bg-gradient-to-r from-slate-50 to-white">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-lg flex items-center justify-center text-white text-xs">📅</span>
日指标明细
</h3>
<span class="px-2 py-1 bg-cyan-50 text-cyan-600 rounded-full text-xs font-medium">${dailyMetrics.length} 条</span>
</div>
</div>
${dailyMetrics.length > 0 ? html`
<div class="overflow-x-auto max-h-[400px] overflow-y-auto scrollbar-thin">
<table class="w-full text-sm">
<thead class="bg-slate-50 sticky top-0">
<tr>
<th class="px-3 py-2.5 text-left text-xs font-semibold text-slate-500">日期</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">人效</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">人效趋势</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">日产量</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">产量趋势</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">废品率</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">拐点</th>
<th class="px-3 py-2.5 text-center text-xs font-semibold text-slate-500">状态</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${dailyMetrics.slice().reverse().map((d, i) => {
const effTrend = getTrend(d.efficiency_trend);
const outTrend = getTrend(d.output_trend);
const effStatus = getStatus(d.efficiency_status);
const outStatus = getStatus(d.output_status);
const hasTurningPoint = d.efficiency_turning_point === 'TURNING_POINT' || d.output_turning_point === 'TURNING_POINT';
const hasAnomaly = d.efficiency_status === 'ANOMALY' || d.output_status === 'ANOMALY';
return html`
<tr key=${i} class="hover:bg-slate-50/50 transition-colors ${hasAnomaly ? 'bg-red-50/30' : ''} ${hasTurningPoint ? 'bg-amber-50/30' : ''}">
<td class="px-3 py-2.5">
<span class="font-semibold text-slate-800 text-sm">${d.metric_date}</span>
</td>
<td class="px-3 py-2.5 text-center">
<div>
<span class="font-bold text-slate-800">${formatNumber(d.hourly_output)}</span>
<p class="text-xs text-slate-400">均线: ${formatNumber(d.efficiency_ma7)}</p>
</div>
</td>
<td class="px-3 py-2.5 text-center">
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${effTrend.bg} ${effTrend.color}">
<${TrendArrow} trend=${d.efficiency_trend} size="sm" />
${effTrend.label}
</span>
</td>
<td class="px-3 py-2.5 text-center">
<div>
<span class="font-bold text-slate-800">${d.daily_output}</span>
<p class="text-xs text-slate-400">均线: ${formatNumber(d.output_ma7)}</p>
</div>
</td>
<td class="px-3 py-2.5 text-center">
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${outTrend.bg} ${outTrend.color}">
<${TrendArrow} trend=${d.output_trend} size="sm" />
${outTrend.label}
</span>
</td>
<td class="px-3 py-2.5 text-center">
<span class="text-sm text-slate-700">${(d.defect_rate * 100).toFixed(2)}%</span>
</td>
<td class="px-3 py-2.5 text-center">
${hasTurningPoint ? html`
<span class="inline-flex items-center gap-1 px-2 py-1 bg-amber-50 border border-amber-200 rounded-lg text-xs font-medium text-amber-600 animate-pulse">
🔄 拐点
</span>
` : html`<span class="text-slate-400 text-xs">-</span>`}
</td>
<td class="px-3 py-2.5 text-center">
${hasAnomaly ? html`
<span class="inline-flex items-center gap-1 px-2 py-1 bg-red-50 border border-red-200 rounded-lg text-xs font-medium text-red-600">
⚠️ 异常
</span>
` : html`
<span class="inline-flex items-center gap-1 px-2 py-1 bg-emerald-50 border border-emerald-200 rounded-lg text-xs font-medium text-emerald-600">
✅ 正常
</span>
`}
</td>
</tr>
`;
})}
</tbody>
</table>
</div>
` : html`<${EmptyState} icon="📅" title="暂无日指标数据" desc="等待数据加载" />`}
</div>
<!-- 周指标明细 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 overflow-hidden">
<div class="px-4 py-3 border-b border-slate-100 bg-gradient-to-r from-slate-50 to-white">
<h3 class="text-sm font-bold text-slate-700 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-violet-400 to-purple-500 rounded-lg flex items-center justify-center text-white text-xs">📆</span>
周指标汇总
</h3>
</div>
${weeklyMetrics.length > 0 ? html`
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 p-4">
${weeklyMetrics.slice().reverse().map((w, i) => {
const effTrend = getTrend(w.efficiency_trend);
const outTrend = getTrend(w.output_trend);
const isPositive = w.efficiency_wow_pct >= 0;
return html`
<div key=${i} class="bg-gradient-to-br from-slate-50 to-white rounded-xl p-4 border border-slate-100 hover:shadow-lg transition-all card-hover animate-slide-in" style=${{ animationDelay: i * 80 + 'ms' }}>
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-bold text-slate-800">${w.week_start}</span>
<span class="px-2 py-1 ${isPositive ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600'} rounded-lg text-xs font-bold">
${formatPercent(w.efficiency_wow_pct)}
</span>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">人效</span>
<div class="flex items-center gap-1">
<span class="text-sm font-bold text-slate-700">${formatNumber(w.hourly_output)}</span>
<${TrendArrow} trend=${w.efficiency_trend} size="sm" />
</div>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">总产量</span>
<span class="text-sm font-medium text-slate-600">${w.total_output}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">总工时</span>
<span class="text-sm font-medium text-slate-600">${w.total_hours}h</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">人数</span>
<span class="text-sm font-medium text-slate-600">${w.worker_count} 人</span>
</div>
</div>
</div>
`;
})}
</div>
` : html`<${EmptyState} icon="📆" title="暂无周数据" desc="等待数据加载" />`}
</div>
<!-- 底部 -->
<div class="text-center pt-2 pb-3">
<p class="text-xs text-slate-400">
<span class="inline-flex items-center gap-1.5 px-4 py-2 glass rounded-full border border-white/50 shadow-sm">
📊 指标趋势分析与拐点预警 · 智能监控
</span>
</p>
</div>
</div>
</div>
`;
}
ReactDOM.render(React.createElement(ChartApp), document.getElementById('root'));
}
</script>
</head>
<body class="m-0 p-0">
<div id="root"></div>
</body>
</html>