|
|
@@ -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>
|