zhanghe hace 5 meses
padre
commit
f4a1320fe5
Se han modificado 1 ficheros con 868 adiciones y 0 borrados
  1. 868 0
      src/main/resources/views/modules/mes/mesScreen4.html

+ 868 - 0
src/main/resources/views/modules/mes/mesScreen4.html

@@ -0,0 +1,868 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>产线阶段统计大屏</title>
+	<link rel="stylesheet" href="${ctxStatic}/bootstrap/css/bootstrap.min.css">
+	<link rel="stylesheet" href="${ctxStatic}/screen/style.css">
+	<style>
+		* { margin: 0; padding: 0; box-sizing: border-box; }
+		body { overflow: hidden; font-family: "Microsoft YaHei", sans-serif; }
+		.mes4-wrapper {
+			position: relative;
+			width: 1920px;
+			height: 1080px;
+			background-color: #00091A;
+			background-image: url("${ctxStatic}/screen/imgs/ljrSreen1.png");
+			background-size: 100% 100%;
+			overflow: hidden;
+			margin: 0 auto;
+		}
+
+		/* ===== 顶部标题栏 ===== */
+		.mes4-header {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 90px;
+			z-index: 10;
+		}
+		.mes4-logo {
+			position: absolute;
+			top: 20px;
+			left: 40px;
+			height: 50px;
+			width: auto;
+		}
+		.mes4-title {
+			position: absolute;
+			left: 50%;
+			top: 20px;
+			transform: translateX(-50%);
+			font-size: 38px;
+			font-weight: bold;
+			color: #FFFFFF;
+			letter-spacing: 3px;
+			text-shadow: 0 0 10px rgba(77, 201, 245, 0.6);
+			white-space: nowrap;
+		}
+		.mes4-datetime {
+			position: absolute;
+			top: 32px;
+			right: 50px;
+			text-align: right;
+		}
+		.mes4-datetime .mes4-date {
+			display: inline-block;
+			font-size: 18px;
+			color: #FFFFFF;
+			line-height: 28px;
+		}
+		.mes4-datetime .mes4-time {
+			display: inline-block;
+			margin-left: 14px;
+			font-size: 22px;
+			color: #FFFFFF;
+			font-weight: bold;
+			line-height: 28px;
+		}
+
+		/* ===== 面板通用 - 使用自定义类名避免与 Bootstrap 的 .panel 冲突 ===== */
+		.mes4-panel {
+			position: absolute;
+			overflow: hidden;
+			background: transparent;
+			border: none;
+			box-shadow: none;
+		}
+		.mes4-panel .mes4-ptitle {
+			height: 42px;
+			line-height: 42px;
+			text-align: center;
+			font-size: 18px;
+			font-weight: bold;
+			color: #4DC9F5;
+			letter-spacing: 1px;
+			background: transparent;
+		}
+		.mes4-panel .mes4-pbody {
+			position: absolute;
+			top: 42px;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			padding: 5px 10px;
+			background: transparent;
+		}
+		.chart { width: 100%; height: 100%; display: block; }
+
+		/* ===== 位置定义 (1920 × 1080 画布) ===== */
+		/* 左列: x=30, 宽=430 */
+		.p-alert    { left: 40px;  top: 130px; width: 430px; height: 290px; }
+		.p-quality  { left: 40px;  top: 455px; width: 430px; height: 265px; }
+		.p-product  { left: 40px;  top: 763px; width: 430px; height: 306px; }
+
+		/* 中列: x=480, 宽=960 */
+		.p-table    { left: 480px; top: 130px; width: 960px; height: 575px; }
+		.p-month    { left: 480px; top: 725px; width: 960px; height: 306px; }
+
+		/* 右列: right=30, 宽=430 */
+		.p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
+		.p-achieve  { right: 40px; top: 455px; width: 430px; height: 265px; }
+		.p-hourly   { right: 40px; top: 763px; width: 430px; height: 306px; }
+
+		/* 阶段报警信息 - 按模板比例调整 */
+		.p-alert .mes4-ptitle {
+			height: 40px;
+			line-height: 40px;
+			font-size: 16px;
+			color: #4DC9F5;
+			font-weight: bold;
+			letter-spacing: 1px;
+		}
+		.p-alert .mes4-pbody  { top: 40px; padding: 0; }
+		/* 总览标题 */
+		.p-overview .mes4-ptitle { height: 40px; line-height: 40px; font-size: 16px; }
+		.p-overview .mes4-pbody  { top: 40px; }
+
+		/* ===== 报警信息 ===== */
+		.alert-head {
+			display: flex;
+			align-items: center;
+			height: 36px;
+			padding: 0 22px;
+			color: #4DC9F5;
+			font-size: 14px;
+			font-weight: bold;
+			border-bottom: 1px solid rgba(77, 201, 245, 0.3);
+		}
+		.alert-head > div:nth-child(1) { width: 70px; }
+		.alert-head > div:nth-child(2) { flex: 1; }
+		.alert-row {
+			display: flex;
+			align-items: center;
+			height: 28px;
+			padding: 0 22px;
+			color: #ffffff;
+			font-size: 13px;
+		}
+		.alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
+		.alert-row > div:nth-child(2) { flex: 1; }
+
+		/* ===== OP 表格 ===== */
+		.op-table {
+			width: 100%;
+			border-collapse: collapse;
+			color: #ffffff;
+			font-size: 16px;
+			table-layout: fixed;
+		}
+		.op-table thead th {
+			height: 46px;
+			color: #4DC9F5;
+			font-weight: normal;
+			font-size: 16px;
+			background: rgba(0, 100, 180, 0.25);
+			border-bottom: 1px solid rgba(77, 201, 245, 0.35);
+			text-align: center;
+		}
+		.op-table tbody td {
+			height: 46px;
+			text-align: center;
+			border-bottom: 1px dashed rgba(77, 201, 245, 0.18);
+		}
+		.op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
+		.op-table td.op-col  { color: #00C8FF; font-weight: bold; font-size: 18px; }
+		.op-table td.op-name { color: #ffffff; text-align: center; }
+		.op-table td.op-val  { color: #00F7FF; font-weight: bold; font-size: 18px; }
+
+		/* ===== 产线阶段数据总览 (按模板排布) ===== */
+		.overview-wrap {
+			position: relative;
+			height: 100%;
+			padding: 14px 18px;
+		}
+		.overview-cards {
+			display: grid;
+			grid-template-columns: 1fr 1fr;
+			gap: 14px 170px;
+			height: 100%;
+		}
+		.overview-card {
+			background: transparent;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			justify-content: center;
+			padding: 4px;
+			padding-top: 19px;
+		}
+		.overview-card .oc-label {
+			color: #4DC9F5;
+			font-size: 14px;
+			margin-bottom: 4px;
+		}
+		.overview-card .oc-value {
+			color: #00F7FF;
+			font-size: 24px;
+			font-weight: bold;
+			line-height: 1;
+		}
+		.overview-card .oc-value .arrow {
+			font-size: 13px;
+			color: #00FF88;
+			margin-left: 3px;
+		}
+		.overview-gauge-wrap {
+			position: absolute;
+			top: 50%;
+			left: 50%;
+			transform: translate(calc(-50% - 10px), calc(-50% + 16px));
+			width: 160px;
+			height: 160px;
+		}
+		.overview-gauge-wrap canvas { width: 100%; height: 100%; }
+		.overview-gauge-wrap .gauge-txt {
+			position: absolute;
+			top: 50%;
+			left: 50%;
+			transform: translate(-50%, -50%);
+			text-align: center;
+			color: #00F7FF;
+			pointer-events: none;
+		}
+		.overview-gauge-wrap .gauge-txt .val {
+			font-size: 30px;
+			font-weight: bold;
+			line-height: 1;
+		}
+		.overview-gauge-wrap .gauge-txt .lbl {
+			font-size: 13px;
+			color: #ffffff;
+			margin-top: 6px;
+		}
+
+		/* ===== 图例 ===== */
+		.legend-inline {
+			display: flex;
+			align-items: center;
+			justify-content: flex-end;
+			padding: 4px 18px 0;
+			font-size: 13px;
+			color: #ffffff;
+			gap: 18px;
+		}
+		.legend-inline .dot {
+			display: inline-block;
+			width: 12px;
+			height: 12px;
+			border-radius: 50%;
+			margin-right: 5px;
+			vertical-align: middle;
+		}
+	</style>
+</head>
+<body>
+<div class="mes4-wrapper" id="app">
+
+	<div class="mes4-header">
+		<img class="mes4-logo" src="${ctxStatic}/screen/imgs/ljrLogo.png" alt="logo">
+		<div class="mes4-title"半成品【P10-P70】阶段统计大屏</div>
+		<div class="mes4-datetime">
+			<span class="mes4-date" id="mes4Date"></span>
+			<span class="mes4-time" id="mes4Time"></span>
+		</div>
+	</div>
+
+	<div class="mes4-panel p-alert">
+		<div class="mes4-ptitle">  产线阶段报警信息  </div>
+		<div class="mes4-pbody" style="padding:0">
+			<div class="alert-head"><div>序号</div><div>报警信息</div></div>
+			<div id="alertList"></div>
+		</div>
+	</div>
+
+	<div class="mes4-panel p-quality">
+		<div class="mes4-ptitle">  近七日产品质检数据分析  </div>
+		<div class="legend-inline">
+			<span><i class="dot" style="background:#00FF88"></i>产出</span>
+			<span><i class="dot" style="background:#00C8FF"></i>不良</span>
+		</div>
+		<div class="mes4-pbody" style="top:62px"><canvas id="chartQuality" class="chart"></canvas></div>
+	</div>
+
+	<div class="mes4-panel p-product">
+		<div class="mes4-ptitle">  近七日产量分析  </div>
+		<div class="mes4-pbody"><canvas id="chartProduct" class="chart"></canvas></div>
+	</div>
+
+	<div class="mes4-panel p-table">
+		<div class="mes4-pbody" style="top:5px;padding:0 20px">
+			<table class="op-table">
+				<thead>
+				<tr>
+					<th style="width:110px">工位</th>
+					<th style="width:200px">工位名称</th>
+					<th>计划产能</th>
+					<th>小时产能</th>
+					<th>实际产能</th>
+					<th>当班产能(12小时内)</th>
+				</tr>
+				</thead>
+				<tbody id="opTableBody"></tbody>
+			</table>
+		</div>
+	</div>
+
+	<div class="mes4-panel p-month">
+		<div class="mes4-ptitle">各月产量统计图</div>
+		<div class="mes4-pbody"><canvas id="chartMonth" class="chart"></canvas></div>
+	</div>
+
+	<div class="mes4-panel p-overview">
+		<div class="mes4-ptitle">  产线阶段数据总览  </div>
+		<div class="mes4-pbody" style="padding:0">
+			<div class="overview-wrap">
+				<div class="overview-cards">
+					<div class="overview-card">
+						<div class="oc-label">生产人数</div>
+						<div class="oc-value"><span id="productionPeople">48</span> <span class="arrow">↑</span></div>
+					</div>
+					<div class="overview-card">
+						<div class="oc-label">设备数量</div>
+						<div class="oc-value"><span id="deviceCount">56</span> <span class="arrow">↑</span></div>
+					</div>
+					<div class="overview-card">
+						<div class="oc-label">计划产量</div>
+						<div class="oc-value"><span id="planProduction">221</span> <span class="arrow">↑</span></div>
+					</div>
+					<div class="overview-card">
+						<div class="oc-label">实际产量</div>
+						<div class="oc-value"><span id="actualProduction">201</span> <span class="arrow">↑</span></div>
+					</div>
+				</div>
+				<div class="overview-gauge-wrap">
+					<canvas id="gaugeRate"></canvas>
+					<div class="gauge-txt">
+						<div class="val"><span id="gaugeVal">70</span>%</div>
+						<div class="lbl">今日达成率</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<div class="mes4-panel p-achieve">
+		<div class="mes4-ptitle">  近七日达成率数据分析  </div>
+		<div class="legend-inline">
+			<span><i class="dot" style="background:#FFDD00"></i>目标</span>
+			<span><i class="dot" style="background:#00C8FF"></i>实际</span>
+		</div>
+		<div class="mes4-pbody" style="top:62px"><canvas id="chartAchieve" class="chart"></canvas></div>
+	</div>
+
+	<div class="mes4-panel p-hourly">
+		<div class="mes4-ptitle">  每小时上下线数据分析  </div>
+		<div class="legend-inline">
+			<span><i class="dot" style="background:#00FF88"></i>上线</span>
+			<span><i class="dot" style="background:#00C8FF"></i>下线</span>
+		</div>
+		<div class="mes4-pbody" style="top:62px"><canvas id="chartHourly" class="chart"></canvas></div>
+	</div>
+</div>
+
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script>
+(function() {
+	function pad(n) { return n < 10 ? '0' + n : '' + n; }
+
+	function updateClock() {
+		var now = new Date();
+		var y = now.getFullYear();
+		var m = now.getMonth() + 1;
+		var d = now.getDate();
+		var w = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][now.getDay()];
+		document.getElementById('mes4Date').textContent = y + '年' + pad(m) + '月' + pad(d) + '日 ' + w;
+		var h = now.getHours(), mi = now.getMinutes(), s = now.getSeconds();
+		document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
+	}
+
+	// ====== 测试数据 ======
+	var mockData = {};
+	function generateMockData() {
+		// 最近7日日期
+		var now = new Date();
+		var weekLabels = [];
+		for (var i = 6; i >= 0; i--) {
+			var d = new Date(now);
+			d.setDate(d.getDate() - i);
+			weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
+		}
+
+		// 报警信息
+		mockData.alerts = [
+			{ no: 1, text: '产线不良率超过3%' },
+			{ no: 2, text: '产线不良率超过5%' },
+			{ no: 3, text: '产线不良率超过3%' },
+			{ no: 4, text: '产线不良率超过7%' },
+			{ no: 5, text: '产线不良率超过3%' },
+			{ no: 6, text: '产线不良率超过7%' }
+		];
+
+		// OP 表格
+		mockData.opTable = [
+			{ code: 'OP10', name: '上料+打码',       plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP25', name: '底板组件-打磨',    plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP40', name: '底板组件-CNC',     plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP50', name: '干冰清洁',         plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP55', name: '模组梁+封板预装',  plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP60', name: '封板正面FSW',      plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP65', name: '打磨',            plan: 50, hour: 50, real: 50, shift: 50 },
+			{ code: 'OP70', name: '封板反面FSW',      plan: 50, hour: 50, real: 50, shift: 50 }
+		];
+
+		// 数据卡
+		mockData.cards = {
+			productionPeople: 48,
+			deviceCount: 56,
+			planProduction: 221,
+			actualProduction: 201
+		};
+		mockData.rate = 70;
+
+		// 近七日产品质检
+		mockData.qualityInspect = {
+			labels: weekLabels,
+			produced: [400, 321, 354, 164, 241, 342, 231],
+			defect:   [345, 256, 312, 123, 214, 286, 189],
+			max: 500
+		};
+
+		// 近七日达成率
+		mockData.achieveRate = {
+			labels: weekLabels,
+			target: [1150, 1160, 1180, 1165, 1180, 1180, 1170],
+			actual: [1150, 1160, 1180, 1165, 1200, 1200, 1170],
+			max: 1300, min: 1100
+		};
+
+		// 近七日产量
+		mockData.productTrend = {
+			labels: weekLabels,
+			data: [1100, 1100, 1200, 1125, 1125, 1180, 1150],
+			max: 1300, min: 1000
+		};
+
+		// 每小时上下线 (24 小时)
+		var hourLabels = [], up = [], down = [];
+		for (var i = 1; i <= 24; i++) {
+			hourLabels.push(pad(i));
+			up.push(Math.round(60 + Math.random() * 30));
+			down.push(Math.round(50 + Math.random() * 30));
+		}
+		mockData.hourly = { labels: hourLabels, up: up, down: down, max: 100 };
+
+		// 各月产量
+		mockData.monthly = {
+			labels: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
+			data:   [ 350, 340, 480, 360, 350, 360, 420, 410, 380, 420, 470, 400 ],
+			max: 500
+		};
+	}
+
+	// ====== 工具函数 ======
+	function hexToRgba(hex, a) {
+		var r = parseInt(hex.slice(1, 3), 16),
+			g = parseInt(hex.slice(3, 5), 16),
+			b = parseInt(hex.slice(5, 7), 16);
+		return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
+	}
+	function prepareCanvas(id) {
+		var c = document.getElementById(id);
+		if (!c) return null;
+		var p = c.parentElement;
+		var w = p.clientWidth, h = p.clientHeight;
+		if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
+		return { ctx: c.getContext('2d'), w: w, h: h };
+	}
+	function drawYAxis(ctx, padL, padT, cw, ch, maxV, minV) {
+		minV = minV || 0;
+		ctx.fillStyle = 'rgba(77,201,245,0.8)';
+		ctx.font = '11px Microsoft YaHei';
+		ctx.textAlign = 'right';
+		ctx.strokeStyle = 'rgba(0,143,253,0.18)';
+		ctx.lineWidth = 1;
+		for (var i = 0; i <= 4; i++) {
+			var val = Math.round(minV + (maxV - minV) * (4 - i) / 4);
+			var y = padT + ch * i / 4;
+			ctx.fillText(val, padL - 5, y + 4);
+			ctx.beginPath();
+			ctx.moveTo(padL, y);
+			ctx.lineTo(padL + cw, y);
+			ctx.stroke();
+		}
+	}
+
+	// 双柱状图 - 近七日产品质检 (产出/不良)
+	function drawDoubleBar(id, a, b, labels, maxV) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var padL = 38, padR = 10, padT = 12, padB = 22;
+		var cw = w - padL - padR, ch = h - padT - padB;
+		ctx.clearRect(0, 0, w, h);
+		drawYAxis(ctx, padL, padT, cw, ch, maxV);
+		if (!a || a.length < 1) return;
+		var gw = cw / a.length, bw = gw * 0.3;
+		for (var i = 0; i < a.length; i++) {
+			var cx = padL + i * gw + gw / 2;
+			var h1 = (a[i] / maxV) * ch;
+			var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
+			g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
+			ctx.fillStyle = g1;
+			ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
+			var h2 = (b[i] / maxV) * ch;
+			var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
+			g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
+			ctx.fillStyle = g2;
+			ctx.fillRect(cx + 2, padT + ch - h2, bw, h2);
+			ctx.fillStyle = '#00FF88'; ctx.font = 'bold 10px Microsoft YaHei';
+			ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
+			ctx.fillStyle = '#00C8FF'; ctx.fillText(b[i], cx + bw / 2 + 2, padT + ch - h2 - 4);
+			ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
+			ctx.fillText(labels[i], cx, h - 6);
+		}
+	}
+
+	// 单折线图 (近七日产量, 平滑+面积)
+	function drawLineChart(id, data, labels, maxV, minV, color) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var padL = 45, padR = 12, padT = 15, padB = 25;
+		var cw = w - padL - padR, ch = h - padT - padB;
+		color = color || '#00C8FF';
+		minV = minV || 0;
+		ctx.clearRect(0, 0, w, h);
+		drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
+		if (!data || data.length < 2) return;
+		var stepX = cw / (data.length - 1);
+		var range = maxV - minV;
+		var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
+		grad.addColorStop(0, hexToRgba(color, 0.4));
+		grad.addColorStop(1, hexToRgba(color, 0.02));
+		ctx.beginPath();
+		ctx.moveTo(padL, padT + ch);
+		for (var i = 0; i < data.length; i++) {
+			var x = padL + i * stepX;
+			var y = padT + ch - ((data[i] - minV) / range) * ch;
+			if (i === 0) ctx.lineTo(x, y);
+			else {
+				var px = padL + (i - 1) * stepX;
+				var py = padT + ch - ((data[i - 1] - minV) / range) * ch;
+				var cpx = (px + x) / 2;
+				ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
+			}
+		}
+		ctx.lineTo(padL + (data.length - 1) * stepX, padT + ch);
+		ctx.closePath();
+		ctx.fillStyle = grad;
+		ctx.fill();
+		ctx.beginPath();
+		for (var j = 0; j < data.length; j++) {
+			var x2 = padL + j * stepX;
+			var y2 = padT + ch - ((data[j] - minV) / range) * ch;
+			if (j === 0) ctx.moveTo(x2, y2);
+			else {
+				var px2 = padL + (j - 1) * stepX;
+				var py2 = padT + ch - ((data[j - 1] - minV) / range) * ch;
+				var cpx2 = (px2 + x2) / 2;
+				ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
+			}
+		}
+		ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
+		ctx.font = 'bold 11px Microsoft YaHei'; ctx.textAlign = 'center';
+		for (var k = 0; k < data.length; k++) {
+			var x3 = padL + k * stepX;
+			var y3 = padT + ch - ((data[k] - minV) / range) * ch;
+			ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
+			ctx.fillStyle = color; ctx.fill();
+			ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
+			ctx.fillStyle = '#ffffff';
+			ctx.fillText(data[k], x3, y3 - 8);
+		}
+		ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
+		for (var m = 0; m < labels.length; m++) {
+			var xx = padL + m * stepX;
+			ctx.fillText(labels[m], xx, h - 6);
+		}
+	}
+
+	// 双折线图 (目标/实际)
+	function drawDualLine(id, a, b, labels, maxV, minV) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var padL = 45, padR = 12, padT = 12, padB = 22;
+		var cw = w - padL - padR, ch = h - padT - padB;
+		minV = minV || 0;
+		var range = maxV - minV;
+		ctx.clearRect(0, 0, w, h);
+		drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
+		if (!a || a.length < 2) return;
+		var stepX = cw / (a.length - 1);
+		function draw(arr, color) {
+			var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
+			grad.addColorStop(0, hexToRgba(color, 0.3));
+			grad.addColorStop(1, hexToRgba(color, 0.02));
+			ctx.beginPath(); ctx.moveTo(padL, padT + ch);
+			for (var i = 0; i < arr.length; i++) {
+				var x = padL + i * stepX;
+				var y = padT + ch - ((arr[i] - minV) / range) * ch;
+				if (i === 0) ctx.lineTo(x, y);
+				else {
+					var px = padL + (i - 1) * stepX;
+					var py = padT + ch - ((arr[i - 1] - minV) / range) * ch;
+					var cpx = (px + x) / 2;
+					ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
+				}
+			}
+			ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
+			ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
+			ctx.beginPath();
+			for (var j = 0; j < arr.length; j++) {
+				var x2 = padL + j * stepX;
+				var y2 = padT + ch - ((arr[j] - minV) / range) * ch;
+				if (j === 0) ctx.moveTo(x2, y2);
+				else {
+					var px2 = padL + (j - 1) * stepX;
+					var py2 = padT + ch - ((arr[j - 1] - minV) / range) * ch;
+					var cpx2 = (px2 + x2) / 2;
+					ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
+				}
+			}
+			ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
+			for (var k = 0; k < arr.length; k++) {
+				var x3 = padL + k * stepX;
+				var y3 = padT + ch - ((arr[k] - minV) / range) * ch;
+				ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
+				ctx.fillStyle = color; ctx.fill();
+			}
+		}
+		draw(a, '#FFDD00');
+		draw(b, '#00C8FF');
+		ctx.fillStyle = '#ffffff'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
+		for (var i = 0; i < b.length; i++) {
+			var x = padL + i * stepX;
+			var y = padT + ch - ((b[i] - minV) / range) * ch;
+			ctx.fillText(b[i], x, y - 6);
+		}
+		ctx.fillStyle = 'rgba(77,201,245,0.9)';
+		for (var i2 = 0; i2 < labels.length; i2++) {
+			var x2 = padL + i2 * stepX;
+			ctx.fillText(labels[i2], x2, h - 6);
+		}
+	}
+
+	// 双面积折线图 (每小时上下线)
+	function drawAreaLine(id, up, down, labels, maxV) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var padL = 30, padR = 12, padT = 12, padB = 22;
+		var cw = w - padL - padR, ch = h - padT - padB;
+		ctx.clearRect(0, 0, w, h);
+		drawYAxis(ctx, padL, padT, cw, ch, maxV);
+		if (!up || up.length < 2) return;
+		var stepX = cw / (up.length - 1);
+		function draw(arr, color) {
+			var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
+			grad.addColorStop(0, hexToRgba(color, 0.45));
+			grad.addColorStop(1, hexToRgba(color, 0.05));
+			ctx.beginPath(); ctx.moveTo(padL, padT + ch);
+			for (var i = 0; i < arr.length; i++) {
+				var x = padL + i * stepX;
+				var y = padT + ch - (arr[i] / maxV) * ch;
+				if (i === 0) ctx.lineTo(x, y);
+				else {
+					var px = padL + (i - 1) * stepX;
+					var py = padT + ch - (arr[i - 1] / maxV) * ch;
+					var cpx = (px + x) / 2;
+					ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
+				}
+			}
+			ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
+			ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
+			ctx.beginPath();
+			for (var j = 0; j < arr.length; j++) {
+				var x2 = padL + j * stepX;
+				var y2 = padT + ch - (arr[j] / maxV) * ch;
+				if (j === 0) ctx.moveTo(x2, y2);
+				else {
+					var px2 = padL + (j - 1) * stepX;
+					var py2 = padT + ch - (arr[j - 1] / maxV) * ch;
+					var cpx2 = (px2 + x2) / 2;
+					ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
+				}
+			}
+			ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.stroke();
+		}
+		draw(up, '#00FF88');
+		draw(down, '#00C8FF');
+		ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
+		for (var i = 0; i < labels.length; i++) {
+			if (i % 2 === 0) {
+				var x = padL + i * stepX;
+				ctx.fillText(labels[i], x, h - 6);
+			}
+		}
+	}
+
+	// 月产量柱状图 (渐变条)
+	function drawMonthBar(id, data, labels, maxV) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		var padL = 42, padR = 12, padT = 15, padB = 25;
+		var cw = w - padL - padR, ch = h - padT - padB;
+		ctx.clearRect(0, 0, w, h);
+		drawYAxis(ctx, padL, padT, cw, ch, maxV);
+		if (!data || data.length < 1) return;
+		var gw = cw / data.length, bw = gw * 0.35;
+		for (var i = 0; i < data.length; i++) {
+			var cx = padL + i * gw + gw / 2;
+			var bh = (data[i] / maxV) * ch;
+			var grad = ctx.createLinearGradient(0, padT + ch - bh, 0, padT + ch);
+			grad.addColorStop(0, '#00FF88');
+			grad.addColorStop(1, 'rgba(0,200,255,0.15)');
+			ctx.fillStyle = grad;
+			ctx.fillRect(cx - bw / 2, padT + ch - bh, bw, bh);
+			ctx.fillStyle = 'rgba(77,201,245,0.9)';
+			ctx.font = '11px Microsoft YaHei';
+			ctx.textAlign = 'center';
+			ctx.fillText(labels[i], cx, h - 6);
+		}
+	}
+
+	// 半环形仪表盘 (今日达成率)
+	function drawGauge(id, percent) {
+		var p = prepareCanvas(id); if (!p) return;
+		var ctx = p.ctx, w = p.w, h = p.h;
+		ctx.clearRect(0, 0, w, h);
+		var cx = w / 2, cy = h / 2;
+		var r = Math.min(w, h) / 2 - 8;
+		// 底环
+		ctx.beginPath();
+		ctx.arc(cx, cy, r, 0, Math.PI * 2);
+		ctx.lineWidth = 10;
+		ctx.strokeStyle = 'rgba(0,143,253,0.25)';
+		ctx.stroke();
+		// 进度环
+		var start = -Math.PI / 2;
+		var end = start + Math.PI * 2 * (percent / 100);
+		var grad = ctx.createLinearGradient(0, 0, w, h);
+		grad.addColorStop(0, '#00C8FF');
+		grad.addColorStop(1, '#00FF88');
+		ctx.beginPath();
+		ctx.arc(cx, cy, r, start, end, false);
+		ctx.lineWidth = 10;
+		ctx.strokeStyle = grad;
+		ctx.lineCap = 'round';
+		ctx.stroke();
+		// 外环刻度
+		ctx.beginPath();
+		ctx.arc(cx, cy, r + 10, 0, Math.PI * 2);
+		ctx.lineWidth = 1;
+		ctx.strokeStyle = 'rgba(0,200,255,0.35)';
+		ctx.stroke();
+	}
+
+	// ====== 渲染列表 ======
+	function renderAlerts() {
+		var html = '';
+		var list = mockData.alerts || [];
+		for (var i = 0; i < list.length; i++) {
+			html += '<div class="alert-row"><div>' + list[i].no + '</div><div>' + list[i].text + '</div></div>';
+		}
+		var el = document.getElementById('alertList');
+		if (el) el.innerHTML = html;
+	}
+	function renderTable() {
+		var html = '';
+		var list = mockData.opTable || [];
+		for (var i = 0; i < list.length; i++) {
+			var r = list[i];
+			html += '<tr>';
+			html += '<td class="op-col">' + r.code + '</td>';
+			html += '<td class="op-name">' + r.name + '</td>';
+			html += '<td class="op-val">' + r.plan + '</td>';
+			html += '<td class="op-val">' + r.hour + '</td>';
+			html += '<td class="op-val">' + r.real + '</td>';
+			html += '<td class="op-val">' + r.shift + '</td>';
+			html += '</tr>';
+		}
+		var el = document.getElementById('opTableBody');
+		if (el) el.innerHTML = html;
+	}
+	function renderCards() {
+		var c = mockData.cards || {};
+		var m = {
+			productionPeople: c.productionPeople,
+			deviceCount: c.deviceCount,
+			planProduction: c.planProduction,
+			actualProduction: c.actualProduction,
+			gaugeVal: mockData.rate
+		};
+		for (var k in m) {
+			var el = document.getElementById(k);
+			if (el && m[k] != null) el.textContent = m[k];
+		}
+	}
+
+	function drawAllCharts() {
+		var q = mockData.qualityInspect;
+		if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
+		var t = mockData.productTrend;
+		if (t) drawLineChart('chartProduct', t.data, t.labels, t.max, t.min, '#00C8FF');
+		var a = mockData.achieveRate;
+		if (a) drawDualLine('chartAchieve', a.target, a.actual, a.labels, a.max, a.min);
+		var hr = mockData.hourly;
+		if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
+		var mo = mockData.monthly;
+		if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
+		drawGauge('gaugeRate', mockData.rate || 0);
+	}
+
+	function refreshData() {
+		generateMockData();
+		renderAlerts();
+		renderTable();
+		renderCards();
+		drawAllCharts();
+	}
+
+	function changeZoom() {
+		var wb = $(window).width() / 1920;
+		var hb = $(window).height() / 1080;
+		$('body').css({ 'zoom': wb >= hb ? hb : wb });
+	}
+
+	$(window).resize(function() {
+		changeZoom();
+		drawAllCharts();
+	});
+
+	$(function() {
+		changeZoom();
+		updateClock();
+		setInterval(updateClock, 1000);
+		refreshData();
+		// 定期刷新数据 (2 分钟)
+		setInterval(refreshData, 1000 * 60 * 2);
+		// 每秒重绘 (应对 zoom / resize)
+		setInterval(drawAllCharts, 1000);
+	});
+})();
+</script>
+</body>
+</html>