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">&times;</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">&times;</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">&laquo;&laquo;</span>  {# 双左箭头 #}
                                        </a>
                                    </li>
                                    <li class="page-item">
                                        <a class="page-link" href="?page={{ records.previous_page_number }}" aria-label="上一页">
                                            <span aria-hidden="true">&laquo;</span>  {# 单左箭头 #}
                                        </a>
                                    </li>
                                {% else %}
                                    {# 没有上一页时,按钮禁用 #}
                                    <li class="page-item disabled">
                                        <span class="page-link" aria-hidden="true">&laquo;&laquo;</span>
                                    </li>
                                    <li class="page-item disabled">
                                        <span class="page-link" aria-hidden="true">&laquo;</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">&raquo;</span>  {# 单右箭头 #}
                                        </a>
                                    </li>
                                    <li class="page-item">
                                        <a class="page-link" href="?page={{ records.paginator.num_pages }}" aria-label="末页">
                                            <span aria-hidden="true">&raquo;&raquo;</span>  {# 双右箭头 #}
                                        </a>
                                    </li>
                                {% else %}
                                    {# 没有下一页时,按钮禁用 #}
                                    <li class="page-item disabled">
                                        <span class="page-link" aria-hidden="true">&raquo;</span>
                                    </li>
                                    <li class="page-item disabled">
                                        <span class="page-link" aria-hidden="true">&raquo;&raquo;</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()

更多推荐