mesQualityScreen.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>质量管理看板</title>
  7. <style>
  8. * { box-sizing: border-box; }
  9. html, body {
  10. width: 100%;
  11. height: 100%;
  12. margin: 0;
  13. overflow: hidden;
  14. background: #06131f;
  15. color: #d8fbff;
  16. font-family: "Microsoft YaHei", Arial, sans-serif;
  17. }
  18. .quality-screen {
  19. position: relative;
  20. width: 100vw;
  21. height: 100vh;
  22. padding: clamp(12px, 1vw, 20px);
  23. background:
  24. linear-gradient(120deg, rgba(15, 92, 113, .18), transparent 36%),
  25. linear-gradient(250deg, rgba(32, 81, 150, .18), transparent 40%),
  26. linear-gradient(180deg, #071522 0%, #092034 52%, #06111d 100%);
  27. }
  28. .quality-screen:before {
  29. content: "";
  30. position: absolute;
  31. inset: 0;
  32. pointer-events: none;
  33. background-image:
  34. linear-gradient(rgba(97, 219, 255, .045) 1px, transparent 1px),
  35. linear-gradient(90deg, rgba(97, 219, 255, .045) 1px, transparent 1px);
  36. background-size: 44px 44px;
  37. }
  38. .shell {
  39. position: relative;
  40. z-index: 1;
  41. display: grid;
  42. grid-template-rows: 72px 104px 1fr;
  43. gap: 14px;
  44. width: 100%;
  45. height: 100%;
  46. min-width: 1200px;
  47. min-height: 680px;
  48. }
  49. .header {
  50. display: grid;
  51. grid-template-columns: 1fr minmax(360px, 620px) 1fr;
  52. align-items: center;
  53. }
  54. .title {
  55. position: relative;
  56. text-align: center;
  57. font-size: clamp(30px, 2.25vw, 44px);
  58. font-weight: 700;
  59. color: #efffff;
  60. text-shadow: 0 0 18px rgba(61, 226, 255, .85);
  61. letter-spacing: 0;
  62. }
  63. .title:before,
  64. .title:after {
  65. content: "";
  66. position: absolute;
  67. top: 50%;
  68. width: 170px;
  69. height: 1px;
  70. background: linear-gradient(90deg, transparent, rgba(83, 221, 255, .95));
  71. }
  72. .title:before { right: calc(100% + 18px); }
  73. .title:after { left: calc(100% + 18px); transform: rotate(180deg); }
  74. .time {
  75. justify-self: end;
  76. color: #8bd9ee;
  77. font-size: 16px;
  78. white-space: nowrap;
  79. }
  80. .kpis {
  81. display: grid;
  82. grid-template-columns: repeat(6, minmax(0, 1fr));
  83. gap: 12px;
  84. }
  85. .kpi,
  86. .panel {
  87. position: relative;
  88. border: 1px solid rgba(72, 206, 255, .42);
  89. background: linear-gradient(180deg, rgba(9, 42, 68, .88), rgba(6, 24, 39, .78));
  90. box-shadow: inset 0 0 24px rgba(52, 194, 228, .13), 0 0 20px rgba(0, 148, 210, .08);
  91. overflow: hidden;
  92. }
  93. .kpi:before,
  94. .panel:before {
  95. content: "";
  96. position: absolute;
  97. inset: 0;
  98. border: 1px solid rgba(158, 238, 255, .08);
  99. pointer-events: none;
  100. }
  101. .kpi {
  102. display: flex;
  103. flex-direction: column;
  104. justify-content: center;
  105. padding: 14px 18px;
  106. min-width: 0;
  107. }
  108. .kpi-label {
  109. color: #9cc9d8;
  110. font-size: 14px;
  111. line-height: 20px;
  112. overflow: hidden;
  113. text-overflow: ellipsis;
  114. white-space: nowrap;
  115. }
  116. .kpi-value {
  117. margin-top: 8px;
  118. font-family: Impact, "Arial Narrow", sans-serif;
  119. font-size: clamp(30px, 2.2vw, 42px);
  120. line-height: 1;
  121. color: #45f1ff;
  122. text-shadow: 0 0 16px rgba(69, 241, 255, .5);
  123. }
  124. .kpi.warn .kpi-value { color: #ffce58; text-shadow: 0 0 16px rgba(255, 206, 88, .36); }
  125. .main {
  126. display: grid;
  127. grid-template-columns: minmax(0, 29fr) minmax(0, 42fr) minmax(0, 29fr);
  128. gap: 14px;
  129. min-height: 0;
  130. min-width: 0;
  131. }
  132. .column {
  133. display: grid;
  134. gap: 14px;
  135. min-height: 0;
  136. min-width: 0;
  137. }
  138. .left { grid-template-rows: 45% 55%; }
  139. .center { grid-template-rows: 58% 42%; }
  140. .right { grid-template-rows: 36% 64%; }
  141. .panel-title {
  142. height: 42px;
  143. display: flex;
  144. align-items: center;
  145. padding: 0 16px;
  146. border-bottom: 1px solid rgba(88, 211, 255, .23);
  147. color: #f0fdff;
  148. font-size: 17px;
  149. font-weight: 700;
  150. }
  151. .panel-title:before {
  152. content: "";
  153. width: 4px;
  154. height: 18px;
  155. margin-right: 9px;
  156. background: #3be7ff;
  157. box-shadow: 0 0 12px #3be7ff;
  158. }
  159. .chart {
  160. width: 100%;
  161. height: calc(100% - 42px);
  162. min-height: 0;
  163. min-width: 0;
  164. }
  165. .dual .chart,
  166. .quad .chart {
  167. height: 100%;
  168. }
  169. .dual {
  170. display: grid;
  171. grid-template-columns: 1fr 1fr;
  172. height: calc(100% - 42px);
  173. min-height: 0;
  174. min-width: 0;
  175. }
  176. .quad {
  177. display: grid;
  178. grid-template-columns: 1fr 1fr;
  179. grid-template-rows: 1fr 1fr;
  180. height: calc(100% - 42px);
  181. min-height: 0;
  182. min-width: 0;
  183. }
  184. .table-wrap {
  185. height: calc(100% - 42px);
  186. padding: 10px 12px 12px;
  187. overflow: hidden;
  188. }
  189. .table-head,
  190. .table-row {
  191. display: grid;
  192. grid-template-columns: 72px 1.05fr .8fr 1.25fr 74px 82px;
  193. gap: 10px;
  194. align-items: center;
  195. min-width: 0;
  196. }
  197. .table-head {
  198. height: 30px;
  199. color: #7abbd0;
  200. font-size: 13px;
  201. border-bottom: 1px solid rgba(96, 202, 255, .18);
  202. }
  203. .table-body {
  204. height: calc(100% - 30px);
  205. overflow-y: auto;
  206. scrollbar-width: thin;
  207. scrollbar-color: rgba(77, 221, 255, .55) rgba(12, 52, 76, .38);
  208. }
  209. .table-body::-webkit-scrollbar {
  210. width: 6px;
  211. }
  212. .table-body::-webkit-scrollbar-track {
  213. background: rgba(12, 52, 76, .38);
  214. }
  215. .table-body::-webkit-scrollbar-thumb {
  216. background: rgba(77, 221, 255, .55);
  217. border-radius: 6px;
  218. }
  219. .table-row {
  220. height: 38px;
  221. color: #d9f8ff;
  222. font-size: 13px;
  223. border-bottom: 1px solid rgba(105, 199, 232, .08);
  224. }
  225. .cell {
  226. min-width: 0;
  227. overflow: hidden;
  228. text-overflow: ellipsis;
  229. white-space: nowrap;
  230. }
  231. .tag {
  232. display: inline-flex;
  233. align-items: center;
  234. justify-content: center;
  235. min-width: 58px;
  236. height: 22px;
  237. padding: 0 8px;
  238. border: 1px solid rgba(79, 219, 255, .34);
  239. background: rgba(22, 118, 147, .25);
  240. color: #9ff4ff;
  241. font-size: 12px;
  242. }
  243. .empty {
  244. height: 100%;
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. color: #6fa9bb;
  249. font-size: 14px;
  250. }
  251. @media (max-aspect-ratio: 15/9) {
  252. .shell { min-width: 1100px; }
  253. .main { grid-template-columns: minmax(0, 30fr) minmax(0, 40fr) minmax(0, 30fr); }
  254. .table-head, .table-row { grid-template-columns: 66px 1fr .75fr 1fr 68px 76px; gap: 8px; }
  255. }
  256. </style>
  257. </head>
  258. <body>
  259. <div class="quality-screen">
  260. <div class="shell">
  261. <div class="header">
  262. <div></div>
  263. <div class="title">质量管理看板</div>
  264. <div class="time" id="timeText"></div>
  265. </div>
  266. <div class="kpis">
  267. <div class="kpi"><div class="kpi-label">今日过程检查</div><div class="kpi-value" id="checkCount">0</div></div>
  268. <div class="kpi"><div class="kpi-label">今日首件检查</div><div class="kpi-value" id="firstCheckCount">0</div></div>
  269. <div class="kpi warn"><div class="kpi-label">今日NG数量</div><div class="kpi-value" id="ngCount">0</div></div>
  270. <div class="kpi"><div class="kpi-label">今日返修数量</div><div class="kpi-value" id="repairCount">0</div></div>
  271. <div class="kpi warn"><div class="kpi-label">今日报废数量</div><div class="kpi-value" id="scrapCount">0</div></div>
  272. <div class="kpi warn"><div class="kpi-label">今日未闭环返修</div><div class="kpi-value" id="pendingRepairCount">0</div></div>
  273. </div>
  274. <div class="main">
  275. <div class="column left">
  276. <div class="panel">
  277. <div class="panel-title">近7日质量趋势</div>
  278. <div class="chart" id="trendChart"></div>
  279. </div>
  280. <div class="panel">
  281. <div class="panel-title">过程检查状态</div>
  282. <div class="chart" id="checkStatusChart"></div>
  283. </div>
  284. </div>
  285. <div class="column center">
  286. <div class="panel">
  287. <div class="panel-title">最新质量异常</div>
  288. <div class="table-wrap">
  289. <div class="table-head">
  290. <div>类型</div><div>产品码</div><div>工位</div><div>内容</div><div>状态</div><div>时间</div>
  291. </div>
  292. <div class="table-body" id="issueList"></div>
  293. </div>
  294. </div>
  295. <div class="panel">
  296. <div class="panel-title">检查类型与首件状态</div>
  297. <div class="dual">
  298. <div class="chart" id="checkTypeChart"></div>
  299. <div class="chart" id="firstStatusChart"></div>
  300. </div>
  301. </div>
  302. </div>
  303. <div class="column right">
  304. <div class="panel">
  305. <div class="panel-title">责任工位TOP</div>
  306. <div class="chart" id="stationTopChart"></div>
  307. </div>
  308. <div class="panel">
  309. <div class="panel-title">NG、返修与报废分析</div>
  310. <div class="quad">
  311. <div class="chart" id="ngTypeChart"></div>
  312. <div class="chart" id="repairStateChart"></div>
  313. <div class="chart" id="repairResultChart"></div>
  314. <div class="chart" id="scrapReasonChart"></div>
  315. </div>
  316. </div>
  317. </div>
  318. </div>
  319. </div>
  320. </div>
  321. <script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
  322. <script src="${ctxStatic}/leaflet/js/echarts.min.js"></script>
  323. <script>
  324. var charts = {};
  325. var colors = ['#37e4ff', '#ffcf5a', '#65ffb4', '#ff7676', '#9c8cff', '#58a6ff', '#f28bff', '#b7e75b'];
  326. var issueScrollTimer = null;
  327. var issueScrollPaused = false;
  328. function initCharts() {
  329. ['trendChart', 'checkStatusChart', 'checkTypeChart', 'firstStatusChart', 'stationTopChart', 'ngTypeChart', 'repairStateChart', 'repairResultChart', 'scrapReasonChart'].forEach(function(id) {
  330. charts[id] = echarts.init(document.getElementById(id));
  331. });
  332. }
  333. function chartTextStyle(size) {
  334. return { color: '#a8d8e8', fontSize: size || 12 };
  335. }
  336. function setPie(id, title, data) {
  337. charts[id].setOption({
  338. color: colors,
  339. tooltip: { trigger: 'item' },
  340. title: { text: title, left: 'center', top: 'center', textStyle: { color: '#e9fbff', fontSize: 14, fontWeight: 400 } },
  341. series: [{
  342. type: 'pie',
  343. radius: ['50%', '72%'],
  344. center: ['50%', '50%'],
  345. avoidLabelOverlap: true,
  346. label: { color: '#cceff8', formatter: '{b}\n{c}' },
  347. labelLine: { lineStyle: { color: 'rgba(180,235,255,.45)' } },
  348. data: data || []
  349. }]
  350. });
  351. }
  352. function setBar(id, names, values, horizontal) {
  353. var option = {
  354. color: ['#39e7ff'],
  355. grid: { left: horizontal ? 88 : 42, right: 22, top: 24, bottom: 34 },
  356. tooltip: { trigger: 'axis' },
  357. xAxis: { type: horizontal ? 'value' : 'category', data: horizontal ? null : names, axisLabel: chartTextStyle(11), axisLine: { lineStyle: { color: '#286982' } }, splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
  358. yAxis: { type: horizontal ? 'category' : 'value', data: horizontal ? names : null, axisLabel: chartTextStyle(11), axisLine: { lineStyle: { color: '#286982' } }, splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
  359. series: [{ type: 'bar', data: values, barWidth: horizontal ? 12 : 16, itemStyle: { borderRadius: horizontal ? [0, 8, 8, 0] : [8, 8, 0, 0] } }]
  360. };
  361. charts[id].setOption(option);
  362. }
  363. function render(data) {
  364. var summary = data.summary || {};
  365. ['checkCount', 'firstCheckCount', 'ngCount', 'repairCount', 'scrapCount', 'pendingRepairCount'].forEach(function(key) {
  366. $('#' + key).text(summary[key] || 0);
  367. });
  368. var trend = data.trend || {};
  369. charts.trendChart.setOption({
  370. color: ['#3de7ff', '#ffcf5a', '#66ffb4', '#ff6f6f'],
  371. tooltip: { trigger: 'axis' },
  372. legend: { top: 10, right: 14, textStyle: chartTextStyle(12), data: ['检查', 'NG', '返修', '报废'] },
  373. grid: { left: 42, right: 22, top: 48, bottom: 34 },
  374. xAxis: { type: 'category', data: data.days || [], axisLabel: chartTextStyle(12), axisLine: { lineStyle: { color: '#286982' } } },
  375. yAxis: { type: 'value', axisLabel: chartTextStyle(12), splitLine: { lineStyle: { color: 'rgba(95,196,230,.12)' } } },
  376. series: [
  377. { name: '检查', type: 'line', smooth: true, data: trend.checks || [] },
  378. { name: 'NG', type: 'line', smooth: true, data: trend.ng || [] },
  379. { name: '返修', type: 'bar', barWidth: 12, data: trend.repair || [] },
  380. { name: '报废', type: 'bar', barWidth: 12, data: trend.scrap || [] }
  381. ]
  382. });
  383. setPie('checkStatusChart', '检查状态', data.checkStatus);
  384. setPie('checkTypeChart', '检查类型', data.checkType);
  385. setPie('firstStatusChart', '首件状态', data.firstCheckStatus);
  386. setPie('ngTypeChart', 'NG类型', data.ngType);
  387. setPie('repairStateChart', '返修状态', data.repairState);
  388. setPie('repairResultChart', '返修结果', data.repairResult);
  389. var station = splitChartData(data.stationTop);
  390. setBar('stationTopChart', station.names, station.values, true);
  391. var scrap = splitChartData(data.scrapReasonTop);
  392. setBar('scrapReasonChart', scrap.names, scrap.values, true);
  393. renderIssues(data.latestIssues || []);
  394. }
  395. function splitChartData(list) {
  396. var names = [], values = [];
  397. (list || []).forEach(function(item) {
  398. names.push(item.name);
  399. values.push(item.value);
  400. });
  401. return { names: names, values: values };
  402. }
  403. function renderIssues(list) {
  404. var $box = $('#issueList');
  405. if (!list.length) {
  406. $box.html('<div class="empty">最近7天暂无质量异常</div>');
  407. return;
  408. }
  409. var html = '';
  410. list.forEach(function(item) {
  411. html += '<div class="table-row">'
  412. + '<div class="cell">' + escapeHtml(item.type) + '</div>'
  413. + '<div class="cell">' + escapeHtml(item.sn) + '</div>'
  414. + '<div class="cell">' + escapeHtml(item.station) + '</div>'
  415. + '<div class="cell">' + escapeHtml(item.content) + '</div>'
  416. + '<div class="cell"><span class="tag">' + escapeHtml(item.state) + '</span></div>'
  417. + '<div class="cell">' + escapeHtml(item.time) + '</div>'
  418. + '</div>';
  419. });
  420. $box.html(html);
  421. startIssueAutoScroll();
  422. }
  423. function startIssueAutoScroll() {
  424. var box = document.getElementById('issueList');
  425. if (issueScrollTimer) {
  426. clearInterval(issueScrollTimer);
  427. issueScrollTimer = null;
  428. }
  429. if (!box || box.scrollHeight <= box.clientHeight) {
  430. if (box) {
  431. box.scrollTop = 0;
  432. }
  433. return;
  434. }
  435. issueScrollTimer = setInterval(function() {
  436. if (issueScrollPaused) {
  437. return;
  438. }
  439. if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
  440. box.scrollTop = 0;
  441. } else {
  442. box.scrollTop += 1;
  443. }
  444. }, 80);
  445. }
  446. function escapeHtml(text) {
  447. return String(text == null ? '' : text).replace(/[&<>"']/g, function(ch) {
  448. return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch];
  449. });
  450. }
  451. function loadData() {
  452. $.post('${ctx}/mes/mesProduct/qualityScreenData', function(ret) {
  453. if (ret && ret.result === 'true') {
  454. render(ret.data || {});
  455. }
  456. });
  457. }
  458. function updateTime() {
  459. var d = new Date();
  460. var pad = function(n) { return n < 10 ? '0' + n : n; };
  461. $('#timeText').text(d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()));
  462. }
  463. $(function() {
  464. initCharts();
  465. updateTime();
  466. loadData();
  467. setInterval(updateTime, 1000);
  468. setInterval(loadData, 60000);
  469. $('#issueList').on('mouseenter', function() { issueScrollPaused = true; });
  470. $('#issueList').on('mouseleave', function() { issueScrollPaused = false; });
  471. $(window).on('resize', function() {
  472. Object.keys(charts).forEach(function(key) { charts[key].resize(); });
  473. startIssueAutoScroll();
  474. });
  475. });
  476. </script>
  477. </body>
  478. </html>