feat(mfg-data-agent): 添加HTML可视化仪表盘和优化项目配置

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

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,543 @@
<!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>

View File

@@ -0,0 +1,791 @@
<!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>

View File

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

View File

@@ -0,0 +1,626 @@
<!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 slideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.animate-slide-up { animation: slideUp 0.5s ease-out forwards; }
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
.animate-pulse { animation: pulse 2s ease-in-out infinite; }
.animate-blink { animation: blink 1.5s ease-in-out infinite; }
.glass { backdrop-filter: blur(20px); background: rgba(255,255,255,0.92); }
.gradient-text { background: linear-gradient(135deg, #f59e0b 0%, #ef4444 50%, #dc2626 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.page-bg { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 25%, #fcd34d 50%, #fbbf24 75%, #f59e0b 100%); min-height: 100vh; }
.card-shadow { box-shadow: 0 4px 24px rgba(245, 158, 11, 0.15), 0 2px 8px rgba(0,0,0,0.08); }
.warning-red { background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); border-color: #fca5a5; }
.warning-yellow { background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border-color: #fcd34d; }
.warning-green { background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); border-color: #86efac; }
.table-scroll { max-height: 480px; overflow-y: auto; }
.table-scroll::-webkit-scrollbar { width: 8px; }
.table-scroll::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
.table-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
.table-scroll::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</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('[OrderDelayWarning] 库加载失败:', error); }
});
function initReactApp() {
const { useState, useEffect, useMemo, useRef } = React;
const html = htm.bind(React.createElement);
const config = { enableLog: true, title: '订单延迟预警分析' };
const lzwcaiComInitDate = '{{lzwcaiComInitDate}}';
const defaultData = { success: false, data: [[]] };
// 预警等级配置
const warningConfig = {
'RED': { label: '高风险', bg: 'bg-red-500', light: 'bg-red-50', text: 'text-red-600', border: 'border-red-200', icon: '🔴', gradient: 'from-red-500 to-rose-600' },
'YELLOW': { label: '中风险', bg: 'bg-amber-500', light: 'bg-amber-50', text: 'text-amber-600', border: 'border-amber-200', icon: '🟡', gradient: 'from-amber-500 to-orange-500' },
'GREEN': { label: '低风险', bg: 'bg-emerald-500', light: 'bg-emerald-50', text: 'text-emerald-600', border: 'border-emerald-200', icon: '🟢', gradient: 'from-emerald-500 to-teal-500' }
};
// 风险因素配置
const riskFactorConfig = {
'NORMAL': { label: '正常', icon: '✅' },
'PRODUCTION_DELAY': { label: '生产延迟', icon: '🏭' },
'LOGISTICS_DELAY': { label: '物流延误', icon: '🚚' },
'EQUIPMENT_FAULT': { label: '设备故障', icon: '⚙️' },
'QUALITY_ISSUE': { label: '质量问题', icon: '🔍' },
'HIGH_DEFECT': { label: '高缺陷率', icon: '⚠️' },
'HIGH_SCRAP': { label: '高报废率', icon: '🗑️' },
'WORK_ORDER_LAG': { label: '工单滞后', icon: '📋' }
};
// 空状态组件
const EmptyState = ({ icon, title, desc }) => html`
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="w-16 h-16 bg-gradient-to-br from-amber-100 to-orange-100 rounded-2xl flex items-center justify-center text-3xl mb-4 border border-amber-200">${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 Pagination = ({ current, total, pageSize, onChange }) => {
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) return null;
const pages = [];
const showPages = 5;
let start = Math.max(1, current - Math.floor(showPages / 2));
let end = Math.min(totalPages, start + showPages - 1);
if (end - start < showPages - 1) start = Math.max(1, end - showPages + 1);
for (let i = start; i <= end; i++) pages.push(i);
return html`
<div class="flex items-center justify-between px-5 py-4 border-t border-slate-100 bg-slate-50/50">
<span class="text-xs text-slate-500">共 ${total} 条记录,第 ${current}/${totalPages} 页</span>
<div class="flex items-center gap-1">
<button onClick=${() => current > 1 && onChange(current - 1)} disabled=${current <= 1}
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${current <= 1 ? 'bg-slate-100 text-slate-400 cursor-not-allowed' : 'bg-white text-slate-600 hover:bg-amber-50 hover:text-amber-600 border border-slate-200'}">
上一页
</button>
${pages.map(p => html`
<button key=${p} onClick=${() => onChange(p)}
class="w-8 h-8 rounded-lg text-xs font-medium transition-all ${p === current ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-md' : 'bg-white text-slate-600 hover:bg-amber-50 border border-slate-200'}">
${p}
</button>
`)}
<button onClick=${() => current < totalPages && onChange(current + 1)} disabled=${current >= totalPages}
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${current >= totalPages ? 'bg-slate-100 text-slate-400 cursor-not-allowed' : 'bg-white text-slate-600 hover:bg-amber-50 hover:text-amber-600 border border-slate-200'}">
下一页
</button>
</div>
</div>
`;
};
function ChartApp() {
const [rawData, setRawData] = useState(defaultData);
const [pageHelper, setPageHelper] = useState(null);
const [error, setError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [sortField, setSortField] = useState('delay_probability_pct');
const [sortOrder, setSortOrder] = useState('desc');
const pageSize = 15;
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 safeArray = (arr) => Array.isArray(arr) ? arr : [];
const safeNumber = (val, def = 0) => {
const n = parseFloat(val);
return isNaN(n) ? def : n;
};
const processedData = useMemo(() => {
if (!rawData.data || rawData.data.length < 1) return null;
const orders = safeArray(rawData.data[0]).map(o => ({
...o,
order_amount: safeNumber(o.order_amount),
avg_production_days: safeNumber(o.avg_production_days),
avg_logistics_delay_days: safeNumber(o.avg_logistics_delay_days),
historical_delay_count: safeNumber(o.historical_delay_count),
defect_rate_pct: safeNumber(o.defect_rate_pct),
scrap_rate_pct: safeNumber(o.scrap_rate_pct),
active_work_order_count: safeNumber(o.active_work_order_count),
lagging_work_order_count: safeNumber(o.lagging_work_order_count),
delay_probability_pct: safeNumber(o.delay_probability_pct)
}));
// 统计各预警等级数量
const warningStats = { RED: 0, YELLOW: 0, GREEN: 0 };
orders.forEach(o => {
const level = o.warning_level || 'GREEN';
if (warningStats[level] !== undefined) warningStats[level]++;
});
// 统计风险因素分布
const riskStats = {};
orders.forEach(o => {
const factor = o.primary_risk_factor || 'NORMAL';
if (!riskStats[factor]) riskStats[factor] = 0;
riskStats[factor]++;
});
// 按客户统计
const customerStats = {};
orders.forEach(o => {
const name = o.customer_name || '未知';
if (!customerStats[name]) customerStats[name] = { count: 0, amount: 0, redCount: 0, yellowCount: 0 };
customerStats[name].count++;
customerStats[name].amount += o.order_amount;
if (o.warning_level === 'RED') customerStats[name].redCount++;
if (o.warning_level === 'YELLOW') customerStats[name].yellowCount++;
});
// 汇总指标
const totalOrders = orders.length;
const totalAmount = orders.reduce((s, o) => s + o.order_amount, 0);
const avgDelayProb = totalOrders > 0 ? orders.reduce((s, o) => s + o.delay_probability_pct, 0) / totalOrders : 0;
const highRiskCount = warningStats.RED + warningStats.YELLOW;
return {
orders, warningStats, riskStats, customerStats,
totalOrders, totalAmount, avgDelayProb, highRiskCount
};
}, [rawData]);
// 排序后的订单
const sortedOrders = useMemo(() => {
if (!processedData) return [];
const sorted = [...processedData.orders];
sorted.sort((a, b) => {
let aVal = a[sortField], bVal = b[sortField];
if (typeof aVal === 'string') aVal = aVal.toLowerCase();
if (typeof bVal === 'string') bVal = bVal.toLowerCase();
if (sortOrder === 'asc') return aVal > bVal ? 1 : -1;
return aVal < bVal ? 1 : -1;
});
return sorted;
}, [processedData, sortField, sortOrder]);
// 分页数据
const pagedOrders = sortedOrders.slice((currentPage - 1) * pageSize, currentPage * pageSize);
// 渲染图表
useEffect(() => {
if (!processedData || !window.G2) return;
// 清理旧图表
[chartInstance1, chartInstance2, chartInstance3].forEach(ref => {
if (ref.current) { ref.current.destroy(); ref.current = null; }
});
// 预警等级分布饼图
if (chartRef1.current && Object.values(processedData.warningStats).some(v => v > 0)) {
const pieData = [
{ name: '高风险', value: processedData.warningStats.RED, color: '#ef4444' },
{ name: '中风险', value: processedData.warningStats.YELLOW, color: '#f59e0b' },
{ name: '低风险', value: processedData.warningStats.GREEN, color: '#10b981' }
].filter(d => d.value > 0);
chartInstance1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 200 });
chartInstance1.current.coordinate({ type: 'theta', innerRadius: 0.6 });
chartInstance1.current.interval()
.data(pieData)
.transform({ type: 'stackY' })
.encode('y', 'value').encode('color', 'name')
.style('stroke', '#fff').style('lineWidth', 3)
.label({ text: d => d.name + '\n' + d.value + '单', position: 'outside', fontSize: 11, fill: '#64748b' })
.scale('color', { range: ['#ef4444', '#f59e0b', '#10b981'] })
.legend(false)
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 单' }] });
chartInstance1.current.render();
}
// 风险因素分布柱状图
if (chartRef2.current && Object.keys(processedData.riskStats).length > 0) {
const riskData = Object.entries(processedData.riskStats)
.map(([key, value]) => ({
factor: (riskFactorConfig[key] || { label: key }).label,
count: value
}))
.sort((a, b) => b.count - a.count);
chartInstance2.current = new G2.Chart({ container: chartRef2.current, autoFit: true, height: 200 });
chartInstance2.current.interval()
.data(riskData)
.encode('x', 'factor').encode('y', 'count').encode('color', 'factor')
.style('radius', 6)
.axis('x', { labelAutoRotate: true, labelFontSize: 10, title: false })
.axis('y', { title: false, labelFormatter: v => v + '单' })
.legend(false)
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 单' }] });
chartInstance2.current.render();
}
// 客户订单金额TOP5
if (chartRef3.current && Object.keys(processedData.customerStats).length > 0) {
const customerData = Object.entries(processedData.customerStats)
.map(([name, stats]) => ({ name: name.length > 8 ? name.slice(0, 8) + '..' : name, amount: stats.amount }))
.sort((a, b) => b.amount - a.amount)
.slice(0, 6);
chartInstance3.current = new G2.Chart({ container: chartRef3.current, autoFit: true, height: 200 });
chartInstance3.current.interval()
.data(customerData)
.encode('x', 'name').encode('y', 'amount').encode('color', 'name')
.style('radius', 6)
.axis('x', { labelAutoRotate: true, labelFontSize: 10, title: false })
.axis('y', { title: false, labelFormatter: v => (v/10000).toFixed(0) + '万' })
.legend(false)
.scale('color', { range: ['#f59e0b', '#fb923c', '#fbbf24', '#fcd34d', '#fde68a', '#fef3c7'] })
.tooltip({ items: [{ channel: 'y', valueFormatter: v => '¥' + (v/10000).toFixed(2) + '万' }] });
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 formatMoney = v => '¥' + Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 });
const formatWan = v => ((v || 0) / 10000).toFixed(2);
const getWarning = level => warningConfig[level] || warningConfig['GREEN'];
const getRiskFactor = factor => riskFactorConfig[factor] || { label: factor, icon: '❓' };
const handleSort = (field) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('desc');
}
setCurrentPage(1);
};
const SortIcon = ({ field }) => {
if (sortField !== field) return html`<span class="text-slate-300 ml-1">↕</span>`;
return html`<span class="text-amber-500 ml-1">${sortOrder === 'asc' ? '↑' : '↓'}</span>`;
};
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-200">
<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-600 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-amber-200">
<div class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl flex items-center justify-center text-3xl animate-pulse">📦</div>
<h3 class="text-lg font-bold text-amber-600 mb-2">加载中</h3>
<p class="text-sm text-slate-400">等待订单数据...</p>
</div>
</div>`;
const { warningStats, riskStats, customerStats, totalOrders, totalAmount, avgDelayProb, highRiskCount } = processedData;
return html`
<div class="page-bg p-4 sm:p-6">
<div class="max-w-7xl mx-auto space-y-5">
<!-- 头部 -->
<div class="glass rounded-2xl p-5 card-shadow 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-amber-500 via-orange-500 to-red-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-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">
${warningStats.RED > 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>
${warningStats.RED} 个高风险
</span>
`}
${warningStats.YELLOW > 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>
${warningStats.YELLOW} 个中风险
</span>
`}
</div>
</div>
</div>
<!-- 核心指标卡片 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
${[
{ icon: '📦', label: '订单总数', value: totalOrders, unit: '单', color: 'from-blue-500 to-indigo-500', shadow: 'shadow-blue-200/50' },
{ icon: '💰', label: '订单总额', value: formatWan(totalAmount), unit: '万', color: 'from-emerald-500 to-teal-500', shadow: 'shadow-emerald-200/50' },
{ icon: '📊', label: '平均延迟概率', value: avgDelayProb.toFixed(1), unit: '%', color: 'from-purple-500 to-pink-500', shadow: 'shadow-purple-200/50' },
{ icon: '⚠️', label: '风险订单', value: highRiskCount, unit: '单', color: highRiskCount > 0 ? 'from-red-500 to-rose-500' : 'from-slate-400 to-slate-500', shadow: highRiskCount > 0 ? 'shadow-red-200/50' : 'shadow-slate-200/50' }
].map((item, i) => html`
<div key=${i} class="glass rounded-2xl p-5 card-shadow border border-white/60 hover:shadow-lg transition-all animate-slide-up" style=${{ animationDelay: i * 80 + 'ms' }}>
<div class="flex items-center justify-between mb-3">
<span class="w-11 h-11 bg-gradient-to-br ${item.color} rounded-xl flex items-center justify-center text-lg shadow-md ${item.shadow}">${item.icon}</span>
</div>
<p class="text-3xl font-black text-slate-800">${item.value}<span class="text-sm font-normal text-slate-400 ml-1">${item.unit}</span></p>
<p class="text-xs text-slate-500 mt-1">${item.label}</p>
</div>
`)}
</div>
<!-- 预警等级分布卡片 -->
<div class="grid grid-cols-3 gap-4">
${[
{ level: 'RED', count: warningStats.RED },
{ level: 'YELLOW', count: warningStats.YELLOW },
{ level: 'GREEN', count: warningStats.GREEN }
].map((item, i) => {
const wc = getWarning(item.level);
const pct = totalOrders > 0 ? (item.count / totalOrders * 100).toFixed(1) : 0;
return html`
<div key=${item.level} class="glass rounded-2xl p-5 card-shadow border ${wc.border} hover:shadow-lg transition-all animate-slide-up" style=${{ animationDelay: i * 60 + 'ms' }}>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<span class="w-10 h-10 bg-gradient-to-br ${wc.gradient} rounded-xl flex items-center justify-center text-lg shadow-md">${wc.icon}</span>
<span class="font-bold ${wc.text}">${wc.label}</span>
</div>
<span class="text-2xl font-black ${wc.text}">${item.count}</span>
</div>
<div class="h-2 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r ${wc.gradient} rounded-full transition-all" style=${{ width: pct + '%' }}></div>
</div>
<p class="text-xs text-slate-400 mt-2 text-right">${pct}%</p>
</div>
`;
})}
</div>
<!-- 图表区域 -->
<div class="grid lg:grid-cols-3 gap-4">
<!-- 预警分布 -->
<div class="glass rounded-2xl card-shadow border border-white/60 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-100 flex items-center gap-3">
<span class="w-8 h-8 bg-gradient-to-br from-amber-500 to-orange-500 rounded-lg flex items-center justify-center text-white text-sm">📊</span>
<h3 class="text-sm font-bold text-slate-700">预警等级分布</h3>
</div>
<div class="p-4">
${Object.values(warningStats).some(v => v > 0)
? html`<div ref=${chartRef1} class="w-full"></div>`
: html`<${EmptyState} icon="📊" title="暂无预警数据" desc="等待订单数据加载" />`
}
</div>
</div>
<!-- 风险因素分布 -->
<div class="glass rounded-2xl card-shadow border border-white/60 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-100 flex items-center gap-3">
<span class="w-8 h-8 bg-gradient-to-br from-rose-500 to-pink-500 rounded-lg flex items-center justify-center text-white text-sm">⚠️</span>
<h3 class="text-sm font-bold text-slate-700">风险因素分布</h3>
</div>
<div class="p-4">
${Object.keys(riskStats).length > 0
? html`<div ref=${chartRef2} class="w-full"></div>`
: html`<${EmptyState} icon="⚠️" title="暂无风险数据" desc="等待风险因素数据加载" />`
}
</div>
</div>
<!-- 客户订单金额TOP -->
<div class="glass rounded-2xl card-shadow border border-white/60 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-100 flex items-center gap-3">
<span class="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center text-white text-sm">👥</span>
<h3 class="text-sm font-bold text-slate-700">客户订单金额TOP</h3>
</div>
<div class="p-4">
${Object.keys(customerStats).length > 0
? html`<div ref=${chartRef3} class="w-full"></div>`
: html`<${EmptyState} icon="👥" title="暂无客户数据" desc="等待客户数据加载" />`
}
</div>
</div>
</div>
<!-- 订单明细表格 -->
<div class="glass rounded-2xl card-shadow border border-white/60 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-8 h-8 bg-gradient-to-br from-amber-500 to-orange-500 rounded-lg flex items-center justify-center text-white text-sm">📋</span>
<h3 class="text-sm font-bold text-slate-700">订单延迟预警明细</h3>
</div>
<span class="px-3 py-1 bg-amber-50 text-amber-600 rounded-full text-xs font-medium border border-amber-200">${totalOrders} 条记录</span>
</div>
${sortedOrders.length > 0 ? html`
<div class="table-scroll">
<table class="w-full text-sm">
<thead class="bg-slate-50 sticky top-0 z-10">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('order_number')}>
订单号<${SortIcon} field="order_number" />
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('customer_name')}>
客户<${SortIcon} field="customer_name" />
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('order_date')}>
订单日期<${SortIcon} field="order_date" />
</th>
<th class="px-4 py-3 text-right text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('order_amount')}>
金额<${SortIcon} field="order_amount" />
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500">生产/物流</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500">缺陷/报废率</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('delay_probability_pct')}>
延迟概率<${SortIcon} field="delay_probability_pct" />
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500 cursor-pointer hover:text-amber-600" onClick=${() => handleSort('warning_level')}>
预警等级<${SortIcon} field="warning_level" />
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-slate-500">风险因素</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${pagedOrders.map((o, i) => {
const wc = getWarning(o.warning_level);
const rf = getRiskFactor(o.primary_risk_factor);
return html`
<tr key=${o.order_number || i} class="hover:bg-amber-50/30 transition-colors animate-fade-in" style=${{ animationDelay: i * 30 + 'ms' }}>
<td class="px-4 py-3">
<span class="font-semibold text-slate-800">${o.order_number || '-'}</span>
</td>
<td class="px-4 py-3">
<p class="font-medium text-slate-700 truncate max-w-[160px]" title=${o.customer_name}>${o.customer_name || '-'}</p>
</td>
<td class="px-4 py-3 text-center text-slate-600">${o.order_date || '-'}</td>
<td class="px-4 py-3 text-right font-bold text-slate-800">${formatMoney(o.order_amount)}</td>
<td class="px-4 py-3 text-center">
<div class="flex flex-col items-center gap-0.5">
<span class="text-xs text-slate-500">🏭 ${o.avg_production_days}天</span>
<span class="text-xs text-slate-500">🚚 ${o.avg_logistics_delay_days}天</span>
</div>
</td>
<td class="px-4 py-3 text-center">
<div class="flex flex-col items-center gap-0.5">
<span class="text-xs ${o.defect_rate_pct > 5 ? 'text-red-500 font-bold' : 'text-slate-500'}">缺陷 ${o.defect_rate_pct.toFixed(1)}%</span>
<span class="text-xs ${o.scrap_rate_pct > 10 ? 'text-red-500 font-bold' : 'text-slate-500'}">报废 ${o.scrap_rate_pct.toFixed(1)}%</span>
</div>
</td>
<td class="px-4 py-3 text-center">
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold ${o.delay_probability_pct >= 50 ? 'bg-red-100 text-red-600' : o.delay_probability_pct >= 20 ? 'bg-amber-100 text-amber-600' : 'bg-emerald-100 text-emerald-600'}">
${o.delay_probability_pct.toFixed(1)}%
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold ${wc.light} ${wc.text} border ${wc.border}">
${wc.icon} ${wc.label}
</span>
</td>
<td class="px-4 py-3 text-center">
<span class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 text-slate-600 rounded-lg text-xs" title=${rf.label}>
${rf.icon} ${rf.label}
</span>
</td>
</tr>
`;
})}
</tbody>
</table>
</div>
<${Pagination} current=${currentPage} total=${sortedOrders.length} pageSize=${pageSize} onChange=${setCurrentPage} />
` : html`<${EmptyState} icon="📋" title="暂无订单数据" desc="等待订单数据加载" />`}
</div>
<!-- 客户风险汇总 -->
${Object.keys(customerStats).length > 0 && html`
<div class="glass rounded-2xl card-shadow border border-white/60 p-5">
<div class="flex items-center gap-3 mb-4">
<span class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-lg flex items-center justify-center text-white text-sm">👥</span>
<h3 class="text-sm font-bold text-slate-700">客户订单风险汇总</h3>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
${Object.entries(customerStats)
.sort((a, b) => b[1].amount - a[1].amount)
.slice(0, 10)
.map(([name, stats], i) => html`
<div key=${name} class="bg-white rounded-xl p-4 border border-slate-100 hover:shadow-md transition-all animate-slide-up" style=${{ animationDelay: i * 40 + 'ms' }}>
<p class="text-xs text-slate-500 mb-1 truncate" title=${name}>${name}</p>
<p class="text-lg font-black text-slate-800">¥${formatWan(stats.amount)}<span class="text-xs text-slate-400">万</span></p>
<div class="flex items-center gap-2 mt-2">
<span class="text-xs text-slate-400">${stats.count}单</span>
${stats.redCount > 0 && html`<span class="px-1.5 py-0.5 bg-red-100 text-red-600 rounded text-xs font-bold">${stats.redCount}高</span>`}
${stats.yellowCount > 0 && html`<span class="px-1.5 py-0.5 bg-amber-100 text-amber-600 rounded text-xs font-bold">${stats.yellowCount}中</span>`}
</div>
</div>
`)}
</div>
</div>
`}
<!-- 底部 -->
<div class="text-center pt-3 pb-4">
<p class="text-xs text-slate-500">
<span class="inline-flex items-center gap-1.5 px-4 py-2 glass rounded-full border border-white/60 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>

View File

@@ -0,0 +1,680 @@
<!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 shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
.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-shake { animation: shake 0.5s ease-in-out infinite; }
.glass { backdrop-filter: blur(16px); background: rgba(255,255,255,0.85); }
.gradient-text { background: linear-gradient(135deg, #ef4444 0%, #f97316 50%, #eab308 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.page-bg { background: linear-gradient(135deg, #fef2f2 0%, #fff7ed 30%, #fefce8 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; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 50; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.2s ease-out; }
.modal-content { background: white; border-radius: 1.5rem; max-width: 90vw; max-height: 85vh; overflow: hidden; animation: scaleIn 0.3s ease-out; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
.clickable-row { cursor: pointer; transition: all 0.2s; }
.clickable-row:hover { background: #f8fafc !important; transform: translateX(2px); }
.clickable-card { cursor: pointer; transition: all 0.2s; }
.clickable-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(0,0,0,0.1); }
</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('[SupplyChainRiskWarning] 库加载失败:', error); }
});
function initReactApp() {
const { useState, useEffect, useMemo, useRef } = React;
const html = htm.bind(React.createElement);
const config = { enableLog: true, title: '供应链风险预警' };
const lzwcaiComInitDate = '{{lzwcaiComInitDate}}';
const defaultData = { success: false, data: [[], [], [], [], [], []] };
// 风险等级配置
const riskLevelConfig = {
'HIGH': { label: '高风险', bg: 'from-red-500 to-rose-600', light: 'bg-red-50', text: 'text-red-700', border: 'border-red-200', icon: '🔴', dot: 'bg-red-500' },
'MEDIUM': { label: '中风险', bg: 'from-amber-400 to-orange-500', light: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200', icon: '🟡', dot: 'bg-amber-500' },
'LOW': { label: '低风险', bg: 'from-emerald-400 to-teal-500', light: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200', icon: '🟢', dot: 'bg-emerald-500' }
};
// 风险模式配置
const riskPatternConfig = {
'DELIVERY_AND_QUALITY': { label: '交期+质量双风险', icon: '⚠️', color: 'text-red-600', bg: 'bg-red-50' },
'DELIVERY_ISSUE': { label: '交期异常', icon: '⏰', color: 'text-orange-600', bg: 'bg-orange-50' },
'QUALITY_ISSUE': { label: '质量问题', icon: '🔍', color: 'text-purple-600', bg: 'bg-purple-50' },
'LOGISTICS_STALL': { label: '物流停滞', icon: '🚚', color: 'text-blue-600', bg: 'bg-blue-50' },
'NORMAL': { label: '正常', icon: '✅', color: 'text-emerald-600', bg: 'bg-emerald-50' }
};
// 行动要求配置
const actionConfig = {
'IMMEDIATE_FOLLOWUP': { label: '立即跟进', icon: '🚨', color: 'text-red-600', bg: 'bg-red-100' },
'MONITOR': { label: '持续监控', icon: '👁️', color: 'text-amber-600', bg: 'bg-amber-100' },
'NORMAL': { label: '正常', icon: '✓', color: 'text-emerald-600', bg: 'bg-emerald-100' }
};
// 风险评估配置
const assessmentConfig = {
'HIGH_RISK': { label: '高风险', color: 'text-red-600', bg: 'bg-red-50' },
'MEDIUM_RISK': { label: '中风险', color: 'text-amber-600', bg: 'bg-amber-50' },
'EXCELLENT': { label: '优秀', color: 'text-emerald-600', bg: 'bg-emerald-50' }
};
// 空状态组件
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 Modal = ({ isOpen, onClose, title, icon, children }) => {
if (!isOpen) return null;
return html`
<div class="modal-overlay" onClick=${onClose}>
<div class="modal-content w-full max-w-2xl" onClick=${e => e.stopPropagation()}>
<div class="px-6 py-4 border-b border-slate-100 bg-gradient-to-r from-slate-50 to-white flex items-center justify-between">
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<span class="text-xl">${icon}</span>
${title}
</h3>
<button onClick=${onClose} class="w-8 h-8 rounded-full bg-slate-100 hover:bg-slate-200 flex items-center justify-center text-slate-500 hover:text-slate-700 transition-colors"></button>
</div>
<div class="p-6 max-h-[60vh] overflow-y-auto scrollbar-thin">
${children}
</div>
</div>
</div>
`;
};
function ChartApp() {
const [rawData, setRawData] = useState(defaultData);
const [pageHelper, setPageHelper] = useState(null);
const [error, setError] = useState(null);
const [supplierPage, setSupplierPage] = useState(1);
const [orderPage, setOrderPage] = useState(1);
const [filterRisk, setFilterRisk] = useState('all');
const [filterPattern, setFilterPattern] = useState('all');
// 弹窗状态
const [modalData, setModalData] = useState(null);
const [modalType, setModalType] = useState(null);
const pageSize = 10;
const chartRef1 = useRef(null);
const chartRef3 = useRef(null);
const chartInstance1 = 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 openModal = (type, data) => {
setModalType(type);
setModalData(data);
};
// 关闭弹窗
const closeModal = () => {
setModalType(null);
setModalData(null);
};
// 处理数据
const processedData = useMemo(() => {
if (!rawData.data || rawData.data.length < 6) return null;
const [suppliers = [], highRiskOrders = [], categoryRisk = [], monthlyTrend = [], metrics = [], riskRanking = []] = rawData.data;
const metricsMap = {};
metrics.forEach(m => { metricsMap[m.metric_name] = m; });
const riskDistribution = { HIGH: 0, MEDIUM: 0, LOW: 0 };
suppliers.forEach(s => {
if (riskDistribution[s.risk_level] !== undefined) riskDistribution[s.risk_level]++;
});
const patternStats = {};
suppliers.forEach(s => {
const pattern = s.risk_pattern || 'NORMAL';
patternStats[pattern] = (patternStats[pattern] || 0) + 1;
});
const categoryStats = categoryRisk.map(c => ({
...c,
supplier_count: parseInt(c.supplier_count || 0),
order_count: parseInt(c.order_count || 0),
return_count: parseInt(c.return_count || 0)
}));
return { suppliers, highRiskOrders, categoryRisk: categoryStats, monthlyTrend, metrics: metricsMap, riskRanking, riskDistribution, patternStats };
}, [rawData]);
const filteredSuppliers = useMemo(() => {
if (!processedData) return [];
let list = processedData.suppliers;
if (filterRisk !== 'all') list = list.filter(s => s.risk_level === filterRisk);
if (filterPattern !== 'all') list = list.filter(s => s.risk_pattern === filterPattern);
return list;
}, [processedData, filterRisk, filterPattern]);
const paginatedSuppliers = useMemo(() => {
const start = (supplierPage - 1) * pageSize;
return filteredSuppliers.slice(start, start + pageSize);
}, [filteredSuppliers, supplierPage]);
const paginatedOrders = useMemo(() => {
if (!processedData) return [];
const start = (orderPage - 1) * pageSize;
return processedData.highRiskOrders.slice(start, start + pageSize);
}, [processedData, orderPage]);
const supplierTotalPages = Math.ceil(filteredSuppliers.length / pageSize);
const orderTotalPages = Math.ceil((processedData?.highRiskOrders?.length || 0) / pageSize);
// 渲染图表
useEffect(() => {
if (!processedData || !window.G2) return;
[chartInstance1, chartInstance3].forEach(ref => {
if (ref.current) { ref.current.destroy(); ref.current = null; }
});
if (chartRef1.current && Object.values(processedData.riskDistribution).some(v => v > 0)) {
const riskData = Object.entries(processedData.riskDistribution)
.filter(([_, v]) => v > 0)
.map(([level, count]) => ({ name: riskLevelConfig[level]?.label || level, value: count }));
chartInstance1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 180 });
chartInstance1.current.coordinate({ type: 'theta', innerRadius: 0.6 });
chartInstance1.current.interval()
.data(riskData)
.transform({ type: 'stackY' })
.encode('y', 'value').encode('color', 'name')
.scale('color', { range: ['#ef4444', '#f59e0b', '#10b981'] })
.style('stroke', '#fff').style('lineWidth', 2)
.label({ text: d => d.value > 0 ? `${d.name}\n${d.value}家` : '', position: 'outside', fontSize: 10 })
.legend(false)
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 家供应商' }] });
chartInstance1.current.render();
}
if (chartRef3.current && processedData.suppliers.length > 0) {
const topRiskSuppliers = [...processedData.suppliers]
.filter(s => s.total_risk_score > 0)
.sort((a, b) => b.total_risk_score - a.total_risk_score)
.slice(0, 6)
.map(s => ({ name: s.supplier_name.length > 4 ? s.supplier_name.slice(0, 4) + '..' : s.supplier_name, 风险分: s.total_risk_score, level: s.risk_level }));
if (topRiskSuppliers.length > 0) {
chartInstance3.current = new G2.Chart({ container: chartRef3.current, autoFit: true, height: 180 });
chartInstance3.current.interval()
.data(topRiskSuppliers)
.encode('x', 'name').encode('y', '风险分').encode('color', 'level')
.scale('color', { domain: ['HIGH', 'MEDIUM', 'LOW'], range: ['#ef4444', '#f59e0b', '#10b981'] })
.scale('y', { domain: [0, 100] })
.style('radius', 6)
.axis('x', { labelFontSize: 10 })
.axis('y', { title: false })
.legend(false)
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 分' }] });
chartInstance3.current.render();
}
}
return () => {
[chartInstance1, 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 || 0).toFixed(1) + '%';
const getRisk = level => riskLevelConfig[level] || riskLevelConfig['LOW'];
const getPattern = pattern => riskPatternConfig[pattern] || riskPatternConfig['NORMAL'];
const getAction = action => actionConfig[action] || actionConfig['NORMAL'];
const getAssessment = assessment => assessmentConfig[assessment] || assessmentConfig['EXCELLENT'];
// 订单详情弹窗内容
const renderOrderDetail = (order) => {
const rc = getRisk(order.risk_level);
const ac = getAction(order.action_required);
const pc = getPattern(order.risk_pattern);
return html`
<div class="space-y-4">
<div class="flex items-center gap-3 p-4 rounded-xl ${order.risk_level === 'HIGH' ? 'bg-red-50' : 'bg-amber-50'}">
<span class="text-3xl">${rc.icon}</span>
<div>
<p class="text-xl font-bold text-slate-800">${order.purchase_order_number}</p>
<p class="text-sm text-slate-600">${order.supplier_name}</p>
</div>
<span class="ml-auto px-3 py-1.5 ${ac.bg} rounded-lg text-sm font-medium ${ac.color}">${ac.icon} ${ac.label}</span>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">供应商类别</p>
<p class="font-semibold text-slate-800">${order.supplier_category || '-'}</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">风险等级</p>
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm font-medium ${rc.light} ${rc.text}">${rc.icon} ${rc.label}</span>
</div>
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">下单日期</p>
<p class="font-semibold text-slate-800">${order.order_date || '-'}</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">已等待天数</p>
<p class="font-semibold ${order.days_since_order > 30 ? 'text-red-600' : 'text-slate-800'}">${order.days_since_order} 天</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">质量合格率</p>
<p class="font-semibold ${order.quality_rate < 80 ? 'text-red-600' : order.quality_rate < 95 ? 'text-amber-600' : 'text-emerald-600'}">${formatPercent(order.quality_rate)}</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl">
<p class="text-xs text-slate-500 mb-1">风险模式</p>
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm font-medium ${pc.bg} ${pc.color}">${pc.icon} ${pc.label}</span>
</div>
</div>
${order.risk_factors && html`
<div class="p-4 bg-orange-50 rounded-xl border border-orange-100">
<p class="text-sm font-semibold text-orange-700 mb-2">⚠️ 风险因素</p>
<p class="text-sm text-orange-600">${order.risk_factors}</p>
</div>
`}
</div>
`;
};
// 供应商详情弹窗内容
const renderSupplierDetail = (s) => {
const rc = getRisk(s.risk_level);
const pc = getPattern(s.risk_pattern);
return html`
<div class="space-y-4">
<div class="flex items-center gap-3 p-4 rounded-xl ${s.risk_level === 'HIGH' ? 'bg-red-50' : s.risk_level === 'MEDIUM' ? 'bg-amber-50' : 'bg-emerald-50'}">
<span class="text-3xl">${rc.icon}</span>
<div class="flex-1">
<p class="text-xl font-bold text-slate-800">${s.supplier_name}</p>
<p class="text-sm text-slate-600">${s.supplier_category}</p>
</div>
<div class="text-center">
<span class="inline-flex items-center justify-center w-14 h-14 rounded-full text-xl font-bold ${s.total_risk_score >= 80 ? 'bg-red-100 text-red-700' : s.total_risk_score >= 50 ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'}">
${s.total_risk_score}
</span>
<p class="text-xs text-slate-500 mt-1">风险评分</p>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-blue-600">${s.order_count}</p>
<p class="text-xs text-slate-500">订单数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-emerald-600">${s.receipt_count}</p>
<p class="text-xs text-slate-500">收货数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-red-600">${s.return_count || 0}</p>
<p class="text-xs text-slate-500">退货数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold ${s.avg_delivery_days > 100 ? 'text-red-600' : 'text-slate-700'}">${s.avg_delivery_days}</p>
<p class="text-xs text-slate-500">平均交期(天)</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold ${s.quality_rate < 80 ? 'text-red-600' : s.quality_rate < 95 ? 'text-amber-600' : 'text-emerald-600'}">${formatPercent(s.quality_rate)}</p>
<p class="text-xs text-slate-500">质量合格率</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm font-medium ${pc.bg} ${pc.color}">${pc.icon} ${pc.label}</span>
<p class="text-xs text-slate-500 mt-1">风险模式</p>
</div>
</div>
<div class="flex items-center gap-2 p-3 rounded-xl ${rc.light} ${rc.border} border">
<span class="text-lg">${rc.icon}</span>
<span class="font-medium ${rc.text}">风险等级: ${rc.label}</span>
</div>
</div>
`;
};
// 排名详情弹窗内容
const renderRankingDetail = (r) => {
const ac = getAssessment(r.risk_assessment);
return html`
<div class="space-y-4">
<div class="flex items-center gap-3 p-4 rounded-xl bg-gradient-to-r from-amber-50 to-orange-50">
<span class="w-12 h-12 flex items-center justify-center rounded-xl text-xl font-bold ${r.risk_rank <= 3 ? 'bg-gradient-to-br from-amber-400 to-orange-500 text-white' : 'bg-slate-100 text-slate-600'}">${r.risk_rank}</span>
<div class="flex-1">
<p class="text-xl font-bold text-slate-800">${r.supplier_name}</p>
<p class="text-sm text-slate-600">${r.supplier_category}</p>
</div>
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${ac.bg} ${ac.color}">${ac.label}</span>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-blue-600">${r.order_count || 0}</p>
<p class="text-xs text-slate-500">订单数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-emerald-600">${r.receipt_count || 0}</p>
<p class="text-xs text-slate-500">收货数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold text-red-600">${r.return_count || 0}</p>
<p class="text-xs text-slate-500">退货数</p>
</div>
<div class="p-3 bg-slate-50 rounded-xl text-center">
<p class="text-2xl font-bold ${r.return_rate > 20 ? 'text-red-600' : r.return_rate > 10 ? 'text-amber-600' : 'text-emerald-600'}">${formatPercent(r.return_rate)}</p>
<p class="text-xs text-slate-500">退货率</p>
</div>
</div>
</div>
`;
};
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-orange-100">
<div class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-orange-400 to-red-500 rounded-2xl flex items-center justify-center text-3xl animate-pulse">🔗</div>
<h3 class="text-lg font-bold text-orange-600 mb-2">加载中</h3>
<p class="text-sm text-slate-400">等待供应链数据...</p>
</div>
</div>`;
const { suppliers, highRiskOrders, categoryRisk, monthlyTrend, metrics, riskRanking, riskDistribution, patternStats } = processedData;
return html`
<div class="page-bg p-4 sm:p-5">
<div class="max-w-7xl mx-auto space-y-4">
<!-- 弹窗 -->
<${Modal} isOpen=${modalType === 'order'} onClose=${closeModal} title="订单详情" icon="📦">
${modalData && renderOrderDetail(modalData)}
<//>
<${Modal} isOpen=${modalType === 'supplier'} onClose=${closeModal} title="供应商详情" icon="🏭">
${modalData && renderSupplierDetail(modalData)}
<//>
<${Modal} isOpen=${modalType === 'ranking'} onClose=${closeModal} title="排名详情" icon="🏆">
${modalData && renderRankingDetail(modalData)}
<//>
<!-- 头部 -->
<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-red-500 via-orange-500 to-amber-500 rounded-2xl flex items-center justify-center shadow-lg shadow-red-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">
${(() => {
const highRisk = parseInt(metrics.high_risk_suppliers?.metric_value || 0);
const overdueOrders = parseInt(metrics.overdue_orders_30d?.metric_value || 0);
const recentReturns = parseInt(metrics.recent_returns_30d?.metric_value || 0);
return html`
${highRisk > 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>
${highRisk} 高风险供应商
</span>
`}
${overdueOrders > 0 && html`
<span class="px-4 py-2 bg-orange-50 border border-orange-200 rounded-xl text-sm font-bold text-orange-600 flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-orange-500 rounded-full"></span>
${overdueOrders} 超期订单
</span>
`}
${recentReturns > 0 && html`
<span class="px-4 py-2 bg-purple-50 border border-purple-200 rounded-xl text-sm font-bold text-purple-600 flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-purple-500 rounded-full"></span>
${recentReturns} 近期退货
</span>
`}
`;
})()}
</div>
</div>
</div>
<!-- 核心指标卡片 -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
${[
{ key: 'high_risk_suppliers', icon: '🔴', label: '高风险供应商', color: 'from-red-500 to-rose-600', shadow: 'shadow-red-200/50', alert: true },
{ key: 'medium_risk_suppliers', icon: '🟡', label: '中风险供应商', color: 'from-amber-400 to-orange-500', shadow: 'shadow-amber-200/50' },
{ key: 'low_risk_suppliers', icon: '🟢', label: '低风险供应商', color: 'from-emerald-400 to-teal-500', shadow: 'shadow-emerald-200/50' },
{ key: 'pending_orders', icon: '📦', label: '待收货订单', color: 'from-blue-400 to-indigo-500', shadow: 'shadow-blue-200/50' },
{ key: 'overdue_orders_30d', icon: '⏰', label: '超期订单(30天)', color: 'from-orange-400 to-red-500', shadow: 'shadow-orange-200/50', alert: true },
{ key: 'recent_returns_30d', icon: '↩️', label: '近期退货(30天)', color: 'from-purple-400 to-pink-500', shadow: 'shadow-purple-200/50' }
].map((item, i) => {
const m = metrics[item.key] || {};
const isAlert = item.alert && parseInt(m.metric_value || 0) > 0;
const status = m.status;
return html`
<div key=${i} class="bg-white rounded-xl p-4 shadow-md border border-slate-100/80 hover:shadow-lg transition-all animate-slide-in ${isAlert ? 'ring-2 ring-red-200 bg-red-50/30' : ''}" style=${{ animationDelay: i * 50 + 'ms' }}>
<div class="flex items-center justify-between mb-2">
<span class="w-10 h-10 bg-gradient-to-br ${item.color} rounded-xl flex items-center justify-center text-base shadow-md ${item.shadow} ${isAlert ? 'animate-shake' : ''}">${item.icon}</span>
${status && status !== 'NORMAL' && html`
<span class="px-1.5 py-0.5 text-xs rounded ${status === 'ATTENTION_NEEDED' ? 'bg-red-100 text-red-600' : status === 'DELIVERY_WARNING' ? 'bg-orange-100 text-orange-600' : 'bg-slate-100 text-slate-600'}">
${status === 'ATTENTION_NEEDED' ? '需关注' : status === 'DELIVERY_WARNING' ? '交期预警' : status}
</span>
`}
</div>
<p class="text-2xl font-black ${isAlert ? 'text-red-600' : 'text-slate-800'}">${m.metric_value || '0'}</p>
<p class="text-xs text-slate-500 mt-0.5">${item.label}</p>
</div>
`;
})}
</div>
<!-- 图表区域 -->
<div class="grid lg:grid-cols-4 gap-4">
<!-- 风险分布 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<h3 class="text-sm font-bold text-slate-700 mb-3 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">📊</span>
风险等级分布
</h3>
${Object.values(riskDistribution).some(v => v > 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">
<h3 class="text-sm font-bold text-slate-700 mb-3 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg flex items-center justify-center text-white text-xs">📈</span>
类别质量合格率
</h3>
${categoryRisk.length > 0 ? html`
<div class="space-y-2 max-h-[180px] overflow-y-auto scrollbar-thin pr-1">
${[...categoryRisk].sort((a, b) => a.quality_rate - b.quality_rate).slice(0, 6).map((c, i) => {
const rc = getRisk(c.category_risk_level);
return html`
<div key=${i} class="flex items-center gap-2 p-2 rounded-lg ${c.category_risk_level === 'HIGH' ? 'bg-red-50' : 'bg-slate-50'} hover:shadow-sm transition-all">
<span class="w-2 h-2 rounded-full ${rc.dot}"></span>
<span class="flex-1 text-xs font-medium text-slate-700 truncate">${c.supplier_category}</span>
<div class="flex items-center gap-2 shrink-0">
<div class="w-16 h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all ${c.quality_rate < 50 ? 'bg-red-500' : c.quality_rate < 80 ? 'bg-orange-500' : c.quality_rate < 95 ? 'bg-amber-500' : 'bg-emerald-500'}" style=${{ width: Math.min(c.quality_rate, 100) + '%' }}></div>
</div>
<span class="text-xs font-bold w-12 text-right ${c.quality_rate < 50 ? 'text-red-600' : c.quality_rate < 80 ? 'text-orange-600' : c.quality_rate < 95 ? 'text-amber-600' : 'text-emerald-600'}">${formatPercent(c.quality_rate)}</span>
</div>
</div>
`;
})}
</div>
` : html`<${EmptyState} icon="📈" title="暂无类别数据" desc="等待数据加载" />`}
</div>
<!-- 风险评分TOP -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<h3 class="text-sm font-bold text-slate-700 mb-3 flex items-center gap-2">
<span class="w-6 h-6 bg-gradient-to-br from-purple-400 to-pink-500 rounded-lg flex items-center justify-center text-white text-xs"></span>
风险评分TOP
</h3>
${suppliers.filter(s => s.total_risk_score > 0).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">
<h3 class="text-sm font-bold text-slate-700 mb-3 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>
${monthlyTrend.length > 0 ? html`
<div class="space-y-2">
${monthlyTrend.map((t, i) => html`
<div key=${i} class="p-3 rounded-xl bg-gradient-to-r from-slate-50 to-white border border-slate-100">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-bold text-slate-700">${t.month_start?.slice(0, 7) || '未知'}</span>
<span class="px-2 py-0.5 rounded text-xs font-medium ${t.return_rate > 20 ? 'bg-red-100 text-red-600' : t.return_rate > 10 ? 'bg-amber-100 text-amber-600' : 'bg-emerald-100 text-emerald-600'}">
退货率 ${formatPercent(t.return_rate)}
</span>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="text-center p-1.5 bg-blue-50 rounded">
<p class="text-blue-600 font-bold">${t.receipt_count}</p>
<p class="text-slate-500">收货数</p>
</div>
<div class="text-center p-1.5 bg-orange-50 rounded">
<p class="text-orange-600 font-bold">${t.avg_delivery_days?.toFixed(0) || 0}天</p>
<p class="text-slate-500">平均交期</p>
</div>
<div class="text-center p-1.5 bg-red-50 rounded">
<p class="text-red-600 font-bold">${t.return_count}</p>
<p class="text-slate-500">退货数</p>
</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-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-blink">🚨</span>
高风险订单预警
<span class="text-xs text-slate-400 font-normal ml-2">点击卡片查看详情</span>
</h3>
<span class="px-2 py-1 bg-red-50 text-red-600 rounded-full text-xs font-medium">${highRiskOrders.length} 项</span>
</div>
${highRiskOrders.length > 0 ? html`
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 max-h-[280px] overflow-y-auto scrollbar-thin pr-1">
${paginatedOrders.map((order, i) => {
const rc = getRisk(order.risk_level);
const ac = getAction(order.action_required);
return html`
<div key=${i} onClick=${() => openModal('order', order)}
class="clickable-card bg-gradient-to-r ${order.risk_level === 'HIGH' ? 'from-red-50 to-orange-50 border-red-100' : 'from-amber-50 to-yellow-50 border-amber-100'} rounded-xl p-3 border animate-slide-in"
style=${{ animationDelay: i * 60 + 'ms' }}>
<div class="flex items-center gap-2 mb-2">
<span class="text-lg">${rc.icon}</span>
<span class="font-bold text-slate-800 text-sm truncate flex-1">${order.purchase_order_number}</span>
</div>
<p class="text-xs text-slate-600 truncate mb-1">${order.supplier_name}</p>
<div class="flex items-center justify-between">
<span class="text-xs text-orange-600">已等 ${order.days_since_order} 天</span>
<span class="text-xs text-slate-400">点击详情 →</span>
</div>
</div>
`;
})}
</div>
${orderTotalPages > 1 && html`
<div class="mt-3 flex items-center justify-center gap-2">
<button onClick=${() => setOrderPage(p => Math.max(1, p - 1))} disabled=${orderPage === 1}
class="px-3 py-1 text-xs rounded-lg border border-slate-200 hover:bg-slate-50 disabled:opacity-50 transition-colors">上一页</button>
<span class="text-xs text-slate-500">${orderPage}/${orderTotalPages}</span>
<button onClick=${() => setOrderPage(p => Math.min(orderTotalPages, p + 1))} disabled=${orderPage === orderTotalPages}
class="px-3 py-1 text-xs rounded-lg border border-slate-200 hover:bg-slate-50 disabled:opacity-50 transition-colors">下一页</button>
</div>
`}
` : html`<${EmptyState} icon="✨" title="暂无高风险订单" desc="供应链运行正常" />`}
</div>

View File

@@ -0,0 +1,696 @@
<!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; } }
.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; }
.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; }
</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('[WorkOrderProgress] 库加载失败:', error); }
});
function initReactApp() {
const { useState, useEffect, useMemo, useRef } = React;
const html = htm.bind(React.createElement);
const config = { enableLog: true, title: '工单执行进度与异常节点' };
const lzwcaiComInitDate = '{{lzwcaiComInitDate}}';
const defaultData = { success: false, data: [[], [], [], [], [], []] };
// 状态映射配置 OPEN→PENDING, STARTED→IN_PROGRESS, CLOSED→COMPLETED
const statusMap = {
'OPEN': 'PENDING', 'STARTED': 'IN_PROGRESS', 'CLOSED': 'COMPLETED',
'PENDING': 'PENDING', 'IN_PROGRESS': 'IN_PROGRESS', 'COMPLETED': 'COMPLETED'
};
const statusConfig = {
'PENDING': { label: '待处理', bg: 'from-amber-400 to-orange-500', light: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200', icon: '⏳', dot: 'bg-amber-400' },
'IN_PROGRESS': { label: '进行中', bg: 'from-blue-400 to-indigo-500', light: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200', icon: '🔄', dot: 'bg-blue-500' },
'COMPLETED': { label: '已完成', bg: 'from-emerald-400 to-teal-500', light: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200', icon: '✅', dot: 'bg-emerald-500' }
};
const riskConfig = {
'HIGH': { label: '高风险', bg: 'from-red-500 to-rose-600', light: 'bg-red-50', text: 'text-red-700', border: 'border-red-200', icon: '🔴' },
'MEDIUM': { label: '中风险', bg: 'from-amber-400 to-orange-500', light: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200', icon: '🟡' },
'LOW': { label: '低风险', bg: 'from-blue-400 to-cyan-500', light: 'bg-blue-50', text: 'text-blue-600', border: 'border-blue-200', icon: '🔵' },
'NONE': { label: '无风险', bg: 'from-slate-300 to-slate-400', light: 'bg-slate-50', text: 'text-slate-600', border: 'border-slate-200', icon: '⚪' }
};
const anomalyConfig = {
'SCHEDULE_DELAY': { label: '进度延迟', icon: '⏰', action: '调整排期', color: 'text-red-600' },
'MATERIAL_SHORTAGE': { label: '物料短缺', icon: '📦', action: '补充物料', color: 'text-orange-600' },
'LABOR_STALLED': { label: '人力停滞', icon: '👷', action: '检查人力', color: 'text-amber-600' },
'EQUIPMENT_FAULT': { label: '设备故障', icon: '🔧', action: '维修设备', color: 'text-purple-600' },
'QUALITY_ISSUE': { label: '质量问题', icon: '🔍', action: '质量检查', color: 'text-pink-600' }
};
const actionConfig = {
'ADJUST_SCHEDULE': '调整排期', 'REPLENISH_MATERIAL': '补充物料', 'CHECK_LABOR': '检查人力',
'REPAIR_EQUIPMENT': '维修设备', 'QUALITY_CHECK': '质量检查'
};
// 空状态组件
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>
`;
function ChartApp() {
const [rawData, setRawData] = useState(defaultData);
const [pageHelper, setPageHelper] = useState(null);
const [error, setError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [filterStatus, setFilterStatus] = useState('all');
const [filterRisk, setFilterRisk] = useState('all');
const pageSize = 10;
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 mapStatus = (status) => statusMap[status] || status;
const processedData = useMemo(() => {
if (!rawData.data || rawData.data.length < 6) return null;
const [workOrders = [], statusSummary = [], anomalyNodes = [], categorySummary = [], timeline = [], metrics = []] = rawData.data;
// 映射工单状态
const mappedWorkOrders = workOrders.map(wo => ({
...wo,
status_name: mapStatus(wo.status_name)
}));
// 核心指标处理
const metricsMap = {};
metrics.forEach(m => { metricsMap[m.metric] = m; });
// 状态汇总处理
const statusStats = {};
statusSummary.forEach(s => {
const mapped = mapStatus(s.status_name);
if (!statusStats[mapped]) statusStats[mapped] = { count: 0, planned: 0, completed: 0, pct: 0 };
statusStats[mapped].count += parseInt(s.order_count || 0);
statusStats[mapped].planned += s.total_planned || 0;
statusStats[mapped].completed += s.total_completed || 0;
statusStats[mapped].pct += s.pct || 0;
});
// 异常节点处理
const anomalyList = anomalyNodes.map(a => ({
...a,
anomalyInfo: anomalyConfig[a.anomaly_type] || { label: a.anomaly_type, icon: '⚠️', action: '检查', color: 'text-slate-600' }
}));
// 按类别汇总
const categoryStats = categorySummary.map(c => ({
...c,
pending: parseInt(c.pending || 0),
in_progress: parseInt(c.in_progress || 0),
completed: parseInt(c.completed || 0)
}));
// 时间线处理
const timelineData = timeline.map(t => ({
...t,
status_name: mapStatus(t.status_name)
}));
// 风险分布统计
const riskStats = { HIGH: 0, MEDIUM: 0, LOW: 0, NONE: 0 };
mappedWorkOrders.forEach(wo => {
const risk = wo.risk_level || 'NONE';
if (riskStats[risk] !== undefined) riskStats[risk]++;
});
return {
workOrders: mappedWorkOrders,
statusSummary: statusStats,
anomalyNodes: anomalyList,
categorySummary: categoryStats,
timeline: timelineData,
metrics: metricsMap,
riskStats
};
}, [rawData]);
// 过滤后的工单列表
const filteredWorkOrders = useMemo(() => {
if (!processedData) return [];
let list = processedData.workOrders;
if (filterStatus !== 'all') list = list.filter(wo => wo.status_name === filterStatus);
if (filterRisk !== 'all') list = list.filter(wo => wo.risk_level === filterRisk);
return list;
}, [processedData, filterStatus, filterRisk]);
// 分页数据
const paginatedWorkOrders = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return filteredWorkOrders.slice(start, start + pageSize);
}, [filteredWorkOrders, currentPage]);
const totalPages = Math.ceil(filteredWorkOrders.length / pageSize);
// 渲染图表
useEffect(() => {
if (!processedData || !window.G2) return;
// 清理旧图表
[chartInstance1, chartInstance2, chartInstance3].forEach(ref => {
if (ref.current) { ref.current.destroy(); ref.current = null; }
});
// 状态分布环形图
if (chartRef1.current && Object.keys(processedData.statusSummary).length > 0) {
const statusData = Object.entries(processedData.statusSummary).map(([status, v]) => ({
name: statusConfig[status]?.label || status,
value: v.count
}));
chartInstance1.current = new G2.Chart({ container: chartRef1.current, autoFit: true, height: 200 });
chartInstance1.current.coordinate({ type: 'theta', innerRadius: 0.6 });
chartInstance1.current.interval()
.data(statusData)
.transform({ type: 'stackY' })
.encode('y', 'value').encode('color', 'name')
.scale('color', { range: ['#f59e0b', '#3b82f6', '#10b981'] })
.style('stroke', '#fff').style('lineWidth', 2)
.label({ text: d => d.value > 0 ? d.name : '', position: 'outside', fontSize: 10 })
.legend('color', { position: 'bottom', layout: { justifyContent: 'center' } })
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 单' }] });
chartInstance1.current.render();
}
// 类别完成率柱状图
if (chartRef2.current && processedData.categorySummary.length > 0) {
const categoryData = processedData.categorySummary.map(c => ({
name: c.product_category.length > 5 ? c.product_category.slice(0, 5) + '..' : c.product_category,
完成率: c.completion_rate
}));
chartInstance2.current = new G2.Chart({ container: chartRef2.current, autoFit: true, height: 200 });
chartInstance2.current.interval()
.data(categoryData)
.encode('x', 'name').encode('y', '完成率').encode('color', 'name')
.style('radius', 6)
.axis('x', { labelAutoRotate: true, labelFontSize: 10 })
.axis('y', { title: false, labelFormatter: v => v + '%' })
.legend(false)
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v.toFixed(1) + '%' }] });
chartInstance2.current.render();
}
// 风险分布饼图
if (chartRef3.current && Object.values(processedData.riskStats).some(v => v > 0)) {
const riskData = Object.entries(processedData.riskStats)
.filter(([_, v]) => v > 0)
.map(([risk, count]) => ({
name: riskConfig[risk]?.label || risk,
value: count
}));
chartInstance3.current = new G2.Chart({ container: chartRef3.current, autoFit: true, height: 200 });
chartInstance3.current.coordinate({ type: 'theta', innerRadius: 0.5 });
chartInstance3.current.interval()
.data(riskData)
.transform({ type: 'stackY' })
.encode('y', 'value').encode('color', 'name')
.scale('color', { range: ['#ef4444', '#f59e0b', '#3b82f6', '#94a3b8'] })
.style('stroke', '#fff').style('lineWidth', 2)
.label({ text: 'name', position: 'outside', fontSize: 10 })
.legend('color', { position: 'bottom', layout: { justifyContent: 'center' } })
.tooltip({ items: [{ channel: 'y', valueFormatter: v => v + ' 单' }] });
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 || 0).toFixed(1) + '%';
const getStatus = status => statusConfig[status] || statusConfig['PENDING'];
const getRisk = risk => riskConfig[risk] || riskConfig['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 { statusSummary, anomalyNodes, categorySummary, timeline, metrics, riskStats } = 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-blue-500 via-indigo-500 to-purple-600 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">
${(() => {
const anomalyCount = parseInt(metrics.anomaly_count?.value || 0);
const pendingCount = parseInt(metrics.pending?.value || 0);
const inProgressCount = parseInt(metrics.in_progress?.value || 0);
return html`
${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>
`}
${pendingCount > 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>
${pendingCount} 待处理
</span>
`}
${inProgressCount > 0 && html`
<span class="px-4 py-2 bg-blue-50 border border-blue-200 rounded-xl text-sm font-bold text-blue-600 flex items-center gap-2">
<span class="w-2.5 h-2.5 bg-blue-500 rounded-full"></span>
${inProgressCount} 进行中
</span>
`}
`;
})()}
</div>
</div>
</div>
<!-- 核心指标卡片 -->
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-3">
${[
{ key: 'total_orders', icon: '📦', label: '总工单', color: 'from-slate-500 to-slate-600', shadow: 'shadow-slate-200/50' },
{ key: 'pending', icon: '⏳', label: '待处理', color: 'from-amber-400 to-orange-500', shadow: 'shadow-amber-200/50' },
{ key: 'in_progress', icon: '🔄', label: '进行中', color: 'from-blue-400 to-indigo-500', shadow: 'shadow-blue-200/50' },
{ key: 'completed', icon: '✅', label: '已完成', color: 'from-emerald-400 to-teal-500', shadow: 'shadow-emerald-200/50' },
{ key: 'completion_rate', icon: '📊', label: '完成率', color: 'from-purple-400 to-pink-500', shadow: 'shadow-purple-200/50' },
{ key: 'anomaly_count', icon: '⚠️', label: '异常数', color: 'from-red-400 to-rose-500', shadow: 'shadow-red-200/50', alert: true },
{ key: 'today_completed', icon: '🎯', label: '今日完成', color: 'from-cyan-400 to-blue-500', shadow: 'shadow-cyan-200/50' }
].map((item, i) => {
const m = metrics[item.key] || {};
const isAlert = item.alert && parseInt(m.value || 0) > 0;
return html`
<div key=${i} class="bg-white rounded-xl p-4 shadow-md border border-slate-100/80 hover:shadow-lg transition-all animate-slide-in ${isAlert ? 'ring-2 ring-red-200 bg-red-50/30' : ''}" style=${{ animationDelay: i * 50 + 'ms' }}>
<div class="flex items-center justify-between mb-2">
<span class="w-10 h-10 bg-gradient-to-br ${item.color} rounded-xl flex items-center justify-center text-base shadow-md ${item.shadow} ${isAlert ? 'animate-pulse' : ''}">${item.icon}</span>
</div>
<p class="text-2xl font-black ${isAlert ? 'text-red-600' : 'text-slate-800'}">${m.value || '0'}</p>
<p class="text-xs text-slate-500 mt-0.5">${item.label}</p>
</div>
`;
})}
</div>
<!-- 主体内容区 -->
<div class="grid lg:grid-cols-3 gap-4">
<!-- 左侧:状态分布 + 风险分布 -->
<div class="space-y-4">
<!-- 状态分布 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<h3 class="text-sm font-bold text-slate-700 mb-3 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>
${Object.keys(statusSummary).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">
<h3 class="text-sm font-bold text-slate-700 mb-3 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">⚡</span>
风险分布
</h3>
${Object.values(riskStats).some(v => v > 0)
? html`<div ref=${chartRef3} class="w-full"></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-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-blink">🚨</span>
异常节点预警
</h3>
<span class="px-2 py-1 bg-red-50 text-red-600 rounded-full text-xs font-medium">${anomalyNodes.length} 项</span>
</div>
${anomalyNodes.length > 0 ? html`
<div class="space-y-2 max-h-[420px] overflow-y-auto scrollbar-thin pr-1">
${anomalyNodes.map((a, i) => html`
<div key=${i} class="bg-gradient-to-r from-red-50 to-orange-50 rounded-xl p-3 border border-red-100 hover:shadow-md transition-all 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">${a.anomalyInfo.icon}</span>
<span class="font-bold text-slate-800 text-sm truncate">${a.work_order_number}</span>
</div>
<p class="text-xs text-slate-600 truncate mb-2">${a.product_name}</p>
<div class="flex flex-wrap gap-1">
<span class="px-2 py-0.5 bg-white/80 rounded text-xs ${a.anomalyInfo.color} font-medium">${a.anomalyInfo.label}</span>
<span class="px-2 py-0.5 bg-white/80 rounded text-xs text-slate-500">${formatPercent(a.completion_rate)} 完成</span>
</div>
</div>
<div class="text-right shrink-0">
<p class="text-xs text-slate-400 mb-1">${a.elapsed_hours?.toFixed(1) || 0}h</p>
<span class="inline-flex items-center px-2 py-1 bg-gradient-to-r from-orange-400 to-red-500 text-white rounded-lg text-xs font-medium shadow-sm">
${actionConfig[a.action] || a.action}
</span>
</div>
</div>
</div>
`)}
</div>
` : html`<${EmptyState} icon="✨" title="暂无异常" desc="所有工单运行正常" />`}
</div>
<!-- 右侧:类别完成率 -->
<div class="bg-white rounded-2xl shadow-md border border-slate-100/80 p-4">
<h3 class="text-sm font-bold text-slate-700 mb-3 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>
${categorySummary.length > 0 ? html`
<div ref=${chartRef2} class="w-full mb-3"></div>
<div class="space-y-2 max-h-[200px] overflow-y-auto scrollbar-thin pr-1">
${categorySummary.map((c, i) => html`
<div key=${i} class="flex items-center justify-between p-2 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors">
<div class="flex items-center gap-2 min-w-0">
<span class="w-2 h-2 rounded-full bg-gradient-to-r from-indigo-400 to-purple-500"></span>
<span class="text-xs font-medium text-slate-700 truncate">${c.product_category}</span>
</div>
<div class="flex items-center gap-3 shrink-0">
<div class="flex gap-1 text-xs">
<span class="text-amber-600">${c.pending}</span>
<span class="text-slate-300">/</span>
<span class="text-blue-600">${c.in_progress}</span>
<span class="text-slate-300">/</span>
<span class="text-emerald-600">${c.completed}</span>
</div>
<span class="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded text-xs font-bold">${formatPercent(c.completion_rate)}</span>
</div>
</div>
`)}
</div>
` : html`<${EmptyState} icon="📈" title="暂无类别数据" desc="等待数据加载" />`}
</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-indigo-400 to-purple-500 rounded-lg flex items-center justify-center text-white text-xs">📋</span>
工单明细
</h3>
<div class="flex flex-wrap items-center gap-2">
<!-- 状态筛选 -->
<select value=${filterStatus} onChange=${e => { setFilterStatus(e.target.value); setCurrentPage(1); }}
class="px-3 py-1.5 text-xs border border-slate-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-indigo-200">
<option value="all">全部状态</option>
<option value="PENDING">待处理</option>
<option value="IN_PROGRESS">进行中</option>
<option value="COMPLETED">已完成</option>
</select>
<!-- 风险筛选 -->
<select value=${filterRisk} onChange=${e => { setFilterRisk(e.target.value); setCurrentPage(1); }}
class="px-3 py-1.5 text-xs border border-slate-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-indigo-200">
<option value="all">全部风险</option>
<option value="HIGH">高风险</option>
<option value="MEDIUM">中风险</option>
<option value="LOW">低风险</option>
<option value="NONE">无风险</option>
</select>
<span class="px-2 py-1 bg-indigo-50 text-indigo-600 rounded-full text-xs font-medium">${filteredWorkOrders.length} 条</span>
</div>
</div>
</div>
${filteredWorkOrders.length > 0 ? html`
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-slate-50">
<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-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-right text-xs font-semibold text-slate-500">工时</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${paginatedWorkOrders.map((wo, i) => {
const sc = getStatus(wo.status_name);
const rc = getRisk(wo.risk_level);
const hasAnomaly = wo.anomaly_flags && wo.anomaly_flags.length > 0;
const anomalyInfo = hasAnomaly ? (anomalyConfig[wo.anomaly_flags] || { label: wo.anomaly_flags, icon: '⚠️', color: 'text-slate-600' }) : null;
return html`
<tr key=${i} class="hover:bg-slate-50/50 transition-colors ${hasAnomaly ? 'bg-red-50/30' : ''}">
<td class="px-3 py-2.5">
<span class="font-semibold text-slate-800 text-sm">${wo.work_order_number}</span>
</td>
<td class="px-3 py-2.5">
<p class="text-slate-700 text-sm truncate max-w-[160px]">${wo.product_name}</p>
<p class="text-xs text-slate-400">${wo.product_category}</p>
</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 ${sc.light} ${sc.text}">
${sc.icon} ${sc.label}
</span>
</td>
<td class="px-3 py-2.5">
<div class="flex items-center gap-2">
<div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-indigo-400 to-purple-500 rounded-full transition-all" style=${{ width: Math.min(wo.completion_rate || 0, 100) + '%' }}></div>
</div>
<span class="text-xs font-bold text-slate-600 w-12 text-right">${formatPercent(wo.completion_rate)}</span>
</div>
</td>
<td class="px-3 py-2.5 text-center">
<span class="text-sm text-slate-700">${wo.completed_qty || 0}<span class="text-slate-400">/</span>${wo.planned_qty || 0}</span>
</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 ${rc.light} ${rc.text}">
${rc.icon} ${rc.label}
</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 rounded-lg text-xs font-medium ${anomalyInfo.color}">
${anomalyInfo.icon} ${anomalyInfo.label}
</span>
` : html`<span class="text-slate-400 text-xs">-</span>`}
</td>
<td class="px-3 py-2.5 text-right">
<span class="text-sm font-medium text-slate-600">${(wo.total_work_hours || 0).toFixed(1)}h</span>
</td>
</tr>
`;
})}
</tbody>
</table>
</div>
<!-- 分页 -->
${totalPages > 1 && html`
<div class="px-4 py-3 border-t border-slate-100 flex items-center justify-between">
<p class="text-xs text-slate-500">共 ${filteredWorkOrders.length} 条,第 ${currentPage}/${totalPages} 页</p>
<div class="flex items-center gap-1">
<button onClick=${() => setCurrentPage(p => Math.max(1, p - 1))} disabled=${currentPage === 1}
class="px-3 py-1.5 text-xs rounded-lg border border-slate-200 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">上一页</button>
${Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let page = i + 1;
if (totalPages > 5) {
if (currentPage <= 3) page = i + 1;
else if (currentPage >= totalPages - 2) page = totalPages - 4 + i;
else page = currentPage - 2 + i;
}
return html`
<button key=${page} onClick=${() => setCurrentPage(page)}
class="w-8 h-8 text-xs rounded-lg border transition-colors ${currentPage === page
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white border-transparent shadow-md'
: 'border-slate-200 hover:bg-slate-50'}">${page}</button>
`;
})}
<button onClick=${() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled=${currentPage === totalPages}
class="px-3 py-1.5 text-xs rounded-lg border border-slate-200 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">下一页</button>
</div>
</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-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">${timeline.length} 条记录</span>
</div>
${timeline.length > 0 ? html`
<div class="relative">
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-gradient-to-b from-indigo-200 via-purple-200 to-pink-200"></div>
<div class="space-y-3 max-h-[300px] overflow-y-auto scrollbar-thin pr-2">
${timeline.slice(0, 15).map((t, i) => {
const sc = getStatus(t.status_name);
return html`
<div key=${i} class="relative pl-10 animate-slide-in" style=${{ animationDelay: i * 40 + 'ms' }}>
<div class="absolute left-2.5 w-3 h-3 rounded-full ${sc.dot} ring-4 ring-white shadow-sm"></div>
<div class="bg-gradient-to-r from-slate-50 to-white rounded-xl p-3 border border-slate-100 hover:shadow-md transition-all">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-bold text-slate-800 text-sm">${t.work_order_number}</span>
<span class="px-1.5 py-0.5 rounded text-xs font-medium ${sc.light} ${sc.text}">${sc.label}</span>
</div>
<p class="text-xs text-slate-600 truncate">${t.product_name}</p>
</div>
<div class="text-right shrink-0">
<p class="text-xs text-slate-400">${t.start_time}</p>
<p class="text-xs font-medium text-indigo-600">${formatPercent(t.completion_rate)}</p>
</div>
</div>
<div class="mt-2 flex items-center gap-3 text-xs text-slate-500">
<span>计划: ${t.planned_qty}</span>
<span>完成: ${t.completed_qty}</span>
<span>耗时: ${(t.duration_hours || 0).toFixed(1)}h</span>
</div>
</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>

File diff suppressed because one or more lines are too long

View File

@@ -157,3 +157,52 @@
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-09 22:50:49 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-09 22:50:56 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-09 22:50:56 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-09 22:50:56 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-09 22:50:56 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-09 22:50:56 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-09 22:50:57 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-09 22:50:57 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:50:57 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:51:01 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-09 22:51:01 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:51:01 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:51:06 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-09 22:51:06 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:51:06 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:51:14 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-09 22:51:14 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:51:14 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:51:22 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-09 22:51:22 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:51:22 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 22:52:54 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-09 22:52:54 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 22:52:54 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 23:05:38 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-09 23:05:38 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 23:05:39 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 23:48:54 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-09 23:48:54 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 23:48:54 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-10 00:10:04 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: EfficiencyOutputLossDashboard
2026-01-10 00:10:04 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-10 00:10:04 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-10 00:26:28 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-10 00:26:28 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-10 00:26:28 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-10 00:47:45 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-10 00:47:45 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-10 00:47:46 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功

View File

@@ -128,7 +128,7 @@ class DataSourceAPIClient:
Exception: 请求失败时抛出
"""
try:
url = f"{self.base_url}/datasource/sqlExecutionLog/testSqlWithSchema"
url = f"{self.base_url}/datasource/sqlExecutionLog/testBatchSqlWithSchema"
# 构建请求头包含Content-Type
headers = self._get_headers()
@@ -154,22 +154,36 @@ class DataSourceAPIClient:
logger.info(f"测试SQL API调用成功")
logger.debug(f"响应数据: {json.dumps(result, ensure_ascii=False, indent=2)}")
# 处理返回数据结构: {code, data: {errorMessage, data}, msg}
# 处理返回数据结构: {code, data: [{errorMessage, data}, ...], msg}
# 检查外层 code
if result.get("code") != 200:
error_msg = result.get("msg", "接口返回错误")
logger.error(f"接口返回错误: code={result.get('code')}, msg={error_msg}")
raise Exception(error_msg)
# 检查内层 errorMessage
inner_data = result.get("data", {})
if inner_data.get("errorMessage"):
error_msg = inner_data.get("errorMessage")
# data 是一个数组,每个元素包含 errorMessage 和 data
data_list = result.get("data", [])
# 如果不是数组,兼容旧格式
if isinstance(data_list, dict):
if data_list.get("errorMessage"):
error_msg = data_list.get("errorMessage")
logger.error(f"接口业务错误: {error_msg}")
raise Exception(error_msg)
return data_list.get("data")
# 返回 data.data
return inner_data.get("data")
# 处理数组格式,提取每个 item 的 data 并组合
combined_data = []
for item in data_list:
if item.get("errorMessage"):
error_msg = item.get("errorMessage")
logger.error(f"接口业务错误: {error_msg}")
raise Exception(error_msg)
item_data = item.get("data", [])
combined_data.append(item_data)
# 返回组合后的数据 [data1, data2, ...]
return combined_data
except httpx.TimeoutException:
error_msg = f"测试SQL API请求超时: {url}"

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "lzwcai-mcpskills-mfg-data-agent"
version = "0.1.1"
version = "0.1.2"
description = "制造业数据智能体 - MCP server for manufacturing data intelligence with dynamic tool generation"
readme = "README.md"
requires-python = ">=3.13"

File diff suppressed because one or more lines are too long

View File

@@ -1,48 +0,0 @@
2026-01-09 18:27:40 - lzwcai_mcpskills_mfg_data_agentv2.utils.api_client - ERROR - [api_client.py:168] - 接口业务错误: Multiple ResultSets were returned by the query.
2026-01-09 18:27:40 - lzwcai_mcpskills_mfg_data_agentv2.utils.api_client - ERROR - [api_client.py:192] - 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
2026-01-09 18:27:40 - mcp_services - ERROR - [main.py:238] - 调用测试SQL API失败: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\main.py", line 226, in handle_call_tool
api_response = test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 251, in test_sql_with_schema
return default_client.test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 193, in test_sql_with_schema
raise Exception(error_msg)
Exception: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
2026-01-09 18:27:46 - lzwcai_mcpskills_mfg_data_agentv2.utils.api_client - ERROR - [api_client.py:168] - 接口业务错误: Multiple ResultSets were returned by the query.
2026-01-09 18:27:46 - lzwcai_mcpskills_mfg_data_agentv2.utils.api_client - ERROR - [api_client.py:192] - 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
2026-01-09 18:27:46 - mcp_services - ERROR - [main.py:238] - 调用测试SQL API失败: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\main.py", line 226, in handle_call_tool
api_response = test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 251, in test_sql_with_schema
return default_client.test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 193, in test_sql_with_schema
raise Exception(error_msg)
Exception: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.

View File

@@ -1,215 +1,117 @@
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-08 00:15:16 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-08 00:15:17 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-08 00:15:17 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-08 00:15:17 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-08 00:15:17 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-08 00:15:17 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-08 00:15:19 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-08 00:15:19 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:15:19 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:15:27 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 00:15:27 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:15:28 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:15:37 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-08 00:15:37 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:15:38 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:15:58 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: EfficiencyOutputLossDashboard
2026-01-08 00:15:58 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:15:59 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:16:07 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-08 00:16:07 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:16:07 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:16:13 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 00:16:13 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:16:13 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-08 00:30:22 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-08 00:30:26 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-08 00:30:26 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-08 00:30:26 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-08 00:30:26 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-08 00:30:26 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-08 00:30:27 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-08 00:30:27 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:27 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:32 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 00:30:32 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:32 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:34 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-08 00:30:34 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:34 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:35 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: EfficiencyOutputLossDashboard
2026-01-08 00:30:35 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:35 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:37 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-08 00:30:37 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:38 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:39 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-08 00:30:39 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:39 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:30:44 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-08 00:30:44 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:30:44 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-08 00:38:55 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-08 00:38:57 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-08 00:38:57 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-08 00:38:57 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-08 00:38:57 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-08 00:38:57 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-08 00:38:59 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-08 00:38:59 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:38:59 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-08 00:52:34 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-08 00:52:36 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-08 00:52:36 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-08 00:52:36 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-08 00:52:36 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-08 00:52:36 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-08 00:52:37 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 00:52:37 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:52:37 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:53:21 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-08 00:53:21 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:53:21 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 00:56:21 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-08 00:56:21 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 00:56:21 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 09:52:27 - mcp_services - INFO - [main.py:344] - MCP 服务器已关闭
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-08 10:00:42 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-08 10:00:45 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-08 10:00:45 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-08 10:00:45 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-08 10:00:45 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-08 10:00:45 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-08 10:02:28 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-08 10:02:28 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 10:02:28 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 10:02:30 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 10:02:30 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 10:02:30 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 10:02:35 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-08 10:02:35 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 10:02:35 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 10:22:23 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OrderDelayWarningAnalysis
2026-01-08 10:22:23 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 10:22:24 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 10:57:20 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: WorkOrderProgressAndAnomalyNodes
2026-01-08 10:57:20 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 10:57:20 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 11:05:47 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplyChainRiskWarning
2026-01-08 11:05:47 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 11:05:48 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 11:13:21 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: EfficiencyOutputLossDashboard
2026-01-08 11:13:21 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 11:13:22 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 11:34:55 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: OnePageDecisionBrief
2026-01-08 11:34:55 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 11:34:56 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: MetricTrendAndTurningPointWarning
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-08 11:59:37 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-09 18:27:37 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-09 18:27:38 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-09 18:27:38 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-09 18:27:38 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-09 18:27:38 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-09 18:27:38 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-09 18:27:39 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplierEvaluationAndSmartReplenishment
2026-01-09 18:27:39 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:27:40 - mcp_services - ERROR - [main.py:238] - 调用测试SQL API失败: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\main.py", line 226, in handle_call_tool
api_response = test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 251, in test_sql_with_schema
return default_client.test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 193, in test_sql_with_schema
raise Exception(error_msg)
Exception: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
2026-01-09 18:27:46 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 18:27:46 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:27:46 - mcp_services - ERROR - [main.py:238] - 调用测试SQL API失败: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 169, in test_sql_with_schema
raise Exception(error_msg)
Exception: Multiple ResultSets were returned by the query.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\main.py", line 226, in handle_call_tool
api_response = test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 251, in test_sql_with_schema
return default_client.test_sql_with_schema(request_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_mfg_data_agentv2\lzwcai_mcpskills_mfg_data_agentv2\utils\api_client.py", line 193, in test_sql_with_schema
raise Exception(error_msg)
Exception: 处理测试SQL API响应时出错: Multiple ResultSets were returned by the query.
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-09 18:44:45 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-09 18:44:46 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-09 18:44:46 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-09 18:44:46 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-09 18:44:46 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-09 18:44:46 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-09 18:44:47 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 18:44:47 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:44:47 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:49:19 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesPerformanceIntelligentStatistics
2026-01-09 18:49:19 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:49:19 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:49:24 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: FinancialAnalyticsDashboard
2026-01-09 18:49:24 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:49:24 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:49:27 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SmartCostPredictionModel
2026-01-09 18:49:27 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:49:27 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:57:52 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 18:57:52 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:57:52 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:58:07 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplierEvaluationAndSmartReplenishment
2026-01-09 18:58:07 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:58:07 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 18:58:13 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 18:58:13 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 18:58:13 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:02:47 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 19:02:47 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:02:47 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-09 19:31:56 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-09 19:31:58 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-09 19:31:58 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-09 19:31:58 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-09 19:31:58 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-09 19:31:58 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-09 19:32:01 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplierEvaluationAndSmartReplenishment
2026-01-09 19:32:01 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:01 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:32:04 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 19:32:04 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:04 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:32:07 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesPerformanceIntelligentStatistics
2026-01-09 19:32:07 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:07 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:32:12 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: FinancialAnalyticsDashboard
2026-01-09 19:32:12 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:12 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:32:16 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SmartCostPredictionModel
2026-01-09 19:32:16 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:16 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:32:33 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 19:32:33 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:32:34 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:34:32 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesBIIntelligentAnalyticsPlatform
2026-01-09 19:34:32 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:34:32 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:48:17 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SupplierEvaluationAndSmartReplenishment
2026-01-09 19:48:17 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:48:17 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 19:57:13 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SalesPerformanceIntelligentStatistics
2026-01-09 19:57:13 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 19:57:14 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:02:14 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: FinancialAnalyticsDashboard
2026-01-09 20:02:14 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:02:14 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:06:58 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: SmartCostPredictionModel
2026-01-09 20:06:58 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:06:58 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:23:12 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: HumanResourcesAnalytics
2026-01-09 20:23:12 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:23:12 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:26:12 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: HumanResourcesAnalytics
2026-01-09 20:26:12 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:26:13 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:38:58 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: HumanResourcesAnalytics
2026-01-09 20:38:58 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:38:58 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:362] - 开始运行 MCP SQL Executor 服务器
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:313] - ============================================================
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:314] - 正在启动 MCP 服务器: lzwcai-mcpskills-analyzeOrder
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:315] - 版本: 0.1.0
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:316] - ============================================================
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:320] - 环境配置 - Database ID: 19
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:321] - 环境配置 - Datasource ID: 19
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:322] - 环境配置 - Skill ID:
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:323] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:324] - ============================================================
2026-01-09 20:43:32 - mcp_services - INFO - [main.py:329] - MCP 服务器已启动,等待客户端连接...
2026-01-09 20:43:33 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2026-01-09 20:43:33 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local...
2026-01-09 20:43:33 - mcp_services - INFO - [main.py:55] - 成功加载 6 个业务查询配置
2026-01-09 20:43:33 - mcp_services - INFO - [main.py:123] - 本地配置: 6 条
2026-01-09 20:43:33 - mcp_services - INFO - [main.py:165] - 成功生成 6 个 MCP 工具
2026-01-09 20:43:35 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: HumanResourcesAnalytics
2026-01-09 20:43:35 - mcp_services - INFO - [main.py:225] - 正在调用测试SQL API...
2026-01-09 20:43:35 - mcp_services - INFO - [main.py:227] - 测试SQL API调用成功

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "lzwcai-mcpskills-mfg-data-agentv2"
version = "0.1.1"
version = "0.1.3"
description = "制造业数据智能体 - MCP server for manufacturing data intelligence with dynamic tool generation"
readme = "README.md"
requires-python = ">=3.13"

View File

@@ -0,0 +1,10 @@
# 制造业数据智能分析技能清单
| 技能名称 | 编码 | 技能描述 |
|---------|------|---------|
| 供应商评估与智能补货 | SupplierEvaluationAndSmartReplenishment | 基于物料消耗、交付周期、到货准时率与来料质检结果,输出供应商绩效指标与安全库存/再订货点/建议补货量 |
| 销售BI智能分析平台 | SalesBIIntelligentAnalyticsPlatform | 面向订单、发货/退货、客户复购与毛利贡献等核心指标提供多维汇总、趋势分析与TOP客户洞察 |
| 销售业绩智能统计系统 | SalesPerformanceIntelligentStatistics | 基于订单成交与合同归属,计算首触/跟进/协同的贡献度分摊,并据此估算推荐佣金与人员贡献排名 |
| 财务数据分析看板 | FinancialAnalyticsDashboard | 覆盖应收/应付月度趋势、现金流收支、逾期应收预警,以及销售毛利与毛利率的月度拆解 |
| 智能成本预测模型 | SmartCostPredictionModel | 基于历史入库单价估算原材料基准成本结合BOM用量拆解标准成本结构并支持原材涨跌价情景推演 |
| 人力资源数据分析 | HumanResourcesAnalytics | 提供月度在岗人数、招聘/离职趋势、产线部门离职率连续上升预警,以及岗位空缺天数分析 |