| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103 |
- <!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">半成品阶段统计大屏</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() + '日');
- }
- // 报警信息由 updateDeviceAlarmAlertsByApi 拉取 mesDeviceAlarm/listData,展示 type
- mockData.alerts = [];
- // 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 escapeHtml(s) {
- if (s == null || s === '') return '';
- return String(s)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"');
- }
- // ====== 渲染列表(text 为设备报警记录的 type 字段)======
- 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>' + escapeHtml(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 updateLineProcessTableByApi() {
- $.ajax({
- url: '${ctx}/mes/mesLineProcess/listData',
- type: 'POST',
- dataType: 'json',
- data: {
- pageNo: 1,
- pageSize: 5000,
- lineSn: 'MP'
- },
- success: function(resp) {
- var rows = extractUserList(resp);
- var out = [];
- for (var i = 0; i < rows.length; i++) {
- var p = rows[i] || {};
- var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
- if (ls !== 'MP') continue;
- out.push({
- code: p.oprno || '',
- name: p.title || '',
- plan: 0,
- hour: 0,
- real: 0,
- shift: 0
- });
- }
- if (out.length > 0) {
- mockData.opTable = out;
- renderTable();
- }
- }
- });
- }
- 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 extractUserList(resp) {
- if (!resp) return [];
- if (Object.prototype.toString.call(resp.list) === '[object Array]') return resp.list;
- if (Object.prototype.toString.call(resp.rows) === '[object Array]') return resp.rows;
- if (resp.data) {
- if (Object.prototype.toString.call(resp.data.list) === '[object Array]') return resp.data.list;
- if (Object.prototype.toString.call(resp.data.rows) === '[object Array]') return resp.data.rows;
- }
- return [];
- }
- function updateDeviceAlarmAlertsByApi() {
- $.ajax({
- url: '${ctx}/mes/mesDeviceAlarm/listData',
- type: 'POST',
- dataType: 'json',
- data: {
- pageNo: 1,
- pageSize: 50
- },
- success: function(resp) {
- var rows = extractUserList(resp);
- var alerts = [];
- for (var i = 0; i < rows.length; i++) {
- var r = rows[i] || {};
- var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
- alerts.push({ no: i + 1, text: typ });
- }
- mockData.alerts = alerts;
- renderAlerts();
- },
- error: function() {
- mockData.alerts = [];
- renderAlerts();
- }
- });
- }
- function updateProductionPeopleByApi() {
- $.ajax({
- url: '${ctx}/sys/empUser/listData',
- type: 'POST',
- dataType: 'json',
- data: {
- pageNo: 1,
- pageSize: 5000,
- 'employee.office.officeName': '生产部'
- },
- success: function(resp) {
- var peopleCount = 0;
- var users = extractUserList(resp);
- for (var i = 0; i < users.length; i++) {
- var u = users[i] || {};
- var office = (u.employee && u.employee.office) ? u.employee.office : {};
- var officeName = office.officeName || '';
- var normalizedOfficeName = (officeName + '').replace(/\s/g, '');
- if (
- normalizedOfficeName === '生产部' ||
- /[/\\>]+生产部$/.test(normalizedOfficeName)
- ) {
- peopleCount++;
- }
- }
- mockData.cards = mockData.cards || {};
- mockData.cards.productionPeople = peopleCount;
- var target = document.getElementById('productionPeople');
- if (target) target.textContent = peopleCount;
- }
- });
- }
- function updateDeviceCountByApi() {
- $.ajax({
- url: '${ctx}/mes/mesDevice/listData',
- type: 'POST',
- dataType: 'json',
- data: {
- pageNo: 1,
- pageSize: 1
- },
- success: function(resp) {
- var deviceCount = 0;
- if (resp && typeof resp.count === 'number') {
- deviceCount = resp.count;
- } else {
- var devices = extractUserList(resp);
- deviceCount = devices.length;
- }
- mockData.cards = mockData.cards || {};
- mockData.cards.deviceCount = deviceCount;
- var target = document.getElementById('deviceCount');
- if (target) target.textContent = deviceCount;
- }
- });
- }
- function isOpr190Family(oprno) {
- var op = (oprno || '').replace(/\s/g, '').toUpperCase();
- if (op.length < 5 || op.indexOf('OP190') !== 0) {
- return false;
- }
- if (op.length === 5) {
- return op === 'OP190';
- }
- return /^OP190[A-Z0-9_-]+$/.test(op);
- }
- function isRecordContentOk(content) {
- var c = (content || '').replace(/\s/g, '').toUpperCase();
- return c === 'OK';
- }
- function applyActualProductionCount(n) {
- mockData.cards = mockData.cards || {};
- mockData.cards.actualProduction = n;
- var target = document.getElementById('actualProduction');
- if (target) target.textContent = n;
- }
- function getCalendarDayUpdateDateRange() {
- var now = new Date();
- var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
- var dateStr = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
- return {
- gte: dateStr + ' 00:00:00',
- lte: dateStr + ' 23:59:59',
- startMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(),
- endMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime()
- };
- }
- function parseRecordDateMs(val) {
- if (val == null || val === '') {
- return NaN;
- }
- if (typeof val === 'number' && !isNaN(val)) {
- return val;
- }
- var s = String(val).trim().replace(/-/g, '/');
- var t = Date.parse(s);
- return isNaN(t) ? NaN : t;
- }
- function isRecordWithinToday(r, day) {
- var t = parseRecordDateMs(r.updateDate || r.update_date);
- if (isNaN(t)) {
- return false;
- }
- return t >= day.startMs && t <= day.endMs;
- }
- function updateActualProductionByApi() {
- var dayRange = getCalendarDayUpdateDateRange();
- var pageSize = 3000;
- var maxPages = 200;
- var pageNo = 1;
- var totalOk = 0;
- function fetchPage() {
- $.ajax({
- url: '${ctx}/mes/mesProductRecord/listData',
- type: 'POST',
- dataType: 'json',
- data: {
- pageNo: pageNo,
- pageSize: pageSize,
- oprno: 'OP190',
- content: 'OK',
- lineSn: 'MP',
- updateDate_gte: dayRange.gte,
- updateDate_lte: dayRange.lte
- },
- success: function(resp) {
- var list = extractUserList(resp);
- for (var i = 0; i < list.length; i++) {
- var r = list[i] || {};
- if (isOpr190Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'MP') {
- totalOk++;
- }
- }
- if (list.length === pageSize && pageNo < maxPages) {
- pageNo++;
- fetchPage();
- } else {
- applyActualProductionCount(totalOk);
- }
- },
- error: function() {
- applyActualProductionCount(totalOk);
- }
- });
- }
- fetchPage();
- }
- 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();
- updateLineProcessTableByApi();
- updateProductionPeopleByApi();
- updateDeviceCountByApi();
- updateActualProductionByApi();
- updateDeviceAlarmAlertsByApi();
- 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>
|