mesScreen4.html 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>产线阶段统计大屏</title>
  8. <link rel="stylesheet" href="${ctxStatic}/bootstrap/css/bootstrap.min.css">
  9. <link rel="stylesheet" href="${ctxStatic}/screen/style.css">
  10. <style>
  11. * { margin: 0; padding: 0; box-sizing: border-box; }
  12. body { overflow: hidden; font-family: "Microsoft YaHei", sans-serif; }
  13. .mes4-wrapper {
  14. position: relative;
  15. width: 1920px;
  16. height: 1080px;
  17. background-color: #00091A;
  18. background-image: url("${ctxStatic}/screen/imgs/ljrSreen1.png");
  19. background-size: 100% 100%;
  20. overflow: hidden;
  21. margin: 0 auto;
  22. }
  23. /* ===== 顶部标题栏 ===== */
  24. .mes4-header {
  25. position: absolute;
  26. top: 0;
  27. left: 0;
  28. width: 100%;
  29. height: 90px;
  30. z-index: 10;
  31. }
  32. .mes4-logo {
  33. position: absolute;
  34. top: 20px;
  35. left: 40px;
  36. height: 50px;
  37. width: auto;
  38. }
  39. .mes4-title {
  40. position: absolute;
  41. left: 50%;
  42. top: 20px;
  43. transform: translateX(-50%);
  44. font-size: 38px;
  45. font-weight: bold;
  46. color: #FFFFFF;
  47. letter-spacing: 3px;
  48. text-shadow: 0 0 10px rgba(77, 201, 245, 0.6);
  49. white-space: nowrap;
  50. }
  51. .mes4-datetime {
  52. position: absolute;
  53. top: 32px;
  54. right: 50px;
  55. text-align: right;
  56. }
  57. .mes4-datetime .mes4-date {
  58. display: inline-block;
  59. font-size: 18px;
  60. color: #FFFFFF;
  61. line-height: 28px;
  62. }
  63. .mes4-datetime .mes4-time {
  64. display: inline-block;
  65. margin-left: 14px;
  66. font-size: 22px;
  67. color: #FFFFFF;
  68. font-weight: bold;
  69. line-height: 28px;
  70. }
  71. /* ===== 面板通用 - 使用自定义类名避免与 Bootstrap 的 .panel 冲突 ===== */
  72. .mes4-panel {
  73. position: absolute;
  74. overflow: hidden;
  75. background: transparent;
  76. border: none;
  77. box-shadow: none;
  78. }
  79. .mes4-panel .mes4-ptitle {
  80. height: 42px;
  81. line-height: 42px;
  82. text-align: center;
  83. font-size: 18px;
  84. font-weight: bold;
  85. color: #4DC9F5;
  86. letter-spacing: 1px;
  87. background: transparent;
  88. }
  89. .mes4-panel .mes4-pbody {
  90. position: absolute;
  91. top: 42px;
  92. left: 0;
  93. right: 0;
  94. bottom: 0;
  95. padding: 5px 10px;
  96. background: transparent;
  97. }
  98. .chart { width: 100%; height: 100%; display: block; }
  99. /* ===== 位置定义 (1920 × 1080 画布) ===== */
  100. /* 左列: x=30, 宽=430 */
  101. .p-alert { left: 40px; top: 130px; width: 430px; height: 290px; }
  102. .p-quality { left: 40px; top: 455px; width: 430px; height: 265px; }
  103. .p-product { left: 40px; top: 763px; width: 430px; height: 306px; }
  104. /* 中列: x=480, 宽=960 */
  105. .p-table { left: 480px; top: 130px; width: 960px; height: 575px; }
  106. .p-month { left: 480px; top: 725px; width: 960px; height: 306px; }
  107. /* 右列: right=30, 宽=430 */
  108. .p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
  109. .p-achieve { right: 40px; top: 455px; width: 430px; height: 265px; }
  110. .p-hourly { right: 40px; top: 763px; width: 430px; height: 306px; }
  111. /* 阶段报警信息 - 按模板比例调整 */
  112. .p-alert .mes4-ptitle {
  113. height: 40px;
  114. line-height: 40px;
  115. font-size: 16px;
  116. color: #4DC9F5;
  117. font-weight: bold;
  118. letter-spacing: 1px;
  119. }
  120. .p-alert .mes4-pbody { top: 40px; padding: 0; }
  121. /* 总览标题 */
  122. .p-overview .mes4-ptitle { height: 40px; line-height: 40px; font-size: 16px; }
  123. .p-overview .mes4-pbody { top: 40px; }
  124. /* ===== 报警信息 ===== */
  125. .alert-head {
  126. display: flex;
  127. align-items: center;
  128. height: 36px;
  129. padding: 0 22px;
  130. color: #4DC9F5;
  131. font-size: 14px;
  132. font-weight: bold;
  133. border-bottom: 1px solid rgba(77, 201, 245, 0.3);
  134. }
  135. .alert-head > div:nth-child(1) { width: 70px; }
  136. .alert-head > div:nth-child(2) { flex: 1; }
  137. .alert-row {
  138. display: flex;
  139. align-items: center;
  140. height: 28px;
  141. padding: 0 22px;
  142. color: #ffffff;
  143. font-size: 13px;
  144. }
  145. .alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
  146. .alert-row > div:nth-child(2) { flex: 1; }
  147. /* ===== OP 表格 ===== */
  148. .op-table {
  149. width: 100%;
  150. border-collapse: collapse;
  151. color: #ffffff;
  152. font-size: 16px;
  153. table-layout: fixed;
  154. }
  155. .op-table thead th {
  156. height: 46px;
  157. color: #4DC9F5;
  158. font-weight: normal;
  159. font-size: 16px;
  160. background: rgba(0, 100, 180, 0.25);
  161. border-bottom: 1px solid rgba(77, 201, 245, 0.35);
  162. text-align: center;
  163. }
  164. .op-table tbody td {
  165. height: 46px;
  166. text-align: center;
  167. border-bottom: 1px dashed rgba(77, 201, 245, 0.18);
  168. }
  169. .op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
  170. .op-table td.op-col { color: #00C8FF; font-weight: bold; font-size: 18px; }
  171. .op-table td.op-name { color: #ffffff; text-align: center; }
  172. .op-table td.op-val { color: #00F7FF; font-weight: bold; font-size: 18px; }
  173. /* ===== 产线阶段数据总览 (按模板排布) ===== */
  174. .overview-wrap {
  175. position: relative;
  176. height: 100%;
  177. padding: 14px 18px;
  178. }
  179. .overview-cards {
  180. display: grid;
  181. grid-template-columns: 1fr 1fr;
  182. gap: 14px 170px;
  183. height: 100%;
  184. }
  185. .overview-card {
  186. background: transparent;
  187. display: flex;
  188. flex-direction: column;
  189. align-items: center;
  190. justify-content: center;
  191. padding: 4px;
  192. padding-top: 19px;
  193. }
  194. .overview-card .oc-label {
  195. color: #4DC9F5;
  196. font-size: 14px;
  197. margin-bottom: 4px;
  198. }
  199. .overview-card .oc-value {
  200. color: #00F7FF;
  201. font-size: 24px;
  202. font-weight: bold;
  203. line-height: 1;
  204. }
  205. .overview-card .oc-value .arrow {
  206. font-size: 13px;
  207. color: #00FF88;
  208. margin-left: 3px;
  209. }
  210. .overview-gauge-wrap {
  211. position: absolute;
  212. top: 50%;
  213. left: 50%;
  214. transform: translate(calc(-50% - 10px), calc(-50% + 16px));
  215. width: 160px;
  216. height: 160px;
  217. }
  218. .overview-gauge-wrap canvas { width: 100%; height: 100%; }
  219. .overview-gauge-wrap .gauge-txt {
  220. position: absolute;
  221. top: 50%;
  222. left: 50%;
  223. transform: translate(-50%, -50%);
  224. text-align: center;
  225. color: #00F7FF;
  226. pointer-events: none;
  227. }
  228. .overview-gauge-wrap .gauge-txt .val {
  229. font-size: 30px;
  230. font-weight: bold;
  231. line-height: 1;
  232. }
  233. .overview-gauge-wrap .gauge-txt .lbl {
  234. font-size: 13px;
  235. color: #ffffff;
  236. margin-top: 6px;
  237. }
  238. /* ===== 图例 ===== */
  239. .legend-inline {
  240. display: flex;
  241. align-items: center;
  242. justify-content: flex-end;
  243. padding: 4px 18px 0;
  244. font-size: 13px;
  245. color: #ffffff;
  246. gap: 18px;
  247. }
  248. .legend-inline .dot {
  249. display: inline-block;
  250. width: 12px;
  251. height: 12px;
  252. border-radius: 50%;
  253. margin-right: 5px;
  254. vertical-align: middle;
  255. }
  256. </style>
  257. </head>
  258. <body>
  259. <div class="mes4-wrapper" id="app">
  260. <div class="mes4-header">
  261. <img class="mes4-logo" src="${ctxStatic}/screen/imgs/ljrLogo.png" alt="logo">
  262. <div class="mes4-title">半成品阶段统计大屏</div>
  263. <div class="mes4-datetime">
  264. <span class="mes4-date" id="mes4Date"></span>
  265. <span class="mes4-time" id="mes4Time"></span>
  266. </div>
  267. </div>
  268. <div class="mes4-panel p-alert">
  269. <div class="mes4-ptitle"> 产线阶段报警信息 </div>
  270. <div class="mes4-pbody" style="padding:0">
  271. <div class="alert-head"><div>序号</div><div>报警类型</div></div>
  272. <div id="alertList"></div>
  273. </div>
  274. </div>
  275. <div class="mes4-panel p-quality">
  276. <div class="mes4-ptitle"> 近七日产品质检数据分析 </div>
  277. <div class="legend-inline">
  278. <span><i class="dot" style="background:#00FF88"></i>产出</span>
  279. <span><i class="dot" style="background:#00C8FF"></i>不良</span>
  280. </div>
  281. <div class="mes4-pbody" style="top:62px"><canvas id="chartQuality" class="chart"></canvas></div>
  282. </div>
  283. <div class="mes4-panel p-product">
  284. <div class="mes4-ptitle"> 近七日产量分析 </div>
  285. <div class="mes4-pbody"><canvas id="chartProduct" class="chart"></canvas></div>
  286. </div>
  287. <div class="mes4-panel p-table">
  288. <div class="mes4-pbody" style="top:5px;padding:0 20px">
  289. <table class="op-table">
  290. <thead>
  291. <tr>
  292. <th style="width:110px">工位</th>
  293. <th style="width:200px">工位名称</th>
  294. <th>计划产能</th>
  295. <th>小时产能</th>
  296. <th>实际产能</th>
  297. <th>当班产能(12小时内)</th>
  298. </tr>
  299. </thead>
  300. <tbody id="opTableBody"></tbody>
  301. </table>
  302. </div>
  303. </div>
  304. <div class="mes4-panel p-month">
  305. <div class="mes4-ptitle">各月产量统计图</div>
  306. <div class="mes4-pbody"><canvas id="chartMonth" class="chart"></canvas></div>
  307. </div>
  308. <div class="mes4-panel p-overview">
  309. <div class="mes4-ptitle"> 产线阶段数据总览 </div>
  310. <div class="mes4-pbody" style="padding:0">
  311. <div class="overview-wrap">
  312. <div class="overview-cards">
  313. <div class="overview-card">
  314. <div class="oc-label">生产人数</div>
  315. <div class="oc-value"><span id="productionPeople">48</span> <span class="arrow">↑</span></div>
  316. </div>
  317. <div class="overview-card">
  318. <div class="oc-label">设备数量</div>
  319. <div class="oc-value"><span id="deviceCount">56</span> <span class="arrow">↑</span></div>
  320. </div>
  321. <div class="overview-card">
  322. <div class="oc-label">计划产量</div>
  323. <div class="oc-value"><span id="planProduction">221</span> <span class="arrow">↑</span></div>
  324. </div>
  325. <div class="overview-card">
  326. <div class="oc-label">实际产量</div>
  327. <div class="oc-value"><span id="actualProduction">201</span> <span class="arrow">↑</span></div>
  328. </div>
  329. </div>
  330. <div class="overview-gauge-wrap">
  331. <canvas id="gaugeRate"></canvas>
  332. <div class="gauge-txt">
  333. <div class="val"><span id="gaugeVal">70</span>%</div>
  334. <div class="lbl">今日达成率</div>
  335. </div>
  336. </div>
  337. </div>
  338. </div>
  339. </div>
  340. <div class="mes4-panel p-achieve">
  341. <div class="mes4-ptitle"> 近七日达成率数据分析 </div>
  342. <div class="legend-inline">
  343. <span><i class="dot" style="background:#FFDD00"></i>目标</span>
  344. <span><i class="dot" style="background:#00C8FF"></i>实际</span>
  345. </div>
  346. <div class="mes4-pbody" style="top:62px"><canvas id="chartAchieve" class="chart"></canvas></div>
  347. </div>
  348. <div class="mes4-panel p-hourly">
  349. <div class="mes4-ptitle"> 每小时上下线数据分析 </div>
  350. <div class="legend-inline">
  351. <span><i class="dot" style="background:#00FF88"></i>上线</span>
  352. <span><i class="dot" style="background:#00C8FF"></i>下线</span>
  353. </div>
  354. <div class="mes4-pbody" style="top:62px"><canvas id="chartHourly" class="chart"></canvas></div>
  355. </div>
  356. </div>
  357. <script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
  358. <script>
  359. (function() {
  360. function pad(n) { return n < 10 ? '0' + n : '' + n; }
  361. function updateClock() {
  362. var now = new Date();
  363. var y = now.getFullYear();
  364. var m = now.getMonth() + 1;
  365. var d = now.getDate();
  366. var w = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][now.getDay()];
  367. document.getElementById('mes4Date').textContent = y + '年' + pad(m) + '月' + pad(d) + '日 ' + w;
  368. var h = now.getHours(), mi = now.getMinutes(), s = now.getSeconds();
  369. document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
  370. }
  371. // ====== 测试数据 ======
  372. var mockData = {};
  373. function generateMockData() {
  374. // 最近7日日期
  375. var now = new Date();
  376. var weekLabels = [];
  377. for (var i = 6; i >= 0; i--) {
  378. var d = new Date(now);
  379. d.setDate(d.getDate() - i);
  380. weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
  381. }
  382. // 报警信息由 updateDeviceAlarmAlertsByApi 拉取 mesDeviceAlarm/listData,展示 type
  383. mockData.alerts = [];
  384. // OP 表格
  385. mockData.opTable = [
  386. { code: 'OP10', name: '上料+打码', plan: 50, hour: 50, real: 50, shift: 50 },
  387. { code: 'OP20', name: '底板组件-正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
  388. { code: 'OP25', name: '底板组件-打磨', plan: 50, hour: 50, real: 50, shift: 50 },
  389. { code: 'OP30', name: '底板组件-反面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
  390. { code: 'OP40', name: '底板组件-CNC', plan: 50, hour: 50, real: 50, shift: 50 },
  391. { code: 'OP50', name: '干冰清洁', plan: 50, hour: 50, real: 50, shift: 50 },
  392. { code: 'OP55', name: '模组梁+封板预装', plan: 50, hour: 50, real: 50, shift: 50 },
  393. { code: 'OP60', name: '封板正面FSW', plan: 50, hour: 50, real: 50, shift: 50 },
  394. { code: 'OP65', name: '打磨', plan: 50, hour: 50, real: 50, shift: 50 },
  395. { code: 'OP70', name: '封板反面FSW', plan: 50, hour: 50, real: 50, shift: 50 }
  396. ];
  397. // 数据卡
  398. mockData.cards = {
  399. productionPeople: 48,
  400. deviceCount: 56,
  401. planProduction: 221,
  402. actualProduction: 201
  403. };
  404. mockData.rate = 70;
  405. // 近七日产品质检
  406. mockData.qualityInspect = {
  407. labels: weekLabels,
  408. produced: [400, 321, 354, 164, 241, 342, 231],
  409. defect: [345, 256, 312, 123, 214, 286, 189],
  410. max: 500
  411. };
  412. // 近七日达成率
  413. mockData.achieveRate = {
  414. labels: weekLabels,
  415. target: [1150, 1160, 1180, 1165, 1180, 1180, 1170],
  416. actual: [1150, 1160, 1180, 1165, 1200, 1200, 1170],
  417. max: 1300, min: 1100
  418. };
  419. // 近七日产量
  420. mockData.productTrend = {
  421. labels: weekLabels,
  422. data: [1100, 1100, 1200, 1125, 1125, 1180, 1150],
  423. max: 1300, min: 1000
  424. };
  425. // 每小时上下线 (24 小时)
  426. var hourLabels = [], up = [], down = [];
  427. for (var i = 1; i <= 24; i++) {
  428. hourLabels.push(pad(i));
  429. up.push(Math.round(60 + Math.random() * 30));
  430. down.push(Math.round(50 + Math.random() * 30));
  431. }
  432. mockData.hourly = { labels: hourLabels, up: up, down: down, max: 100 };
  433. // 各月产量
  434. mockData.monthly = {
  435. labels: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
  436. data: [ 350, 340, 480, 360, 350, 360, 420, 410, 380, 420, 470, 400 ],
  437. max: 500
  438. };
  439. }
  440. // ====== 工具函数 ======
  441. function hexToRgba(hex, a) {
  442. var r = parseInt(hex.slice(1, 3), 16),
  443. g = parseInt(hex.slice(3, 5), 16),
  444. b = parseInt(hex.slice(5, 7), 16);
  445. return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
  446. }
  447. function prepareCanvas(id) {
  448. var c = document.getElementById(id);
  449. if (!c) return null;
  450. var p = c.parentElement;
  451. var w = p.clientWidth, h = p.clientHeight;
  452. if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
  453. return { ctx: c.getContext('2d'), w: w, h: h };
  454. }
  455. function drawYAxis(ctx, padL, padT, cw, ch, maxV, minV) {
  456. minV = minV || 0;
  457. ctx.fillStyle = 'rgba(77,201,245,0.8)';
  458. ctx.font = '11px Microsoft YaHei';
  459. ctx.textAlign = 'right';
  460. ctx.strokeStyle = 'rgba(0,143,253,0.18)';
  461. ctx.lineWidth = 1;
  462. for (var i = 0; i <= 4; i++) {
  463. var val = Math.round(minV + (maxV - minV) * (4 - i) / 4);
  464. var y = padT + ch * i / 4;
  465. ctx.fillText(val, padL - 5, y + 4);
  466. ctx.beginPath();
  467. ctx.moveTo(padL, y);
  468. ctx.lineTo(padL + cw, y);
  469. ctx.stroke();
  470. }
  471. }
  472. // 双柱状图 - 近七日产品质检 (产出/不良)
  473. function drawDoubleBar(id, a, b, labels, maxV) {
  474. var p = prepareCanvas(id); if (!p) return;
  475. var ctx = p.ctx, w = p.w, h = p.h;
  476. var padL = 38, padR = 10, padT = 12, padB = 22;
  477. var cw = w - padL - padR, ch = h - padT - padB;
  478. ctx.clearRect(0, 0, w, h);
  479. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  480. if (!a || a.length < 1) return;
  481. var gw = cw / a.length, bw = gw * 0.3;
  482. for (var i = 0; i < a.length; i++) {
  483. var cx = padL + i * gw + gw / 2;
  484. var h1 = (a[i] / maxV) * ch;
  485. var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
  486. g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
  487. ctx.fillStyle = g1;
  488. ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
  489. var h2 = (b[i] / maxV) * ch;
  490. var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
  491. g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
  492. ctx.fillStyle = g2;
  493. ctx.fillRect(cx + 2, padT + ch - h2, bw, h2);
  494. ctx.fillStyle = '#00FF88'; ctx.font = 'bold 10px Microsoft YaHei';
  495. ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
  496. ctx.fillStyle = '#00C8FF'; ctx.fillText(b[i], cx + bw / 2 + 2, padT + ch - h2 - 4);
  497. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
  498. ctx.fillText(labels[i], cx, h - 6);
  499. }
  500. }
  501. // 单折线图 (近七日产量, 平滑+面积)
  502. function drawLineChart(id, data, labels, maxV, minV, color) {
  503. var p = prepareCanvas(id); if (!p) return;
  504. var ctx = p.ctx, w = p.w, h = p.h;
  505. var padL = 45, padR = 12, padT = 15, padB = 25;
  506. var cw = w - padL - padR, ch = h - padT - padB;
  507. color = color || '#00C8FF';
  508. minV = minV || 0;
  509. ctx.clearRect(0, 0, w, h);
  510. drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
  511. if (!data || data.length < 2) return;
  512. var stepX = cw / (data.length - 1);
  513. var range = maxV - minV;
  514. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  515. grad.addColorStop(0, hexToRgba(color, 0.4));
  516. grad.addColorStop(1, hexToRgba(color, 0.02));
  517. ctx.beginPath();
  518. ctx.moveTo(padL, padT + ch);
  519. for (var i = 0; i < data.length; i++) {
  520. var x = padL + i * stepX;
  521. var y = padT + ch - ((data[i] - minV) / range) * ch;
  522. if (i === 0) ctx.lineTo(x, y);
  523. else {
  524. var px = padL + (i - 1) * stepX;
  525. var py = padT + ch - ((data[i - 1] - minV) / range) * ch;
  526. var cpx = (px + x) / 2;
  527. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  528. }
  529. }
  530. ctx.lineTo(padL + (data.length - 1) * stepX, padT + ch);
  531. ctx.closePath();
  532. ctx.fillStyle = grad;
  533. ctx.fill();
  534. ctx.beginPath();
  535. for (var j = 0; j < data.length; j++) {
  536. var x2 = padL + j * stepX;
  537. var y2 = padT + ch - ((data[j] - minV) / range) * ch;
  538. if (j === 0) ctx.moveTo(x2, y2);
  539. else {
  540. var px2 = padL + (j - 1) * stepX;
  541. var py2 = padT + ch - ((data[j - 1] - minV) / range) * ch;
  542. var cpx2 = (px2 + x2) / 2;
  543. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  544. }
  545. }
  546. ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
  547. ctx.font = 'bold 11px Microsoft YaHei'; ctx.textAlign = 'center';
  548. for (var k = 0; k < data.length; k++) {
  549. var x3 = padL + k * stepX;
  550. var y3 = padT + ch - ((data[k] - minV) / range) * ch;
  551. ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
  552. ctx.fillStyle = color; ctx.fill();
  553. ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
  554. ctx.fillStyle = '#ffffff';
  555. ctx.fillText(data[k], x3, y3 - 8);
  556. }
  557. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
  558. for (var m = 0; m < labels.length; m++) {
  559. var xx = padL + m * stepX;
  560. ctx.fillText(labels[m], xx, h - 6);
  561. }
  562. }
  563. // 双折线图 (目标/实际)
  564. function drawDualLine(id, a, b, labels, maxV, minV) {
  565. var p = prepareCanvas(id); if (!p) return;
  566. var ctx = p.ctx, w = p.w, h = p.h;
  567. var padL = 45, padR = 12, padT = 12, padB = 22;
  568. var cw = w - padL - padR, ch = h - padT - padB;
  569. minV = minV || 0;
  570. var range = maxV - minV;
  571. ctx.clearRect(0, 0, w, h);
  572. drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
  573. if (!a || a.length < 2) return;
  574. var stepX = cw / (a.length - 1);
  575. function draw(arr, color) {
  576. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  577. grad.addColorStop(0, hexToRgba(color, 0.3));
  578. grad.addColorStop(1, hexToRgba(color, 0.02));
  579. ctx.beginPath(); ctx.moveTo(padL, padT + ch);
  580. for (var i = 0; i < arr.length; i++) {
  581. var x = padL + i * stepX;
  582. var y = padT + ch - ((arr[i] - minV) / range) * ch;
  583. if (i === 0) ctx.lineTo(x, y);
  584. else {
  585. var px = padL + (i - 1) * stepX;
  586. var py = padT + ch - ((arr[i - 1] - minV) / range) * ch;
  587. var cpx = (px + x) / 2;
  588. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  589. }
  590. }
  591. ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
  592. ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
  593. ctx.beginPath();
  594. for (var j = 0; j < arr.length; j++) {
  595. var x2 = padL + j * stepX;
  596. var y2 = padT + ch - ((arr[j] - minV) / range) * ch;
  597. if (j === 0) ctx.moveTo(x2, y2);
  598. else {
  599. var px2 = padL + (j - 1) * stepX;
  600. var py2 = padT + ch - ((arr[j - 1] - minV) / range) * ch;
  601. var cpx2 = (px2 + x2) / 2;
  602. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  603. }
  604. }
  605. ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
  606. for (var k = 0; k < arr.length; k++) {
  607. var x3 = padL + k * stepX;
  608. var y3 = padT + ch - ((arr[k] - minV) / range) * ch;
  609. ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
  610. ctx.fillStyle = color; ctx.fill();
  611. }
  612. }
  613. draw(a, '#FFDD00');
  614. draw(b, '#00C8FF');
  615. ctx.fillStyle = '#ffffff'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
  616. for (var i = 0; i < b.length; i++) {
  617. var x = padL + i * stepX;
  618. var y = padT + ch - ((b[i] - minV) / range) * ch;
  619. ctx.fillText(b[i], x, y - 6);
  620. }
  621. ctx.fillStyle = 'rgba(77,201,245,0.9)';
  622. for (var i2 = 0; i2 < labels.length; i2++) {
  623. var x2 = padL + i2 * stepX;
  624. ctx.fillText(labels[i2], x2, h - 6);
  625. }
  626. }
  627. // 双面积折线图 (每小时上下线)
  628. function drawAreaLine(id, up, down, labels, maxV) {
  629. var p = prepareCanvas(id); if (!p) return;
  630. var ctx = p.ctx, w = p.w, h = p.h;
  631. var padL = 30, padR = 12, padT = 12, padB = 22;
  632. var cw = w - padL - padR, ch = h - padT - padB;
  633. ctx.clearRect(0, 0, w, h);
  634. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  635. if (!up || up.length < 2) return;
  636. var stepX = cw / (up.length - 1);
  637. function draw(arr, color) {
  638. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  639. grad.addColorStop(0, hexToRgba(color, 0.45));
  640. grad.addColorStop(1, hexToRgba(color, 0.05));
  641. ctx.beginPath(); ctx.moveTo(padL, padT + ch);
  642. for (var i = 0; i < arr.length; i++) {
  643. var x = padL + i * stepX;
  644. var y = padT + ch - (arr[i] / maxV) * ch;
  645. if (i === 0) ctx.lineTo(x, y);
  646. else {
  647. var px = padL + (i - 1) * stepX;
  648. var py = padT + ch - (arr[i - 1] / maxV) * ch;
  649. var cpx = (px + x) / 2;
  650. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  651. }
  652. }
  653. ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
  654. ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
  655. ctx.beginPath();
  656. for (var j = 0; j < arr.length; j++) {
  657. var x2 = padL + j * stepX;
  658. var y2 = padT + ch - (arr[j] / maxV) * ch;
  659. if (j === 0) ctx.moveTo(x2, y2);
  660. else {
  661. var px2 = padL + (j - 1) * stepX;
  662. var py2 = padT + ch - (arr[j - 1] / maxV) * ch;
  663. var cpx2 = (px2 + x2) / 2;
  664. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  665. }
  666. }
  667. ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.stroke();
  668. }
  669. draw(up, '#00FF88');
  670. draw(down, '#00C8FF');
  671. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
  672. for (var i = 0; i < labels.length; i++) {
  673. if (i % 2 === 0) {
  674. var x = padL + i * stepX;
  675. ctx.fillText(labels[i], x, h - 6);
  676. }
  677. }
  678. }
  679. // 月产量柱状图 (渐变条)
  680. function drawMonthBar(id, data, labels, maxV) {
  681. var p = prepareCanvas(id); if (!p) return;
  682. var ctx = p.ctx, w = p.w, h = p.h;
  683. var padL = 42, padR = 12, padT = 15, padB = 25;
  684. var cw = w - padL - padR, ch = h - padT - padB;
  685. ctx.clearRect(0, 0, w, h);
  686. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  687. if (!data || data.length < 1) return;
  688. var gw = cw / data.length, bw = gw * 0.35;
  689. for (var i = 0; i < data.length; i++) {
  690. var cx = padL + i * gw + gw / 2;
  691. var bh = (data[i] / maxV) * ch;
  692. var grad = ctx.createLinearGradient(0, padT + ch - bh, 0, padT + ch);
  693. grad.addColorStop(0, '#00FF88');
  694. grad.addColorStop(1, 'rgba(0,200,255,0.15)');
  695. ctx.fillStyle = grad;
  696. ctx.fillRect(cx - bw / 2, padT + ch - bh, bw, bh);
  697. ctx.fillStyle = 'rgba(77,201,245,0.9)';
  698. ctx.font = '11px Microsoft YaHei';
  699. ctx.textAlign = 'center';
  700. ctx.fillText(labels[i], cx, h - 6);
  701. }
  702. }
  703. // 半环形仪表盘 (今日达成率)
  704. function drawGauge(id, percent) {
  705. var p = prepareCanvas(id); if (!p) return;
  706. var ctx = p.ctx, w = p.w, h = p.h;
  707. ctx.clearRect(0, 0, w, h);
  708. var cx = w / 2, cy = h / 2;
  709. var r = Math.min(w, h) / 2 - 8;
  710. // 底环
  711. ctx.beginPath();
  712. ctx.arc(cx, cy, r, 0, Math.PI * 2);
  713. ctx.lineWidth = 10;
  714. ctx.strokeStyle = 'rgba(0,143,253,0.25)';
  715. ctx.stroke();
  716. // 进度环
  717. var start = -Math.PI / 2;
  718. var end = start + Math.PI * 2 * (percent / 100);
  719. var grad = ctx.createLinearGradient(0, 0, w, h);
  720. grad.addColorStop(0, '#00C8FF');
  721. grad.addColorStop(1, '#00FF88');
  722. ctx.beginPath();
  723. ctx.arc(cx, cy, r, start, end, false);
  724. ctx.lineWidth = 10;
  725. ctx.strokeStyle = grad;
  726. ctx.lineCap = 'round';
  727. ctx.stroke();
  728. // 外环刻度
  729. ctx.beginPath();
  730. ctx.arc(cx, cy, r + 10, 0, Math.PI * 2);
  731. ctx.lineWidth = 1;
  732. ctx.strokeStyle = 'rgba(0,200,255,0.35)';
  733. ctx.stroke();
  734. }
  735. function escapeHtml(s) {
  736. if (s == null || s === '') return '';
  737. return String(s)
  738. .replace(/&/g, '&amp;')
  739. .replace(/</g, '&lt;')
  740. .replace(/>/g, '&gt;')
  741. .replace(/"/g, '&quot;');
  742. }
  743. // ====== 渲染列表(text 为设备报警记录的 type 字段)======
  744. function renderAlerts() {
  745. var html = '';
  746. var list = mockData.alerts || [];
  747. for (var i = 0; i < list.length; i++) {
  748. html += '<div class="alert-row"><div>' + list[i].no + '</div><div>' + escapeHtml(list[i].text) + '</div></div>';
  749. }
  750. var el = document.getElementById('alertList');
  751. if (el) el.innerHTML = html;
  752. }
  753. function renderTable() {
  754. var html = '';
  755. var list = mockData.opTable || [];
  756. for (var i = 0; i < list.length; i++) {
  757. var r = list[i];
  758. html += '<tr>';
  759. html += '<td class="op-col">' + r.code + '</td>';
  760. html += '<td class="op-name">' + r.name + '</td>';
  761. html += '<td class="op-val">' + r.plan + '</td>';
  762. html += '<td class="op-val">' + r.hour + '</td>';
  763. html += '<td class="op-val">' + r.real + '</td>';
  764. html += '<td class="op-val">' + r.shift + '</td>';
  765. html += '</tr>';
  766. }
  767. var el = document.getElementById('opTableBody');
  768. if (el) el.innerHTML = html;
  769. }
  770. function updateLineProcessTableByApi() {
  771. $.ajax({
  772. url: '${ctx}/mes/mesLineProcess/listData',
  773. type: 'POST',
  774. dataType: 'json',
  775. data: {
  776. pageNo: 1,
  777. pageSize: 5000,
  778. lineSn: 'MP'
  779. },
  780. success: function(resp) {
  781. var rows = extractUserList(resp);
  782. var out = [];
  783. for (var i = 0; i < rows.length; i++) {
  784. var p = rows[i] || {};
  785. var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
  786. if (ls !== 'MP') continue;
  787. out.push({
  788. code: p.oprno || '',
  789. name: p.title || '',
  790. plan: 0,
  791. hour: 0,
  792. real: 0,
  793. shift: 0
  794. });
  795. }
  796. if (out.length > 0) {
  797. mockData.opTable = out;
  798. renderTable();
  799. }
  800. }
  801. });
  802. }
  803. function renderCards() {
  804. var c = mockData.cards || {};
  805. var m = {
  806. productionPeople: c.productionPeople,
  807. deviceCount: c.deviceCount,
  808. planProduction: c.planProduction,
  809. actualProduction: c.actualProduction,
  810. gaugeVal: mockData.rate
  811. };
  812. for (var k in m) {
  813. var el = document.getElementById(k);
  814. if (el && m[k] != null) el.textContent = m[k];
  815. }
  816. }
  817. function extractUserList(resp) {
  818. if (!resp) return [];
  819. if (Object.prototype.toString.call(resp.list) === '[object Array]') return resp.list;
  820. if (Object.prototype.toString.call(resp.rows) === '[object Array]') return resp.rows;
  821. if (resp.data) {
  822. if (Object.prototype.toString.call(resp.data.list) === '[object Array]') return resp.data.list;
  823. if (Object.prototype.toString.call(resp.data.rows) === '[object Array]') return resp.data.rows;
  824. }
  825. return [];
  826. }
  827. function updateDeviceAlarmAlertsByApi() {
  828. $.ajax({
  829. url: '${ctx}/mes/mesDeviceAlarm/listData',
  830. type: 'POST',
  831. dataType: 'json',
  832. data: {
  833. pageNo: 1,
  834. pageSize: 50
  835. },
  836. success: function(resp) {
  837. var rows = extractUserList(resp);
  838. var alerts = [];
  839. for (var i = 0; i < rows.length; i++) {
  840. var r = rows[i] || {};
  841. var typ = (r.type != null && r.type !== undefined) ? String(r.type) : '';
  842. alerts.push({ no: i + 1, text: typ });
  843. }
  844. mockData.alerts = alerts;
  845. renderAlerts();
  846. },
  847. error: function() {
  848. mockData.alerts = [];
  849. renderAlerts();
  850. }
  851. });
  852. }
  853. function updateProductionPeopleByApi() {
  854. $.ajax({
  855. url: '${ctx}/sys/empUser/listData',
  856. type: 'POST',
  857. dataType: 'json',
  858. data: {
  859. pageNo: 1,
  860. pageSize: 5000,
  861. 'employee.office.officeName': '生产部'
  862. },
  863. success: function(resp) {
  864. var peopleCount = 0;
  865. var users = extractUserList(resp);
  866. for (var i = 0; i < users.length; i++) {
  867. var u = users[i] || {};
  868. var office = (u.employee && u.employee.office) ? u.employee.office : {};
  869. var officeName = office.officeName || '';
  870. var normalizedOfficeName = (officeName + '').replace(/\s/g, '');
  871. if (
  872. normalizedOfficeName === '生产部' ||
  873. /[/\\>]+生产部$/.test(normalizedOfficeName)
  874. ) {
  875. peopleCount++;
  876. }
  877. }
  878. mockData.cards = mockData.cards || {};
  879. mockData.cards.productionPeople = peopleCount;
  880. var target = document.getElementById('productionPeople');
  881. if (target) target.textContent = peopleCount;
  882. }
  883. });
  884. }
  885. function updateDeviceCountByApi() {
  886. $.ajax({
  887. url: '${ctx}/mes/mesDevice/listData',
  888. type: 'POST',
  889. dataType: 'json',
  890. data: {
  891. pageNo: 1,
  892. pageSize: 1
  893. },
  894. success: function(resp) {
  895. var deviceCount = 0;
  896. if (resp && typeof resp.count === 'number') {
  897. deviceCount = resp.count;
  898. } else {
  899. var devices = extractUserList(resp);
  900. deviceCount = devices.length;
  901. }
  902. mockData.cards = mockData.cards || {};
  903. mockData.cards.deviceCount = deviceCount;
  904. var target = document.getElementById('deviceCount');
  905. if (target) target.textContent = deviceCount;
  906. }
  907. });
  908. }
  909. function isOpr190Family(oprno) {
  910. var op = (oprno || '').replace(/\s/g, '').toUpperCase();
  911. if (op.length < 5 || op.indexOf('OP190') !== 0) {
  912. return false;
  913. }
  914. if (op.length === 5) {
  915. return op === 'OP190';
  916. }
  917. return /^OP190[A-Z0-9_-]+$/.test(op);
  918. }
  919. function isRecordContentOk(content) {
  920. var c = (content || '').replace(/\s/g, '').toUpperCase();
  921. return c === 'OK';
  922. }
  923. function applyActualProductionCount(n) {
  924. mockData.cards = mockData.cards || {};
  925. mockData.cards.actualProduction = n;
  926. var target = document.getElementById('actualProduction');
  927. if (target) target.textContent = n;
  928. }
  929. function getCalendarDayUpdateDateRange() {
  930. var now = new Date();
  931. var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
  932. var dateStr = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
  933. return {
  934. gte: dateStr + ' 00:00:00',
  935. lte: dateStr + ' 23:59:59',
  936. startMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(),
  937. endMs: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime()
  938. };
  939. }
  940. function parseRecordDateMs(val) {
  941. if (val == null || val === '') {
  942. return NaN;
  943. }
  944. if (typeof val === 'number' && !isNaN(val)) {
  945. return val;
  946. }
  947. var s = String(val).trim().replace(/-/g, '/');
  948. var t = Date.parse(s);
  949. return isNaN(t) ? NaN : t;
  950. }
  951. function isRecordWithinToday(r, day) {
  952. var t = parseRecordDateMs(r.updateDate || r.update_date);
  953. if (isNaN(t)) {
  954. return false;
  955. }
  956. return t >= day.startMs && t <= day.endMs;
  957. }
  958. function updateActualProductionByApi() {
  959. var dayRange = getCalendarDayUpdateDateRange();
  960. var pageSize = 3000;
  961. var maxPages = 200;
  962. var pageNo = 1;
  963. var totalOk = 0;
  964. function fetchPage() {
  965. $.ajax({
  966. url: '${ctx}/mes/mesProductRecord/listData',
  967. type: 'POST',
  968. dataType: 'json',
  969. data: {
  970. pageNo: pageNo,
  971. pageSize: pageSize,
  972. oprno: 'OP190',
  973. content: 'OK',
  974. lineSn: 'MP',
  975. updateDate_gte: dayRange.gte,
  976. updateDate_lte: dayRange.lte
  977. },
  978. success: function(resp) {
  979. var list = extractUserList(resp);
  980. for (var i = 0; i < list.length; i++) {
  981. var r = list[i] || {};
  982. if (isOpr190Family(r.oprno) && isRecordContentOk(r.content) && isRecordWithinToday(r, dayRange) && String(r.lineSn || r.line_sn || '').toUpperCase() === 'MP') {
  983. totalOk++;
  984. }
  985. }
  986. if (list.length === pageSize && pageNo < maxPages) {
  987. pageNo++;
  988. fetchPage();
  989. } else {
  990. applyActualProductionCount(totalOk);
  991. }
  992. },
  993. error: function() {
  994. applyActualProductionCount(totalOk);
  995. }
  996. });
  997. }
  998. fetchPage();
  999. }
  1000. function drawAllCharts() {
  1001. var q = mockData.qualityInspect;
  1002. if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
  1003. var t = mockData.productTrend;
  1004. if (t) drawLineChart('chartProduct', t.data, t.labels, t.max, t.min, '#00C8FF');
  1005. var a = mockData.achieveRate;
  1006. if (a) drawDualLine('chartAchieve', a.target, a.actual, a.labels, a.max, a.min);
  1007. var hr = mockData.hourly;
  1008. if (hr) drawAreaLine('chartHourly', hr.up, hr.down, hr.labels, hr.max);
  1009. var mo = mockData.monthly;
  1010. if (mo) drawMonthBar('chartMonth', mo.data, mo.labels, mo.max);
  1011. drawGauge('gaugeRate', mockData.rate || 0);
  1012. }
  1013. function refreshData() {
  1014. generateMockData();
  1015. renderAlerts();
  1016. renderTable();
  1017. renderCards();
  1018. updateLineProcessTableByApi();
  1019. updateProductionPeopleByApi();
  1020. updateDeviceCountByApi();
  1021. updateActualProductionByApi();
  1022. updateDeviceAlarmAlertsByApi();
  1023. drawAllCharts();
  1024. }
  1025. function changeZoom() {
  1026. var wb = $(window).width() / 1920;
  1027. var hb = $(window).height() / 1080;
  1028. $('body').css({ 'zoom': wb >= hb ? hb : wb });
  1029. }
  1030. $(window).resize(function() {
  1031. changeZoom();
  1032. drawAllCharts();
  1033. });
  1034. $(function() {
  1035. changeZoom();
  1036. updateClock();
  1037. setInterval(updateClock, 1000);
  1038. refreshData();
  1039. // 定期刷新数据 (2 分钟)
  1040. setInterval(refreshData, 1000 * 60 * 2);
  1041. // 每秒重绘 (应对 zoom / resize)
  1042. setInterval(drawAllCharts, 1000);
  1043. });
  1044. })();
  1045. </script>
  1046. </body>
  1047. </html>