mesScreen5.html 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346
  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: 110px;
  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. .mes-fullscreen-btn {
  72. position: absolute;
  73. top: 28px;
  74. right: 18px;
  75. z-index: 30;
  76. padding: 6px 14px;
  77. font-size: 14px;
  78. line-height: 1.2;
  79. color: #4DC9F5;
  80. background: rgba(0, 20, 50, 0.65);
  81. border: 1px solid #4DC9F5;
  82. border-radius: 4px;
  83. cursor: pointer;
  84. font-family: "Microsoft YaHei", sans-serif;
  85. }
  86. .mes-fullscreen-btn:hover {
  87. color: #FFFFFF;
  88. background: rgba(77, 201, 245, 0.28);
  89. }
  90. html:fullscreen .mes-fullscreen-btn,
  91. html:-webkit-full-screen .mes-fullscreen-btn {
  92. display: none !important;
  93. }
  94. /* ===== 面板通用 - 使用自定义类名避免与 Bootstrap 的 .panel 冲突 ===== */
  95. .mes4-panel {
  96. position: absolute;
  97. overflow: hidden;
  98. background: transparent;
  99. border: none;
  100. box-shadow: none;
  101. }
  102. .mes4-panel .mes4-ptitle {
  103. height: 42px;
  104. line-height: 42px;
  105. text-align: center;
  106. font-size: 18px;
  107. font-weight: bold;
  108. color: #4DC9F5;
  109. letter-spacing: 1px;
  110. background: transparent;
  111. }
  112. .mes4-panel .mes4-pbody {
  113. position: absolute;
  114. top: 42px;
  115. left: 0;
  116. right: 0;
  117. bottom: 0;
  118. padding: 5px 10px;
  119. background: transparent;
  120. }
  121. .chart { width: 100%; height: 100%; display: block; }
  122. /* ===== 位置:与 screen3 高度规则一致 ===== */
  123. .p-alert { left: 36px; top: 118px; width: 440px; height: 430px; }
  124. .p-quality { left: 36px; top: 568px; width: 440px; height: 460px; }
  125. .p-product { display: none; }
  126. .p-table { left: 496px; top: 118px; width: 928px; height: 560px; }
  127. .p-month { display: none; }
  128. .p-overview { right: 30px; top: 130px; width: 430px; height: 290px; }
  129. .p-achieve { display: none; }
  130. .p-hourly { right: 36px; top: 568px; width: 440px; height: 460px; }
  131. .p-alert .mes4-ptitle,
  132. .p-overview .mes4-ptitle,
  133. .p-quality .mes4-ptitle,
  134. .p-hourly .mes4-ptitle {
  135. height: 40px;
  136. line-height: 40px;
  137. font-size: 16px;
  138. color: #4DC9F5;
  139. font-weight: bold;
  140. letter-spacing: 1px;
  141. }
  142. .p-alert .mes4-pbody { top: 40px; padding: 0 6px 8px; }
  143. .p-overview .mes4-pbody { top: 40px; padding: 0; }
  144. .p-table .mes4-pbody { top: 8px; padding: 8px 28px 12px; }
  145. .p-quality .mes4-pbody,
  146. .p-hourly .mes4-pbody { top: 68px; }
  147. /* ===== 报警信息 ===== */
  148. .alert-head {
  149. display: flex;
  150. align-items: center;
  151. height: 36px;
  152. padding: 0 22px;
  153. color: #4DC9F5;
  154. font-size: 14px;
  155. font-weight: bold;
  156. border-bottom: 1px solid rgba(77, 201, 245, 0.3);
  157. }
  158. .alert-head > div:nth-child(1) { width: 70px; }
  159. .alert-head > div:nth-child(2) { flex: 1; }
  160. .alert-row {
  161. display: flex;
  162. align-items: center;
  163. height: 28px;
  164. padding: 0 22px;
  165. color: #ffffff;
  166. font-size: 13px;
  167. }
  168. .alert-row > div:nth-child(1) { width: 70px; color: #4DC9F5; font-weight: bold; }
  169. .alert-row > div:nth-child(2) { flex: 1; }
  170. /* ===== OP 表格(6 工位铺满,字号加大) ===== */
  171. .op-table {
  172. width: 100%;
  173. height: calc(100% - 28px);
  174. border-collapse: collapse;
  175. color: #ffffff;
  176. font-size: 20px;
  177. table-layout: fixed;
  178. }
  179. .op-table thead th {
  180. height: 56px;
  181. color: #4DC9F5;
  182. font-weight: bold;
  183. font-size: 20px;
  184. letter-spacing: 1px;
  185. background: rgba(0, 100, 180, 0.28);
  186. border-bottom: 2px solid rgba(77, 201, 245, 0.5);
  187. text-align: center;
  188. }
  189. .op-table tbody td {
  190. height: 76px;
  191. text-align: center;
  192. border-bottom: 1px solid rgba(77, 201, 245, 0.22);
  193. vertical-align: middle;
  194. }
  195. .op-table tbody tr:last-child td {
  196. border-bottom: 2px solid rgba(77, 201, 245, 0.35);
  197. }
  198. .op-table tbody tr:hover td { background: rgba(0, 200, 255, 0.06); }
  199. .op-table tbody tr.op-row-alt td {
  200. background: rgba(0, 80, 140, 0.14);
  201. }
  202. .op-table tbody tr.op-row-alt:hover td { background: rgba(0, 200, 255, 0.08); }
  203. .op-table tbody tr.op-row-empty td {
  204. height: 76px;
  205. color: transparent;
  206. border-bottom: 1px solid rgba(77, 201, 245, 0.12);
  207. user-select: none;
  208. }
  209. .op-table-page-hint {
  210. text-align: right;
  211. height: 26px;
  212. line-height: 26px;
  213. font-size: 15px;
  214. color: #4DC9F5;
  215. padding-right: 4px;
  216. margin-bottom: 2px;
  217. letter-spacing: 1px;
  218. }
  219. .op-table td.op-col { color: #00C8FF; font-weight: bold; font-size: 26px; }
  220. .op-table td.op-name { color: #ffffff; text-align: center; font-size: 22px; }
  221. .op-table td.op-val { color: #00F7FF; font-weight: bold; font-size: 28px; }
  222. /* ===== 产线阶段数据总览 (按模板排布) ===== */
  223. .overview-wrap {
  224. position: relative;
  225. height: 100%;
  226. padding: 14px 18px;
  227. }
  228. .overview-cards {
  229. display: grid;
  230. grid-template-columns: 1fr 1fr;
  231. gap: 14px 170px;
  232. height: 100%;
  233. }
  234. .overview-card {
  235. background: transparent;
  236. display: flex;
  237. flex-direction: column;
  238. align-items: center;
  239. justify-content: center;
  240. padding: 4px;
  241. padding-top: 19px;
  242. }
  243. .overview-card .oc-label {
  244. color: #4DC9F5;
  245. font-size: 14px;
  246. margin-bottom: 4px;
  247. }
  248. .overview-card .oc-value {
  249. color: #00F7FF;
  250. font-size: 24px;
  251. font-weight: bold;
  252. line-height: 1;
  253. }
  254. .overview-gauge-wrap {
  255. display: none;
  256. }
  257. .overview-gauge-wrap canvas { width: 100%; height: 100%; }
  258. .overview-gauge-wrap .gauge-txt {
  259. position: absolute;
  260. top: 50%;
  261. left: 50%;
  262. transform: translate(-50%, -50%);
  263. text-align: center;
  264. color: #00F7FF;
  265. pointer-events: none;
  266. }
  267. .overview-gauge-wrap .gauge-txt .val {
  268. font-size: 30px;
  269. font-weight: bold;
  270. line-height: 1;
  271. }
  272. .overview-gauge-wrap .gauge-txt .lbl {
  273. font-size: 13px;
  274. color: #ffffff;
  275. margin-top: 6px;
  276. }
  277. /* ===== 图例 ===== */
  278. .legend-inline {
  279. display: flex;
  280. align-items: center;
  281. justify-content: flex-end;
  282. padding: 4px 18px 0;
  283. font-size: 13px;
  284. color: #ffffff;
  285. gap: 18px;
  286. }
  287. .legend-inline .dot {
  288. display: inline-block;
  289. width: 12px;
  290. height: 12px;
  291. border-radius: 50%;
  292. margin-right: 5px;
  293. vertical-align: middle;
  294. }
  295. </style>
  296. </head>
  297. <body>
  298. <div class="mes4-wrapper" id="app">
  299. <div class="mes4-header">
  300. <img class="mes4-logo" src="${ctxStatic}/screen/imgs/ljrLogo.png" alt="logo">
  301. <div class="mes4-title">机加阶段统计大屏</div>
  302. <div class="mes4-datetime">
  303. <span class="mes4-date" id="mes4Date"></span>
  304. <span class="mes4-time" id="mes4Time"></span>
  305. </div>
  306. <button type="button" class="mes-fullscreen-btn" id="btnFullscreen" title="全屏">全屏</button>
  307. </div>
  308. <div class="mes4-panel p-alert">
  309. <div class="mes4-ptitle"> 产线阶段报警信息 </div>
  310. <div class="mes4-pbody">
  311. <div class="alert-head"><div>序号</div><div>工位 / 报警类型</div></div>
  312. <div id="alertList"></div>
  313. </div>
  314. </div>
  315. <div class="mes4-panel p-quality">
  316. <div class="mes4-ptitle"> 近七日产品质检数据分析 </div>
  317. <div class="legend-inline">
  318. <span><i class="dot" style="background:#00FF88"></i>产出</span>
  319. <span><i class="dot" style="background:#00C8FF"></i>不良</span>
  320. </div>
  321. <div class="mes4-pbody"><canvas id="chartQuality" class="chart"></canvas></div>
  322. </div>
  323. <div class="mes4-panel p-product">
  324. <div class="mes4-ptitle"> 近七日产量分析 </div>
  325. <div class="mes4-pbody"><canvas id="chartProduct" class="chart"></canvas></div>
  326. </div>
  327. <div class="mes4-panel p-table">
  328. <div class="mes4-pbody">
  329. <div class="op-table-page-hint" id="opTablePageHint"></div>
  330. <table class="op-table">
  331. <thead>
  332. <tr>
  333. <th style="width:160px">工位</th>
  334. <th style="width:300px">工位名称</th>
  335. <th>当天产量</th>
  336. <th>当班产量</th>
  337. <th>上班产量</th>
  338. </tr>
  339. </thead>
  340. <tbody id="opTableBody"></tbody>
  341. </table>
  342. </div>
  343. </div>
  344. <div class="mes4-panel p-month">
  345. <div class="mes4-ptitle">各月产量统计图</div>
  346. <div class="mes4-pbody"><canvas id="chartMonth" class="chart"></canvas></div>
  347. </div>
  348. <div class="mes4-panel p-overview">
  349. <div class="mes4-ptitle"> 产线阶段数据总览 </div>
  350. <div class="mes4-pbody" style="padding:0">
  351. <div class="overview-wrap">
  352. <div class="overview-cards">
  353. <div class="overview-card">
  354. <div class="oc-label">生产人数</div>
  355. <div class="oc-value"><span id="productionPeople">48</span></div>
  356. </div>
  357. <div class="overview-card">
  358. <div class="oc-label">设备数量</div>
  359. <div class="oc-value"><span id="deviceCount">56</span></div>
  360. </div>
  361. <div class="overview-card">
  362. <div class="oc-label">计划产量</div>
  363. <div class="oc-value"><span id="planProduction">221</span></div>
  364. </div>
  365. <div class="overview-card">
  366. <div class="oc-label">实际产量</div>
  367. <div class="oc-value"><span id="actualProduction">201</span></div>
  368. </div>
  369. </div>
  370. </div>
  371. </div>
  372. </div>
  373. <div class="mes4-panel p-achieve">
  374. <div class="mes4-ptitle"> 近七日达成率数据分析 </div>
  375. <div class="legend-inline">
  376. <span><i class="dot" style="background:#FFDD00"></i>目标</span>
  377. <span><i class="dot" style="background:#00C8FF"></i>实际</span>
  378. </div>
  379. <div class="mes4-pbody" style="top:62px"><canvas id="chartAchieve" class="chart"></canvas></div>
  380. </div>
  381. <div class="mes4-panel p-hourly">
  382. <div class="mes4-ptitle"> 近12小时上下线数据分析 </div>
  383. <div class="legend-inline">
  384. <span><i class="dot" style="background:#00FF88"></i>上线</span>
  385. <span><i class="dot" style="background:#00C8FF"></i>下线</span>
  386. </div>
  387. <div class="mes4-pbody"><canvas id="chartHourly" class="chart"></canvas></div>
  388. </div>
  389. </div>
  390. <script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
  391. <script>
  392. (function() {
  393. var __screenFail = 0, __screenReloadAt = 0;
  394. $(document).ajaxError(function(e, xhr) {
  395. if (!xhr) return;
  396. var text = xhr.responseText || '';
  397. if (xhr.status === 0 || xhr.status === 401 || xhr.status === 403 || xhr.status === 502 || xhr.status === 503
  398. || text.indexOf('login') >= 0 || text.indexOf('登录') >= 0) {
  399. __screenFail++;
  400. if (__screenFail >= 3 && Date.now() - __screenReloadAt > 60000) {
  401. __screenReloadAt = Date.now();
  402. location.reload();
  403. }
  404. }
  405. });
  406. $(document).ajaxSuccess(function() { __screenFail = 0; });
  407. function pad(n) { return n < 10 ? '0' + n : '' + n; }
  408. /** 工位表:每页条数与自动翻页间隔(毫秒) */
  409. var OP_TABLE_PAGE_SIZE = 6;
  410. var OP_TABLE_PAGE_ROTATE_MS = 8000;
  411. var opTablePageIndex = 0;
  412. var opTableRotateTimer = null;
  413. function padOpTableRowsToPageSize(html, rowCount) {
  414. for (var j = rowCount; j < OP_TABLE_PAGE_SIZE; j++) {
  415. var alt = (j % 2 === 1) ? ' op-row-alt' : '';
  416. html += '<tr class="op-row-empty' + alt + '"><td colspan="5">&nbsp;</td></tr>';
  417. }
  418. return html;
  419. }
  420. function getOpTablePageCount(list) {
  421. list = list || [];
  422. if (list.length === 0) {
  423. return 1;
  424. }
  425. return Math.ceil(list.length / OP_TABLE_PAGE_SIZE);
  426. }
  427. function stopOpTablePageRotate() {
  428. if (opTableRotateTimer) {
  429. clearInterval(opTableRotateTimer);
  430. opTableRotateTimer = null;
  431. }
  432. }
  433. function startOpTablePageRotate() {
  434. stopOpTablePageRotate();
  435. var listNow = mockData.opTable || [];
  436. if (getOpTablePageCount(listNow) <= 1) {
  437. return;
  438. }
  439. opTableRotateTimer = setInterval(function() {
  440. var list = mockData.opTable || [];
  441. var pageCount = getOpTablePageCount(list);
  442. if (pageCount <= 1) {
  443. return;
  444. }
  445. opTablePageIndex = (opTablePageIndex + 1) % pageCount;
  446. renderTable();
  447. }, OP_TABLE_PAGE_ROTATE_MS);
  448. }
  449. function updateClock() {
  450. var now = new Date();
  451. var y = now.getFullYear();
  452. var m = now.getMonth() + 1;
  453. var d = now.getDate();
  454. var w = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][now.getDay()];
  455. document.getElementById('mes4Date').textContent = y + '年' + pad(m) + '月' + pad(d) + '日 ' + w;
  456. var h = now.getHours(), mi = now.getMinutes(), s = now.getSeconds();
  457. document.getElementById('mes4Time').textContent = pad(h) + ':' + pad(mi) + ':' + pad(s);
  458. }
  459. // ====== 页面数据 ======
  460. var mockData = {};
  461. /** 本屏产线:XT(工位 OP110-OP160) */
  462. var LINE_SN = 'XT';
  463. var OP_FROM = 110;
  464. var OP_TO = 160;
  465. /** 大屏配置类型:机加 */
  466. var SCREEN_TYPE = 'JJ';
  467. /** 每小时上下线:上线 OP110 / 下线 OP160 */
  468. var HOURLY_API_CFG = { lineSn: LINE_SN, onlineOprno: 'OP110', offlineOprno: 'OP160' };
  469. function buildRecentHourLabels12() {
  470. var labels = [];
  471. var nowH = new Date().getHours();
  472. for (var i = 11; i >= 0; i--) {
  473. labels.push(String((nowH - i + 24) % 24));
  474. }
  475. return labels;
  476. }
  477. function updateHourlyUpDownByApi() {
  478. $.ajax({
  479. url: '${ctx}/mes/mesProduct/hourlyUpDownOkData',
  480. type: 'POST',
  481. dataType: 'json',
  482. data: {
  483. lineSn: HOURLY_API_CFG.lineSn,
  484. onlineOprno: HOURLY_API_CFG.onlineOprno,
  485. offlineOprno: HOURLY_API_CFG.offlineOprno,
  486. recentHours: 12
  487. },
  488. success: function(resp) {
  489. if (resp && resp.result === 'true' && resp.data) {
  490. var d = resp.data;
  491. if (!d.up || !d.down || d.up.length < 1) {
  492. return;
  493. }
  494. mockData.hourly = {
  495. labels: d.labels && d.labels.length ? d.labels : buildRecentHourLabels12(),
  496. up: d.up,
  497. down: d.down,
  498. max: 4
  499. };
  500. drawAllCharts();
  501. }
  502. }
  503. });
  504. }
  505. function generateMockData() {
  506. // 最近7日日期
  507. var now = new Date();
  508. var weekLabels = [];
  509. for (var i = 6; i >= 0; i--) {
  510. var d = new Date(now);
  511. d.setDate(d.getDate() - i);
  512. weekLabels.push((d.getMonth() + 1) + '月' + d.getDate() + '日');
  513. }
  514. // 报警信息由 updateDeviceAlarmAlertsByApi 拉取
  515. mockData.alerts = [];
  516. // 工位表由 mesLineProcess(XT,OP110-OP160)拉取
  517. mockData.opTable = [];
  518. // 数据卡(总览固定展示)
  519. mockData.cards = {
  520. productionPeople: 48,
  521. deviceCount: 56,
  522. planProduction: 221,
  523. actualProduction: 201
  524. };
  525. mockData.rate = 70;
  526. // 近七日产品质检
  527. mockData.qualityInspect = {
  528. labels: weekLabels,
  529. produced: [400, 321, 354, 164, 241, 342, 231],
  530. defect: [345, 256, 312, 123, 214, 286, 189],
  531. max: 500
  532. };
  533. // 近七日达成率
  534. mockData.achieveRate = {
  535. labels: weekLabels,
  536. target: [1150, 1160, 1180, 1165, 1180, 1180, 1170],
  537. actual: [1150, 1160, 1180, 1165, 1200, 1200, 1170],
  538. max: 1300, min: 1100
  539. };
  540. // 近七日产量
  541. mockData.productTrend = {
  542. labels: weekLabels,
  543. data: [1100, 1100, 1200, 1125, 1125, 1180, 1150],
  544. max: 1300, min: 1000
  545. };
  546. // 近12小时上下线
  547. var hourLabels = buildRecentHourLabels12(), up = [], down = [];
  548. for (var i = 0; i < 12; i++) {
  549. up.push(Math.round(Math.random() * 4));
  550. down.push(Math.round(Math.random() * 4));
  551. }
  552. mockData.hourly = { labels: hourLabels, up: up, down: down, max: 4 };
  553. // 各月产量
  554. mockData.monthly = {
  555. labels: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
  556. data: [ 350, 340, 480, 360, 350, 360, 420, 410, 380, 420, 470, 400 ],
  557. max: 500
  558. };
  559. }
  560. // ====== 工具函数 ======
  561. function hexToRgba(hex, a) {
  562. var r = parseInt(hex.slice(1, 3), 16),
  563. g = parseInt(hex.slice(3, 5), 16),
  564. b = parseInt(hex.slice(5, 7), 16);
  565. return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
  566. }
  567. function prepareCanvas(id) {
  568. var c = document.getElementById(id);
  569. if (!c) return null;
  570. var p = c.parentElement;
  571. var w = p.clientWidth, h = p.clientHeight;
  572. if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
  573. return { ctx: c.getContext('2d'), w: w, h: h };
  574. }
  575. function drawYAxis(ctx, padL, padT, cw, ch, maxV, minV) {
  576. minV = minV || 0;
  577. ctx.fillStyle = 'rgba(77,201,245,0.8)';
  578. ctx.font = '11px Microsoft YaHei';
  579. ctx.textAlign = 'right';
  580. ctx.strokeStyle = 'rgba(0,143,253,0.18)';
  581. ctx.lineWidth = 1;
  582. for (var i = 0; i <= 4; i++) {
  583. var val = Math.round(minV + (maxV - minV) * (4 - i) / 4);
  584. var y = padT + ch * i / 4;
  585. ctx.fillText(val, padL - 5, y + 4);
  586. ctx.beginPath();
  587. ctx.moveTo(padL, y);
  588. ctx.lineTo(padL + cw, y);
  589. ctx.stroke();
  590. }
  591. }
  592. // 双柱状图 - 近七日产品质检 (产出/不良) / 每小时上下线
  593. function drawDoubleBar(id, a, b, labels, maxV) {
  594. var p = prepareCanvas(id); if (!p) return;
  595. var ctx = p.ctx, w = p.w, h = p.h;
  596. var padL = 38, padR = 10, padT = 12, padB = 22;
  597. var cw = w - padL - padR, ch = h - padT - padB;
  598. ctx.clearRect(0, 0, w, h);
  599. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  600. if (!a || a.length < 1) return;
  601. var n = a.length;
  602. var dense = n > 12;
  603. var gw = cw / n, bw = gw * (dense ? 0.32 : 0.3);
  604. var labelStep = dense ? 2 : 1;
  605. var valFont = dense ? 'bold 9px Microsoft YaHei' : 'bold 10px Microsoft YaHei';
  606. var lblFont = dense ? '10px Microsoft YaHei' : '11px Microsoft YaHei';
  607. for (var i = 0; i < n; i++) {
  608. var cx = padL + i * gw + gw / 2;
  609. var h1 = (a[i] / maxV) * ch;
  610. var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
  611. g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
  612. ctx.fillStyle = g1;
  613. ctx.fillRect(cx - bw - 2, padT + ch - h1, bw, h1);
  614. var h2 = ((b && b[i] != null ? b[i] : 0) / maxV) * ch;
  615. var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
  616. g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
  617. ctx.fillStyle = g2;
  618. ctx.fillRect(cx + 2, padT + ch - h2, bw, h2);
  619. if (!dense || a[i] > 0) {
  620. ctx.fillStyle = '#00FF88'; ctx.font = valFont;
  621. ctx.textAlign = 'center'; ctx.fillText(a[i], cx - bw / 2 - 2, padT + ch - h1 - 4);
  622. }
  623. if (!dense || (b && b[i] > 0)) {
  624. ctx.fillStyle = '#00C8FF'; ctx.font = valFont;
  625. ctx.textAlign = 'center';
  626. ctx.fillText(b && b[i] != null ? b[i] : 0, cx + bw / 2 + 2, padT + ch - h2 - 4);
  627. }
  628. if (i % labelStep === 0) {
  629. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = lblFont;
  630. ctx.textAlign = 'center';
  631. ctx.fillText(labels[i], cx, h - 6);
  632. }
  633. }
  634. }
  635. // 近12小时上下线双柱:纵轴 0–4,横坐标/柱顶数值与柱组中心对齐
  636. function drawHourlyDoubleBar(id, a, b, labels) {
  637. var p = prepareCanvas(id); if (!p) return;
  638. var ctx = p.ctx, w = p.w, h = p.h;
  639. var maxV = 4;
  640. var padL = 32, padR = 8, padT = 16, padB = 26;
  641. var cw = w - padL - padR, ch = h - padT - padB;
  642. ctx.clearRect(0, 0, w, h);
  643. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  644. if (!a || a.length < 1) return;
  645. var n = a.length;
  646. var gw = cw / n;
  647. var bw = Math.max(3, Math.min(14, gw * 0.28));
  648. var gap = 2;
  649. ctx.textAlign = 'center';
  650. for (var i = 0; i < n; i++) {
  651. var cx = padL + (i + 0.5) * gw;
  652. var v1 = Number(a[i]) || 0;
  653. var v2 = Number(b && b[i] != null ? b[i] : 0) || 0;
  654. var h1 = (Math.min(v1, maxV) / maxV) * ch;
  655. var h2 = (Math.min(v2, maxV) / maxV) * ch;
  656. var x1 = cx - gap / 2 - bw;
  657. var x2 = cx + gap / 2;
  658. var g1 = ctx.createLinearGradient(0, padT + ch - h1, 0, padT + ch);
  659. g1.addColorStop(0, '#00FF88'); g1.addColorStop(1, 'rgba(0,255,136,0.15)');
  660. ctx.fillStyle = g1;
  661. ctx.fillRect(x1, padT + ch - h1, bw, h1);
  662. var g2 = ctx.createLinearGradient(0, padT + ch - h2, 0, padT + ch);
  663. g2.addColorStop(0, '#00C8FF'); g2.addColorStop(1, 'rgba(0,200,255,0.15)');
  664. ctx.fillStyle = g2;
  665. ctx.fillRect(x2, padT + ch - h2, bw, h2);
  666. ctx.font = 'bold 11px Microsoft YaHei';
  667. ctx.fillStyle = '#00FF88';
  668. ctx.fillText(String(v1), x1 + bw / 2, padT + ch - h1 - 4);
  669. ctx.fillStyle = '#00C8FF';
  670. ctx.fillText(String(v2), x2 + bw / 2, padT + ch - h2 - 4);
  671. ctx.fillStyle = 'rgba(77,201,245,0.95)';
  672. ctx.font = '12px Microsoft YaHei';
  673. ctx.fillText(labels[i] != null ? String(labels[i]) : String(i), cx, h - 6);
  674. }
  675. }
  676. // 单折线图 (近七日产量, 平滑+面积)
  677. function drawLineChart(id, data, labels, maxV, minV, color) {
  678. var p = prepareCanvas(id); if (!p) return;
  679. var ctx = p.ctx, w = p.w, h = p.h;
  680. var padL = 45, padR = 12, padT = 15, padB = 25;
  681. var cw = w - padL - padR, ch = h - padT - padB;
  682. color = color || '#00C8FF';
  683. minV = minV || 0;
  684. ctx.clearRect(0, 0, w, h);
  685. drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
  686. if (!data || data.length < 2) return;
  687. var stepX = cw / (data.length - 1);
  688. var range = maxV - minV;
  689. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  690. grad.addColorStop(0, hexToRgba(color, 0.4));
  691. grad.addColorStop(1, hexToRgba(color, 0.02));
  692. ctx.beginPath();
  693. ctx.moveTo(padL, padT + ch);
  694. for (var i = 0; i < data.length; i++) {
  695. var x = padL + i * stepX;
  696. var y = padT + ch - ((data[i] - minV) / range) * ch;
  697. if (i === 0) ctx.lineTo(x, y);
  698. else {
  699. var px = padL + (i - 1) * stepX;
  700. var py = padT + ch - ((data[i - 1] - minV) / range) * ch;
  701. var cpx = (px + x) / 2;
  702. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  703. }
  704. }
  705. ctx.lineTo(padL + (data.length - 1) * stepX, padT + ch);
  706. ctx.closePath();
  707. ctx.fillStyle = grad;
  708. ctx.fill();
  709. ctx.beginPath();
  710. for (var j = 0; j < data.length; j++) {
  711. var x2 = padL + j * stepX;
  712. var y2 = padT + ch - ((data[j] - minV) / range) * ch;
  713. if (j === 0) ctx.moveTo(x2, y2);
  714. else {
  715. var px2 = padL + (j - 1) * stepX;
  716. var py2 = padT + ch - ((data[j - 1] - minV) / range) * ch;
  717. var cpx2 = (px2 + x2) / 2;
  718. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  719. }
  720. }
  721. ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
  722. ctx.font = 'bold 11px Microsoft YaHei'; ctx.textAlign = 'center';
  723. for (var k = 0; k < data.length; k++) {
  724. var x3 = padL + k * stepX;
  725. var y3 = padT + ch - ((data[k] - minV) / range) * ch;
  726. ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
  727. ctx.fillStyle = color; ctx.fill();
  728. ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
  729. ctx.fillStyle = '#ffffff';
  730. ctx.fillText(data[k], x3, y3 - 8);
  731. }
  732. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '11px Microsoft YaHei';
  733. for (var m = 0; m < labels.length; m++) {
  734. var xx = padL + m * stepX;
  735. ctx.fillText(labels[m], xx, h - 6);
  736. }
  737. }
  738. // 双折线图 (目标/实际)
  739. function drawDualLine(id, a, b, labels, maxV, minV) {
  740. var p = prepareCanvas(id); if (!p) return;
  741. var ctx = p.ctx, w = p.w, h = p.h;
  742. var padL = 45, padR = 12, padT = 12, padB = 22;
  743. var cw = w - padL - padR, ch = h - padT - padB;
  744. minV = minV || 0;
  745. var range = maxV - minV;
  746. ctx.clearRect(0, 0, w, h);
  747. drawYAxis(ctx, padL, padT, cw, ch, maxV, minV);
  748. if (!a || a.length < 2) return;
  749. var stepX = cw / (a.length - 1);
  750. function draw(arr, color) {
  751. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  752. grad.addColorStop(0, hexToRgba(color, 0.3));
  753. grad.addColorStop(1, hexToRgba(color, 0.02));
  754. ctx.beginPath(); ctx.moveTo(padL, padT + ch);
  755. for (var i = 0; i < arr.length; i++) {
  756. var x = padL + i * stepX;
  757. var y = padT + ch - ((arr[i] - minV) / range) * ch;
  758. if (i === 0) ctx.lineTo(x, y);
  759. else {
  760. var px = padL + (i - 1) * stepX;
  761. var py = padT + ch - ((arr[i - 1] - minV) / range) * ch;
  762. var cpx = (px + x) / 2;
  763. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  764. }
  765. }
  766. ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
  767. ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
  768. ctx.beginPath();
  769. for (var j = 0; j < arr.length; j++) {
  770. var x2 = padL + j * stepX;
  771. var y2 = padT + ch - ((arr[j] - minV) / range) * ch;
  772. if (j === 0) ctx.moveTo(x2, y2);
  773. else {
  774. var px2 = padL + (j - 1) * stepX;
  775. var py2 = padT + ch - ((arr[j - 1] - minV) / range) * ch;
  776. var cpx2 = (px2 + x2) / 2;
  777. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  778. }
  779. }
  780. ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
  781. for (var k = 0; k < arr.length; k++) {
  782. var x3 = padL + k * stepX;
  783. var y3 = padT + ch - ((arr[k] - minV) / range) * ch;
  784. ctx.beginPath(); ctx.arc(x3, y3, 3, 0, Math.PI * 2);
  785. ctx.fillStyle = color; ctx.fill();
  786. }
  787. }
  788. draw(a, '#FFDD00');
  789. draw(b, '#00C8FF');
  790. ctx.fillStyle = '#ffffff'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
  791. for (var i = 0; i < b.length; i++) {
  792. var x = padL + i * stepX;
  793. var y = padT + ch - ((b[i] - minV) / range) * ch;
  794. ctx.fillText(b[i], x, y - 6);
  795. }
  796. ctx.fillStyle = 'rgba(77,201,245,0.9)';
  797. for (var i2 = 0; i2 < labels.length; i2++) {
  798. var x2 = padL + i2 * stepX;
  799. ctx.fillText(labels[i2], x2, h - 6);
  800. }
  801. }
  802. // 双面积折线图 (每小时上下线)
  803. function drawAreaLine(id, up, down, labels, maxV) {
  804. var p = prepareCanvas(id); if (!p) return;
  805. var ctx = p.ctx, w = p.w, h = p.h;
  806. var padL = 30, padR = 12, padT = 12, padB = 22;
  807. var cw = w - padL - padR, ch = h - padT - padB;
  808. ctx.clearRect(0, 0, w, h);
  809. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  810. if (!up || up.length < 2) return;
  811. var stepX = cw / (up.length - 1);
  812. function draw(arr, color) {
  813. var grad = ctx.createLinearGradient(0, padT, 0, padT + ch);
  814. grad.addColorStop(0, hexToRgba(color, 0.45));
  815. grad.addColorStop(1, hexToRgba(color, 0.05));
  816. ctx.beginPath(); ctx.moveTo(padL, padT + ch);
  817. for (var i = 0; i < arr.length; i++) {
  818. var x = padL + i * stepX;
  819. var y = padT + ch - (arr[i] / maxV) * ch;
  820. if (i === 0) ctx.lineTo(x, y);
  821. else {
  822. var px = padL + (i - 1) * stepX;
  823. var py = padT + ch - (arr[i - 1] / maxV) * ch;
  824. var cpx = (px + x) / 2;
  825. ctx.bezierCurveTo(cpx, py, cpx, y, x, y);
  826. }
  827. }
  828. ctx.lineTo(padL + (arr.length - 1) * stepX, padT + ch);
  829. ctx.closePath(); ctx.fillStyle = grad; ctx.fill();
  830. ctx.beginPath();
  831. for (var j = 0; j < arr.length; j++) {
  832. var x2 = padL + j * stepX;
  833. var y2 = padT + ch - (arr[j] / maxV) * ch;
  834. if (j === 0) ctx.moveTo(x2, y2);
  835. else {
  836. var px2 = padL + (j - 1) * stepX;
  837. var py2 = padT + ch - (arr[j - 1] / maxV) * ch;
  838. var cpx2 = (px2 + x2) / 2;
  839. ctx.bezierCurveTo(cpx2, py2, cpx2, y2, x2, y2);
  840. }
  841. }
  842. ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.stroke();
  843. }
  844. draw(up, '#00FF88');
  845. draw(down, '#00C8FF');
  846. ctx.fillStyle = 'rgba(77,201,245,0.9)'; ctx.font = '10px Microsoft YaHei'; ctx.textAlign = 'center';
  847. for (var i = 0; i < labels.length; i++) {
  848. if (i % 2 === 0) {
  849. var x = padL + i * stepX;
  850. ctx.fillText(labels[i], x, h - 6);
  851. }
  852. }
  853. }
  854. // 月产量柱状图 (渐变条)
  855. function drawMonthBar(id, data, labels, maxV) {
  856. var p = prepareCanvas(id); if (!p) return;
  857. var ctx = p.ctx, w = p.w, h = p.h;
  858. var padL = 42, padR = 12, padT = 15, padB = 25;
  859. var cw = w - padL - padR, ch = h - padT - padB;
  860. ctx.clearRect(0, 0, w, h);
  861. drawYAxis(ctx, padL, padT, cw, ch, maxV);
  862. if (!data || data.length < 1) return;
  863. var gw = cw / data.length, bw = gw * 0.35;
  864. for (var i = 0; i < data.length; i++) {
  865. var cx = padL + i * gw + gw / 2;
  866. var bh = (data[i] / maxV) * ch;
  867. var grad = ctx.createLinearGradient(0, padT + ch - bh, 0, padT + ch);
  868. grad.addColorStop(0, '#00FF88');
  869. grad.addColorStop(1, 'rgba(0,200,255,0.15)');
  870. ctx.fillStyle = grad;
  871. ctx.fillRect(cx - bw / 2, padT + ch - bh, bw, bh);
  872. ctx.fillStyle = 'rgba(77,201,245,0.9)';
  873. ctx.font = '11px Microsoft YaHei';
  874. ctx.textAlign = 'center';
  875. ctx.fillText(labels[i], cx, h - 6);
  876. }
  877. }
  878. // 半环形仪表盘 (今日达成率)
  879. function drawGauge(id, percent) {
  880. var p = prepareCanvas(id); if (!p) return;
  881. var ctx = p.ctx, w = p.w, h = p.h;
  882. ctx.clearRect(0, 0, w, h);
  883. var cx = w / 2, cy = h / 2;
  884. var r = Math.min(w, h) / 2 - 8;
  885. // 底环
  886. ctx.beginPath();
  887. ctx.arc(cx, cy, r, 0, Math.PI * 2);
  888. ctx.lineWidth = 10;
  889. ctx.strokeStyle = 'rgba(0,143,253,0.25)';
  890. ctx.stroke();
  891. // 进度环
  892. var start = -Math.PI / 2;
  893. var end = start + Math.PI * 2 * (percent / 100);
  894. var grad = ctx.createLinearGradient(0, 0, w, h);
  895. grad.addColorStop(0, '#00C8FF');
  896. grad.addColorStop(1, '#00FF88');
  897. ctx.beginPath();
  898. ctx.arc(cx, cy, r, start, end, false);
  899. ctx.lineWidth = 10;
  900. ctx.strokeStyle = grad;
  901. ctx.lineCap = 'round';
  902. ctx.stroke();
  903. // 外环刻度
  904. ctx.beginPath();
  905. ctx.arc(cx, cy, r + 10, 0, Math.PI * 2);
  906. ctx.lineWidth = 1;
  907. ctx.strokeStyle = 'rgba(0,200,255,0.35)';
  908. ctx.stroke();
  909. }
  910. function escapeHtml(s) {
  911. if (s == null || s === '') return '';
  912. return String(s)
  913. .replace(/&/g, '&amp;')
  914. .replace(/</g, '&lt;')
  915. .replace(/>/g, '&gt;')
  916. .replace(/"/g, '&quot;');
  917. }
  918. // ====== 渲染列表(text 为设备报警记录的 type 字段)======
  919. function renderAlerts() {
  920. var html = '';
  921. var list = mockData.alerts || [];
  922. for (var i = 0; i < list.length; i++) {
  923. html += '<div class="alert-row"><div>' + list[i].no + '</div><div>' + escapeHtml(list[i].text) + '</div></div>';
  924. }
  925. var el = document.getElementById('alertList');
  926. if (el) el.innerHTML = html;
  927. }
  928. function renderTable() {
  929. var list = mockData.opTable || [];
  930. var pageCount = getOpTablePageCount(list);
  931. if (opTablePageIndex >= pageCount) {
  932. opTablePageIndex = 0;
  933. }
  934. var start = opTablePageIndex * OP_TABLE_PAGE_SIZE;
  935. var slice = list.slice(start, start + OP_TABLE_PAGE_SIZE);
  936. var html = '';
  937. for (var i = 0; i < slice.length; i++) {
  938. var r = slice[i];
  939. html += '<tr' + ((i % 2 === 1) ? ' class="op-row-alt"' : '') + '>';
  940. html += '<td class="op-col">' + r.code + '</td>';
  941. html += '<td class="op-name">' + r.name + '</td>';
  942. html += '<td class="op-val">' + r.real + '</td>';
  943. html += '<td class="op-val">' + r.shift + '</td>';
  944. html += '<td class="op-val">' + (r.prevShift || 0) + '</td>';
  945. html += '</tr>';
  946. }
  947. html = padOpTableRowsToPageSize(html, slice.length);
  948. var el = document.getElementById('opTableBody');
  949. if (el) el.innerHTML = html;
  950. var hint = document.getElementById('opTablePageHint');
  951. if (hint) {
  952. if (list.length === 0) {
  953. hint.textContent = '';
  954. } else if (pageCount > 1) {
  955. hint.textContent = '第 ' + (opTablePageIndex + 1) + ' / ' + pageCount + ' 页 / 每页 ' + OP_TABLE_PAGE_SIZE + ' 条 / 共 ' + list.length + ' 条';
  956. } else {
  957. hint.textContent = '共 ' + list.length + ' 条';
  958. }
  959. }
  960. }
  961. function applyLatestPlanNumsToTable(planNums) {}
  962. function syncAchieveRateWithPlan() {}
  963. function parseOprnoNum(code) {
  964. var m = String(code || '').toUpperCase().match(/^OP(\d+)/);
  965. return m ? parseInt(m[1], 10) : -1;
  966. }
  967. function isOprnoInRange(code, fromNum, toNum) {
  968. var n = parseOprnoNum(code);
  969. return n >= fromNum && n <= toNum;
  970. }
  971. function updateLineProcessTableByApi() {
  972. $.ajax({
  973. url: '${ctx}/mes/mesProductScreen/lineProcessList',
  974. type: 'POST',
  975. dataType: 'json',
  976. data: {
  977. pageNo: 1,
  978. pageSize: 5000,
  979. lineSn: LINE_SN
  980. },
  981. success: function(resp) {
  982. var rows = extractUserList(resp);
  983. var out = [];
  984. for (var i = 0; i < rows.length; i++) {
  985. var p = rows[i] || {};
  986. var ls = String(p.lineSn || p.line_sn || '').toUpperCase();
  987. if (ls !== LINE_SN) continue;
  988. var code = p.oprno || '';
  989. if (!isOprnoInRange(code, OP_FROM, OP_TO)) continue;
  990. out.push({
  991. code: code,
  992. name: p.title || '',
  993. real: 0,
  994. shift: 0,
  995. prevShift: 0,
  996. _opNum: parseOprnoNum(code)
  997. });
  998. }
  999. out.sort(function(a, b) { return a._opNum - b._opNum; });
  1000. mockData.opTable = out;
  1001. opTablePageIndex = 0;
  1002. renderTable();
  1003. startOpTablePageRotate();
  1004. if (out.length > 0) {
  1005. updateHourlyCapacityByApi();
  1006. }
  1007. updateDeviceAlarmAlertsByApi();
  1008. }
  1009. });
  1010. }
  1011. function updateHourlyCapacityByApi() {
  1012. var list = mockData.opTable || [];
  1013. if (!list.length) return;
  1014. var oprnos = [];
  1015. for (var i = 0; i < list.length; i++) {
  1016. if (list[i] && list[i].code) oprnos.push(list[i].code);
  1017. }
  1018. if (!oprnos.length) return;
  1019. fetchCapacityInto('dayCapacityData', LINE_SN, oprnos, 'real', {});
  1020. fetchCapacityInto('shiftCapacityData', LINE_SN, oprnos, 'shift', { fixedTwoShift: 'true' });
  1021. fetchCapacityInto('prevShiftCapacityData', LINE_SN, oprnos, 'prevShift', { fixedTwoShift: 'true' });
  1022. }
  1023. function fetchCapacityInto(action, lineSn, oprnos, field, extraData) {
  1024. var data = { lineSn: lineSn, oprnos: oprnos.join(',') };
  1025. if (extraData) {
  1026. for (var k in extraData) {
  1027. if (extraData.hasOwnProperty(k)) data[k] = extraData[k];
  1028. }
  1029. }
  1030. $.ajax({
  1031. url: '${ctx}/mes/mesProductRecord/' + action,
  1032. type: 'POST',
  1033. dataType: 'json',
  1034. data: data,
  1035. success: function(resp) {
  1036. if (!resp || resp.result !== 'true' || !resp.data) return;
  1037. var map = resp.data || {};
  1038. var list = mockData.opTable || [];
  1039. for (var i = 0; i < list.length; i++) {
  1040. var code = list[i].code || '';
  1041. var formatted = code.length === 6 ? code.substring(0, 5) : code;
  1042. list[i][field] = Number(map[code] != null ? map[code] : map[formatted]) || 0;
  1043. }
  1044. renderTable();
  1045. }
  1046. });
  1047. }
  1048. // 近七日质检:产出=OP160 OK,不良=返工 OP120-OP160
  1049. function updateWeekQualityByApi() {
  1050. $.ajax({
  1051. url: '${ctx}/mes/mesProductRecord/weekQualityData',
  1052. type: 'POST',
  1053. dataType: 'json',
  1054. data: {
  1055. lineSn: LINE_SN,
  1056. producedOprno: 'OP160',
  1057. defectOprnoFrom: 'OP120',
  1058. defectOprnoTo: 'OP160'
  1059. },
  1060. success: function(resp) {
  1061. if (!resp || resp.result !== 'true' || !resp.data) return;
  1062. var d = resp.data;
  1063. var produced = d.produced || [];
  1064. var defect = d.defect || [];
  1065. var maxV = 10;
  1066. for (var i = 0; i < produced.length; i++) {
  1067. if (Number(produced[i]) > maxV) maxV = Number(produced[i]);
  1068. }
  1069. for (var j = 0; j < defect.length; j++) {
  1070. if (Number(defect[j]) > maxV) maxV = Number(defect[j]);
  1071. }
  1072. mockData.qualityInspect = {
  1073. labels: d.labels || [],
  1074. produced: produced,
  1075. defect: defect,
  1076. max: Math.ceil(maxV * 1.15) || 10
  1077. };
  1078. drawAllCharts();
  1079. }
  1080. });
  1081. }
  1082. function updateMonthlyProductByApi() {}
  1083. function updateLatestPlanProductionByApi() {}
  1084. function renderCards() {}
  1085. function extractUserList(resp) {
  1086. if (!resp) return [];
  1087. if (Object.prototype.toString.call(resp.list) === '[object Array]') return resp.list;
  1088. if (Object.prototype.toString.call(resp.rows) === '[object Array]') return resp.rows;
  1089. if (resp.data) {
  1090. if (Object.prototype.toString.call(resp.data.list) === '[object Array]') return resp.data.list;
  1091. if (Object.prototype.toString.call(resp.data.rows) === '[object Array]') return resp.data.rows;
  1092. }
  1093. return [];
  1094. }
  1095. function parseDeviceSnParts(deviceSn) {
  1096. var s = String(deviceSn || '').trim().toUpperCase();
  1097. var parts = s.split('_').filter(function(p) { return !!p; });
  1098. var line = parts.length > 0 ? parts[0] : '';
  1099. var oprno = '';
  1100. for (var i = 0; i < parts.length; i++) {
  1101. if (/^OP\d+$/.test(parts[i])) {
  1102. oprno = parts[i];
  1103. break;
  1104. }
  1105. }
  1106. if (!oprno) {
  1107. var m = s.match(/OP\d+/);
  1108. if (m) oprno = m[0];
  1109. }
  1110. return { line: line, oprno: oprno };
  1111. }
  1112. function resolveAlertProcessName(oprno) {
  1113. var code = String(oprno || '').toUpperCase();
  1114. var num = parseOprnoNum(code);
  1115. var list = mockData.opTable || [];
  1116. for (var i = 0; i < list.length; i++) {
  1117. var c = String((list[i] && list[i].code) || '').toUpperCase();
  1118. if (!c) continue;
  1119. if (c === code || (num >= 0 && parseOprnoNum(c) === num)) {
  1120. return list[i].name || '';
  1121. }
  1122. }
  1123. return '';
  1124. }
  1125. function updateDeviceAlarmAlertsByApi() {
  1126. // 报警记录接口暂未开发完成,先不请求
  1127. mockData.alerts = [];
  1128. renderAlerts();
  1129. }
  1130. /** 总览三卡:生产人数 / 设备数量 / 计划产量 <- mesProductScreen 最新配置 */
  1131. function updateScreenConfigByApi() {
  1132. $.ajax({
  1133. url: '${ctx}/mes/mesProductScreen/latestConfig',
  1134. type: 'POST',
  1135. dataType: 'json',
  1136. data: { screenType: SCREEN_TYPE },
  1137. success: function(resp) {
  1138. if (!resp || resp.result !== 'true' || !resp.data) return;
  1139. var d = resp.data;
  1140. mockData.cards = mockData.cards || {};
  1141. mockData.cards.productionPeople = Number(d.peopleNums) || 0;
  1142. mockData.cards.deviceCount = Number(d.deviceNums) || 0;
  1143. mockData.cards.planProduction = Number(d.planNums) || 0;
  1144. var elPeople = document.getElementById('productionPeople');
  1145. var elDevice = document.getElementById('deviceCount');
  1146. var elPlan = document.getElementById('planProduction');
  1147. if (elPeople) elPeople.textContent = mockData.cards.productionPeople;
  1148. if (elDevice) elDevice.textContent = mockData.cards.deviceCount;
  1149. if (elPlan) elPlan.textContent = mockData.cards.planProduction;
  1150. }
  1151. });
  1152. }
  1153. function isOpr180Family(oprno) {
  1154. var op = (oprno || '').replace(/\s/g, '').toUpperCase();
  1155. if (op.length < 5 || op.indexOf('OP180') !== 0) {
  1156. return false;
  1157. }
  1158. if (op.length === 5) {
  1159. return op === 'OP180';
  1160. }
  1161. return /^OP180[A-Z0-9_-]+$/.test(op);
  1162. }
  1163. function isRecordContentOk(content) {
  1164. var c = (content || '').replace(/\s/g, '').toUpperCase();
  1165. return c === 'OK';
  1166. }
  1167. function applyActualProductionCount(n) {
  1168. mockData.cards = mockData.cards || {};
  1169. mockData.cards.actualProduction = Number(n) || 0;
  1170. var el = document.getElementById('actualProduction');
  1171. if (el) el.textContent = mockData.cards.actualProduction;
  1172. }
  1173. /** 实际产量:取本阶段末工位当天 OK 数(机加 OP160) */
  1174. function updateActualProductionByApi() {
  1175. var oprno = HOURLY_API_CFG.offlineOprno || 'OP160';
  1176. $.ajax({
  1177. url: '${ctx}/mes/mesProductRecord/dayCapacityData',
  1178. type: 'POST',
  1179. dataType: 'json',
  1180. data: {
  1181. lineSn: LINE_SN,
  1182. oprnos: oprno
  1183. },
  1184. success: function(resp) {
  1185. if (!resp || resp.result !== 'true' || !resp.data) {
  1186. applyActualProductionCount(0);
  1187. return;
  1188. }
  1189. var map = resp.data || {};
  1190. var formatted = oprno.length === 6 ? oprno.substring(0, 5) : oprno;
  1191. applyActualProductionCount(Number(map[oprno] != null ? map[oprno] : map[formatted]) || 0);
  1192. },
  1193. error: function() {
  1194. applyActualProductionCount(0);
  1195. }
  1196. });
  1197. }
  1198. function drawAllCharts() {
  1199. var q = mockData.qualityInspect;
  1200. if (q) drawDoubleBar('chartQuality', q.produced, q.defect, q.labels, q.max);
  1201. var hr = mockData.hourly;
  1202. if (hr) drawHourlyDoubleBar('chartHourly', hr.up, hr.down, hr.labels);
  1203. }
  1204. function refreshData() {
  1205. generateMockData();
  1206. opTablePageIndex = 0;
  1207. renderAlerts();
  1208. renderTable();
  1209. startOpTablePageRotate();
  1210. updateScreenConfigByApi();
  1211. updateActualProductionByApi();
  1212. updateLineProcessTableByApi();
  1213. updateDeviceAlarmAlertsByApi();
  1214. updateHourlyUpDownByApi();
  1215. updateWeekQualityByApi();
  1216. drawAllCharts();
  1217. }
  1218. function changeZoom() {
  1219. var wb = $(window).width() / 1920;
  1220. var hb = $(window).height() / 1080;
  1221. var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
  1222. var $app = $('#app');
  1223. if (isFs) {
  1224. $('html, body').css({ 'zoom': 1, 'width': '100%', 'height': '100%', 'overflow': 'hidden', 'margin': 0, 'background': '#00091A' });
  1225. $app.css({
  1226. 'transform': 'scale(' + wb + ', ' + hb + ')',
  1227. 'transform-origin': 'left top',
  1228. 'margin': 0,
  1229. 'position': 'absolute',
  1230. 'left': 0,
  1231. 'top': 0
  1232. });
  1233. } else {
  1234. $('html').css({ 'width': '', 'height': '', 'overflow': '', 'margin': '', 'background': '', 'zoom': '' });
  1235. $app.css({ 'transform': 'none', 'transform-origin': '', 'margin': '0 auto', 'position': 'relative', 'left': '', 'top': '' });
  1236. $('body').css({ 'zoom': wb >= hb ? hb : wb, 'width': '', 'height': '', 'overflow': 'hidden', 'margin': '', 'background': '' });
  1237. }
  1238. }
  1239. $(window).resize(function() {
  1240. changeZoom();
  1241. drawAllCharts();
  1242. });
  1243. function initFullscreenBtn() {
  1244. var btn = document.getElementById('btnFullscreen');
  1245. if (!btn) return;
  1246. btn.addEventListener('click', function () {
  1247. var el = document.documentElement;
  1248. if (el.requestFullscreen) {
  1249. el.requestFullscreen();
  1250. } else if (el.webkitRequestFullscreen) {
  1251. el.webkitRequestFullscreen();
  1252. }
  1253. });
  1254. function syncFullscreenBtn() {
  1255. var fs = document.fullscreenElement || document.webkitFullscreenElement;
  1256. btn.style.display = fs ? 'none' : '';
  1257. changeZoom();
  1258. drawAllCharts();
  1259. }
  1260. document.addEventListener('fullscreenchange', syncFullscreenBtn);
  1261. document.addEventListener('webkitfullscreenchange', syncFullscreenBtn);
  1262. }
  1263. $(function() {
  1264. changeZoom();
  1265. updateClock();
  1266. setInterval(updateClock, 1000);
  1267. refreshData();
  1268. initFullscreenBtn();
  1269. // 定期刷新数据 (2 分钟)
  1270. setInterval(refreshData, 1000 * 60 * 2);
  1271. // 每秒重绘 (应对 zoom / resize)
  1272. setInterval(drawAllCharts, 1000);
  1273. });
  1274. })();
  1275. </script>
  1276. </body>
  1277. </html>