Просмотр исходного кода

PDA新增生产运营看板展示页面

yutianhao 4 часов назад
Родитель
Сommit
fef1e030d4

+ 6 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductController.java

@@ -338,6 +338,12 @@ public class MesProductController extends BaseController {
 		return "modules/mes/mesScreen10";
 	}
 
+	/** PDA / 手机 H5 生产运营看板 */
+	@RequestMapping(value = "screen10m")
+	public String screen10m(Model model) {
+		return "modules/mes/mesScreen10m";
+	}
+
 	@RequestMapping(value = "screen11")
 	public String screen11(Model model) {
 		return "modules/mes/mesScreen11";

+ 1 - 0
src/main/resources/config/application.yml

@@ -629,6 +629,7 @@ shiro:
     ${adminPath}/mes/mesProduct/screen8 = anon
     ${adminPath}/mes/mesProduct/screen9 = anon
     ${adminPath}/mes/mesProduct/screen10 = anon
+    ${adminPath}/mes/mesProduct/screen10m = anon
     ${adminPath}/mes/mesProduct/screen11 = anon
     ${adminPath}/mes/mesProduct/screenKpiData = anon
     ${adminPath}/mes/mesProduct/screenRepairData = anon

+ 254 - 0
src/main/resources/static/screen/kpi-screen-m.css

@@ -0,0 +1,254 @@
+* {
+	box-sizing: border-box;
+	margin: 0;
+	padding: 0;
+	-webkit-tap-highlight-color: transparent;
+}
+
+html, body {
+	width: 100%;
+	min-height: 100%;
+	background: #eef2f7;
+	color: #1f2a37;
+	font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif;
+	font-size: 14px;
+	line-height: 1.4;
+}
+
+body {
+	padding-bottom: env(safe-area-inset-bottom);
+}
+
+.m-page {
+	min-height: 100vh;
+	display: flex;
+	flex-direction: column;
+}
+
+.m-header {
+	position: sticky;
+	top: 0;
+	z-index: 20;
+	display: grid;
+	grid-template-columns: 44px 1fr auto;
+	align-items: center;
+	min-height: 48px;
+	padding: calc(8px + env(safe-area-inset-top)) 12px 8px;
+	background: #1b3a5f;
+	color: #fff;
+	box-shadow: 0 2px 10px rgba(15, 35, 64, 0.25);
+}
+
+.m-back {
+	width: 36px;
+	height: 36px;
+	border: none;
+	border-radius: 8px;
+	background: rgba(255, 255, 255, 0.08);
+	color: #fff;
+	font-size: 28px;
+	line-height: 1;
+	cursor: pointer;
+}
+
+.m-title {
+	font-size: 17px;
+	font-weight: 600;
+	text-align: center;
+	letter-spacing: 1px;
+}
+
+.m-clock {
+	min-width: 58px;
+	text-align: right;
+	font-size: 12px;
+	color: #b7d4f5;
+}
+
+.m-main {
+	flex: 1;
+	padding: 12px 12px 24px;
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.m-card {
+	background: #fff;
+	border-radius: 12px;
+	padding: 14px 12px;
+	box-shadow: 0 4px 16px rgba(24, 48, 84, 0.06);
+}
+
+.m-card-top {
+	margin-bottom: 12px;
+}
+
+.m-day-line {
+	display: flex;
+	flex-direction: column;
+	gap: 2px;
+	color: #5b6b7c;
+	font-size: 12px;
+}
+
+.summary-grid {
+	display: grid;
+	grid-template-columns: repeat(3, 1fr);
+	gap: 10px 8px;
+}
+
+.summary-cell {
+	text-align: center;
+	padding: 8px 4px;
+	border-radius: 8px;
+	background: #f6f8fb;
+}
+
+.summary-label {
+	color: #7a8796;
+	font-size: 12px;
+	margin-bottom: 4px;
+}
+
+.summary-value {
+	color: #1f2a37;
+	font-size: 20px;
+	font-weight: 700;
+	line-height: 1.2;
+}
+
+.summary-value.is-actual { color: #2f7cff; }
+.summary-value.is-ok { color: #1f9d57; }
+.summary-value.is-ng { color: #e35b5b; }
+.summary-value.is-repair { color: #ef9b1a; }
+.summary-value.is-shift {
+	font-size: 16px;
+	color: #1b3a5f;
+}
+
+.gauge-list {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.gauge-item {
+	display: grid;
+	grid-template-columns: 96px 1fr;
+	align-items: center;
+	gap: 10px;
+	padding: 6px 4px;
+	border-radius: 10px;
+	background: #f7f9fc;
+}
+
+.gauge-chart {
+	width: 96px;
+	height: 88px;
+}
+
+.gauge-name {
+	font-size: 15px;
+	font-weight: 700;
+	color: #1f2a37;
+	margin-bottom: 2px;
+}
+
+.gauge-desc,
+.gauge-extra {
+	font-size: 12px;
+	color: #7a8796;
+}
+
+.gauge-extra {
+	margin-top: 4px;
+	color: #4a5a6b;
+}
+
+.gauge-factor {
+	display: flex;
+	justify-content: space-between;
+	gap: 8px;
+	font-size: 12px;
+	color: #7a8796;
+	margin-top: 2px;
+}
+
+.gauge-factor b {
+	color: #1f2a37;
+	font-weight: 600;
+}
+
+.m-card-title {
+	font-size: 15px;
+	font-weight: 700;
+	color: #1f2a37;
+	margin-bottom: 8px;
+}
+
+.chart-box {
+	width: 100%;
+	height: 220px;
+}
+
+.chart-box-tall {
+	height: 240px;
+}
+
+.table-scroll {
+	width: 100%;
+	overflow-x: auto;
+	-webkit-overflow-scrolling: touch;
+}
+
+.m-table {
+	width: 100%;
+	min-width: 520px;
+	border-collapse: collapse;
+	font-size: 12px;
+}
+
+.m-table th,
+.m-table td {
+	padding: 8px 6px;
+	text-align: center;
+	white-space: nowrap;
+	border-bottom: 1px solid #eef2f7;
+}
+
+.m-table th {
+	color: #7a8796;
+	font-weight: 600;
+	background: #f7f9fc;
+}
+
+.m-table td {
+	color: #1f2a37;
+}
+
+.m-table tr.is-current td {
+	background: #f0f7ff;
+}
+
+.m-table .empty-row td {
+	color: #98a4b3;
+	padding: 16px 8px;
+}
+
+.is-good { color: #1f9d57 !important; font-weight: 700; }
+.is-ok { color: #2f7cff !important; font-weight: 700; }
+.is-warn { color: #ef9b1a !important; font-weight: 700; }
+.is-bad { color: #e35b5b !important; font-weight: 700; }
+
+@media (min-width: 480px) {
+	.m-main {
+		max-width: 560px;
+		margin: 0 auto;
+		width: 100%;
+	}
+
+	.summary-value {
+		font-size: 22px;
+	}
+}

+ 356 - 0
src/main/resources/static/screen/kpi-screen-m.js

@@ -0,0 +1,356 @@
+(function ($, window) {
+	"use strict";
+
+	var config = window.kpiScreenMConfig || {};
+	var charts = {};
+	var refreshTimer = null;
+	var TEXT = "#5b6b7c";
+	var AXIS = "#c5d0dc";
+
+	function pad(num) {
+		return num < 10 ? "0" + num : String(num);
+	}
+
+	function updateClock() {
+		var now = new Date();
+		$("#screenTime").text(pad(now.getHours()) + ":" + pad(now.getMinutes()));
+	}
+
+	function escapeHtml(text) {
+		return String(text == null ? "" : text)
+			.replace(/&/g, "&amp;")
+			.replace(/</g, "&lt;")
+			.replace(/>/g, "&gt;")
+			.replace(/"/g, "&quot;");
+	}
+
+	function num(value) {
+		var n = Number(value);
+		return isNaN(n) ? 0 : n;
+	}
+
+	function rateText(value) {
+		return num(value).toFixed(1) + "%";
+	}
+
+	function rateClass(value) {
+		var n = num(value);
+		if (n >= 95) {
+			return "is-good";
+		}
+		if (n >= 85) {
+			return "is-ok";
+		}
+		if (n >= 70) {
+			return "is-warn";
+		}
+		return "is-bad";
+	}
+
+	function rateColor(value) {
+		var level = rateClass(value);
+		if (level === "is-good") {
+			return "#1f9d57";
+		}
+		if (level === "is-ok") {
+			return "#2f7cff";
+		}
+		if (level === "is-warn") {
+			return "#ef9b1a";
+		}
+		return "#e35b5b";
+	}
+
+	function chartOf(id) {
+		if (!charts[id]) {
+			charts[id] = echarts.init(document.getElementById(id));
+		}
+		return charts[id];
+	}
+
+	function renderGauge(id, value, neutral) {
+		var raw = num(value);
+		var ratio = Math.max(0, Math.min(raw, 100)) / 100;
+		var color = neutral ? "#9aa8b8" : rateColor(raw);
+		chartOf(id).setOption({
+			series: [{
+				type: "gauge",
+				min: 0,
+				max: 100,
+				startAngle: 210,
+				endAngle: -30,
+				radius: "96%",
+				center: ["50%", "58%"],
+				pointer: { show: false },
+				axisTick: { show: false },
+				splitLine: { show: false },
+				axisLabel: { show: false },
+				axisLine: {
+					lineStyle: {
+						width: 10,
+						color: [[ratio, color], [1, "rgba(30, 50, 80, 0.08)"]]
+					}
+				},
+				detail: {
+					offsetCenter: [0, "8%"],
+					color: "#1f2a37",
+					fontSize: 16,
+					fontWeight: 700,
+					formatter: function () {
+						return raw.toFixed(1) + "%";
+					}
+				},
+				data: [{ value: raw }]
+			}]
+		}, true);
+	}
+
+	function renderSummary(data) {
+		var summary = data.summary || {};
+		var kpi = data.kpi || {};
+		$("#statDay").text("统计日期 " + (data.day || "--"));
+		$("#statWindow").text(data.window || "08:00~次日08:00");
+		$("#planCount").text(summary.plan || 0);
+		$("#actualCount").text(summary.actual || 0);
+		$("#okCount").text(summary.ok || 0);
+		$("#ngCount").text(summary.ng || 0);
+		$("#repairCount").text(summary.repair || 0);
+		$("#shiftTitle").text(data.shiftTitle || "未排班");
+		$("#achieveExtra").text(summary.plan ? ((summary.actual || 0) + " / " + summary.plan) : "未排班次计划");
+		$("#fpyExtra").text("合格 " + (summary.ok || 0) + " / 检验 " + ((summary.ok || 0) + (summary.ng || 0)));
+		$("#availabilityRate").text(rateText(kpi.availability));
+		$("#performanceRate").text(rateText(kpi.performance));
+		$("#qualityRate").text(rateText(kpi.quality));
+		renderGauge("achieveGauge", kpi.achieve, !summary.plan);
+		renderGauge("fpyGauge", kpi.fpy, !(summary.ok || summary.ng || summary.actual));
+		renderGauge("oeeGauge", kpi.oee, !summary.plan && !(summary.ok || summary.ng));
+	}
+
+	function lineSeries(name, data, color) {
+		return {
+			name: name,
+			type: "line",
+			smooth: true,
+			symbol: "circle",
+			symbolSize: 6,
+			data: data || [],
+			itemStyle: { color: color },
+			lineStyle: { width: 2, color: color }
+		};
+	}
+
+	function renderTrend(trend) {
+		trend = trend || {};
+		chartOf("trendChart").setOption({
+			tooltip: {
+				trigger: "axis",
+				confine: true,
+				backgroundColor: "rgba(255,255,255,0.96)",
+				borderColor: "#d7e2ef",
+				textStyle: { color: "#1f2a37", fontSize: 12 }
+			},
+			legend: {
+				top: 0,
+				right: 0,
+				itemWidth: 10,
+				itemHeight: 8,
+				textStyle: { color: TEXT, fontSize: 11 },
+				data: ["达成率", "直通率", "OEE"]
+			},
+			grid: { left: 36, right: 10, top: 30, bottom: 28 },
+			xAxis: {
+				type: "category",
+				data: trend.labels || [],
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, fontSize: 11 }
+			},
+			yAxis: {
+				type: "value",
+				min: 0,
+				max: 100,
+				axisLine: { show: false },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, formatter: "{value}%", fontSize: 11 },
+				splitLine: { lineStyle: { color: "#eef2f7" } }
+			},
+			series: [
+				lineSeries("达成率", trend.achieve, "#2f7cff"),
+				lineSeries("直通率", trend.fpy, "#1f9d57"),
+				lineSeries("OEE", trend.oee, "#ef9b1a")
+			]
+		}, true);
+	}
+
+	function renderHours(hours) {
+		hours = hours || {};
+		chartOf("hourChart").setOption({
+			tooltip: {
+				trigger: "axis",
+				confine: true,
+				backgroundColor: "rgba(255,255,255,0.96)",
+				borderColor: "#d7e2ef",
+				textStyle: { color: "#1f2a37", fontSize: 12 }
+			},
+			legend: {
+				top: 0,
+				right: 0,
+				itemWidth: 10,
+				itemHeight: 8,
+				textStyle: { color: TEXT, fontSize: 11 },
+				data: ["完工", "节拍目标"]
+			},
+			grid: { left: 34, right: 10, top: 30, bottom: 28 },
+			xAxis: {
+				type: "category",
+				data: hours.labels || [],
+				axisLine: { lineStyle: { color: AXIS } },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, fontSize: 10, rotate: 30 }
+			},
+			yAxis: {
+				type: "value",
+				minInterval: 1,
+				axisLine: { show: false },
+				axisTick: { show: false },
+				axisLabel: { color: TEXT, fontSize: 11 },
+				splitLine: { lineStyle: { color: "#eef2f7" } }
+			},
+			series: [
+				{
+					name: "完工",
+					type: "bar",
+					barWidth: 12,
+					data: hours.actual || [],
+					itemStyle: {
+						color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+							{ offset: 0, color: "#5aa4ff" },
+							{ offset: 1, color: "#2f7cff" }
+						]),
+						barBorderRadius: [3, 3, 0, 0]
+					}
+				},
+				{
+					name: "节拍目标",
+					type: "line",
+					data: hours.plan || [],
+					symbol: "none",
+					itemStyle: { color: "#ef9b1a" },
+					lineStyle: { type: "dashed", width: 2, color: "#ef9b1a" }
+				}
+			]
+		}, true);
+	}
+
+	function renderShifts(rows) {
+		var html = "";
+		if (!rows || !rows.length) {
+			html = '<tr class="empty-row"><td colspan="9">暂无班次报工</td></tr>';
+		} else {
+			rows.forEach(function (row) {
+				html += '<tr class="' + (row.current ? "is-current" : "") + '">' +
+					"<td>" + escapeHtml(row.title) + (row.current ? " · 当前" : "") + "</td>" +
+					"<td>" + escapeHtml(row.range) + "</td>" +
+					"<td>" + num(row.plan) + "</td>" +
+					"<td>" + num(row.actual) + "</td>" +
+					"<td>" + num(row.ok) + "</td>" +
+					"<td>" + num(row.ng) + "</td>" +
+					"<td>" + num(row.repair) + "</td>" +
+					'<td class="' + rateClass(row.achieve) + '">' + rateText(row.achieve) + "</td>" +
+					'<td class="' + rateClass(row.fpy) + '">' + rateText(row.fpy) + "</td>" +
+					"</tr>";
+			});
+		}
+		$("#shiftBody").html(html);
+	}
+
+	function renderProducts(rows) {
+		var html = "";
+		if (!rows || !rows.length) {
+			html = '<tr class="empty-row"><td colspan="4">暂无产品计划</td></tr>';
+		} else {
+			rows.forEach(function (row) {
+				html += "<tr>" +
+					"<td>" + escapeHtml(row.title) + "</td>" +
+					"<td>" + num(row.plan) + "</td>" +
+					"<td>" + num(row.actual) + "</td>" +
+					'<td class="' + rateClass(row.achieve) + '">' + rateText(row.achieve) + "</td>" +
+					"</tr>";
+			});
+		}
+		$("#productBody").html(html);
+	}
+
+	function applyData(data) {
+		renderSummary(data);
+		renderTrend(data.trend);
+		renderHours(data.hours);
+		renderShifts(data.shifts);
+		renderProducts(data.products);
+		resizeCharts();
+	}
+
+	function loadData() {
+		$.ajax({
+			url: config.apiUrl,
+			type: "GET",
+			dataType: "json",
+			timeout: 20000
+		}).done(function (res) {
+			if (res && (res.result === "true" || res.result === true) && res.data) {
+				applyData(res.data);
+				scheduleRefresh(config.refreshInterval || 60000);
+			} else {
+				scheduleRefresh(5000);
+			}
+		}).fail(function () {
+			scheduleRefresh(5000);
+		});
+	}
+
+	function scheduleRefresh(delay) {
+		if (refreshTimer) {
+			clearTimeout(refreshTimer);
+		}
+		refreshTimer = setTimeout(loadData, delay || 60000);
+	}
+
+	function resizeCharts() {
+		Object.keys(charts).forEach(function (key) {
+			charts[key].resize();
+		});
+	}
+
+	function tryNativeBack() {
+		try {
+			if (window.AndroidBridge && typeof window.AndroidBridge.closePage === "function") {
+				window.AndroidBridge.closePage();
+				return true;
+			}
+			if (window.Android && typeof window.Android.closePage === "function") {
+				window.Android.closePage();
+				return true;
+			}
+		} catch (e) {
+			/* ignore */
+		}
+		if (window.history.length > 1) {
+			window.history.back();
+			return true;
+		}
+		return false;
+	}
+
+	$(function () {
+		$("#screenTitle").text(config.title || "生产运营看板");
+		updateClock();
+		setInterval(updateClock, 1000);
+		$("#btnBack").on("click", function () {
+			tryNativeBack();
+		});
+		loadData();
+		$(window).on("resize orientationchange", function () {
+			setTimeout(resizeCharts, 120);
+		});
+	});
+})(jQuery, window);

+ 147 - 0
src/main/resources/views/modules/mes/mesScreen10m.html

@@ -0,0 +1,147 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
+	<meta name="format-detection" content="telephone=no">
+	<meta name="apple-mobile-web-app-capable" content="yes">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>生产运营看板</title>
+	<link rel="stylesheet" href="${ctxStatic}/screen/kpi-screen-m.css?v=20261016">
+</head>
+<body>
+<div class="m-page">
+	<header class="m-header">
+		<button type="button" class="m-back" id="btnBack" aria-label="返回">‹</button>
+		<h1 class="m-title" id="screenTitle">生产运营看板</h1>
+		<span class="m-clock" id="screenTime"></span>
+	</header>
+
+	<main class="m-main">
+		<section class="m-card">
+			<div class="m-card-top">
+				<div class="m-day-line">
+					<span id="statDay">统计日期 --</span>
+					<span id="statWindow">08:00~次日08:00</span>
+				</div>
+			</div>
+			<div class="summary-grid">
+				<div class="summary-cell">
+					<div class="summary-label">计划产量</div>
+					<div class="summary-value" id="planCount">0</div>
+				</div>
+				<div class="summary-cell">
+					<div class="summary-label">实际完工</div>
+					<div class="summary-value is-actual" id="actualCount">0</div>
+				</div>
+				<div class="summary-cell">
+					<div class="summary-label">检验合格</div>
+					<div class="summary-value is-ok" id="okCount">0</div>
+				</div>
+				<div class="summary-cell">
+					<div class="summary-label">检验不良</div>
+					<div class="summary-value is-ng" id="ngCount">0</div>
+				</div>
+				<div class="summary-cell">
+					<div class="summary-label">返修件数</div>
+					<div class="summary-value is-repair" id="repairCount">0</div>
+				</div>
+				<div class="summary-cell">
+					<div class="summary-label">当前班次</div>
+					<div class="summary-value is-shift" id="shiftTitle">--</div>
+				</div>
+			</div>
+		</section>
+
+		<section class="m-card">
+			<div class="gauge-list">
+				<div class="gauge-item">
+					<div class="gauge-chart" id="achieveGauge"></div>
+					<div class="gauge-side">
+						<div class="gauge-name">生产达成率</div>
+						<div class="gauge-desc">完工 / 班次计划</div>
+						<div class="gauge-extra" id="achieveExtra">0 / 0</div>
+					</div>
+				</div>
+				<div class="gauge-item">
+					<div class="gauge-chart" id="fpyGauge"></div>
+					<div class="gauge-side">
+						<div class="gauge-name">直通率</div>
+						<div class="gauge-desc">工位一次合格占比</div>
+						<div class="gauge-extra" id="fpyExtra">合格 0 / 检验 0</div>
+					</div>
+				</div>
+				<div class="gauge-item">
+					<div class="gauge-chart" id="oeeGauge"></div>
+					<div class="gauge-side">
+						<div class="gauge-name">OEE</div>
+						<div class="gauge-factor"><span>时间稼动</span><b id="availabilityRate">0%</b></div>
+						<div class="gauge-factor"><span>性能稼动</span><b id="performanceRate">0%</b></div>
+						<div class="gauge-factor"><span>合格率</span><b id="qualityRate">0%</b></div>
+					</div>
+				</div>
+			</div>
+		</section>
+
+		<section class="m-card">
+			<div class="m-card-title">近七日达成率 / 直通率 / OEE</div>
+			<div class="chart-box" id="trendChart"></div>
+		</section>
+
+		<section class="m-card">
+			<div class="m-card-title">当日两小时完工</div>
+			<div class="chart-box chart-box-tall" id="hourChart"></div>
+		</section>
+
+		<section class="m-card">
+			<div class="m-card-title">班次报工</div>
+			<div class="table-scroll">
+				<table class="m-table">
+					<thead>
+					<tr>
+						<th>班次</th>
+						<th>时段</th>
+						<th>计划</th>
+						<th>完工</th>
+						<th>合格</th>
+						<th>不良</th>
+						<th>返修</th>
+						<th>达成率</th>
+						<th>直通率</th>
+					</tr>
+					</thead>
+					<tbody id="shiftBody"></tbody>
+				</table>
+			</div>
+		</section>
+
+		<section class="m-card">
+			<div class="m-card-title">产品达成</div>
+			<div class="table-scroll">
+				<table class="m-table">
+					<thead>
+					<tr>
+						<th>产品</th>
+						<th>计划</th>
+						<th>完工</th>
+						<th>达成率</th>
+					</tr>
+					</thead>
+					<tbody id="productBody"></tbody>
+				</table>
+			</div>
+		</section>
+	</main>
+</div>
+<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+<script src="${ctxStatic}/echarts/4.2/echarts.min.js"></script>
+<script>
+	window.kpiScreenMConfig = {
+		title: "生产运营看板",
+		refreshInterval: 60000,
+		apiUrl: "${ctx}/mes/mesProduct/screenKpiData"
+	};
+</script>
+<script src="${ctxStatic}/screen/kpi-screen-m.js?v=20261016"></script>
+</body>
+</html>