|
@@ -0,0 +1,155 @@
|
|
|
|
|
+<!doctype html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="utf-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
+ <title>大屏展示</title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, PingFang SC, Microsoft YaHei, sans-serif; background: #0b1020; color: #e8ecf1; }
|
|
|
|
|
+ .header { display:flex; justify-content: space-between; align-items:center; padding: 14px 20px; background: #10182b; border-bottom: 1px solid #1e2a44; }
|
|
|
|
|
+ .title { font-size: 18px; font-weight: 600; }
|
|
|
|
|
+ .btn { height: 34px; padding: 0 12px; border-radius: 8px; border: 1px solid #2a3b5f; background: #0e1424; color: #e8ecf1; cursor:pointer; }
|
|
|
|
|
+ .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; padding: 16px; }
|
|
|
|
|
+ .card { background: #121a2a; border: 1px solid #1e2a44; border-radius: 12px; padding: 16px; }
|
|
|
|
|
+ .metric { display:flex; gap: 16px; margin-top: 8px; }
|
|
|
|
|
+ .kv { background: #0e1424; border: 1px solid #2a3b5f; border-radius: 10px; padding: 12px; min-width: 140px; text-align:center; }
|
|
|
|
|
+ .k { font-size: 12px; color: #9fb0ca; }
|
|
|
|
|
+ .v { font-size: 22px; font-weight: 700; margin-top: 6px; }
|
|
|
|
|
+ .error { color: #ff6b6b; font-size: 13px; min-height: 18px; margin-top: 6px; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ let personChart;
|
|
|
|
|
+ let vehicleChart;
|
|
|
|
|
+ function ensureToken() {
|
|
|
|
|
+ const token = localStorage.getItem('access_token');
|
|
|
|
|
+ if (!token) {
|
|
|
|
|
+ window.location.href = '/login';
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchCount(algorithmId) {
|
|
|
|
|
+ const token = ensureToken();
|
|
|
|
|
+ if (!token) return null;
|
|
|
|
|
+ const resp = await fetch(`/api/v1/CrossCount/getCrossCountByAlgorithmsId/${algorithmId}`, {
|
|
|
|
|
+ headers: { 'Authorization': `Bearer ${token}` }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!resp.ok) throw new Error(await resp.text());
|
|
|
|
|
+ return resp.json();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function initChartsIfNeeded() {
|
|
|
|
|
+ if (!personChart) {
|
|
|
|
|
+ const ctx = document.getElementById('person-chart').getContext('2d');
|
|
|
|
|
+ personChart = new Chart(ctx, {
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ data: { labels: [], datasets: [
|
|
|
|
|
+ { label: '进线', data: [], borderColor: '#4cafef', backgroundColor: 'rgba(76,175,239,.2)', tension: .3 },
|
|
|
|
|
+ { label: '出线', data: [], borderColor: '#ff9f40', backgroundColor: 'rgba(255,159,64,.2)', tension: .3 },
|
|
|
|
|
+ { label: '在场', data: [], borderColor: '#62d26f', backgroundColor: 'rgba(98,210,111,.2)', tension: .3 }
|
|
|
|
|
+ ]},
|
|
|
|
|
+ options: { responsive: true, plugins: { legend: { labels: { color: '#9fb0ca' } } }, scales: { x: { ticks: { color: '#9fb0ca' }, grid: { color: '#1e2a44' } }, y: { ticks: { color: '#9fb0ca' }, grid: { color: '#1e2a44' } } } }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!vehicleChart) {
|
|
|
|
|
+ const ctx = document.getElementById('vehicle-chart').getContext('2d');
|
|
|
|
|
+ vehicleChart = new Chart(ctx, {
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ data: { labels: [], datasets: [
|
|
|
|
|
+ { label: '进线', data: [], borderColor: '#4cafef', backgroundColor: 'rgba(76,175,239,.2)', tension: .3 },
|
|
|
|
|
+ { label: '出线', data: [], borderColor: '#ff9f40', backgroundColor: 'rgba(255,159,64,.2)', tension: .3 },
|
|
|
|
|
+ { label: '在场', data: [], borderColor: '#62d26f', backgroundColor: 'rgba(98,210,111,.2)', tension: .3 }
|
|
|
|
|
+ ]},
|
|
|
|
|
+ options: { responsive: true, plugins: { legend: { labels: { color: '#9fb0ca' } } }, scales: { x: { ticks: { color: '#9fb0ca' }, grid: { color: '#1e2a44' } }, y: { ticks: { color: '#9fb0ca' }, grid: { color: '#1e2a44' } } } }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function pushChartPoint(chart, label, up, down, total) {
|
|
|
|
|
+ const maxPoints = 60; // 保留最近5分钟的 5s 间隔点
|
|
|
|
|
+ chart.data.labels.push(label);
|
|
|
|
|
+ chart.data.datasets[0].data.push(up);
|
|
|
|
|
+ chart.data.datasets[1].data.push(down);
|
|
|
|
|
+ chart.data.datasets[2].data.push(total);
|
|
|
|
|
+ if (chart.data.labels.length > maxPoints) {
|
|
|
|
|
+ chart.data.labels.shift();
|
|
|
|
|
+ chart.data.datasets.forEach(d => d.data.shift());
|
|
|
|
|
+ }
|
|
|
|
|
+ chart.update('none');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function loadData() {
|
|
|
|
|
+ const err = document.getElementById('error');
|
|
|
|
|
+ err.textContent = '';
|
|
|
|
|
+ try {
|
|
|
|
|
+ initChartsIfNeeded();
|
|
|
|
|
+ const [person, vehicle] = await Promise.all([
|
|
|
|
|
+ fetchCount(2),
|
|
|
|
|
+ fetchCount(3)
|
|
|
|
|
+ ]);
|
|
|
|
|
+ renderCard('person', person);
|
|
|
|
|
+ renderCard('vehicle', vehicle);
|
|
|
|
|
+ const ts = new Date();
|
|
|
|
|
+ const label = ts.toLocaleTimeString();
|
|
|
|
|
+ pushChartPoint(personChart, label, person.crossTotalUpCount ?? 0, person.crossTotalDownCount ?? 0, person.totalCount ?? 0);
|
|
|
|
|
+ pushChartPoint(vehicleChart, label, vehicle.crossTotalUpCount ?? 0, vehicle.crossTotalDownCount ?? 0, vehicle.totalCount ?? 0);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ err.textContent = e.message || '加载失败';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderCard(prefix, data) {
|
|
|
|
|
+ document.getElementById(`${prefix}-up`).textContent = data.crossTotalUpCount ?? 0;
|
|
|
|
|
+ document.getElementById(`${prefix}-down`).textContent = data.crossTotalDownCount ?? 0;
|
|
|
|
|
+ document.getElementById(`${prefix}-total`).textContent = data.totalCount ?? 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function logout() {
|
|
|
|
|
+ localStorage.removeItem('access_token');
|
|
|
|
|
+ window.location.href = '/login';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', loadData);
|
|
|
|
|
+ // 每60秒自动刷新一次数据
|
|
|
|
|
+ setInterval(loadData, 60000);
|
|
|
|
|
+ </script>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <div class="header">
|
|
|
|
|
+ <div class="title">大屏展示</div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <button class="btn" onclick="loadData()">刷新</button>
|
|
|
|
|
+ <button class="btn" onclick="logout()">退出</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="grid">
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="title">办公室人员进出入统计</div>
|
|
|
|
|
+ <div class="metric">
|
|
|
|
|
+ <div class="kv"><div class="k">进线总数</div><div id="person-up" class="v">0</div></div>
|
|
|
|
|
+ <div class="kv"><div class="k">出线总数</div><div id="person-down" class="v">0</div></div>
|
|
|
|
|
+ <div class="kv"><div class="k">当前在场</div><div id="person-total" class="v">0</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="margin-top: 12px;">
|
|
|
|
|
+ <canvas id="person-chart" height="120"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <div class="title">园区车辆进出入统计</div>
|
|
|
|
|
+ <div class="metric">
|
|
|
|
|
+ <div class="kv"><div class="k">进线总数</div><div id="vehicle-up" class="v">0</div></div>
|
|
|
|
|
+ <div class="kv"><div class="k">出线总数</div><div id="vehicle-down" class="v">0</div></div>
|
|
|
|
|
+ <div class="kv"><div class="k">当前在场</div><div id="vehicle-total" class="v">0</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="margin-top: 12px;">
|
|
|
|
|
+ <canvas id="vehicle-chart" height="120"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="error" class="error" style="padding: 0 16px;"></div>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|
|
|
|
|
+
|
|
|
|
|
+
|