基于YOLO与大模型的安全防护检测系统的设计与实现
·
1. 前端HTML模板
1.base.html网站模版文件
{# 加载Django静态文件标签,用于引入CSS、JS、图片等静态资源 #} {% load static %} {# HTML文档根标签,定义网页语言为英文 #} <!DOCTYPE html> <html lang="en"> {# 头部区域:包含网页配置、标题、引入的样式和图标 #} <head> {# 设置字符编码为UTF-8,保证中文不乱码 #} <meta charset="utf-8"> {# 兼容IE浏览器内核 #} <meta http-equiv="X-UA-Compatible" content="IE=edge"> {# 响应式布局配置,适配手机、平板、电脑等不同设备 #} <meta name="viewport" content="width=device-width, initial-scale=1"> {# 网页描述信息(SEO用) #} <meta name="description" content=""> {# 网页作者信息 #} <meta name="author" content=""> {# 网页图标,显示在浏览器标签栏 #} <link rel="icon" type="image/png" sizes="16x16" href="{% static 'assets/images/favicon.png' %}"> {# 网页标题,浏览器标签显示内容 #} <title>安全防护检测系统</title> {# ===================== 引入外部CSS样式文件 ===================== #} {# 引入Bootstrap前端框架,用于快速构建响应式页面和布局 #} <link href="{% static 'assets/plugins/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet"> {# 引入Chartist图表插件样式,用于数据可视化图表展示 #} <link href="{% static 'assets/plugins/chartist-js/dist/chartist.min.css' %}" rel="stylesheet"> <link href="{% static 'assets/plugins/chartist-js/dist/chartist-init.css' %}" rel="stylesheet"> <link href="{% static 'assets/plugins/chartist-plugin-tooltip-master/dist/chartist-plugin-tooltip.css' %}" rel="stylesheet"> {# 引入C3图表库样式,用于生成统计图表 #} <link href="{% static 'assets/plugins/c3-master/c3.min.css' %}" rel="stylesheet"> {# 系统自定义的公共样式 #} <link href="{% static 'css/style.css' %}" rel="stylesheet"> {# 系统主题颜色样式文件(蓝色主题) #} <link href="{% static 'css/colors/blue.css' %}" id="theme" rel="stylesheet"> {# 兼容IE9以下浏览器,解决HTML5新标签不识别问题 #} <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <![endif]--> </head> {# 页面主体部分,fix-header固定顶部,fix-sidebar固定侧边栏 #} <body class="fix-header fix-sidebar card-no-border"> {# 页面加载动画:进入系统时显示的加载等待效果 #} <div class="preloader"> <svg class="circular" viewBox="25 25 50 50"> <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"/> </svg> </div> {# ===================== 页面整体布局容器 ===================== #} <div id="main-wrapper"> {# ===================== 顶部导航栏 ===================== #} <header class="topbar"> <nav class="navbar top-navbar navbar-toggleable-sm navbar-light"> {# 左侧:系统Logo区域 #} <div class="navbar-header"> {# 点击Logo跳转到首页 #} <a class="navbar-brand" href="{% url 'index' %}"> <b> {# 左侧小图标Logo #} <img src="{% static 'assets/images/logo-light-icon.png' %}" alt="homepage" class="light-logo"/> </b> <span> {# 系统文字Logo #} <img src="{% static 'assets/images/logo-light-text.png' %}" class="light-logo" alt="homepage"/> </span> </a> </div> {# 右侧:导航菜单 + 用户信息区域 #} <div class="navbar-collapse"> {# 左侧菜单按钮(手机端显示) #} <ul class="navbar-nav mr-auto mt-md-0"> {# 移动端菜单折叠按钮 #} <li class="nav-item"> <a class="nav-link nav-toggler hidden-md-up text-muted waves-effect waves-dark" href="javascript:void(0)"> <i class="mdi mdi-menu"></i> </a> </li> {# 搜索框(页面占位,未实现搜索功能) #} <li class="nav-item hidden-sm-down search-box"> <a class="nav-link hidden-sm-down text-muted waves-effect waves-dark" href="javascript:void(0)"> <i class="ti-search"></i> </a> <form class="app-search"> <input type="text" class="form-control" placeholder="Search & enter"> <a class="srh-btn"><i class="ti-close"></i></a> </form> </li> </ul> {# ===================== 右上角用户头像/登录区域 ===================== #} <ul class="navbar-nav my-lg-0"> <li class="nav-item dropdown"> {# 判断用户是否已登录 #} {% if user %} {# 已登录:显示头像 + 用户名 #} <a class="nav-link dropdown-toggle text-muted waves-effect waves-dark" href="" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {# 判断用户是否上传头像,有则显示用户头像,无则显示默认头像 #} <img src="{% if user.avatar %}{{ user.avatar.url }}{% else %}{% static 'assets/images/users/5.jpg' %}{% endif %}" alt="user" class="profile-pic m-r-10" style="width: 30px; height: 30px; object-fit: cover; border-radius: 50%;"/> {{ user.username }} </a> {# 点击头像弹出下拉菜单 #} <div class="dropdown-menu dropdown-menu-right animated flipInY"> <ul class="dropdown-user"> <li> {# 用户信息卡片:头像、名称、邮箱 #} <div class="dw-user-box"> <div class="u-img"> <img src="{% if user.avatar %}{{ user.avatar.url }}{% else %}{% static 'assets/images/users/5.jpg' %}{% endif %}" alt="user"> </div> <div class="u-text"> <h4>{{ user.username }}</h4> <p class="text-muted">{{ user.email|default:"" }}</p> {# 查看个人资料按钮 #} <a href="{% url 'pages_profile' %}" class="btn btn-rounded btn-danger btn-sm">查看资料</a> </div> </div> </li> {# 分割线 #} <li role="separator" class="divider"></li> {# 退出登录按钮 #} <li> <a href="{% url 'login_page' %}" onclick="logout(); return false;"> <i class="fa fa-power-off"></i> 退出登录 </a> </li> </ul> </div> {% else %} {# 未登录:显示登录按钮 #} <a class="nav-link text-muted waves-effect waves-dark" href="{% url 'login_page' %}"> <img src="{% static 'assets/images/users/1.jpg' %}" alt="user" class="profile-pic m-r-10" style="width: 30px; height: 30px; object-fit: cover; border-radius: 50%;"/> 登录 </a> {% endif %} </li> </ul> </div> </nav> </header> {# ===================== 左侧侧边栏菜单 ===================== #} <aside class="left-sidebar"> {# 侧边栏滚动区域 #} <div class="scroll-sidebar"> {# 侧边栏导航菜单 #} <nav class="sidebar-nav"> <ul id="sidebarnav"> {# 首页菜单 #} <li> <a class="waves-effect waves-dark" href="{% url 'index' %}" aria-expanded="false"> <i class="mdi mdi-gauge"></i> <span class="hide-menu">首页</span> </a> </li> {# 设备管理模块分组 #} <li class="divider">设备管理模块</li> <li> <a class="waves-effect waves-dark" href="{% url 'device_list' %}" aria-expanded="false"> <i class="mdi mdi-lan-connect"></i> <span class="hide-menu">设备管理</span> </a> </li> <li> <a class="waves-effect waves-dark" href="{% url 'device_add' %}" aria-expanded="false"> <i class="mdi mdi-plus-box"></i> <span class="hide-menu">添加设备</span> </a> </li> {# 图像识别模块分组 #} <li class="divider">图像识别模块</li> <li> <a class="waves-effect waves-dark" href="{% url 'image_recognition' %}" aria-expanded="false"> <i class="mdi mdi-image"></i> <span class="hide-menu">图片识别</span> </a> </li> <li> <a class="waves-effect waves-dark" href="{% url 'video_recognition' %}" aria-expanded="false"> <i class="mdi mdi-video"></i> <span class="hide-menu">视频识别</span> </a> </li> <li> <a class="waves-effect waves-dark" href="{% url 'camera_recognition' %}" aria-expanded="false"> <i class="mdi mdi-film"></i> <span class="hide-menu">摄像头识别</span> </a> </li> {# 检测记录模块分组 #} <li class="divider">检测记录模块</li> <li> <a class="waves-effect waves-dark" href="{% url 'recognition_records' %}" aria-expanded="false"> <i class="mdi mdi-history"></i> <span class="hide-menu">检测记录</span> </a> </li> {# 个人信息模块分组 #} <li class="divider">个人信息模块</li> <li> <a class="waves-effect waves-dark" href="{% url 'pages_profile' %}" aria-expanded="false"> <i class="mdi mdi-account-check"></i> <span class="hide-menu">个人信息管理</span> </a> </li> </ul> </nav> </div> </aside> {# ===================== 页面内容展示区域 ===================== #} {# 子页面通过继承该模板,将内容填充到这里 #} {% block content %} {% endblock %} </div> {# ===================== 引入JavaScript脚本文件 ===================== #} {# jQuery核心库,前端必备 #} <script src="{% static 'assets/plugins/jquery/jquery.min.js' %}"></script> {# Bootstrap依赖的tether工具 #} <script src="{% static 'assets/plugins/bootstrap/js/tether.min.js' %}"></script> {# Bootstrap脚本 #} <script src="{% static 'assets/plugins/bootstrap/js/bootstrap.min.js' %}"></script> {# 侧边栏滚动插件 #} <script src="{% static 'js/jquery.slimscroll.js' %}"></script> {# 按钮波浪动画效果 #} <script src="{% static 'js/waves.js' %}"></script> {# 侧边栏菜单展开收缩 #} <script src="{% static 'js/sidebarmenu.js' %}"></script> {# 吸顶插件 #} <script src="{% static 'assets/plugins/sticky-kit-master/dist/sticky-kit.min.js' %}"></script> {# 系统自定义脚本 #} <script src="{% static 'js/custom.min.js' %}"></script> {# 图表相关插件 #} <script src="{% static 'assets/plugins/chartist-js/dist/chartist.min.js' %}"></script> <script src="{% static 'assets/plugins/chartist-plugin-tooltip-master/dist/chartist-plugin-tooltip.min.js' %}"></script> <script src="{% static 'assets/plugins/d3/d3.min.js' %}"></script> <script src="{% static 'assets/plugins/c3-master/c3.min.js' %}"></script> <script src="{% static 'js/dashboard1.js' %}"></script> {# ===================== 退出登录JS函数 ===================== #} <script> // 退出登录函数:向后端发送请求,清除登录状态 function logout() { // 发送POST请求到后端退出接口 $.post('{% url "logout" %}', function (res) { alert(res.msg || '退出成功'); // 退出成功后跳转到首页 window.location.href = '/'; }).fail(function () { // 请求失败也强制跳回首页 window.location.href = '/'; }); } </script> </body> </html>2.
camera_recognition.html- 摄像头识别页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">摄像头识别</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">摄像头识别</li> </ol> </div> </div> {# ===================== 主内容卡片 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> <div class="card-block"> {# 卡片标题 #} <h4 class="card-title">摄像头安全帽检测</h4> {# 左右两列布局:左侧视频区域,右侧控制区域 #} <div class="row"> {# ===================== 左侧:视频显示区域 ===================== #} <div class="col-md-8"> {# 视频容器,相对定位用于叠加canvas #} <div class="video-container" style="position: relative;"> {# 视频元素,显示摄像头画面 #} <video id="video" width="100%" height="auto" autoplay muted></video> {# 结果画布,叠加在视频上绘制检测框,初始隐藏 #} <canvas id="resultCanvas" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; border: 1px solid #ddd; border-radius: 4px;"></canvas> {# 临时画布,用于捕获视频帧,隐藏不可见 #} <canvas id="canvas" style="display: none;"></canvas> </div> </div> {# ===================== 右侧:控制面板 ===================== #} <div class="col-md-4"> {# 摄像头设备选择下拉框 #} <div class="form-group"> <label for="deviceSelect">选择摄像头</label> <select id="deviceSelect" class="form-control"></select> </div> {# 系统设备关联下拉框(用于记录识别结果关联的设备) #} <div class="form-group"> <label for="systemDeviceSelect">选择系统设备</label> <select id="systemDeviceSelect" class="form-control"> <option value="">-- 不关联设备 --</option> {# 遍历后端传递的设备列表,生成选项 #} {% for device in devices %} <option value="{{ device.id }}">{{ device.name }} ({{ device.device_type }})</option> {% endfor %} </select> </div> {# 控制按钮组 #} <button id="startBtn" class="btn btn-success btn-block">开始识别</button> <button id="stopBtn" class="btn btn-danger btn-block mt-2" disabled>停止识别</button> <button id="captureBtn" class="btn btn-primary btn-block mt-2" disabled>捕获截图</button> {# 识别结果展示区域 #} <div class="mt-4"> <h5>识别结果</h5> <div id="detectionResult" class="mt-2" style="max-height: 300px; overflow-y: auto; border: 1px solid #e9ecef; padding: 10px; border-radius: 4px;"> </div> </div> </div> </div> {# ===================== JavaScript代码区域 ===================== #} <script> // ========== DOM元素获取 ========== // 获取视频元素 const video = document.getElementById('video'); // 获取临时画布(用于捕获帧) const canvas = document.getElementById('canvas'); // 获取结果画布(用于绘制检测框) const resultCanvas = document.getElementById('resultCanvas'); // 获取临时画布的2D绘图上下文 const ctx = canvas.getContext('2d'); // 获取结果画布的2D绘图上下文 const resultCtx = resultCanvas.getContext('2d'); // 获取摄像头选择下拉框 const deviceSelect = document.getElementById('deviceSelect'); // 获取开始按钮 const startBtn = document.getElementById('startBtn'); // 获取停止按钮 const stopBtn = document.getElementById('stopBtn'); // 获取截图按钮 const captureBtn = document.getElementById('captureBtn'); // 获取识别结果显示区域 const detectionResult = document.getElementById('detectionResult'); // ========== 状态变量 ========== let stream = null; // 摄像头视频流对象 let recognitionInterval = null; // 定时器ID let isRecognizing = false; // 是否正在识别中 // ========== 函数1:加载可用摄像头设备 ========== async function loadDevices() { try { // 获取所有媒体设备(摄像头、麦克风等) const devices = await navigator.mediaDevices.enumerateDevices(); // 筛选出视频输入设备(摄像头) const videoDevices = devices.filter(device => device.kind === 'videoinput'); // 清空下拉框 deviceSelect.innerHTML = ''; // 遍历摄像头设备,添加到下拉框 videoDevices.forEach(device => { const option = document.createElement('option'); option.value = device.deviceId; option.text = device.label || `摄像头 ${deviceSelect.options.length + 1}`; deviceSelect.appendChild(option); }); } catch (error) { console.error('无法获取设备列表:', error); } } // ========== 函数2:启动摄像头 ========== async function startCamera() { try { const deviceId = deviceSelect.value; // 请求摄像头权限并获取视频流 stream = await navigator.mediaDevices.getUserMedia({ video: {deviceId: deviceId ? {exact: deviceId} : undefined}, audio: false }); // 将视频流绑定到video元素 video.srcObject = stream; // 更新按钮状态 startBtn.disabled = true; stopBtn.disabled = false; captureBtn.disabled = false; } catch (error) { console.error('无法启动摄像头:', error); alert('无法启动摄像头: ' + error.message); } } // ========== 函数3:停止摄像头 ========== function stopCamera() { if (stream) { // 停止所有视频轨道 stream.getTracks().forEach(track => track.stop()); stream = null; video.srcObject = null; // 更新按钮状态 startBtn.disabled = false; stopBtn.disabled = true; captureBtn.disabled = true; // 停止识别 stopRecognition(); } } // ========== 函数4:开始识别 ========== function startRecognition() { // 检查是否已在识别中或摄像头未启动 if (isRecognizing || !stream) { if (!stream) { alert('请先启动摄像头后再开始识别'); } return; } isRecognizing = true; resultCanvas.style.display = 'block'; detectionResult.innerHTML = '<div class="alert alert-info">正在识别中...</div>'; // 每5秒执行一次识别 recognitionInterval = setInterval(async () => { // 将当前视频帧绘制到临时canvas canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 将canvas图像转为base64格式 const imageData = canvas.toDataURL('image/jpeg'); try { console.log('正在发送识别请求...'); const systemDeviceId = document.getElementById('systemDeviceSelect').value; // 发送POST请求到后端识别接口 const response = await fetch('/detect_mask/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ image: imageData, recognition_type: 'camera', device_id: systemDeviceId || null, location: '摄像头实时识别' }) }); if (!response.ok) { throw new Error(`HTTP错误! 状态: ${response.status}`); } const data = await response.json(); if (data.error) { console.error('识别错误:', data.error); detectionResult.innerHTML = '<div class="alert alert-danger">识别错误: ' + data.error + '</div>'; stopRecognition(); return; } console.log('识别成功:', data); // 绘制检测结果 drawDetectionResults(data.detections); // 更新识别结果 updateDetectionResult(data.detections); } catch (error) { console.error('发送识别请求失败:', error); detectionResult.innerHTML = '<div class="alert alert-danger">请求失败: ' + error.message + '</div>'; stopRecognition(); } }, 5000); } // ========== 函数5:停止识别 ========== function stopRecognition() { if (!isRecognizing) return; // 清除定时器 clearInterval(recognitionInterval); isRecognizing = false; resultCanvas.style.display = 'none'; detectionResult.innerHTML = ''; } // ========== 函数6:获取标签信息(颜色和文字) ========== function getLabelInfo(label) { const labelMap = { 'mask': {color: '#00ff00', text: '佩戴口罩'}, 'no_mask': {color: '#ff0000', text: '未佩戴口罩'}, 'hat': {color: '#00ff00', text: '佩戴安全帽'}, 'person': {color: '#ff0000', text: '未佩戴安全帽'} }; return labelMap[label] || {color: '#ffff00', text: '未知'}; } // ========== 函数7:绘制检测结果(画框和标签) ========== function drawDetectionResults(detections) { // 如果没有检测结果,只显示原始视频帧 if (!detections || detections.length === 0) { resultCanvas.width = canvas.width; resultCanvas.height = canvas.height; resultCtx.drawImage(canvas, 0, 0); return; } // 设置画布尺寸并绘制原始视频帧 resultCanvas.width = canvas.width; resultCanvas.height = canvas.height; resultCtx.drawImage(canvas, 0, 0); // 遍历每个检测结果,绘制矩形框和标签 detections.forEach(detection => { const [x, y, width, height] = detection.bbox; const label = detection.label; const confidence = detection.confidence; const labelInfo = getLabelInfo(label); const color = labelInfo.color; const labelText = labelInfo.text; const text = `${labelText} ${(confidence * 100).toFixed(1)}%`; // 绘制矩形框 resultCtx.strokeStyle = color; resultCtx.lineWidth = 2; resultCtx.strokeRect(x, y, width, height); // 绘制标签背景 resultCtx.font = '16px Arial'; const textWidth = resultCtx.measureText(text).width; const textHeight = 20; resultCtx.fillStyle = color; resultCtx.fillRect(x, y - textHeight - 4, textWidth + 8, textHeight + 4); resultCtx.fillStyle = '#ffffff'; resultCtx.fillText(text, x + 4, y - 6); }); } // ========== 函数8:更新识别结果显示(文字) ========== function updateDetectionResult(detections) { // 没有检测结果 if (!detections || detections.length === 0) { detectionResult.innerHTML = '<div class="alert alert-info">未检测到目标</div>'; return; } let resultHTML = ''; detections.forEach((detection, index) => { const label = detection.label; const confidence = (detection.confidence * 100).toFixed(1); let labelText = ''; let colorClass = ''; // 根据标签类型设置显示文字和颜色 switch (label) { case 'mask': labelText = '佩戴口罩'; colorClass = 'success'; break; case 'no_mask': labelText = '未佩戴口罩'; colorClass = 'danger'; break; case 'hat': labelText = '佩戴安全帽'; colorClass = 'success'; break; case 'person': labelText = '未佩戴安全帽'; colorClass = 'danger'; break; default: labelText = '未知'; colorClass = 'warning'; } resultHTML += ` <div class="alert alert-${colorClass} mt-2"> <p><strong>目标 ${index + 1}:</strong> ${labelText}</p> <p><strong>置信度:</strong> ${confidence}%</p> </div>`; }); detectionResult.innerHTML = resultHTML; } // ========== 函数9:捕获截图 ========== function captureScreenshot() { if (!stream) return; // 绘制当前视频帧 canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 创建下载链接 const link = document.createElement('a'); link.download = 'screenshot_' + new Date().toISOString() + '.png'; link.href = canvas.toDataURL('image/png'); link.click(); } // ========== 事件绑定 ========== // 开始按钮:启动摄像头并开始识别 startBtn.addEventListener('click', async () => { await startCamera(); startRecognition(); }); // 停止按钮:停止摄像头 stopBtn.addEventListener('click', stopCamera); // 截图按钮:捕获截图 captureBtn.addEventListener('click', captureScreenshot); // 页面加载时初始化摄像头列表 window.addEventListener('load', loadDevices); </script> </div> </div> </div> </div> {% endblock %}
3.device_add.html- 添加设备页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# ===================== 内容容器 ===================== #} <!-- ============================================================== --> <!-- Container fluid --> <!-- ============================================================== --> <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <!-- ============================================================== --> <!-- Bread crumb and right sidebar toggle --> <!-- ============================================================== --> <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">添加设备</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">添加设备</li> </ol> </div> </div> <!-- ============================================================== --> <!-- End Bread crumb and right sidebar toggle --> <!-- ============================================================== --> {# ===================== 主要内容区域 ===================== #} <!-- ============================================================== --> <!-- Start Page Content --> <!-- ============================================================== --> <div class="row"> <div class="col-12"> {# 卡片容器,用于承载表单 #} <div class="card"> <div class="card-block"> {# ===================== 添加设备表单 ===================== #} {# method="post":使用POST方式提交,用于提交数据 #} {# enctype="multipart/form-data":支持文件上传(视频文件) #} <form id="addDeviceForm" method="post" enctype="multipart/form-data"> {# Django CSRF防护令牌,防止跨站请求伪造攻击 #} {% csrf_token %} {# 表单主体区域 #} <div class="form-body"> {# 第一行:设备名称 + 设备类型(两列布局) #} <div class="row"> {# 左侧:设备名称输入框 #} <div class="col-md-6"> <div class="form-group"> {# 设备名称标签,span表示必填项(红色星号) #} <label for="deviceName">设备名称 <span class="text-danger">*</span></label> {# 文本输入框,name="name" 对应后端Device模型的name字段 #} {# required 表示必填 #} <input type="text" id="deviceName" name="name" class="form-control" placeholder="请输入设备名称" required> </div> </div> {# 右侧:设备类型下拉框 #} <div class="col-md-6"> <div class="form-group"> <label for="deviceType">设备类型</label> {# 下拉选择框,name="device_type" 对应后端Device模型的device_type字段 #} <select id="deviceType" name="device_type" class="form-control"> <option value="camera">摄像头</option> <option value="other">其他</option> </select> </div> </div> </div> {# 第二行:设备描述(占满整行) #} <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="deviceDescription">设备描述</label> {# 多行文本输入框,name="description" 对应后端Device模型的description字段 #} <textarea id="deviceDescription" name="description" class="form-control" rows="3" placeholder="请输入设备描述"></textarea> </div> </div> </div> {# 第三行:安装位置 + 视频文件 #} <div class="row"> {# 左侧:安装位置输入框 #} <div class="col-md-6"> <div class="form-group"> <label for="deviceLocation">安装位置</label> {# 文本输入框,name="location" 对应后端Device模型的location字段 #} <input type="text" id="deviceLocation" name="location" class="form-control" placeholder="请输入设备安装位置"> </div> </div> {# 右侧:视频文件上传 #} <div class="col-md-6"> <div class="form-group"> <label for="videoFile">视频文件</label> {# 文件上传输入框,name="video_file" 对应后端Device模型的video_file字段 #} {# accept="video/*" 限制只能选择视频文件 #} <input type="file" id="videoFile" name="video_file" class="form-control" accept="video/*"> </div> </div> </div> {# 第四行:设备状态 #} <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="deviceStatus">设备状态</label> {# 下拉选择框,name="status" 对应后端Device模型的status字段 #} <select id="deviceStatus" name="status" class="form-control"> <option value="online">在线</option> <option value="offline">离线</option> <option value="maintenance">维护中</option> </select> </div> </div> </div> </div> {# ===================== 表单按钮区域 ===================== #} <div class="form-actions"> {# 提交按钮:保存设备信息 #} <button type="submit" class="btn btn-success"> <i class="mdi mdi-check"></i> 保存 </button> {# 重置按钮:清空表单所有输入 #} <button type="reset" class="btn btn-inverse">重置</button> {# 取消按钮:返回设备列表页面 #} <a href="{% url 'device_list' %}" class="btn btn-secondary">取消</a> </div> </form> </div> </div> </div> </div> <!-- ============================================================== --> <!-- End PAge Content --> <!-- ============================================================== --> </div> </div> {# 结束 content 块 #} {% endblock %}4.
device_detail.html- 设备详情页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">设备详情</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="{% url 'index' %}">首页</a></li> <li class="breadcrumb-item active">设备详情</li> </ol> </div> </div> {# ===================== 第一行:设备信息 + 统计信息(两列布局) ===================== #} <div class="row"> {# ===================== 左侧:设备详细信息卡片 ===================== #} <div class="col-md-6"> <div class="card"> {# 卡片头部:蓝色背景,显示标题 #} <div class="card-header bg-info"> <h4 class="mb-0 text-white"> <i class="mdi mdi-information-variant mr-2"></i>设备详细信息 </h4> </div> {# 卡片主体:表格形式展示设备信息 #} <div class="card-block"> <div class="table-responsive"> <table class="table table-bordered"> <tbody> {# 设备名称行 #} <tr> <td width="30%"><strong>设备名称</strong></td> <td>{{ device.name }}</td> </tr> {# 设备类型行 #} <tr> <td><strong>设备类型</strong></td> <td> {# 如果有设备类型,显示蓝色标签;否则显示灰色"未分类" #} {% if device.device_type %} <span class="badge badge-primary">{{ device.device_type }}</span> {% else %} <span class="badge badge-secondary">未分类</span> {% endif %} </td> </tr> {# 设备位置行 #} <tr> <td><strong>设备位置</strong></td> <td> {# 如果有位置,显示位置图标和文字;否则显示灰色"未设置位置" #} {% if device.location %} <i class="mdi mdi-map-marker mr-1"></i>{{ device.location }} {% else %} <span class="text-muted">未设置位置</span> {% endif %} </td> </tr> {# 设备状态行 #} <tr> <td><strong>设备状态</strong></td> <td> {# 根据状态显示不同颜色的标签 #} {% if device.status == 'online' %} <span class="badge badge-success">在线</span> {% elif device.status == 'offline' %} <span class="badge badge-danger">离线</span> {% elif device.status == 'maintenance' %} <span class="badge badge-warning">维护中</span> {% else %} <span class="badge badge-secondary">{{ device.status }}</span> {% endif %} </td> </tr> {# 创建时间行 #} <tr> <td><strong>创建时间</strong></td> <td>{{ device.created_at|date:"Y-m-d H:i:s" }}</td> </tr> {# 更新时间行 #} <tr> <td><strong>更新时间</strong></td> <td>{{ device.updated_at|date:"Y-m-d H:i:s" }}</td> </tr> {# 设备描述行 #} <tr> <td><strong>设备描述</strong></td> <td> {# 如果有描述则显示,否则显示灰色"暂无描述" #} {% if device.description %} {{ device.description }} {% else %} <span class="text-muted">暂无描述</span> {% endif %} </td> </tr> </tbody> </table> </div> </div> </div> </div> {# ===================== 右侧:设备统计信息卡片 ===================== #} <div class="col-md-6"> <div class="card"> {# 卡片头部:黄色背景,显示标题 #} <div class="card-header bg-warning"> <h4 class="mb-0 text-white"> <i class="mdi mdi-chart-bar mr-2"></i>设备统计信息 </h4> </div> {# 卡片主体 #} <div class="card-block"> {# 总识别次数统计卡片 #} <div class="row"> <div class="col-md-12"> <div class="card text-center"> <div class="card-block"> <h4 class="card-title"> <i class="mdi mdi-history text-info"></i> </h4> {# 显示总识别次数,默认0 #} <h2 class="text-info"> {{ total_records|default:0 }} </h2> <p class="card-text text-muted">总识别次数</p> </div> </div> </div> </div> {# 识别结果分布进度条 #} <div class="row mt-3"> <div class="col-md-12"> <h5 class="text-center">识别类型分布</h5> {# 如果有识别记录,显示进度条 #} {% if total_records|default:0 > 0 %} {# 安全佩戴(mask/hat)进度条 #} <div class="progress mb-2"> <div class="progress-bar bg-success" role="progressbar" style="width: {% widthratio mask_count|default:0 total_records|default:0 100 %}%"> 安全佩戴: {{ mask_count|default:0 }} </div> </div> {# 未安全佩戴(no_mask/person)进度条 #} <div class="progress"> <div class="progress-bar bg-danger" role="progressbar" style="width: {% widthratio no_mask_count|default:0 total_records|default:0 100 %}%"> 未安全佩戴: {{ no_mask_count|default:0 }} </div> </div> {% else %} {# 没有识别记录时显示提示 #} <p class="text-center text-muted">暂无识别数据</p> {% endif %} </div> </div> </div> </div> </div> </div> {# ===================== 第二行:视频监控区域 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> {# 卡片头部:浅灰色背景 #} <div class="card-header bg-light"> <h4 class="mb-0"> <i class="mdi mdi-video mr-2"></i>实时视频监控 </h4> </div> {# 卡片主体:视频播放器 #} <div class="card-block position-relative"> {# 如果设备有上传视频文件 #} {% if device.video_file %} <div class="video-wrapper" style="display: inline-block; position: relative;"> {# HTML5视频播放器 #} <video id="videoPlayer" width="640" height="360" controls style="max-width: 100%; height: auto; border-radius: 8px;"> {# 视频源文件 #} <source src="{{ device.video_file.url }}" type="video/mp4"> 您的浏览器不支持视频标签。 </video> {# 叠加画布(用于绘制检测框,目前未使用) #} <canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; border-radius: 8px; object-fit: cover;"> </canvas> </div> {% else %} {# 没有视频文件时显示提示 #} <div class="alert alert-info text-center"> 该设备暂无上传视频文件 </div> {% endif %} </div> </div> </div> </div> </div> </div> {# 结束 content 块 #} {% endblock %}5.
device_list.html- 设备列表页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor">设备管理</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="{% url 'index' %}">首页</a></li> <li class="breadcrumb-item active">设备管理</li> </ol> </div> {# 右上角添加设备按钮 #} <div class="col-md-7 col-4 align-self-center"> {# 绿色按钮,点击跳转到添加设备页面 #} <a href="{% url 'device_add' %}" class="btn btn-success pull-right"> 添加设备 </a> </div> </div> {# ===================== 设备列表表格区域 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> <div class="card-body"> {# 响应式表格容器(水平滚动) #} <div class="table-responsive"> {# 设备列表表格 #} <table class="table table-bordered table-hover"> {# 表格头部:列标题 #} <thead> <tr> <th>设备名称</th> <th>设备类型</th> <th>设备状态</th> <th>安装位置</th> <th>操作</th> </tr> </thead> {# 表格主体:遍历显示设备列表 #} <tbody> {# Django模板循环:遍历后端传递的 devices 列表 #} {% for device in devices %} <tr> {# 设备名称列 #} <td>{{ device.name }}</td> {# 设备类型列 #} <td>{{ device.device_type }}</td> {# 设备状态列:根据状态显示不同颜色标签 #} <td> {% if device.status == 'online' %} <span class="badge bg-success">在线</span> {% elif device.status == 'offline' %} <span class="badge bg-secondary">离线</span> {% else %} <span class="badge bg-warning">维护</span> {% endif %} </td> {# 安装位置列 #} <td>{{ device.location }}</td> {# 操作列:包含详情按钮和删除按钮 #} <td> {# 详情按钮:跳转到设备详情页 #} <a href="{% url 'device_detail' device.id %}" class="btn btn-info btn-sm">详情</a> {# 删除按钮:触发AJAX删除请求 #} {# data-id 属性存储设备ID,供JavaScript使用 #} <button class="btn btn-danger btn-sm delete-device" data-id="{{ device.id }}"> 删除 </button> </td> </tr> {# 如果没有设备数据,显示空状态提示 #} {% empty %} <tr> <td colspan="5" class="text-center">暂无设备数据</td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> </div> {# 页脚版权信息 #} <footer class="footer">© 2026 安全防护穿戴识别系统</footer> </div> {# ===================== 删除功能 JavaScript ===================== #} <script> // 使用jQuery的$(function(){...}) 等同于 $(document).ready(...) // 确保DOM加载完成后才执行内部代码 $(function(){ // 绑定删除按钮的点击事件 // $(".delete-device") 选择所有class为"delete-device"的元素 $(".delete-device").click(function(){ // 获取被点击按钮上 data-id 属性的值(设备ID) let deviceId = $(this).attr("data-id"); // 弹出确认对话框,用户点击"确定"返回true,点击"取消"返回false if(!confirm("确定要删除该设备?删除后无法恢复!")){ return; // 用户点击取消,退出函数,不发送删除请求 } // 发送AJAX删除请求到后端 $.ajax({ url: `/api/device/${deviceId}/delete/`, // 后端删除接口URL type: "POST", // 使用POST方法 headers: { "X-CSRFToken":"{{ csrf_token }}" // 添加CSRF令牌,防止跨站请求伪造 }, // 请求成功时的回调函数 success:function(res){ // 检查后端返回的success标志 if(res.success){ alert("删除成功"); window.location.reload(); // 刷新当前页面,更新设备列表 }else{ alert(res.msg); // 显示后端返回的错误信息 } }, // 请求失败时的回调函数(网络错误、服务器错误等) error:function(){ alert("网络错误,删除失败"); } }); }); }); </script> {# 结束 content 块 #} {% endblock %}6.
image_recognition.html- 图片识别页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">图片识别</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">图片识别</li> </ol> </div> </div> {# ===================== 主内容卡片 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> <div class="card-block"> {# 卡片标题:支持口罩+安全帽识别 #} <h4 class="card-title">图片安全防护识别(口罩+安全帽)</h4> {# 左右两列布局:左侧图片上传区域,右侧结果显示区域 #} <div class="row"> {# ===================== 左侧:图片上传和预览区域 ===================== #} <div class="col-md-6"> {# 图片上传控件 #} <div class="form-group"> <label for="imageUpload">选择图片</label> {# 文件选择框,只接受图片格式 #} <input type="file" id="imageUpload" accept="image/*" class="form-control"> </div> {# 图片预览区域 #} <div class="preview-container mt-4"> {# 原始图片预览(初始隐藏) #} <img id="imagePreview" src="" alt="图片预览" style="max-width: 100%; max-height: 400px; display: none;"> {# 绘制检测结果的画布(初始隐藏) #} <canvas id="resultCanvas" style="max-width: 100%; max-height: 400px; display: none; border: 1px solid #ddd;"></canvas> </div> {# 开始识别按钮(初始禁用,选择图片后启用) #} <button id="recognizeBtn" class="btn btn-primary btn-block mt-4" disabled>开始识别</button> </div> {# ===================== 右侧:识别结果显示区域 ===================== #} <div class="col-md-6"> {# YOLO识别结果区域 #} <h5>识别结果</h5> <div id="detectionResult" class="mt-2" style="max-height: 200px; overflow-y: auto; border: 1px solid #e9ecef; padding: 10px; border-radius: 4px;"></div> {# AI大模型分析结果区域 #} <h5 class="mt-3">AI分析结果</h5> <div id="aiAnalysisResult" class="mt-2" style="max-height: 200px; overflow-y: auto; border: 1px solid #e9ecef; padding: 10px; border-radius: 4px;"> <div class="alert alert-info">等待AI分析...</div> </div> </div> </div> </div> </div> </div> </div> {# ===================== JavaScript代码区域 ===================== #} <script> // ========== DOM元素获取 ========== const imageUpload = document.getElementById('imageUpload'); // 图片上传输入框 const imagePreview = document.getElementById('imagePreview'); // 图片预览元素 const resultCanvas = document.getElementById('resultCanvas'); // 结果画布 const recognizeBtn = document.getElementById('recognizeBtn'); // 开始识别按钮 const detectionResult = document.getElementById('detectionResult'); // YOLO识别结果显示区域 const aiAnalysisResult = document.getElementById('aiAnalysisResult'); // AI分析结果显示区域 // ========== 函数1:获取标签信息(颜色、文字、Bootstrap类) ========== // 统一管理4种类别的显示样式 function getLabelInfo(label) { const labelMap = { 'mask': { color: '#00ff00', text: '佩戴口罩', badgeClass: 'success' }, // 绿色 'no_mask': { color: '#ff0000', text: '未佩戴口罩', badgeClass: 'danger' }, // 红色 'hat': { color: '#00ff00', text: '佩戴安全帽', badgeClass: 'success' }, // 绿色 'person': { color: '#ff0000', text: '未佩戴安全帽', badgeClass: 'danger' } // 红色 }; // 如果标签未知,返回默认黄色 return labelMap[label] || { color: '#ffff00', text: '未知', badgeClass: 'warning' }; } // ========== 函数2:图片预览(选择图片后显示) ========== imageUpload.addEventListener('change', function (e) { const file = e.target.files[0]; // 获取选择的文件 if (file) { const reader = new FileReader(); // 创建FileReader对象 reader.onload = function (event) { imagePreview.src = event.target.result; // 设置预览图片的src imagePreview.style.display = 'block'; // 显示预览图片 recognizeBtn.disabled = false; // 启用识别按钮 } reader.readAsDataURL(file); // 读取文件为DataURL } }); // ========== 函数3:开始识别(发送POST请求到后端) ========== recognizeBtn.addEventListener('click', async function () { // 检查是否已选择图片 if (!imagePreview.src) { alert('请先选择图片'); return; } // 显示识别中提示,清空之前的分析结果 detectionResult.innerHTML = '<div class="alert alert-info">识别中,请稍候...</div>'; aiAnalysisResult.innerHTML = ''; try { // 发送POST请求到后端识别接口 const response = await fetch('/detect_mask/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ image: imagePreview.src, // base64格式的图片 recognition_type: 'image' // 识别类型:图片 }) }); const data = await response.json(); // 解析JSON响应 // 检查是否有错误 if (data.error) { detectionResult.innerHTML = `<div class="alert alert-danger">识别失败:${data.error}</div>`; return; } // 更新识别结果显示 updateDetectionResult(data.detections); // 绘制带标注的图像 drawDetectionResults(data.detections); // 显示AI分析结果 displayAIAnalysis(data.ai_analysis); } catch (error) { console.error('请求异常:', error); detectionResult.innerHTML = `<div class="alert alert-danger">请求失败:${error.message}</div>`; } }); // ========== 函数4:绘制带标注的图像(在canvas上画框和文字) ========== function drawDetectionResults(detections) { // 如果没有检测结果,显示原始图片,隐藏画布 if (!detections || detections.length === 0) { imagePreview.style.display = 'block'; resultCanvas.style.display = 'none'; return; } // 隐藏原始图片,显示画布 imagePreview.style.display = 'none'; resultCanvas.style.display = 'block'; // 创建Image对象加载原图 const img = new Image(); img.onload = function () { const ctx = resultCanvas.getContext('2d'); // 获取画布2D上下文 resultCanvas.width = img.width; // 设置画布宽度等于图片宽度 resultCanvas.height = img.height; // 设置画布高度等于图片高度 ctx.drawImage(img, 0, 0); // 在画布上绘制原图 // 遍历每个检测结果,绘制矩形框和标签 detections.forEach(detection => { const [x, y, width, height] = detection.bbox; // 获取边界框坐标 const label = detection.label; // 获取标签(mask/no_mask/hat/person) const confidence = detection.confidence; // 获取置信度 // 获取标签对应的颜色和文字 const labelInfo = getLabelInfo(label); const color = labelInfo.color; const labelText = labelInfo.text; const text = `${labelText} ${(confidence * 100).toFixed(1)}%`; // 绘制矩形框 ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.strokeRect(x, y, width, height); // 绘制标签背景 ctx.font = '16px Arial'; const textWidth = ctx.measureText(text).width; // 测量文字宽度 const textHeight = 20; // 绘制半透明背景 ctx.fillStyle = color; ctx.fillRect(x, y - textHeight - 4, textWidth + 8, textHeight + 4); // 绘制白色文字 ctx.fillStyle = '#ffffff'; ctx.fillText(text, x + 4, y - 6); }); }; img.src = imagePreview.src; // 开始加载图片 } // ========== 函数5:更新识别结果显示(文字列表) ========== function updateDetectionResult(detections) { // 没有检测结果 if (!detections || detections.length === 0) { detectionResult.innerHTML = '<div class="alert alert-info">未检测到目标</div>'; return; } let resultHTML = ''; detections.forEach((detection, index) => { const label = detection.label; const confidence = (detection.confidence * 100).toFixed(1); // 根据标签设置显示文字和Bootstrap颜色类 let labelText = ''; let badgeClass = ''; switch(label) { case 'mask': labelText = '佩戴口罩'; badgeClass = 'success'; // 绿色 break; case 'no_mask': labelText = '未佩戴口罩'; badgeClass = 'danger'; // 红色 break; case 'hat': labelText = '佩戴安全帽'; badgeClass = 'success'; // 绿色 break; case 'person': labelText = '未佩戴安全帽'; badgeClass = 'danger'; // 红色 break; default: labelText = '未知'; badgeClass = 'warning'; // 黄色 } // 拼接HTML resultHTML += ` <div class="alert alert-${badgeClass} mt-2"> <p><strong>目标 ${index + 1}:</strong> ${labelText}</p> <p><strong>置信度:</strong> ${confidence}%</p> </div>`; }); detectionResult.innerHTML = resultHTML; } // ========== 函数6:显示AI大模型分析结果 ========== function displayAIAnalysis(aiAnalysis) { if (aiAnalysis) { // 有分析结果,显示成功样式 aiAnalysisResult.innerHTML = ` <div class="alert alert-success"> <strong>AI分析结果:</strong><br> ${aiAnalysis} </div>`; } else { // 没有分析结果,显示警告样式 aiAnalysisResult.innerHTML = ` <div class="alert alert-warning"> AI分析暂时不可用,请稍后重试 </div>`; } } </script> </div> {% endblock %}7.
index.html- 首页仪表盘页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# ===================== 内容容器 ===================== #} <!-- ============================================================== --> <!-- Container fluid --> <!-- ============================================================== --> <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <!-- ============================================================== --> <!-- Bread crumb and right sidebar toggle --> <!-- ============================================================== --> <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor">首页</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">仪表盘</li> </ol> </div> </div> <!-- ============================================================== --> <!-- End Bread crumb and right sidebar toggle --> <!-- ============================================================== --> {# ===================== 第一行:4个统计卡片 ===================== #} <!-- ============================================================== --> <!-- Start Page Content --> <!-- ============================================================== --> <div class="row"> {# 卡片1:总设备数 #} <div class="col-md-3"> <div class="card mini-stats-wid"> <div class="card-body"> <div class="d-flex"> {# 左侧:统计数值 #} <div class="flex-grow-1"> <p class="text-muted fw-medium mb-2">总设备数</p> <h4 class="mb-0">{{ total_devices }}</h4> </div> {# 右侧:图标 #} <div class="flex-shrink-0 align-self-center"> <div class="mini-stat-icon avatar-sm rounded-circle bg-primary"> <span class="avatar-title"> <i class="bx bx-devices font-size-24"></i> </span> </div> </div> </div> </div> </div> </div> {# 卡片2:在线设备数量 #} <div class="col-md-3"> <div class="card mini-stats-wid"> <div class="card-body"> <div class="d-flex"> <div class="flex-grow-1"> <p class="text-muted fw-medium mb-2">在线识别数量</p> <h4 class="mb-0">{{ online_devices }}</h4> </div> <div class="flex-shrink-0 align-self-center"> <div class="mini-stat-icon avatar-sm rounded-circle bg-primary"> <span class="avatar-title"> <i class="bx bx-devices font-size-24"></i> </span> </div> </div> </div> </div> </div> </div> {# 卡片3:今日识别数量 #} <div class="col-md-3"> <div class="card mini-stats-wid"> <div class="card-body"> <div class="d-flex"> <div class="flex-grow-1"> <p class="text-muted fw-medium mb-2">今日识别数量</p> <h4 class="mb-0">{{ today_records }}</h4> </div> <div class="flex-shrink-0 align-self-center"> <div class="mini-stat-icon avatar-sm rounded-circle bg-primary"> <span class="avatar-title"> <i class="bx bx-devices font-size-24"></i> </span> </div> </div> </div> </div> </div> </div> {# 卡片4:总识别数量 #} <div class="col-md-3"> <div class="card mini-stats-wid"> <div class="card-body"> <div class="d-flex"> <div class="flex-grow-1"> <p class="text-muted fw-medium mb-2">总识别数量</p> <h4 class="mb-0">{{ total_record }}</h4> </div> <div class="flex-shrink-0 align-self-center"> <div class="mini-stat-icon avatar-sm rounded-circle bg-primary"> <span class="avatar-title"> <i class="bx bx-devices font-size-24"></i> </span> </div> </div> </div> </div> </div> </div> </div> <!-- Row --> {# ===================== 第二行:图表区域(折线图 + 饼图) ===================== #} <div class="row"> {# 左侧:每日识别趋势折线图 #} <div class="col-lg-8 col-md-7"> <div class="card"> <div class="card-block"> <h3 class="card-title">每天识别次数的变化</h3> {# ECharts 折线图容器 #} <div id="daily-chart" style="height: 500px;width: 100%"></div> </div> </div> </div> {# 右侧:识别结果比例饼图 #} <div class="col-lg-4 col-md-5"> <div class="card"> <div class="card-block"> <h3 class="card-title">识别结果比例</h3> {# ECharts 饼图容器 #} <div id="pie-chart" style="height: 500px;width: 100%"></div> </div> </div> </div> </div> <!-- Row --> {# ===================== 第三行:设备类型分布 + 最近识别记录 ===================== #} <div class="row"> {# 左侧:设备类型分布表格 #} <div class="col-lg-6"> <div class="card"> <div class="card-body"> <h4 class="card-title mb-4">设备类型分布</h4> <div class="table-responsive"> <table class="table table-centered table-nowrap mb-0"> <thead> <tr> <th>设备类型</th> <th>数量</th> <th>占比</th> </tr> </thead> <tbody> {# 遍历设备类型统计数据 #} {% for type in device_types %} <tr> <td>{{ type.device_type }}</td> {# 设备类型名称 #} <td>{{ type.count }}</td> {# 该类型的设备数量 #} <td> {# 进度条显示占比 #} <div class="progress progress-sm"> <div class="progress-bar bg-primary" role="progressbar" style="width: {% widthratio type.count total_devices 100 %}%"> {% widthratio type.count total_devices 100 %}% </div> </div> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> {# 右侧:最近识别记录表格 #} <div class="col-lg-6"> <div class="card"> <div class="card-body"> <h4 class="card-title mb-4">最近识别记录</h4> <div class="table-responsive"> <table class="table table-centered table-nowrap mb-0"> <thead> <tr> <th>时间</th> <th>设备</th> <th>结果</th> <th>置信度</th> </tr> </thead> <tbody> {# 遍历最近识别记录 #} {% for record in recent_records %} <tr> <td>{{ record.timestamp|date:"m-d H:i" }}</td> {# 识别时间 #} <td>{{ record.device.name }}</td> {# 设备名称 #} <td> {# 根据结果类型显示不同颜色的标签 #} {% if record.result == "mask" %} <span class="badge bg-success">安全佩戴</span> {% elif record.result == "no_mask" %} <span class="badge bg-danger">未安全佩戴</span> {% else %} <span class="badge bg-warning">不确定</span> {% endif %} </td> <td>{{ record.confidence|default:"0" }}%</td> {# 置信度 #} </tr> {% empty %} <tr> <td colspan="4" class="text-center">暂无记录</td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> <!-- Row --> <!-- ============================================================== --> <!-- End PAge Content --> <!-- ============================================================== --> </div> <!-- ============================================================== --> <!-- End Container fluid --> <!-- ============================================================== --> {# ===================== 页脚 ===================== #} <!-- ============================================================== --> <!-- footer --> <!-- ============================================================== --> <footer class="footer"> 首页 </footer> <!-- ============================================================== --> <!-- End footer --> <!-- ============================================================== --> </div> {# ===================== ECharts 图表库 ===================== #} {# 引入 ECharts CDN,用于绘制图表 #} <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script> <script> // ========== 图表1:每日识别趋势折线图 ========== // 初始化折线图,绑定到 id="daily-chart" 的元素 var dailyChart = echarts.init(document.getElementById('daily-chart')); // 准备X轴数据(日期) var dailyDates = [{% for stat in daily_stats %}"{{ stat.date }}"{% if not forloop.last %}, {% endif %}{% endfor %}]; // 准备Y轴数据(识别次数) var dailyCounts = [{% for stat in daily_stats %}{{ stat.count }}{% if not forloop.last %}, {% endif %}{% endfor %}]; // 折线图配置项 var dailyOption = { title: { text: '最近7天识别趋势', // 图表标题 left: 'center' // 标题居中 }, tooltip: { trigger: 'axis', // 触发类型:坐标轴触发 formatter: '{c}:{c}次识别' // 提示框格式 }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true // 包含标签 }, xAxis: { type: 'category', // X轴类型:类目轴 boundaryGap: false, // 不留白边 data: dailyDates // X轴数据(日期) }, yAxis: { type: 'value' // Y轴类型:数值轴 }, series: [{ name: '识别次数', type: 'line', // 图表类型:折线图 smooth: true, // 平滑曲线 data: dailyCounts, // 数据 lineStyle: { width: 3, // 线宽 color: '#55eee6' // 线条颜色 }, areaStyle: { // 区域填充样式 color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgb(85, 110, 230, 0.6)' }]) } }] }; // 应用配置 dailyChart.setOption(dailyOption); // ========== 图表2:识别结果分布饼图 ========== // 初始化饼图,绑定到 id="pie-chart" 的元素 var pieChart = echarts.init(document.getElementById('pie-chart')); // 饼图配置项 var pieOption = { title: { text: '识别结果分布', // 图表标题 left: 'center' // 标题居中 }, tooltip: { trigger: 'item', // 触发类型:数据项触发 formatter: '{c}次({d}%)' // 提示框格式:次数(百分比) }, legend: { bottom: '5%', // 图例位置:底部 left: 'center' // 图例居中 }, series: [{ name: '识别结果', type: 'pie', // 图表类型:饼图 radius: ['40%', '70%'], // 内外半径(环形饼图) avoidLabelOverlap: false, itemStyle: { borderRadius: 10, // 圆角 borderColor: '#fff', // 边框颜色 borderWidth: 2 // 边框宽度 }, label: { show: false, // 不显示标签 position: 'center' }, emphasis: { label: { show: true, // 高亮时显示标签 fontSize: '18', fontWeight: 'bold' } }, labelLine: { show: false }, data: [ // 4种识别结果的数据 { value: {{ mask_records|default:0 }}, name: '佩戴口罩', itemStyle: { color: '#34c38f' } // 绿色 }, { value: {{ no_mask_records|default:0 }}, name: '未佩戴口罩', itemStyle: { color: '#ff9800' } // 橙色 }, { value: {{ hat_records|default:0 }}, name: '佩戴安全帽', itemStyle: { color: '#2196f3' } // 蓝色 }, { value: {{ person_records|default:0 }}, name: '未佩戴安全帽', itemStyle: { color: '#f46a6a' } // 红色 } ] }] }; // 应用配置 pieChart.setOption(pieOption); // ========== 响应窗口大小变化 ========== // 当浏览器窗口大小改变时,自动调整图表尺寸 window.addEventListener('resize', function() { dailyChart.resize(); // 折线图自适应 pieChart.resize(); // 饼图自适应 }); </script> {# 结束 content 块 #} {% endblock %}8.
login.html- 用户登录页面{# 加载Django静态文件标签,用于引用CSS、JS、图片等静态资源 #} {% load static %} {# HTML文档类型声明,告诉浏览器这是一个HTML5文档 #} <!DOCTYPE html> {# HTML根标签,lang="zxx"表示语言未指定或占位 #} <html lang="zxx"> {# 头部区域:包含网页配置、标题、引入的样式 #} <head> {# 网页标题,显示在浏览器标签页上 #} <title>安全防护检测系统</title> <!-- Meta tag Keywords --> {# 设置视口,确保在移动设备上正确缩放显示 #} <meta name="viewport" content="width=device-width, initial-scale=1"> {# 设置字符编码为UTF-8,保证中文不乱码 #} <meta charset="UTF-8"/> {# 页面关键词,用于SEO搜索引擎优化 #} <meta name="keywords" content="Hotair Login Form Responsive web template, Bootstrap Web Templates, Flat Web Templates, Android Compatible web template, Smartphone Compatible web template, free webdesigns for Nokia, Samsung, LG, SonyEricsson, Motorola web design"/> <!-- //Meta tag Keywords --> {# 引入Google字体:Noto Serif JP(日文衬线字体,也支持中文) #} <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400;600;700;900&display=swap" rel="stylesheet"> <!--/Style-CSS --> {# 引入登录页面自定义样式 #} <link rel="stylesheet" href="{% static 'login/css/style.css' %}" type="text/css" media="all"/> <!--//Style-CSS --> {# 引入FontAwesome图标库,用于显示图标 #} <link rel="stylesheet" href="{% static 'login/css/font-awesome.min.css' %}" type="text/css" media="all"> </head> {# 页面主体开始 #} <body> <!-- form section start --> {# 登录表单区域容器 #} <section class="w3l-hotair-form"> {# 系统主标题 #} <h1>基于YOLO与大模型的安全防护穿戴识别系统</h1> <div class="container"> <!-- /form --> <div class="workinghny-form-grid"> <div class="main-hotair"> {# 表单内容区域 #} <div class="content-wthree"> {# 表单标题 #} <h2>请输入你的账户</h2> <!-- 添加 Django 安全验证 + 正确提交 --> {# 登录表单,action指向Django的login视图 #} <form action="{% url 'login'%}" method="post"> {# Django CSRF防护令牌,防止跨站请求伪造攻击 #} {% csrf_token %} {# 邮箱/用户名输入框 #} <input type="text" class="text" name="email" placeholder="用户名(邮箱)" required=""> {# 密码输入框 #} <input type="password" class="password" name="password" placeholder="密码" required=""> {# 登录按钮 #} <button class="btn" type="submit">登录</button> </form> {# 注册链接:提示无账户的用户去注册 #} <p class="account">无账户? <a href="{% url 'register' %}">注册</a></p> </div> {# 右侧空白区域(用于装饰/占位) #} <div class="w3l_form align-self"> <div class="left_grid_info"> </div> </div> </div> </div> <!-- //form --> </div> <!-- copyright--> {# 版权信息区域 #} <div class="copyright text-center"> <p class="copy-footer-29">版权@ 2026.安全防护检测系统</p> </div> <!-- //copyright--> </section> <!-- //form section start --> {# 引入jQuery库 #} <script src="{% static 'login/js/jquery.min.js' %}"></script> {# AJAX登录脚本 #} <script> // 当文档加载完成后执行 $(document).ready(function (c) { // 拦截登录表单的提交事件 $('form[action="{% url 'login' %}"]').on('submit', function (e) { e.preventDefault(); // 阻止表单默认提交行为(不刷新页面) var form = $(this); // 获取当前表单对象 var url = form.attr('action'); // 获取表单提交地址 var formData = form.serialize(); // 序列化表单数据 // 发送AJAX POST请求 $.ajax({ type: 'POST', // 请求方式:POST url: url, // 请求地址:登录接口 data: formData, // 请求数据:邮箱和密码 // 请求成功时的回调函数 success: function (response) { // 检查后端返回的消息 if (response.msg === '登录成功') { // 登录成功,跳转到首页 window.location.href = '{% url 'index' %}'; } else { // 登录失败,显示错误信息 alert(response.msg || '登录失败'); } }, // 请求失败时的回调函数 error: function (xhr) { alert('请求失败'); } }); }); }); </script> </body> </html>9.
pages_profile.html- 个人信息管理页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 加载Django静态文件标签,用于引用CSS、JS、图片等静态资源 #} {% load static %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} <!-- Page wrapper --> <!-- ============================================================== --> {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> <!-- ============================================================== --> <!-- Container fluid --> <!-- ============================================================== --> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> <!-- ============================================================== --> <!-- Bread crumb and right sidebar toggle --> <!-- ============================================================== --> {# 页面标题和面包屑导航区域 #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">个人信息管理</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">个人信息管理</li> </ol> </div> <div class="col-md-7 col-4 align-self-center"> {# 右上角按钮(模板示例,实际未使用) #} <a href="#" class="btn waves-effect waves-light btn-danger pull-right hidden-sm-down"> Upgrade to Pro</a> </div> </div> <!-- ============================================================== --> <!-- End Bread crumb and right sidebar toggle --> <!-- ============================================================== --> <!-- ============================================================== --> <!-- Start Page Content --> <!-- ============================================================== --> {# ===================== 左侧:用户头像和信息卡片 ===================== #} <div class="col-lg-4 col-xlg-3 col-md-5"> <div class="card"> <div class="card-block"> {# 内容居中 #} <center class="m-t-30"> {# ========== 头像上传区域 ========== #} <div class="avatar-upload"> {# 头像标签,点击触发文件选择 #} <label for="avatarInput" class="avatar-label"> {# 头像图片 #} <img id="avatarPreview" src=" {% if user.avatar %}{{ user.avatar.url }}{% else %}{% static 'assets/images/users/5.jpg' %}{% endif %}" class="img-circle" width="150" height="150" style="object-fit: cover; cursor: pointer;"/> {# 头像悬停时显示的相机图标 #} <div class="avatar-overlay"> <i class="fa fa-camera"></i> </div> </label> {# 隐藏的文件选择输入框 #} <input type="file" id="avatarInput" name="avatar" accept="image/*" style="display: none;"> </div> {# 用户姓名(名字+姓氏) #} <h4 class="card-title m-t-10">{{ user.first_name|default:'' }} {{ user.last_name|default:'' }}</h4> {# 用户邮箱 #} <h6 class="card-subtitle">{{ user.email }}</h6> {# 用户统计信息行:用户名 + 注册日期 #} <div class="row text-center justify-content-md-center"> <div class="col-4"> <a href="javascript:void(0)" class="link"> <i class="icon-user"></i> <font class="font-medium">{{ user.username }}</font> </a> </div> <div class="col-4"> <a href="javascript:void(0)" class="link"> <i class="icon-calendar"></i> <font class="font-medium">{{ user.created_at|date:"Y-m-d" }}</font> </a> </div> </div> </center> </div> </div> </div> {# ===================== 右侧:个人信息编辑表单 ===================== #} <div class="col-lg-8 col-xlg-9 col-md-7"> <div class="card"> <div class="card-block"> {# 成功消息提示(如保存成功) #} {% if success_message %} <div class="alert alert-success alert-dismissible fade show" role="alert"> {{ success_message }} <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> {% endif %} {# ========== 个人信息编辑表单 ========== #} {# enctype="multipart/form-data" 支持文件上传(头像) #} <form id="profileForm" method="post" enctype="multipart/form-data" class="form-horizontal form-material"> {% csrf_token %} {# 用户名输入框(必填) #} <div class="form-group"> <label class="col-md-12">用户名 <span class="text-danger">*</span></label> <div class="col-md-12"> <input type="text" name="username" value="{{ user.username }}" class="form-control form-control-line" required> </div> </div> {# 邮箱输入框(必填) #} <div class="form-group"> <label class="col-md-12">邮箱 <span class="text-danger">*</span></label> <div class="col-md-12"> <input type="email" name="email" value="{{ user.email }}" class="form-control form-control-line" required> </div> </div> {# 姓氏输入框 #} <div class="form-group"> <label class="col-md-12">姓氏</label> <div class="col-md-12"> <input type="text" name="last_name" value="{{ user.last_name|default:'' }}" class="form-control form-control-line"> </div> </div> {# 名字输入框 #} <div class="form-group"> <label class="col-md-12">名字</label> <div class="col-md-12"> <input type="text" name="first_name" value="{{ user.first_name|default:'' }}" class="form-control form-control-line"> </div> </div> {# 手机号输入框 #} <div class="form-group"> <label class="col-md-12">手机号</label> <div class="col-md-12"> <input type="tel" name="phone" value="{{ user.phone|default:'' }}" class="form-control form-control-line"> </div> </div> {# 所在地输入框 #} <div class="form-group"> <label class="col-md-12">所在地</label> <div class="col-md-12"> <input type="text" name="location" value="{{ user.location|default:'' }}" class="form-control form-control-line"> </div> </div> {# 个人网站输入框 #} <div class="form-group"> <label class="col-md-12">个人网站</label> <div class="col-md-12"> <input type="url" name="website" value="{{ user.website|default:'' }}" class="form-control form-control-line"> </div> </div> {# 个人简介文本框 #} <div class="form-group"> <label class="col-md-12">个人简介</label> <div class="col-md-12"> <textarea name="bio" rows="4" class="form-control form-control-line" placeholder="介绍一下自己...">{{ user.bio|default:'' }}</textarea> </div> </div> {# 按钮区域:保存修改 + 重置 #} <div class="form-group"> <div class="col-sm-12"> <button type="submit" class="btn btn-success">保存修改</button> <button type="button" class="btn btn-secondary ml-2" onclick="resetForm()">重置</button> </div> </div> </form> {# ========== 修改密码卡片 ========== #} <div class="card mt-4"> <div class="card-block"> <h4 class="card-title">修改密码</h4> {# 修改密码表单 #} <form id="passwordForm" method="post" class="form-horizontal form-material"> {% csrf_token %} {# 新密码输入框 #} <div class="form-group"> <label class="col-md-12">新密码</label> <div class="col-md-12"> <input type="password" name="new_password" class="form-control form-control-line" placeholder="输入新密码"> </div> </div> {# 确认密码输入框 #} <div class="form-group"> <label class="col-md-12">确认密码</label> <div class="col-md-12"> <input type="password" name="confirm_password" class="form-control form-control-line" placeholder="再次输入新密码"> </div> </div> {# 修改密码按钮 #} <div class="form-group"> <div class="col-sm-12"> <button type="button" class="btn btn-warning" onclick="changePassword()"> 修改密码 </button> </div> </div> </form> </div> </div> </div> </div> </div> <!-- Column --> </div> <!-- Row --> <!-- ============================================================== --> <!-- End PAge Content --> <!-- ============================================================== --> </div> <!-- ============================================================== --> <!-- End Container fluid --> <!-- ============================================================== --> <!-- ============================================================== --> <!-- footer --> <!-- ============================================================== --> <!-- ============================================================== --> <!-- End footer --> <!-- ============================================================== --> </div> {# ===================== 自定义CSS样式 ===================== #} <style> /* 头像上传容器:相对定位 */ .avatar-upload { position: relative; display: inline-block; } /* 头像标签:鼠标变为手型 */ .avatar-label { cursor: pointer; position: relative; display: inline-block; } /* 头像悬停遮罩层:显示相机图标 */ .avatar-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.5); color: white; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; opacity: 0; /* 默认透明(隐藏) */ transition: opacity 0.3s; } /* 鼠标悬停时显示遮罩层 */ .avatar-label:hover .avatar-overlay { opacity: 1; } /* 提示框下边距 */ .alert { margin-bottom: 20px; } </style> {# ===================== JavaScript代码 ===================== #} {# 引入jQuery(CDN) #} <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> // ========== 头像预览和上传 ========== $('#avatarInput').change(function () { if (this.files && this.files[0]) { const file = this.files[0]; // 检查文件类型是否为图片 if (!file.type.startsWith('image/')) { showAlert('warning', '请选择图片文件'); return; } // 检查文件大小(限制为5MB) if (file.size > 5 * 1024 * 1024) { showAlert('warning', '图片文件大小不能超过5MB'); return; } // 预览图片(使用FileReader) const reader = new FileReader(); reader.onload = function (e) { $('#avatarPreview').attr('src', e.target.result); } reader.readAsDataURL(file); // 自动上传头像 uploadAvatar(file); } }); // ========== 单独上传头像函数 ========== function uploadAvatar(file) { const formData = new FormData(); formData.append('avatar', file); // 添加头像文件 formData.append('csrfmiddlewaretoken', $('[name=csrfmiddlewaretoken]').val()); // 添加CSRF令牌 $.ajax({ url: '', // 当前URL(提交到同一个页面) type: 'POST', data: formData, processData: false, // 不处理数据(让jQuery直接发送FormData) contentType: false, // 不设置Content-Type(让浏览器自动设置) beforeSend: function () { showAlert('info', '正在上传头像...'); }, success: function (response) { if (response.success) { showAlert('success', '头像上传成功'); } else { showAlert('danger', response.error || '头像上传失败'); // 恢复原来的头像 $('#avatarPreview').attr('src', '{{ user.get_avatar_url }}'); } }, error: function (xhr) { const error = xhr.responseJSON ? xhr.responseJSON.error : '网络错误'; showAlert('danger', '头像上传失败: ' + error); // 恢复原来的头像 $('#avatarPreview').attr('src', '{{ user.get_avatar_url }}'); } }); } // ========== 保存个人信息函数 ========== function saveProfile() { const formData = new FormData($('#profileForm')[0]); // 如果有头像文件,添加到FormData const avatarFile = $('#avatarInput')[0].files[0]; if (avatarFile) { formData.append('avatar', avatarFile); } $.ajax({ url: '', // 当前URL type: 'POST', data: formData, processData: false, contentType: false, success: function (response) { if (response.success) { showAlert('success', response.message || '个人信息更新成功'); // 如果头像更新成功,刷新页面以显示新头像 setTimeout(function () { window.location.reload(); }, 1500); } else { showAlert('danger', response.error || '更新失败'); } }, error: function (xhr) { const error = xhr.responseJSON ? xhr.responseJSON.error : '网络错误'; showAlert('danger', error); } }); } // ========== 修改密码函数 ========== function changePassword() { const newPassword = $('input[name="new_password"]').val(); const confirmPassword = $('input[name="confirm_password"]').val(); // 验证密码是否填写 if (!newPassword || !confirmPassword) { showAlert('warning', '请填写密码'); return; } // 验证两次密码是否一致 if (newPassword !== confirmPassword) { showAlert('warning', '两次输入的密码不一致'); return; } // 发送AJAX请求修改密码 $.ajax({ url: '{% url "change_password" %}', // 密码修改接口 type: 'POST', data: { 'new_password': newPassword, 'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val() }, success: function (response) { if (response.success) { showAlert('success', '密码修改成功'); // 清空密码输入框 $('input[name="new_password"]').val(''); $('input[name="confirm_password"]').val(''); } else { showAlert('danger', response.error); } }, error: function () { showAlert('danger', '网络错误'); } }); } // ========== 重置表单函数 ========== function resetForm() { $('#profileForm')[0].reset(); // 重置表单所有字段 $('#avatarPreview').attr('src', '{{ user.get_avatar_url }}'); // 恢复头像 } // ========== 显示提示信息函数 ========== function showAlert(type, message) { const alertHtml = ` <div class="alert alert-${type} alert-dismissible fade show" role="alert"> ${message} <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> `; $('.card-block').prepend(alertHtml); // 在卡片顶部添加提示框 setTimeout(() => $('.alert').fadeOut(), 3000); // 3秒后自动消失 } // ========== 表单提交事件 ========== $('#profileForm').submit(function (e) { e.preventDefault(); // 阻止默认提交 saveProfile(); // 调用保存函数 }); </script> {# 结束 content 块 #} {% endblock %}10.
recognition_records.html- 检测记录页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor">检测记录</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="{% url 'index' %}">首页</a></li> <li class="breadcrumb-item active">检测记录</li> </ol> </div> {# 右上角添加设备按钮 #} <div class="col-md-7 col-4 align-self-center"> <a href="{% url 'device_add' %}" class="btn btn-success pull-right"> 添加设备 </a> </div> </div> {# ===================== 检测记录表格 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> <div class="card-block"> {# 响应式表格容器(水平滚动) #} <div class="table-responsive"> {# 检测记录表格 #} <table class="table table-hover"> {# 表格头部:列标题 #} <thead> <tr> <th>用户</th> {# 执行识别的用户 #} <th>设备</th> {# 关联的设备名称 #} <th>识别类型</th> {# 图片/视频/摄像头 #} <th>识别结果</th> {# mask/no_mask/hat/person #} <th>置信度</th> {# 识别准确率 #} <th>识别时间</th> {# 检测时间戳 #} <th>位置</th> {# 识别地点 #} <th>图像</th> {# 检测结果截图 #} </tr> </thead> {# 表格主体:遍历显示检测记录列表 #} <tbody> {% for record in records %} <tr> {# 用户列:显示用户名,如果没有则显示"未知用户" #} <td>{{ record.user.username|default:"未知用户" }}</td> {# 设备列:显示设备名称,如果没有则显示"未绑定设备" #} <td>{{ record.device.name|default:"未绑定设备" }}</td> {# 识别类型列:调用模型的get_xxx_display方法获取中文显示 #} <td>{{ record.get_recognition_type_display }}</td> {# 识别结果列:根据结果类型显示不同颜色的标签(4种结果) #} <td> {# 佩戴口罩 - 绿色标签 #} {% if record.result == 'mask' %} <span class="badge badge-success">佩戴口罩</span> {# 未佩戴口罩 - 红色标签 #} {% elif record.result == 'no_mask' %} <span class="badge badge-danger">未佩戴口罩</span> {# 佩戴安全帽 - 绿色标签 #} {% elif record.result == 'hat' %} <span class="badge badge-success">佩戴安全帽</span> {# 未佩戴安全帽 - 红色标签 #} {% elif record.result == 'person' %} <span class="badge badge-danger">未佩戴安全帽</span> {# 其他未知结果 - 黄色标签 #} {% else %} <span class="badge badge-warning">未知</span> {% endif %} </td> {# 置信度列:floatformat:2 表示保留2位小数 #} <td>{{ record.confidence|floatformat:2 }}%</td> {# 识别时间列:格式化为 年-月-日 时:分:秒 #} <td>{{ record.timestamp|date:"Y-m-d H:i:s" }}</td> {# 位置列:如果没有位置则显示"未记录" #} <td>{{ record.location|default:"未记录" }}</td> {# 图像列:显示检测结果截图缩略图 #} <td> {% if record.image %} <img src="{{ record.image.url }}" alt="识别图像" style="max-height: 50px;"> {% else %} 无图像 {% endif %} </td> </tr> {# 如果没有检测记录,显示空状态提示 #} {% empty %} <tr> <td colspan="8" class="text-center">暂无识别记录</td> </tr> {% endfor %} </tbody> </table> </div> {# ===================== 分页控件 ===================== #} {# 只有当有多页时才显示分页 #} {% if records.has_other_pages %} <nav aria-label="Page navigation"> <ul class="pagination justify-content-center"> {# ========== 首页和上一页按钮 ========== #} {% if records.has_previous %} {# 有上一页时,按钮可点击 #} <li class="page-item"> <a class="page-link" href="?page=1" aria-label="首页"> <span aria-hidden="true">««</span> {# 双左箭头 #} </a> </li> <li class="page-item"> <a class="page-link" href="?page={{ records.previous_page_number }}" aria-label="上一页"> <span aria-hidden="true">«</span> {# 单左箭头 #} </a> </li> {% else %} {# 没有上一页时,按钮禁用 #} <li class="page-item disabled"> <span class="page-link" aria-hidden="true">««</span> </li> <li class="page-item disabled"> <span class="page-link" aria-hidden="true">«</span> </li> {% endif %} {# ========== 页码按钮(显示当前页前后各3页) ========== #} {% for num in records.paginator.page_range %} {# 当前页:高亮显示 #} {% if records.number == num %} <li class="page-item active"> <span class="page-link">{{ num }}</span> </li> {# 非当前页:显示在当前页前后3页范围内 #} {% elif num > records.number|add:'-3' and num < records.number|add:'3' %} <li class="page-item"> <a class="page-link" href="?page={{ num }}">{{ num }}</a> </li> {% endif %} {% endfor %} {# ========== 下一页和末页按钮 ========== #} {% if records.has_next %} {# 有下一页时,按钮可点击 #} <li class="page-item"> <a class="page-link" href="?page={{ records.next_page_number }}" aria-label="下一页"> <span aria-hidden="true">»</span> {# 单右箭头 #} </a> </li> <li class="page-item"> <a class="page-link" href="?page={{ records.paginator.num_pages }}" aria-label="末页"> <span aria-hidden="true">»»</span> {# 双右箭头 #} </a> </li> {% else %} {# 没有下一页时,按钮禁用 #} <li class="page-item disabled"> <span class="page-link" aria-hidden="true">»</span> </li> <li class="page-item disabled"> <span class="page-link" aria-hidden="true">»»</span> </li> {% endif %} </ul> </nav> {# 分页信息统计 #} <div class="text-center mt-3"> <small class="text-muted"> 共 {{ records.paginator.count }} 条记录, 第 {{ records.number }} 页 / 共 {{ records.paginator.num_pages }} 页 </small> </div> {% endif %} </div> </div> </div> </div> </div> {# 页脚版权信息 #} <footer class="footer">©安全防护检测系统</footer> </div> {# 结束 content 块 #} {% endblock %}11.
register.html- 用户注册页面{# 加载Django静态文件标签,用于引用CSS、JS、图片等静态资源 #} {% load static %} {# HTML文档类型声明,告诉浏览器这是一个HTML5文档 #} <!DOCTYPE html> {# HTML根标签,lang="zxx"表示语言未指定或占位 #} <html lang="zxx"> {# 头部区域:包含网页配置、标题、引入的样式 #} <head> {# 网页标题,显示在浏览器标签页上 #} <title>安全防护检测系统</title> <!-- Meta tag Keywords --> {# 设置视口,确保在移动设备上正确缩放显示 #} <meta name="viewport" content="width=device-width, initial-scale=1"> {# 设置字符编码为UTF-8,保证中文不乱码 #} <meta charset="UTF-8" /> {# 页面关键词,用于SEO搜索引擎优化 #} <meta name="keywords" content="Hotair Login Form Responsive web template, Bootstrap Web Templates, Flat Web Templates, Android Compatible web template, Smartphone Compatible web template, free webdesigns for Nokia, Samsung, LG, SonyEricsson, Motorola web design" /> <!-- //Meta tag Keywords --> {# 引入Google字体:Noto Serif JP(日文衬线字体,也支持中文) #} <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400;600;700;900&display=swap" rel="stylesheet"> <!--/Style-CSS --> {# 引入登录/注册页面自定义样式 #} <link rel="stylesheet" href="{% static 'login/css/style.css' %}" type="text/css" media="all" /> <!--//Style-CSS --> {# 引入FontAwesome图标库,用于显示图标 #} <link rel="stylesheet" href="{% static 'login/css/font-awesome.min.css' %}" type="text/css" media="all"> </head> {# 页面主体开始 #} <body> <!-- form section start --> {# 注册表单区域容器 #} <section class="w3l-hotair-form"> {# 系统主标题 #} <h1>基于YOLO与大模型的安全防护穿戴识别系统</h1> <div class="container"> <!-- /form --> <div class="workinghny-form-grid"> <div class="main-hotair"> {# ===================== 注册表单内容区域 ===================== #} <div class="content-wthree"> {# 表单标题 #} <h2>注册</h2> {# 提示文字:注册您的账户 #} <p>注册您的账户<p> <!-- 添加 Django 安全验证 + 正确提交 --> {# 注册表单,action指向Django的register视图 #} <form action="{% url 'register'%}" method="post"> {# Django CSRF防护令牌,防止跨站请求伪造攻击 #} {% csrf_token %} {# 用户名输入框 #} <input type="text" class="text" name="username" placeholder="用户名" required=""> {# 邮箱输入框 #} <input type="email" class="text" name="email" placeholder="邮箱" required=""> {# 密码输入框 #} <input type="password" class="password" name="password" placeholder="密码" required=""> {# 注册按钮 #} <button class="btn" type="submit">注册</button> </form> {# 登录链接:提示已有账号的用户去登录 #} <p class="account">已有账号? <a href="{% url 'login_page' %}">去登录</a></p> </div> {# 右侧空白区域(用于装饰/占位) #} <div class="w3l_form align-self"> <div class="left_grid_info"> </div> </div> </div> </div> <!-- //form --> </div> <!-- copyright--> {# 版权信息区域 #} <div class="copyright text-center"> <p class="copy-footer-29">版权@ 2026.安全防护检测系统</p> </div> <!-- //copyright--> </section> <!-- //form section start --> {# 引入jQuery库(本页面未使用AJAX,仅用于可能的扩展) #} <script src="{% static 'login/js/jquery.min.js' %}"></script> </body> </html>12.
video_recognition.html- 视频识别页面{# 继承 base.html 基础模板,复用导航栏、侧边栏等公共部分 #} {% extends 'base.html' %} {# 开始填充 content 块,这部分内容会插入到 base.html 的 {% block content %} 位置 #} {% block content %} {# 页面外层容器,用于控制页面布局和边距 #} <div class="page-wrapper"> {# 内容区域容器,控制内边距 #} <div class="container-fluid"> {# ===================== 页面标题和面包屑导航 ===================== #} <div class="row page-titles"> <div class="col-md-5 col-8 align-self-center"> {# 页面主标题 #} <h3 class="text-themecolor m-b-0 m-t-0">视频识别</h3> {# 面包屑导航,显示当前页面位置 #} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="javascript:void(0)">首页</a></li> <li class="breadcrumb-item active">视频识别</li> </ol> </div> </div> {# ===================== 主内容卡片 ===================== #} <div class="row"> <div class="col-12"> <div class="card"> <div class="card-block"> {# 卡片标题:支持口罩+安全帽识别 #} <h4 class="card-title">视频安全防护识别(口罩+安全帽)</h4> {# 左右两列布局:左侧视频区域,右侧结果显示区域 #} <div class="row"> {# ===================== 左侧:视频选择和播放区域 ===================== #} <div class="col-md-7"> {# 视频文件上传控件 #} <div class="form-group"> <label for="videoUpload">选择视频</label> <input type="file" id="videoUpload" accept="video/*" class="form-control"> </div> {# 设备选择下拉框(用于关联识别记录) #} <div class="form-group"> <label for="deviceSelect">选择设备</label> <select id="deviceSelect" class="form-control"> <option value="">-- 不关联设备 --</option> {# 遍历后端传递的设备列表 #} {% for device in devices %} <option value="{{ device.id }}">{{ device.name }} ({{ device.device_type }})</option> {% endfor %} </select> </div> {# 视频播放器容器 #} <div class="video-container mt-4"> {# HTML5视频播放器 #} <video id="videoPlayer" width="100%" height="auto" controls></video> {# 隐藏画布,用于捕获视频帧 #} <canvas id="videoCanvas" style="display: none;"></canvas> </div> {# 控制按钮组 #} <div class="mt-3"> <button id="startRecognizeBtn" class="btn btn-primary mr-2" disabled>开始识别</button> <button id="stopRecognizeBtn" class="btn btn-danger" disabled>停止识别</button> </div> </div> {# ===================== 右侧:识别结果显示区域 ===================== #} <div class="col-md-5"> <h5>识别结果</h5> <div id="detectionResult" class="mt-2 overflow-auto" style="max-height: 400px; overflow-y: auto; border: 1px solid #e9ecef; padding: 15px; border-radius: 4px;"> <div class="alert alert-info">暂无识别结果</div> </div> </div> </div> </div> {# ===================== JavaScript代码区域 ===================== #} <script> // ========== DOM元素获取 ========== const videoUpload = document.getElementById('videoUpload'); // 视频上传输入框 const videoPlayer = document.getElementById('videoPlayer'); // 视频播放器 const videoCanvas = document.getElementById('videoCanvas'); // 隐藏画布 const ctx = videoCanvas.getContext('2d'); // 画布2D上下文 const startRecognizeBtn = document.getElementById('startRecognizeBtn'); // 开始识别按钮 const stopRecognizeBtn = document.getElementById('stopRecognizeBtn'); // 停止识别按钮 const deviceSelect = document.getElementById('deviceSelect'); // 设备选择下拉框 const detectionResult = document.getElementById('detectionResult'); // 结果显示区域 // ========== 状态变量 ========== let recognitionInterval = null; // 定时器ID let isRecognizing = false; // 是否正在识别中 let recognitionResults = []; // 存储所有识别结果 // ========== 函数1:获取标签信息(文字和Bootstrap类) ========== // 统一管理4种类别的显示样式 function getLabelInfo(label) { const labelMap = { 'mask': { text: '佩戴口罩', badgeClass: 'success' }, // 绿色 'no_mask': { text: '未佩戴口罩', badgeClass: 'danger' }, // 红色 'hat': { text: '佩戴安全帽', badgeClass: 'success' }, // 绿色 'person': { text: '未佩戴安全帽', badgeClass: 'danger' } // 红色 }; return labelMap[label] || { text: '未知', badgeClass: 'warning' }; // 默认黄色 } // ========== 函数2:监听视频文件选择 ========== videoUpload.addEventListener('change', function (e) { const file = e.target.files[0]; // 获取选择的文件 if (file) { const videoURL = URL.createObjectURL(file); // 创建本地URL videoPlayer.src = videoURL; // 设置视频源 startRecognizeBtn.disabled = false; // 启用开始识别按钮 recognitionResults = []; // 清空历史结果 detectionResult.innerHTML = ''; // 清空结果显示 } }); // ========== 函数3:开始识别 ========== startRecognizeBtn.addEventListener('click', function () { // 检查是否已在识别中或没有视频源 if (isRecognizing || !videoPlayer.src) { return; } isRecognizing = true; startRecognizeBtn.disabled = true; stopRecognizeBtn.disabled = false; recognitionResults = []; detectionResult.innerHTML = '<div class="alert alert-info">识别中...</div>'; // 检查视频是否已加载完成 if (videoPlayer.readyState < 2) { alert('视频尚未加载完成,请等待视频加载后再开始识别'); stopRecognition(); return; } videoPlayer.play(); // 开始播放视频 // 每5秒执行一次识别 recognitionInterval = setInterval(async () => { // 检查视频是否已暂停或结束 if (videoPlayer.paused || videoPlayer.ended) { stopRecognition(); return; } // 捕获当前视频帧 captureVideoFrame(); // 将画布图像转为base64 const imageData = videoCanvas.toDataURL('image/jpeg'); try { const deviceId = deviceSelect.value || null; console.log('正在发送识别请求,时间:', formatTime(videoPlayer.currentTime)); // 发送POST请求到后端识别接口 const response = await fetch('/detect_mask/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ image: imageData, recognition_type: 'video', device_id: deviceId || null, location: '视频识别 - ' + (videoUpload.files[0]?.name || '未知视频') }) }); if (!response.ok) { throw new Error(`HTTP错误! 状态: ${response.status}`); } const data = await response.json(); if (data.error) { console.error('识别错误:', data.error); detectionResult.innerHTML = '<div class="alert alert-danger">识别错误: ' + data.error + '</div>'; return; } // 记录识别结果 const timestamp = formatTime(videoPlayer.currentTime); recognitionResults.push({ timestamp: timestamp, detections: data.detections || [], screenshot: imageData }); console.log('识别成功,检测到目标数量:', data.detections ? data.detections.length : 0); updateDetectionResult(); // 更新UI显示 } catch (error) { console.error('发送识别请求失败:', error); detectionResult.innerHTML = '<div class="alert alert-danger">发送识别请求失败: ' + error.message + '</div>'; stopRecognition(); } }, 5000); // 5000毫秒 = 5秒 }); // ========== 函数4:停止识别 ========== stopRecognizeBtn.addEventListener('click', stopRecognition); function stopRecognition() { if (!isRecognizing) return; clearInterval(recognitionInterval); // 清除定时器 isRecognizing = false; startRecognizeBtn.disabled = false; stopRecognizeBtn.disabled = true; if (videoPlayer) { videoPlayer.pause(); // 暂停视频播放 } } // ========== 函数5:捕获视频帧到画布 ========== function captureVideoFrame() { videoCanvas.width = videoPlayer.videoWidth; // 设置画布宽度 videoCanvas.height = videoPlayer.videoHeight; // 设置画布高度 ctx.drawImage(videoPlayer, 0, 0, videoCanvas.width, videoCanvas.height); // 绘制当前帧 } // ========== 函数6:格式化时间(秒 → MM:SS) ========== function formatTime(seconds) { const minutes = Math.floor(seconds / 60); // 计算分钟 const remainingSeconds = Math.floor(seconds % 60); // 计算剩余秒数 return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } // ========== 函数7:更新识别结果显示(支持4类别) ========== function updateDetectionResult() { if (recognitionResults.length === 0) { detectionResult.innerHTML = '<div class="alert alert-info">暂无识别结果</div>'; return; } let resultHTML = ''; const recentResults = recognitionResults.slice(-10); // 只显示最近10条结果 recentResults.forEach((result, index) => { const hasDetections = result.detections && result.detections.length > 0; const timeClass = hasDetections ? 'text-primary' : 'text-secondary'; resultHTML += `<div class="mt-3 border p-2 rounded"> <h6 class="${timeClass}">📷 时间: ${result.timestamp}</h6>`; if (hasDetections) { // 遍历每个检测结果 result.detections.forEach((detection, idx) => { const label = detection.label; const confidence = (detection.confidence * 100).toFixed(1); // 使用统一的类别映射 const labelInfo = getLabelInfo(label); const labelText = labelInfo.text; const badgeClass = labelInfo.badgeClass; resultHTML += ` <div class="alert alert-${badgeClass} mt-1" style="padding: 5px 10px; font-size: 0.9em;"> <p><strong>目标 ${idx + 1}:</strong> ${labelText}</p> <p><strong>置信度:</strong> ${confidence}%</p> </div>`; }); } else { resultHTML += `<div class="alert alert-info mt-1" style="padding: 5px 10px; font-size: 0.9em;">😷 未检测到目标</div>`; } resultHTML += `</div>`; }); detectionResult.innerHTML = resultHTML; } // =================================================== </script> </div> </div> </div> </div> {% endblock %}
2.admin.py- Django后台管理配置# 导入Django的admin模块,用于注册模型到后台管理界面 from django.contrib import admin # ==================== 导入需要注册的模型 ==================== # Register your models here. # 从detection应用的models.py中导入三个数据模型 from detection.models import User, Device, RecognitionRecord # ==================== User模型的后台管理配置 ==================== class UserAdmin(admin.ModelAdmin): """ User模型的后台管理类 定义用户在Django Admin后台的显示方式 """ # list_display: 列表页显示的字段(列) # 在用户列表页面显示:用户名、邮箱、电话、创建时间 list_display = ('username', 'email', 'phone', 'created_at') # search_fields: 列表页的搜索字段 # 可以通过用户名、邮箱、电话进行搜索 search_fields = ('username', 'email', 'phone') # fields: 编辑页面显示的字段 # 编辑用户时只显示:用户名、邮箱、电话、头像 fields = ('username', 'email', 'phone', 'avatar') # readonly_fields: 只读字段(不可编辑) # 创建时间和更新时间自动生成,不允许手动修改 readonly_fields = ('created_at', 'updated_at') # ==================== Device模型的后台管理配置 ==================== class DeviceAdmin(admin.ModelAdmin): """ Device模型的后台管理类 定义设备在Django Admin后台的显示方式 """ # list_display: 列表页显示的字段 # 显示:设备名称、设备类型、负责人、描述 list_display = ('name', 'device_type', 'owner', 'description') # list_filter: 列表页右侧的过滤器 # 可以按状态和设备类型进行筛选 list_filter = ('status', 'device_type') # search_fields: 搜索字段 # 可以通过设备名称和设备类型进行搜索 search_fields = ('name', 'device_type') # fields: 编辑页面显示的字段 fields = ('name', 'device_type', 'video_file', 'owner', 'status') # ==================== RecognitionRecord模型的后台管理配置 ==================== class RecognitionRecordAdmin(admin.ModelAdmin): """ RecognitionRecord模型的后台管理类 定义识别记录在Django Admin后台的显示方式 """ # list_display: 列表页显示的字段 # 显示:用户、识别类型、识别结果、置信度 list_display = ('user', 'recognition_type', 'result', 'confidence') # list_filter: 列表页右侧的过滤器 # 可以按识别类型和识别结果进行筛选 list_filter = ('recognition_type', 'result') # search_fields: 搜索字段 # 可以通过用户名(跨表查询user__username)和位置进行搜索 # user__username 表示关联User模型的username字段 search_fields = ('user__username', 'location') # readonly_fields: 只读字段 # 识别时间自动记录,不允许手动修改 readonly_fields = ('timestamp',) # ==================== 注册模型到后台 ==================== # 将User模型及其管理类注册到Django Admin admin.site.register(User, UserAdmin) # 将Device模型及其管理类注册到Django Admin admin.site.register(Device, DeviceAdmin) # 将RecognitionRecord模型及其管理类注册到Django Admin admin.site.register(RecognitionRecord, RecognitionRecordAdmin)3.
apps.py- Django应用配置# 导入Django的AppConfig类,用于配置应用程序 # AppConfig是每个Django应用都必须有的配置文件 from django.apps import AppConfig # ==================== Detection应用配置类 ==================== class DetectionConfig(AppConfig): """ Detection应用的配置类 这个类告诉Django关于detection应用的元信息 """ # default_auto_field: 设置模型自动生成的主键字段类型 # 'django.db.models.BigAutoField' 表示使用64位整数作为主键(BigIntegerField) # 这是Django 3.2+ 的默认设置,可以支持更多的数据记录 # 如果不设置,Django会使用 AutoField(32位整数) default_auto_field = 'django.db.models.BigAutoField' # name: 应用的Python路径(必须设置) # 告诉Django这个应用的名字是 'detection' # Django会通过这个名字来查找应用的模型、模板等文件 # 这个名称必须与项目中的文件夹名称一致 name = 'detection'4.
models.py- 数据库模型定义# 导入Django核心模型模块,所有数据表必须继承此类 # models模块提供了定义数据库表结构的各种字段类型 from django.db import models # ========================== # 用户表:存储系统所有用户信息 # ========================== class User(models.Model): """ 用户模型类 对应数据库中的 detection_user 表 存储用户的账号信息和个人资料 """ # ==================== 基础认证字段 ==================== # 用户名字段:最长150字符,唯一(不能重复),在后台显示为"用户名" username = models.CharField(max_length=150, unique=True, verbose_name='用户名') # 邮箱字段:必须唯一,用于登录和找回密码 email = models.EmailField(unique=True, verbose_name='邮箱') # 密码字段:最长255字符(存储加密后的密码) password = models.CharField(max_length=255, verbose_name='密码') # 电话字段:最长20字符,允许为空(blank=True),null=True表示数据库允许NULL值 phone = models.CharField(max_length=20, verbose_name='电话', blank=True, null=True) # ========== 个人资料扩展字段 ========== # 名字:最长30字符,可选 first_name = models.CharField(max_length=30, blank=True, null=True, verbose_name='名字') # 姓氏:最长30字符,可选 last_name = models.CharField(max_length=30, blank=True, null=True, verbose_name='姓氏') # 所在地:最长100字符,可选 location = models.CharField(max_length=100, blank=True, null=True, verbose_name='所在地') # 个人网站:URLField会自动验证URL格式,可选 website = models.URLField(blank=True, null=True, verbose_name='个人网站') # 个人简介:长文本,无长度限制,可选 bio = models.TextField(blank=True, null=True, verbose_name='个人简介') # ===================================== # 头像:ImageField用于存储图片文件,上传到 avatars/ 目录,可选 avatar = models.ImageField(upload_to='avatars/', verbose_name='头像', blank=True, null=True) # 创建时间:auto_now_add=True 表示对象首次保存时自动设置为当前时间 created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') # 更新时间:auto_now=True 表示每次保存对象时自动更新为当前时间 updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') # ==================== 元数据配置 ==================== class Meta: # 单数形式的名称(在后台显示) verbose_name = '用户' # 复数形式的名称(在后台显示) verbose_name_plural = '用户' # 指定数据库中的表名 db_table = 'detection_user' # ==================== 实例方法 ==================== def __str__(self): """当打印User对象时,返回用户名(便于调试和后台显示)""" return self.username def get_avatar_url(self): """获取用户头像URL,如果没有头像则返回默认头像""" # 如果用户有头像,返回头像的URL if self.avatar: return self.avatar.url # 否则返回默认头像路径 return '/static/assets/images/users/5.jpg' # ========================== # 设备表:存储监控设备信息 # ========================== class Device(models.Model): """ 设备模型类 对应数据库中的 detection_device 表 存储监控设备的基本信息 """ # ==================== 设备状态选项 ==================== # STATUS_CHOICES 定义了设备状态的选项 # 每个选项是一个元组:(存储值, 显示值) STATUS_CHOICES = [ ('offline', '离线'), # 离线状态 ('online', '在线'), # 在线状态 ('maintenance', '维护'), # 维护中状态 ] # ==================== 设备基本字段 ==================== # 设备名称:唯一,不可重复,最长150字符 name = models.CharField(max_length=150, unique=True, verbose_name='设备名称') # 设备类型:如"摄像头"、"其他",最长50字符,允许为空 device_type = models.CharField(max_length=50, blank=True, verbose_name='设备类型') # 设备视频文件:上传到 device_videos/ 目录,可选 video_file = models.FileField(upload_to='device_videos/', verbose_name='视频文件', blank=True, null=True) # 设备负责人:外键关联User模型 # on_delete=models.CASCADE:当用户被删除时,该用户的所有设备也被删除 # related_name='devices':通过 user.devices 可以获取该用户的所有设备 # null=True, blank=True:允许没有负责人 owner = models.ForeignKey( User, on_delete=models.CASCADE, related_name='devices', null=True, blank=True, verbose_name='负责人' ) # 设备状态:使用上面定义的STATUS_CHOICES,默认值为'offline' status = models.CharField(max_length=20, default='offline', verbose_name='状态', choices=STATUS_CHOICES) # 设备描述/备注:长文本,允许为空 description = models.TextField(blank=True, verbose_name='备注') # 设备安装位置:最长100字符,允许为空 location = models.CharField(max_length=100, blank=True, verbose_name='设备位置') # 创建时间:自动添加 created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') # 更新时间:自动更新 updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') # ==================== 元数据配置 ==================== class Meta: verbose_name = '设备' db_table = 'detection_device' def __str__(self): """返回设备名称""" return self.name # ========================== # 识别记录表:存储口罩/安全防护检测结果 # ========================== class RecognitionRecord(models.Model): """ 识别记录模型类 对应数据库中的 detection_recognitionrecord 表 存储每次识别的详细结果 """ # ==================== 识别类型选项 ==================== RECOGNITION_TYPE_CHOICES = [ ('camera', '摄像头识别'), # 实时摄像头识别 ('image', '图片识别'), # 上传图片识别 ('video', '视频识别'), # 上传视频识别 ] # ==================== 识别结果选项(已修改为4类别) ==================== RESULT_CHOICES = [ ('mask', '佩戴口罩'), # 正确佩戴口罩 ('no_mask', '未佩戴口罩'), # 没有戴口罩 ('hat', '佩戴安全帽'), # 正确佩戴安全帽 ('person', '未佩戴安全帽'), # 没有戴安全帽 ] # ==================== 关联字段 ==================== # 关联用户:外键,当用户被删除时,该用户的识别记录也被删除 # related_name='recognition_record':通过 user.recognition_record 获取用户的识别记录 user = models.ForeignKey( User, on_delete=models.CASCADE, related_name='recognition_record', null=True, blank=True ) # 关联设备:外键,当设备被删除时,该设备的识别记录也被删除 device = models.ForeignKey( Device, on_delete=models.CASCADE, related_name='recognition_record', null=True, blank=True ) # ==================== 识别结果字段 ==================== # 识别类型:从上面定义的选项中选择 recognition_type = models.CharField(max_length=20, choices=RECOGNITION_TYPE_CHOICES, verbose_name='识别类型') # 识别结果:从上面定义的RESULT_CHOICES中选择(4种结果) result = models.CharField(max_length=20, choices=RESULT_CHOICES, verbose_name='识别结果') # 置信度:浮点数,表示识别准确率,默认0.0 confidence = models.FloatField(default=0.0, verbose_name='置信度') # 检测时间:自动记录创建时间 timestamp = models.DateTimeField(auto_now_add=True, verbose_name='检测时间') # 检测截图:上传到 recognition_images/ 目录,可选 image = models.ImageField(upload_to='recognition_images/', verbose_name='检测图片', blank=True, null=True) # 附加信息:长文本,用于存储额外的检测信息,可选 info = models.TextField(blank=True, verbose_name='附加信息') # 识别地点:字符串,记录识别发生的位置,可选 location = models.CharField(max_length=100, blank=True, verbose_name='识别位置') # ==================== 元数据配置 ==================== class Meta: verbose_name = '识别记录' db_table = 'detection_recognitionrecord' def __str__(self): """返回识别类型和结果组合的描述""" # get_recognition_type_display() 获取识别类型的中文显示 # get_result_display() 获取识别结果的中文显示 return f'{self.get_recognition_type_display()} - {self.get_result_display()}'5.
qianfan_service.py- 百度千帆大模型服务# 导入base64模块,用于将图片编码为base64字符串(便于在JSON中传输) import base64 # 导入requests模块,用于发送HTTP请求调用API import requests # 从typing模块导入类型提示,用于指定函数参数和返回值的类型(提高代码可读性) from typing import Dict, Any # ==================== 百度千帆大模型服务类 ==================== class QianfanService: """ 百度千帆大模型服务类 用于调用百度千帆平台的AI大模型API,实现图像理解和分析功能 """ def __init__(self): """ 构造函数:初始化API配置 在创建QianfanService对象时自动调用 """ # API接口地址(百度千帆平台的聊天补全接口) # 这是一个支持多模态(文本+图像)的API self.url = "https://aistudio.baidu.com/llm/lmapi/v3/chat/completions" # ⚠️ 注意:API密钥应该从环境变量读取,不要硬编码在代码中! # 这里是示例,实际使用时应该改为:self.api_key = os.environ.get('QIANFAN_API_KEY') self.api_key = "4f06daf1f413bd36c18deddb669fd830cb131adb" # 使用的模型名称:ernie-4.5-turbo-vl(文心一言多模态模型) # vl = Vision Language,支持图像理解 self.model = "ernie-4.5-turbo-vl" def encode_image_to_base64(self, image_path: str) -> str: """ 将图片编码为Base64字符串 参数: image_path (str): 图片文件的路径 返回: str: 图片的Base64编码字符串 """ # 以二进制只读模式打开图片文件 with open(image_path, 'rb') as image_file: # 读取文件内容 → 使用base64编码 → 解码为UTF-8字符串 return base64.b64encode(image_file.read()).decode('utf-8') def analyze_image(self, image_path: str, prompt: str = "描述图片中的内容") -> Dict[str, Any]: """ 调用大模型API分析图片 参数: image_path (str): 图片文件的路径 prompt (str): 提问的内容,默认"描述图片中的内容" 返回: Dict[str, Any]: 包含分析结果的字典 - success: 是否成功 - content: 分析内容(成功时) - error: 错误信息(失败时) """ try: # ========== 步骤1:将图片编码为base64 ========== base64_image = self.encode_image_to_base64(image_path) # ========== 步骤2:构建请求体(OpenAI兼容格式) ========== payload = { # 使用的模型名称 "model": self.model, # 消息列表 "messages": [ { # 角色:user(用户) "role": "user", # 消息内容:包含文本和图片(多模态) "content": [ { "type": "text", # 内容类型:文本 "text": prompt # 用户提问的文本 }, { "type": "image_url", # 内容类型:图片URL "image_url": { # 图片以base64格式嵌入(data URL格式) "url": f"data:image/jpeg;base64,{base64_image}" } } ] } ], # 最大输出token数(限制回复长度) "max_tokens": 500, # 温度参数:控制回复的随机性(0-1之间,越高越随机) "temperature": 0.7 } # ========== 步骤3:设置请求头 ========== headers = { # 内容类型:JSON 'Content-Type': 'application/json', # 认证方式:Bearer Token 'Authorization': f'Bearer {self.api_key}' } # ========== 步骤4:发送POST请求 ========== # timeout=30 表示等待响应最多30秒 response = requests.post(self.url, headers=headers, json=payload, timeout=30) # ========== 步骤5:处理响应 ========== # 检查HTTP状态码是否为200(成功) if response.status_code == 200: # 解析JSON响应 result = response.json() # 检查响应中是否包含choices字段且有内容 if 'choices' in result and len(result['choices']) > 0: # 提取AI回复的内容 content = result['choices'][0]['message']['content'] print(f"AI分析结果: {content}") # 调试输出 # 返回成功结果 return { 'success': True, 'content': content } else: # 响应格式异常 return { 'success': False, 'content': 'AI分析结果解析失败', 'error': str(result) } else: # HTTP请求失败(非200状态码) print(f"API调用失败,状态码: {response.status_code}") print(f"响应内容: {response.text}") return { 'success': False, 'content': 'AI分析服务暂时不可用', 'error': f"HTTP {response.status_code}" } # ========== 异常处理 ========== except requests.exceptions.Timeout: # 请求超时异常 return { 'success': False, 'content': 'AI分析请求超时,请稍后重试', 'error': 'Timeout' } except Exception as e: # 其他异常(网络错误、文件不存在等) print(f"AI分析异常: {str(e)}") return { 'success': False, 'content': 'AI分析服务异常', 'error': str(e) } # ==================== 创建全局实例 ==================== # 创建一个全局的QianfanService实例,方便在其他模块中直接导入使用 # 其他文件可以用:from detection.qianfan_service import qianfan_service qianfan_service = QianfanService()6.
urls.py(detection应用) - URL路由配置""" safety_detection URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ # 导入Django的admin模块,用于启用后台管理界面 from django.contrib import admin # 导入Django的path函数,用于定义URL路由规则 from django.urls import path # 从detection应用中导入views模块(所有视图函数) from detection import views # ==================== URL路由列表 ==================== # urlpatterns 是一个列表,Django会按顺序匹配URL urlpatterns = [ # ==================== 用户认证相关 ==================== # 根路径(空路径)→ 显示登录页面 # name='login_page' 用于模板中的 {% url 'login_page' %} path('', views.login_page, name='login_page'), # 登录接口:处理登录表单提交 path('login/', views.login, name='login'), # 注册接口:处理注册表单提交 path('register/', views.register, name='register'), # 首页:登录后显示的仪表盘页面 path('index/', views.index, name='index'), # ==================== 设备管理相关 ==================== # 设备列表页面:显示当前用户的所有设备 path('device_list/', views.device_list, name='device_list'), # 设备详情页面:显示特定设备的详细信息 # <int:device_id> 表示URL中的动态参数,会被转换为整数传递给视图 # 例如:/device_detail/5/ → device_id=5 path('device_detail/<int:device_id>/', views.device_detail, name='device_detail'), # API接口:更新设备状态(在线/离线/维护) # <int:device_id> 指定要更新的设备ID path('api/device/<int:device_id>/status/', views.update_device_status, name='update_device_status'), # 添加设备页面:显示添加设备的表单和处理提交 path('device_add/', views.device_add, name='device_add'), # API接口:删除设备 # 通过AJAX请求调用,删除指定的设备 path('api/device/<int:device_id>/delete/', views.delete_device, name='delete_device'), # ==================== 图像识别相关 ==================== # 图片识别页面:上传图片进行识别 path('image_recognition/', views.image_recognition, name='image_recognition'), # 核心识别接口:接收图片,调用YOLO模型进行识别 # 被图片识别、视频识别、摄像头识别三个页面共同调用 path('detect_mask/', views.detect_mask, name='detect_mask'), # 视频识别页面:上传视频进行逐帧识别 path('video_recognition/', views.video_recognition, name='video_recognition'), # 摄像头识别页面:实时摄像头识别 path('camera_recognition/', views.camera_recognition, name='camera_recognition'), # ==================== 记录和个人信息相关 ==================== # 识别记录页面:查看历史识别记录(支持分页) path('recognition_records/', views.recognition_records, name='recognition_records'), # 个人资料页面:查看和修改个人信息 path('pages_profile/', views.pages_profile, name='pages_profile'), # API接口:修改密码 path('change_password/', views.change_password, name='change_password'), # 退出登录接口:清除session并跳转到登录页 path('logout/', views.logout, name='logout'), ]7.
views.py- 核心业务逻辑# ==================== 导入模块 ==================== # 导入json模块,用于处理前端传递的JSON数据 import base64 import io import json import tempfile import uuid # 导入时间差模块,用于统计近7天数据 from datetime import timedelta import cv2 import numpy as np from PIL import Image from django.core.files.base import ContentFile from django.core.paginator import Paginator # 导入Django的JSON响应类,用于返回接口数据 from django.http import JsonResponse # 导入渲染页面、重定向工具函数 from django.shortcuts import render, redirect # 导入数据库统计函数Count,用于分组统计数量 from django.db.models import Count from django.views.decorators.csrf import csrf_exempt # 导入项目里的3个数据表模型 from detection.models import User, Device, RecognitionRecord from detection.qianfan_service import qianfan_service # ==================== 统一类别映射(新增) ==================== # YOLO模型输出的类别ID到标签的映射 # 注意:需要根据实际训练时的类别顺序调整 CLASS_MAPPING = { 0: 'mask', # 戴口罩 1: 'no_mask', # 没戴口罩 2: 'hat', # 戴安全帽 3: 'person' # 没戴安全帽 } # 标签到中文名称的映射 CLASS_NAMES_CN = { 'mask': '佩戴口罩', 'no_mask': '未佩戴口罩', 'hat': '佩戴安全帽', 'person': '未佩戴安全帽' } # ====================================== # 工具函数:获取当前登录的用户 # ====================================== def get_current_user(request): """ 从session中获取当前登录的用户信息 参数: request - Django请求对象 返回: User对象 或 None(未登录时) """ # 从session中获取登录时保存的user_id user_id = request.session.get('user_id') # 如果能获取到user_id,说明已登录 if user_id: # 根据ID查询用户并返回 return User.objects.get(id=user_id) # 没有登录则返回None return None # ====================================== # 页面:显示登录页面 # ====================================== def login_page(request): """显示登录页面""" # 直接渲染login.html模板 return render(request, 'login.html') # ====================================== # 功能:处理登录请求 # ====================================== def login(request): """处理用户登录的AJAX请求""" if request.method == 'POST': # 获取表单提交的邮箱和密码 email = request.POST.get('email') password = request.POST.get('password') # 使用 filter 方法查询用户,如果用户不存在,user 变量会是 None user = User.objects.filter(email=email).first() # 用户不存在 if user is None: return JsonResponse({'msg': '用户不存在,请先注册'}) # 密码验证(注意:生产环境应使用加密验证) if user.password == password: # 登录成功,将用户信息存入session request.session['user_id'] = user.id request.session['username'] = user.username return JsonResponse({'msg': '登录成功'}) # 密码错误 return JsonResponse({'msg': '密码错误'}) # GET请求显示登录页面 return render(request, 'login.html') # ====================================== # 功能:处理注册请求 # ====================================== def register(request): """处理用户注册请求""" if request.method == 'POST': # 获取注册表单数据 email = request.POST.get('email') username = request.POST.get('username') password = request.POST.get('password') # 判断邮箱是否已被注册 if User.objects.filter(email=email).exists(): return JsonResponse({'msg': '用户邮箱已存在'}) # 判断用户名是否已被注册 if User.objects.filter(username=username).exists(): return JsonResponse({'msg': '用户名已存在'}) # 创建新用户对象(注意:密码应加密存储) user = User(username=username, email=email, password=password) # 保存到数据库 user.save() # 注册成功,跳转到登录页 return redirect('login_page') # GET请求,显示注册页面 return render(request, 'register.html') # ====================================== # 页面:首页(数据统计大盘) # ====================================== def index(request): """首页仪表盘,显示各种统计数据""" # 获取当前登录用户 user = get_current_user(request) # ========== 设备统计 ========== # 统计所有设备总数 total_devices = Device.objects.count() # 统计在线设备数量 online_devices = Device.objects.filter(status='online').count() # 统计离线设备数量 offline_devices = Device.objects.filter(status='offline').count() # 统计维护中设备数量 maintenance_devices = Device.objects.filter(status='maintenance').count() # ========== 识别记录统计 ========== # 统计所有识别记录总数 total_record = RecognitionRecord.objects.count() # 导入Django时区工具 from django.utils import timezone # 获取今天 00:00:00 today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) # 获取今天 23:59:59 today_end = timezone.now().replace(hour=23, minute=59, second=59, microsecond=99999) # 统计今日识别记录数 today_records = RecognitionRecord.objects.filter( timestamp__gte=today_start, timestamp__lte=today_end ).count() # ========== 4种识别结果统计 ========== mask_records = RecognitionRecord.objects.filter(result='mask').count() no_mask_records = RecognitionRecord.objects.filter(result='no_mask').count() hat_records = RecognitionRecord.objects.filter(result='hat').count() person_records = RecognitionRecord.objects.filter(result='person').count() # 总计 total_records = mask_records + no_mask_records + hat_records + person_records # ========== 设备类型分布统计 ========== device_types = Device.objects.exclude( device_type__isnull=True ).exclude( device_type='' ).values('device_type').annotate( count=Count('id') ).order_by('-count') # ========== 最近识别记录 ========== # 获取最近10条识别记录,按时间倒序 recent_records = RecognitionRecord.objects.select_related('device').order_by('-timestamp')[:10] # ========== 近7天每日识别数量统计 ========== daily_stats = [] for i in range(6, -1, -1): # 循环获取今天、昨天...前6天 current_date = timezone.now() - timedelta(days=i) day_start = current_date.replace(hour=0, minute=0, second=0, microsecond=0) day_end = current_date.replace(hour=23, minute=59, second=59, microsecond=99999) # 查询当天记录数 count = RecognitionRecord.objects.filter( timestamp__gte=day_start, timestamp__lte=day_end ).count() # 存入数组,用于前端图表展示 daily_stats.append({ 'date': current_date.strftime('%m-%d'), 'count': count }) # ========== 打包数据传递给前端 ========== context = { 'user': user, 'total_devices': total_devices, 'online_devices': online_devices, 'offline_devices': offline_devices, 'maintenance_devices': maintenance_devices, 'today_records': today_records, 'total_record': total_record, 'mask_records': mask_records, 'no_mask_records': no_mask_records, 'hat_records': hat_records, 'person_records': person_records, 'total_records': total_records, 'device_types': device_types, 'recent_records': recent_records, 'daily_stats': daily_stats, } # 渲染首页模板 return render(request, 'index.html', context) # ====================================== # 页面:设备列表页 # ====================================== def device_list(request): """显示当前用户的所有设备""" # 获取当前登录用户 user = get_current_user(request) # 未登录则跳转到登录页 if not user: return render(request, 'login.html') # 查询当前用户的所有设备,按创建时间倒序 devices = Device.objects.filter(owner=user).order_by('-created_at') # 渲染设备列表页面 return render(request, 'device_list.html', {'devices': devices}) # ====================================== # 页面:设备详情 # ====================================== def device_detail(request, device_id): """显示单个设备的详细信息""" # 根据ID查询设备 device = Device.objects.get(id=device_id) # 统计该设备的识别记录 total_records = device.recognition_record.count() mask_count = device.recognition_record.filter(result='mask').count() no_mask_count = device.recognition_record.filter(result='no_mask').count() context = { 'device': device, 'total_records': total_records, 'mask_count': mask_count, 'no_mask_count': no_mask_count, } return render(request, 'device_detail.html', context) # ====================================== # 页面:添加设备 # ====================================== def device_add(request): """添加新设备""" user = get_current_user(request) if not user: return render(request, 'login.html') if request.method == 'POST': # 获取表单数据 name = request.POST.get('name') device_type = request.POST.get('device_type') location = request.POST.get('location') description = request.POST.get('description') video_file = request.FILES.get('video_file') status = request.POST.get('status', 'offline') # 创建设备对象 device = Device( name=name, device_type=device_type, location=location, description=description, video_file=video_file, status=status, owner=user, ) device.save() return redirect('device_list') return render(request, 'device_add.html') # ====================================== # 接口:修改设备状态 # ====================================== def update_device_status(request, device_id): """更新设备状态(在线/离线/维护)""" if request.method == 'POST': # 解析前端传来的JSON数据 data = json.loads(request.body) new_status = data.get('status') if not new_status: return JsonResponse({'success': False, 'error': '状态不能为空'}) # 查询并更新设备状态 device = Device.objects.get(id=device_id) original_status = device.status device.status = new_status device.save() return JsonResponse({ 'success': True, 'message': '设备状态更新成功', 'new_status': new_status, 'original_status': original_status, }) return JsonResponse({'success': False, 'error': '只支持POST请求'}) # ====================================== # 接口:删除设备 # ====================================== def delete_device(request, device_id): """删除指定设备""" # 校验登录 user = get_current_user(request) if not user: return JsonResponse({'success': False, 'msg': '请先登录'}) try: # 只允许删除自己的设备 device = Device.objects.get(id=device_id, owner=user) device.delete() return JsonResponse({'success': True, 'msg': '删除成功'}) except Device.DoesNotExist: return JsonResponse({'success': False, 'msg': '设备不存在或无权限'}) # ==================== YOLO模型加载 ==================== from ultralytics import YOLO from pathlib import Path # 使用相对路径,提高代码可移植性 BASE_DIR = Path(__file__).resolve().parent.parent model_path = BASE_DIR / 'best.pt' # 模型放在项目根目录 try: model = YOLO(str(model_path)) print(f'✅ 模型加载成功: {model_path}') except Exception as e: print(f'❌ 模型加载失败: {e}') model = None # ====================================== # 页面:图片识别 # ====================================== def image_recognition(request): """显示图片识别页面""" return render(request, 'image_recognition.html') # ====================================== # 核心接口:识别检测 # ====================================== @csrf_exempt # 允许跨域请求(简化API调用) def detect_mask(request): """ 核心识别接口 接收图片,调用YOLO模型检测,返回检测结果和AI分析 """ if request.method == 'POST': # 获取当前用户 user = get_current_user(request) if not user: return render(request, 'login.html') # 解析请求体中的JSON数据 data = json.loads(request.body) image_data = data['image'] original_image_data = image_data # 提取base64编码的图片数据 if image_data.startswith('data:image'): image_data = image_data.split(',')[1] # 将base64解码为图片 image_bytes = base64.b64decode(image_data) image = Image.open(io.BytesIO(image_bytes)) image = np.array(image)[:, :, ::-1] # RGB转BGR(YOLO需要) # YOLO模型推理 results = model(image) print(results) # 解析检测结果 detections = [] for result in results: boxes = result.boxes for box in boxes: xyxy = box.xyxy[0].cpu().numpy() conf = box.conf.cpu().numpy()[0] cls_id = int(box.cls.cpu().numpy()[0]) # 【修改点1】使用统一的类别映射 label = CLASS_MAPPING.get(cls_id, 'unknown') x1, y1, x2, y2 = xyxy bbox = [int(x1), int(y1), int(x2 - x1), int(y2 - y1)] if float(conf) > 0.5: detections.append({ 'label': label, 'confidence': float(conf), 'bbox': bbox, }) # 获取识别类型和位置信息 recognition_type = data.get('recognition_type', 'camera') location = data.get('location', '') device_id = data.get('device_id') print(detections) # 关联设备 device = None if device_id: try: device = Device.objects.get(id=device_id) except Device.DoesNotExist: pass # 保存检测截图 image_file = None if original_image_data.startswith('data:image'): format, image_str = original_image_data.split(';base64') ext = format.split('/')[-1] image_content = base64.b64decode(image_str) image_file = ContentFile(image_content, name=f'recognition_{uuid.uuid4()}.{ext}') # 【修改点2】只存储置信度最高的检测结果 if detections: best_detection = max(detections, key=lambda x: x['confidence']) RecognitionRecord.objects.create( user=user, recognition_type=recognition_type, result=best_detection['label'], confidence=best_detection['confidence'], image=image_file, location=location, device=device ) # ========== AI大模型分析 ========== ai_analysis = None tmp_path = None with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file: tmp_path = tmp_file.name tmp_file.close() # 保存图片到临时文件 image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) image_pil.save(tmp_path) # 调用千帆大模型API prompt = '请详细描述图片中的人员是否正确佩戴了安全帽,并根据场景给出安全建议。如果佩戴了安全帽请说明,如果没有佩戴安全帽请明确指出。' result = qianfan_service.analyze_image(tmp_path, prompt) print(result) if result.get('success'): ai_analysis = result.get('content') else: ai_analysis = 'AI分析服务暂时不可用' # 返回检测结果和AI分析 return JsonResponse({ 'detections': detections, 'ai_analysis': ai_analysis }) return render(request, 'image_recognition.html') # ====================================== # 页面:视频识别 # ====================================== def video_recognition(request): """显示视频识别页面""" user = get_current_user(request) if not user: return render(request, 'login.html') devices = Device.objects.filter(owner=user) return render(request, 'video_recognition.html', {"devices": devices}) # ====================================== # 页面:摄像头识别 # ====================================== def camera_recognition(request): """显示摄像头识别页面""" user = get_current_user(request) if not user: return render(request, 'login.html') devices = Device.objects.filter(owner=user) return render(request, 'camera_recognition.html', {"devices": devices}) # ====================================== # 页面:检测记录 # ====================================== def recognition_records(request): """显示历史识别记录(带分页)""" user = get_current_user(request) if not user: return render(request, 'login.html') # 查询当前用户的所有识别记录,按时间倒序 records_list = RecognitionRecord.objects.filter(user=user).order_by("-timestamp") # 分页:每页10条 paginator = Paginator(records_list, 10) page = request.GET.get('page') records = paginator.get_page(page) return render(request, 'recognition_records.html', {"records": records}) # ====================================== # 页面:个人信息管理 # ====================================== def pages_profile(request): """个人资料页面:查看和修改个人信息""" user = get_current_user(request) if not user: return render(request, 'login.html') if request.method == 'POST': # 获取所有表单字段 username = request.POST.get('username', '') email = request.POST.get('email', '') phone = request.POST.get('phone', '') first_name = request.POST.get('first_name', '') last_name = request.POST.get('last_name', '') location = request.POST.get('location', '') website = request.POST.get('website', '') bio = request.POST.get('bio', '') # 验证邮箱是否已被其他用户使用 if email != user.email and User.objects.filter(email=email).exists(): if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({'error': '邮箱已存在'}, status=400) return render(request, 'pages-profile.html', { 'user': user, 'error_message': '邮箱已存在' }) # 验证用户名是否已被其他用户使用 if username != user.username and User.objects.filter(username=username).exists(): if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({'error': '用户名已存在'}, status=400) return render(request, 'pages-profile.html', { 'user': user, 'error_message': '用户名已存在' }) # 更新所有用户字段 user.username = username user.email = email user.phone = phone user.first_name = first_name user.last_name = last_name user.location = location user.website = website user.bio = bio # 处理头像上传 if request.FILES.get('avatar'): user.avatar = request.FILES['avatar'] # 保存所有更改 user.save() # 更新session中的用户名 request.session['username'] = user.username # 判断是否为AJAX请求 if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({'success': True, 'message': '个人信息更新成功'}) return render(request, 'pages-profile.html', { 'user': user, 'success_message': '个人信息更新成功' }) return render(request, 'pages-profile.html', {'user': user}) # ====================================== # 接口:修改密码 # ====================================== def change_password(request): """修改用户密码""" if request.method == 'POST': # 解析请求数据(支持JSON和表单) data = json.loads(request.body) if request.content_type == 'application/json' else request.POST new_password = data.get('new_password') if not new_password: return JsonResponse({'success': False, 'error': '请输入新密码'}, status=400) if len(new_password) < 6: return JsonResponse({'success': False, 'error': '请输入6位以上的密码'}, status=400) user = get_current_user(request) user.password = new_password # 注意:生产环境应加密存储 user.save() return JsonResponse({'success': True, 'message': '更新成功'}) return JsonResponse({'success': False, 'error': '只支持POST请求'}, status=405) # ====================================== # 接口:退出登录 # ====================================== @csrf_exempt def logout(request): """用户退出登录,清除session""" if request.method == 'POST': # 清除session中的用户信息 if 'user_id' in request.session: del request.session['user_id'] if 'username' in request.session: del request.session['username'] return JsonResponse({'msg': '退出成功'}) return JsonResponse({'msg': '请求方法错误'}, status=405)8.
settings.py- Django项目配置文件# 从pathlib模块导入Path类,用于处理文件路径(跨平台兼容) from pathlib import Path # 项目根目录路径:当前文件所在目录的父目录的父目录 # __file__ 是当前文件的路径,resolve() 解析为绝对路径,.parent 获取父目录 # 例如:D:/safety_yolo/safety_detection/settings.py → D:/safety_yolo BASE_DIR = Path(__file__).resolve().parent.parent # Django项目的密钥,用于加密session、CSRF等安全功能 # ⚠️ 生产环境必须修改为随机字符串,且不能泄露! SECRET_KEY = 'django-insecure-6opj&9h9ms!jlny-#fnvev6h-968^=jt6&fpt+gg#0n!n08&is' # 调试模式:True表示开发模式,会显示详细错误信息 # ⚠️ 生产环境必须设置为 False! DEBUG = True # 允许访问的主机列表(DEBUG=False时需要配置) ALLOWED_HOSTS = [] # ====================== # 已安装的应用(APP列表) # 注意:simpleui 必须放在最前面,否则样式可能不生效 # ====================== INSTALLED_APPS = [ 'simpleui', # 美化Django Admin后台界面 'django.contrib.admin', # Django内置后台管理 'django.contrib.auth', # Django内置认证系统 'django.contrib.contenttypes', # 内容类型框架 'django.contrib.sessions', # 会话框架 'django.contrib.messages', # 消息框架 'django.contrib.staticfiles', # 静态文件管理 'detection', # 自定义的detection应用(核心) ] # 中间件配置(处理请求/响应的钩子) MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', # 安全中间件 'django.contrib.sessions.middleware.SessionMiddleware', # Session管理 'django.middleware.common.CommonMiddleware', # 通用中间件 'django.middleware.csrf.CsrfViewMiddleware', # CSRF防护 'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证 'django.contrib.messages.middleware.MessageMiddleware', # 消息处理 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护 ] # 根URL配置:指定项目的主路由文件 ROOT_URLCONF = 'safety_detection.urls' # 模板配置 TEMPLATES = [ { # 模板引擎:Django模板 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 额外的模板目录(空表示只在APP内查找) 'DIRS': [], # 是否在APP内查找模板(True表示会在各APP的templates目录查找) 'APP_DIRS': True, # 模板上下文处理器(在模板中可用的变量) 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', # 调试信息 'django.template.context_processors.request', # 请求对象 'django.contrib.auth.context_processors.auth', # 用户信息 'django.contrib.messages.context_processors.messages', # 消息 ], }, }, ] # WSGI应用(用于部署) WSGI_APPLICATION = 'safety_detection.wsgi.application' # ====================== # 数据库配置 # ====================== DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库 'NAME': 'django_safety_detection', # 数据库名称 'USER': 'root', # 数据库用户名 'PASSWORD': '123456', # 数据库密码 'HOST': 'localhost', # 数据库主机地址 'PORT': '3306', # 数据库端口(MySQL默认3306) 'OPTIONS': { # 连接初始化命令:设置存储引擎为INNODB,时区为东八区 'init_command': 'SET default_storage_engine=INNODB,time_zone="+8:00"' } } } # ====================== # 密码验证器配置(Django自带用户系统用) # ====================== AUTH_PASSWORD_VALIDATORS = [ # 用户属性相似性验证(密码不能太像用户名) {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, # 最小长度验证 {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, # 常见密码验证(如123456、password等) {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, # 纯数字密码验证 {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] # ====================== # 国际化和本地化配置 # ====================== LANGUAGE_CODE = 'zh-hans' # 语言:简体中文 TIME_ZONE = 'Asia/Shanghai' # 时区:上海/北京时间 USE_I18N = True # 启用国际化(多语言支持) USE_L10N = True # 启用本地化(数字、日期格式) USE_TZ = False # 关闭时区支持(使用本地时间) # ====================== # 静态文件和媒体文件配置 # ====================== STATIC_URL = '/static/' # 静态文件的URL前缀 STATICFILES_DIRS = [BASE_DIR / "static"] # 额外的静态文件目录 MEDIA_URL = '/media/' # 用户上传文件的URL前缀 MEDIA_ROOT = BASE_DIR / 'media' # 用户上传文件的存储目录 # 默认主键字段类型(使用64位整数,支持更多数据) DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # ====================== # SimpleUI 后台美化配置 # ====================== SIMPLEUI_HOME_TITLE = '安全防护检测系统' # 后台首页标题 SIMPLEUI_HOME_ICON = 'fa fa-shield' # 后台首页图标 SIMPLEUI_STATIC_OFFLINE = True # 使用本地静态文件(不联网) SIMPLEUI_DEFAULT_THEME = 'primary' # 默认主题颜色 SIMPLEUI_LOGO = '/static/logo.png' # 后台Logo图片(可自定义) # 后台自定义菜单配置 SIMPLEUI_CONFIG = { 'system_keep': False, # 是否保留系统默认菜单 'dynamic': True, # 动态菜单(根据权限显示) 'menus': [ # 自定义菜单列表 { 'name': '系统管理', # 菜单名称 'icon': 'fa fa-cog', # 菜单图标 'models': [ # 子菜单列表 {'name': '用户管理', 'url': '/admin/detection/user/', 'icon': 'fa fa-user'}, {'name': '设备管理', 'url': '/admin/detection/device/', 'icon': 'fa fa-server'}, {'name': '识别记录', 'url': '/admin/detection/recognitionrecord/', 'icon': 'fa fa-list'}, ] } ] }9.
manage.py- Django项目管理脚本#!/usr/bin/env python """ Shebang行(仅Linux/Mac有效): 告诉系统使用当前Python环境来执行这个脚本 Windows会忽略这行,但保留它不影响运行 """ """Django's command-line utility for administrative tasks.""" # 模块文档字符串:说明这个文件是Django的命令行管理工具 # 导入操作系统模块,用于读取和设置环境变量 import os # 导入系统模块,用于获取命令行参数 import sys def main(): """Run administrative tasks.""" # 函数文档字符串:运行管理任务 # 设置Django默认的环境变量,指定项目使用的配置文件 # DJANGO_SETTINGS_MODULE 告诉Django应该使用哪个settings模块 # 'safety_detection.settings' 表示使用 safety_detection 目录下的 settings.py # setdefault 表示:如果环境变量已存在则不覆盖,否则设置为默认值 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'safety_detection.settings') try: # 尝试从django.core.management导入执行命令的函数 # execute_from_command_line 负责解析命令行参数并执行相应的Django命令 from django.core.management import execute_from_command_line except ImportError as exc: # 如果导入失败(Django未安装),抛出友好的错误提示 raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc # 执行命令行命令 # sys.argv 是命令行参数列表,例如:python manage.py runserver # sys.argv[0] 是 'manage.py',后面的参数依次传入 execute_from_command_line(sys.argv) # 判断是否直接运行这个脚本(而不是被导入) # 当执行 python manage.py 时,__name__ 等于 '__main__' # 此时调用 main() 函数开始执行 if __name__ == '__main__': main()
更多推荐
所有评论(0)