Kaynağa Gözat

更新接口

liu 7 ay önce
ebeveyn
işleme
a45c05ea96

+ 4 - 1
app/__init__.py

@@ -41,11 +41,14 @@ def create_app(config_object=None):
     )
 
     # 导入并注册控制器(命名空间)
-    from app.controller import auth_controller, algorithm_controller, cross_count_controller
+    from app.controller import auth_controller, algorithm_controller, cross_count_controller, web_controller
     api.add_namespace(auth_controller.api, path='/api/v1/auth')
     api.add_namespace(algorithm_controller.api, path='/api/v1/algorithm')
     api.add_namespace(cross_count_controller.api, path='/api/v1/CrossCount')
 
+    # 注册网页蓝图
+    app.register_blueprint(web_controller.web_bp)
+
     # 注册全局错误处理器
     from app.utils.error_handler import register_error_handlers
     register_error_handlers(app)

+ 2 - 2
app/controller/cross_count_controller.py

@@ -6,10 +6,10 @@ from app.utils.logger import Logger, log_request_info, ErrorHandler
 from app import db
 
 # 创建认证API命名空间
-api = Namespace('CrossCount', description='越线统计相关接口')
+api = Namespace('crossCount', description='越线统计相关接口')
 
 # 定义API模型
-cross_count_model = api.model('Algorithm', {
+cross_count_model = api.model('cross_count', {
     'crossTotalUpCount': fields.Integer(required=True, description='进线总数'),
     'crossTotalDownCount': fields.Integer(required=True, description='出线总数'),
     'totalCount': fields.Integer(description='总数')

+ 21 - 0
app/controller/web_controller.py

@@ -0,0 +1,21 @@
+from flask import Blueprint, render_template, redirect, url_for
+
+
+web_bp = Blueprint('web', __name__)
+
+
+@web_bp.route('/')
+def index():
+    return redirect(url_for('web.login'))
+
+
+@web_bp.route('/login')
+def login():
+    return render_template('login.html')
+
+
+@web_bp.route('/dashboard')
+def dashboard():
+    return render_template('dashboard.html')
+
+

+ 155 - 0
app/templates/dashboard.html

@@ -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>
+
+

+ 67 - 0
app/templates/login.html

@@ -0,0 +1,67 @@
+<!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; }
+    .container { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
+    .card { width: 360px; background: #121a2a; border: 1px solid #1e2a44; border-radius: 12px; padding: 28px; box-shadow: 0 10px 30px rgba(0,0,0,.35); }
+    .title { font-size: 20px; font-weight: 600; margin-bottom: 18px; }
+    label { font-size: 13px; color: #9fb0ca; display: block; margin: 12px 0 6px; }
+    input { width: 100%; height: 40px; border-radius: 8px; border: 1px solid #2a3b5f; background: #0e1424; color: #e8ecf1; padding: 0 12px; outline: none; }
+    input:focus { border-color: #3c7bff; box-shadow: 0 0 0 3px rgba(60,123,255,.2); }
+    .btn { margin-top: 16px; width: 100%; height: 40px; border-radius: 8px; border: none; background: linear-gradient(135deg, #2a6bff, #6b9dff); color: #fff; font-weight: 600; cursor: pointer; }
+    .btn:disabled { background: #2a3b5f; cursor: not-allowed; }
+    .error { color: #ff6b6b; font-size: 13px; margin-top: 8px; min-height: 18px; }
+  </style>
+  <script>
+    async function handleLogin(event) {
+      event.preventDefault();
+      const btn = document.getElementById('submitBtn');
+      btn.disabled = true;
+      const username = document.getElementById('username').value.trim();
+      const password = document.getElementById('password').value;
+      const errorEl = document.getElementById('error');
+      errorEl.textContent = '';
+
+      try {
+        const resp = await fetch('/api/v1/auth/login', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ username, password })
+        });
+        if (!resp.ok) {
+          const msg = await resp.text();
+          throw new Error(msg || '登录失败');
+        }
+        const data = await resp.json();
+        const token = data.access_token;
+        if (!token) throw new Error('未获取到token');
+        localStorage.setItem('access_token', token);
+        window.location.href = '/dashboard';
+      } catch (e) {
+        errorEl.textContent = e.message;
+      } finally {
+        btn.disabled = false;
+      }
+    }
+  </script>
+</head>
+<body>
+  <div class="container">
+    <form class="card" onsubmit="handleLogin(event)">
+      <div class="title">系统登录</div>
+      <label for="username">用户名</label>
+      <input id="username" name="username" placeholder="请输入用户名" required />
+      <label for="password">密码</label>
+      <input id="password" name="password" type="password" placeholder="请输入密码" required />
+      <button id="submitBtn" class="btn" type="submit">登录</button>
+      <div id="error" class="error"></div>
+    </form>
+  </div>
+</body>
+</html>
+
+

+ 1 - 1
run.py

@@ -48,4 +48,4 @@ if __name__ == '__main__':
     
     Logger.info("开始启动Web服务器")
     # app.run(debug=True, port=5000)
-    serve(app, host='0.0.0.0', port=5001)
+    serve(app, host='0.0.0.0', port=5003)

+ 50 - 0
run.spec

@@ -0,0 +1,50 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(
+    ['run.py'],
+    pathex=[],
+    binaries=[],
+    datas=[('app\\templates', 'app\\templates')],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    win_no_prefer_redirects=False,
+    win_private_assemblies=False,
+    cipher=block_cipher,
+    noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    [],
+    exclude_binaries=True,
+    name='run',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)
+coll = COLLECT(
+    exe,
+    a.binaries,
+    a.zipfiles,
+    a.datas,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    name='run',
+)