This commit is contained in:
2025-08-08 10:38:10 +08:00
commit c7dc4d6c02
114 changed files with 11010 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# 所有对话和文档都使用中文
# 文档使用 markdown 格式
# 该项目的python虚拟环境在"/mnt/nfs/.venv/bin/activate"目录下
## 通用礼节 (General Etiquette)
- 优先保证代码简洁易懂。- 别搞过度设计,简单实用就好。
- 写代码时,要注意圈复杂度,函数尽量小,尽量可以复用,尽量不写重复代码。
- 写代码时,注意模块设计,尽量使用设计模式。
- 给我解释代码的时候,说人话,别拽专业术语。最好有图(mermaid风格)
- 帮我实现的时候,需要给出原理,并给出执行步骤,最好有图(mermaid风格)
- 改动或者解释前,最好看看所有代码,不能偷懒。
- 改动前,要做最小化修改,尽量不修改到其他模块的代码- 改动后,假定10条case 输入,并给出预期结果。
# 实验性规则 (Experimental Rule)
当你被要求修复一个 Bug 时,请遵循以下步骤:
1.  **理解问题 (Understand):** 仔细阅读 Bug 描述和相关代码,复述你对问题的理解。
2.  **分析原因 (Analyze):** 提出至少两种可能的根本原因。
3.  **制定计划 (Plan):** 描述你打算如何验证这些原因,并给出修复方案。
4.  **请求确认 (Confirm):** 在动手修改前,向我确认你的计划。
5.  **执行修复 (Execute):** 实施修复。
6.  **审查 (Review):** 查看自己的修改有没有问题。
7.  **解释说明 (Explain):** 解释你做了哪些修改以及为什么。
Executable
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# 激活虚拟环境
VENV_PATH="/mnt/nfs/.venv"
if [ -d "$VENV_PATH" ]; then
source "$VENV_PATH/bin/activate"
else
echo "虚拟环境路径不存在:$VENV_PATH"
exit 1
fi
# 定义 uWSGI 的启动配置文件路径
INI_FILE="/opt/1panel/docker/compose/4an/app/uwsgi.ini"
# 定义 uWSGI 的 PID 文件路径
PID_FILE="/opt/1panel/docker/compose/4an/uwsgi.pid"
# 定义日志文件路径
LOG_FILE="/opt/1panel/docker/compose/4an/uwsgi.log"
# 检查配置文件是否存在
if [ ! -f "$INI_FILE" ]; then
echo "uWSGI 配置文件不存在:$INI_FILE"
exit 1
fi
# 检查 PID 文件是否存在
if [ -f "$PID_FILE" ]; then
echo "停止 uWSGI 服务..."
uwsgi --stop "$PID_FILE"
if [ $? -eq 0 ]; then
echo "uWSGI 服务已成功停止。"
else
echo "停止 uWSGI 服务失败,请检查 PID 文件:$PID_FILE"
exit 1
fi
else
echo "PID 文件不存在,跳过停止服务步骤。"
fi
Executable
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
# 激活虚拟环境
VENV_PATH="/mnt/nfs/.venv"
if [ -d "$VENV_PATH" ]; then
source "$VENV_PATH/bin/activate"
else
echo "虚拟环境路径不存在:$VENV_PATH"
exit 1
fi
# 定义 uWSGI 的启动配置文件路径
INI_FILE="/opt/1panel/docker/compose/4an/app/uwsgi.ini"
# 定义 uWSGI 的 PID 文件路径
PID_FILE="/opt/1panel/docker/compose/4an/uwsgi.pid"
# 定义日志文件路径
LOG_FILE="/opt/1panel/docker/compose/4an/uwsgi.log"
# 检查配置文件是否存在
if [ ! -f "$INI_FILE" ]; then
echo "uWSGI 配置文件不存在:$INI_FILE"
exit 1
fi
# 检查 PID 文件是否存在
if [ -f "$PID_FILE" ]; then
echo "停止 uWSGI 服务..."
uwsgi --stop "$PID_FILE"
if [ $? -eq 0 ]; then
echo "uWSGI 服务已成功停止。"
else
echo "停止 uWSGI 服务失败,请检查 PID 文件:$PID_FILE"
exit 1
fi
else
echo "PID 文件不存在,跳过停止服务步骤。"
fi
# 等待 2 秒,确保服务已完全停止
sleep 2
# 启动 uWSGI 服务
echo "启动 uWSGI 服务..."
uwsgi --ini "$INI_FILE" --pidfile "$PID_FILE" --logto "$LOG_FILE"
if [ $? -eq 0 ]; then
echo "uWSGI 服务已成功启动。"
else
echo "uWSGI 服务启动失败,请检查日志:$LOG_FILE"
exit 1
fi
# 检查 PID 文件是否重新生成
if [ -f "$PID_FILE" ]; then
echo "uWSGI 服务已成功重启。"
else
echo "uWSGI 服务启动失败,PID 文件未生成。"
exit 1
fi
+162
View File
@@ -0,0 +1,162 @@
# 4AN项目部署指南
## 环境变量配置
配置文件`app/config.py`已更新为支持环境变量。所有配置项可通过环境变量设置,包括:
- `SECRET_KEY`: 应用密钥
- `DATABASE_URI`: 数据库连接URI
- `LZMX_WEBHOOK_URL`: 临猪消息Webhook
- `UPLOAD_FOLDER`: 文件上传目录
- `KEY110_DB_HOST`: 1key110数据库主机
- `KEY110_DB_USER`: 1key110数据库用户
- `KEY110_DB_PASSWORD`: 1key110数据库密码
- `KEY110_DB_DATABASE`: 1key110数据库名称
## 使用Supervisor管理项目
### 安装依赖
项目需要安装python-dotenv包以支持环境变量:
```bash
/mnt/nfs/.venv/bin/pip install python-dotenv
```
### 环境变量文件
我们已创建环境变量文件 `/opt/1panel/docker/compose/4an/4an.env`,其中包含所有配置参数:
```
# 应用密钥
SECRET_KEY=
# 数据库连接
DATABASE_URI=mysql+pymysql://4an:GtbJp6Ai5azntnTB@10.10.10.253/4an
# 临猪消息Webhook
LZMX_WEBHOOK_URL=http://imtwo.zdxlz.com/im-external/v1/webhook/send?key=ef1dc605827f425e96491ce5725b5274
# 文件上传目录
UPLOAD_FOLDER=static/uploads
# 1key110数据库配置
KEY110_DB_HOST=10.10.10.253
KEY110_DB_USER=1key110
KEY110_DB_PASSWORD=KzdKtf7KfkXe7iCN
KEY110_DB_DATABASE=1key110
```
在启动应用前需要加载这些环境变量:
```bash
# 加载环境变量文件
export $(cat /opt/1panel/docker/compose/4an/4an.env | grep -v '^#' | xargs)
```
### 配置Supervisor
1. 确保修改app/config.py使其支持环境变量后,再使用Supervisor管理应用
2.`4an_supervisor.conf`文件复制到Supervisor配置目录:
```bash
sudo cp 4an_supervisor.conf /etc/supervisor/conf.d/
```
3. 在配置文件目录中创建一个启动脚本,用于加载环境变量并启动应用:
```bash
sudo nano /opt/1panel/docker/compose/4an/start_with_env.sh
```
脚本内容:
```bash
#!/bin/bash
# 加载环境变量
export $(cat /opt/1panel/docker/compose/4an/4an.env | grep -v '^#' | xargs)
# 启动uwsgi
exec /mnt/nfs/.venv/bin/uwsgi --ini /opt/1panel/docker/compose/4an/app/uwsgi.ini
```
设置脚本权限:
```bash
sudo chmod +x /opt/1panel/docker/compose/4an/start_with_env.sh
```
4. 修改Supervisor配置使用该脚本:
```bash
sudo nano /etc/supervisor/conf.d/4an_supervisor.conf
```
将command行修改为:
```
command=/opt/1panel/docker/compose/4an/start_with_env.sh
```
5. 更新Supervisor配置:
```bash
sudo supervisorctl reread
sudo supervisorctl update
```
6. 管理4an服务:
```bash
# 启动服务
sudo supervisorctl start 4an
# 停止服务
sudo supervisorctl stop 4an
# 重启服务
sudo supervisorctl restart 4an
# 查看服务状态
sudo supervisorctl status 4an
# 查看日志
sudo tail -f /opt/1panel/docker/compose/4an/supervisor.log
```
## 使用systemd(可选)
如果需要使用systemd而不是Supervisor,可以创建服务文件:
```bash
sudo nano /etc/systemd/system/4an.service
```
服务文件内容示例:
```ini
[Unit]
Description=4AN Application
After=network.target
[Service]
User=root
WorkingDirectory=/opt/1panel/docker/compose/4an/app
ExecStart=/mnt/nfs/.venv/bin/uwsgi --ini /opt/1panel/docker/compose/4an/app/uwsgi.ini
Restart=always
StandardOutput=file:/opt/1panel/docker/compose/4an/systemd.log
StandardError=file:/opt/1panel/docker/compose/4an/systemd_error.log
EnvironmentFile=/opt/1panel/docker/compose/4an/4an.env
[Install]
WantedBy=multi-user.target
```
然后启用并启动服务:
```bash
sudo systemctl daemon-reload
sudo systemctl enable 4an
sudo systemctl start 4an
```
+14
View File
@@ -0,0 +1,14 @@
[program:4an]
command=/mnt/nfs/.venv/bin/uwsgi --ini /opt/1panel/docker/compose/4an/app/uwsgi.ini
directory=/opt/1panel/docker/compose/4an/app
user=root
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/opt/1panel/docker/compose/4an/supervisor.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
environment=PYTHONPATH="/opt/1panel/docker/compose/4an/app"
environment_file=/opt/1panel/docker/compose/4an/.env
Executable
+36
View File
@@ -0,0 +1,36 @@
# 应用密钥
SECRET_KEY=kU/W5y3uFbWuK1x+RTTOPgLp38yM6Z7d
# 数据库连接
DATABASE_URI=mysql+pymysql://4an:GtbJp6Ai5azntnTB@10.10.10.253/4an
# 量子密信消息Webhook
LZMX_WEBHOOK_URL=http://imtwo.zdxlz.com/im-external/v1/webhook/send?key=ef1dc605827f425e96491ce5725b5274
# 文件上传目录
UPLOAD_FOLDER=static/uploads
# 1key110数据库配置
KEY110_DB_HOST=10.10.10.253
KEY110_DB_USER=1key110
KEY110_DB_PASSWORD=KzdKtf7KfkXe7iCN
KEY110_DB_DATABASE=1key110
# uWSGI基本配置
UWSGI_HTTP=0.0.0.0:18019
UWSGI_PROCESSES=4
UWSGI_THREADS=2
UWSGI_LOG_FILE=/opt/1panel/docker/compose/4an/uwsgi.log
UWSGI_VIRTUALENV=/mnt/nfs/.venv
UWSGI_CHDIR=/opt/1panel/docker/compose/4an/app
UWSGI_PID_FILE=/opt/1panel/docker/compose/4an/uwsgi.pid
UWSGI_SOCKET=/opt/1panel/docker/compose/4an/uwsgi.sock
# uWSGI性能优化配置
UWSGI_MASTER=true
UWSGI_HARAKIRI=30
UWSGI_MAX_REQUESTS=1000
UWSGI_BUFFER_SIZE=32768
UWSGI_DISABLE_LOGGING=true
UWSGI_MEMORY_REPORT=true
UWSGI_ENABLE_THREADS=true
Binary file not shown.
Binary file not shown.
Executable
+95
View File
@@ -0,0 +1,95 @@
from flask import Flask, redirect, url_for
from flask_login import LoginManager
from flask_migrate import Migrate
from scripts.models import db, User, Device, WorkOrder, WorkOrderHandling, ManualWorkOrder
from config import Config
from views.auth import init_auth_routes
from views.work_order import init_work_order_routes
from views.statistics import init_statistics_routes
from views.history import init_history_routes
from views.manual import init_manual_routes
from views.device import init_device_routes
from views.about import init_about_routes
from views.dashboard import init_dashboard_routes
import logging
from logging.handlers import RotatingFileHandler
import os
from utils.error_handlers import ErrorHandler
def create_app():
# 创建应用实例
app = Flask(__name__)
app.config.from_object(Config)
# 配置日志记录
if not os.path.exists('logs'):
os.makedirs('logs')
# 创建日志处理器,设置最大文件大小为10MB,保留5个备份文件
file_handler = RotatingFileHandler(
'logs/app.log',
maxBytes=10*1024*1024, # 10MB
backupCount=5,
encoding='utf-8'
)
# 设置日志格式
formatter = logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s',
'%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
# 设置日志级别
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('应用启动')
# 初始化数据库
db.init_app(app)
migrate = Migrate(app, db)
# 初始化异常处理器
error_handler = ErrorHandler(app)
# 初始化登录管理器
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
try:
return db.session.get(User, user_id)
except Exception as e:
app.logger.error(f"加载用户时出错: {str(e)}")
return None
# 注册路由
@app.route('/')
def index():
return redirect(url_for('dashboard'))
# 初始化各模块路由
init_auth_routes(app)
init_work_order_routes(app)
init_statistics_routes(app)
init_history_routes(app)
init_manual_routes(app)
init_device_routes(app)
init_about_routes(app)
init_dashboard_routes(app)
return app
app = create_app()
if __name__ == '__main__':
with app.app_context():
try:
db.create_all()
app.logger.info("数据库表创建成功")
except Exception as e:
app.logger.error(f"创建数据库表时出错: {str(e)}")
app.run(host='0.0.0.0', port=18061, debug=True)
Executable
+25
View File
@@ -0,0 +1,25 @@
import os
from dotenv import load_dotenv
# 加载.env文件 (相对于应用根目录)
load_dotenv()
class Config:
"""配置类,支持从环境变量获取配置
环境变量优先级高于默认值,可在系统环境变量中设置这些参数
"""
SECRET_KEY = os.environ.get('SECRET_KEY', os.urandom(24))
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')
LZMX_WEBHOOK_URL = os.environ.get('LZMX_WEBHOOK_URL')
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'static/uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
# 添加1key110数据库配置
KEY110_DB = {
'host': os.environ.get('KEY110_DB_HOST'),
'user': os.environ.get('KEY110_DB_USER'),
'password': os.environ.get('KEY110_DB_PASSWORD'),
'database': os.environ.get('KEY110_DB_DATABASE')
}
+594
View File
@@ -0,0 +1,594 @@
2025-04-23 09:18:08 INFO: 应用启动
2025-04-23 09:18:19 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-23 09:18:34 INFO: 用户 韦矍森 访问统计页面
2025-04-23 09:18:35 INFO: 用户 韦矍森 访问历史记录页面
2025-04-23 09:18:36 INFO: 用户 韦矍森 访问统计页面
2025-04-23 09:18:38 INFO: 用户 韦矍森 访问历史记录页面
2025-04-23 09:18:42 INFO: 用户 韦矍森 访问历史记录页面
2025-04-23 09:18:44 INFO: 用户 韦矍森 访问统计页面
2025-04-23 09:18:45 INFO: 用户 韦矍森 访问历史记录页面
2025-04-23 09:18:46 INFO: 用户 韦矍森 访问统计页面
2025-04-23 10:56:36 INFO: 用户 韦矍森 访问历史记录页面
2025-04-23 10:56:49 INFO: 用户 韦矍森 访问统计页面
2025-04-23 11:05:57 INFO: 用户 韦矍森 开始同步离线设备
2025-04-23 11:05:57 INFO: 同步完成:创建了 77 个工单,恢复了 0 个设备
2025-04-23 11:05:59 INFO: 用户请求验证码
2025-04-23 11:05:59 INFO: 验证码获取成功
2025-04-23 11:06:04 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-04-23 11:06:04 INFO: 尝试登录中兴平台
2025-04-23 11:06:04 INFO: 登录成功,开始获取在线信息
2025-04-23 11:06:04 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-04-23 11:06:04 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 589 个
2025-04-23 11:06:05 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-04-23 11:06:05 INFO: 查询到 1294 个需要同步的设备
2025-04-23 11:06:06 INFO: 发现 472 个离线设备需要创建工单
2025-04-23 11:06:07 INFO: 成功创建 472 个工单
2025-04-23 11:06:42 INFO: 应用启动
2025-04-23 11:06:47 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-23 11:06:58 INFO: 用户 韦矍森 开始同步离线设备
2025-04-23 11:06:58 INFO: 同步完成:创建了 77 个工单,恢复了 0 个设备
2025-04-23 11:07:01 INFO: 用户请求验证码
2025-04-23 11:07:01 INFO: 验证码获取成功
2025-04-23 11:07:05 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-04-23 11:07:05 INFO: 尝试登录中兴平台
2025-04-23 11:07:06 INFO: 登录成功,开始获取在线信息
2025-04-23 11:07:06 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-04-23 11:07:06 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 589 个
2025-04-23 11:07:09 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-04-23 11:07:09 INFO: 查询到 1107 个需要同步的设备
2025-04-23 11:07:10 INFO: 发现 308 个离线设备需要创建工单
2025-04-23 11:07:10 INFO: 成功创建 308 个工单
2025-04-23 11:07:18 INFO: 用户 韦矍森 访问统计页面
2025-04-24 10:28:55 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-24 10:31:33 INFO: 用户 韦矍森 访问统计页面
2025-04-24 10:32:55 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-24 10:33:16 INFO: 用户 韦矍森 访问统计页面
2025-04-24 10:33:17 INFO: 用户 韦矍森 访问历史记录页面
2025-04-24 10:35:18 INFO: 用户 韦矍森 开始同步离线设备
2025-04-24 10:35:18 INFO: 同步完成:创建了 3 个工单,恢复了 3 个设备
2025-04-24 16:01:53 INFO: 用户 韦宗兴(15307786493) 登录成功
2025-04-24 17:25:05 INFO: 用户 韦宗兴 访问统计页面
2025-04-24 17:25:09 INFO: 用户 韦宗兴 访问统计页面
2025-04-24 17:25:46 INFO: 用户 韦宗兴 访问统计页面
2025-04-24 17:29:40 INFO: 用户 韦宗兴 访问统计页面
2025-04-24 17:32:35 INFO: 用户 韦宗兴 开始创建手工故障单
2025-04-24 17:32:39 INFO: 用户 韦宗兴 开始创建手工故障单
2025-04-24 17:32:51 INFO: 用户 韦宗兴 创建手工故障单成功:M202504240001
2025-04-24 17:32:51 INFO: 用户 韦宗兴 访问历史记录页面
2025-04-24 17:32:54 INFO: 用户 韦宗兴 访问历史记录页面
2025-04-24 17:36:32 INFO: 用户 韦宗兴 开始创建手工故障单
2025-04-24 17:36:36 INFO: 用户 韦宗兴 创建手工故障单成功:M202504240002
2025-04-24 17:36:36 INFO: 用户 韦宗兴 访问历史记录页面
2025-04-24 17:36:51 INFO: 用户 韦宗兴 查看工单 M202504240001 的详情
2025-04-24 17:36:54 INFO: 用户 韦宗兴 查看工单 M202504240001 的详情
2025-04-25 10:00:56 INFO: 应用启动
2025-04-28 09:36:26 INFO: 应用启动
2025-04-29 15:10:25 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-29 15:11:42 INFO: 用户 韦矍森 访问历史记录页面
2025-04-29 15:12:16 INFO: 用户 韦矍森 访问历史记录页面
2025-04-29 15:12:17 INFO: 用户 韦矍森 访问统计页面
2025-04-29 15:13:14 INFO: 用户 覃建(18174938515) 登录成功
2025-04-29 15:14:07 INFO: 用户 韦矍森(19977899008) 登出
2025-04-29 15:14:12 INFO: 用户 韦矍森(19977899008) 登录成功
2025-04-29 15:14:19 INFO: 用户 覃建 访问统计页面
2025-04-29 15:14:42 INFO: 用户 韦矍森 访问历史记录页面
2025-04-29 15:20:42 INFO: 用户 覃建 开始创建手工故障单
2025-04-29 15:20:45 INFO: 用户 覃建 创建手工故障单成功:M202504290001
2025-04-29 15:20:45 INFO: 用户 覃建 访问历史记录页面
2025-05-03 01:30:02 INFO: 应用启动
2025-05-07 08:50:16 INFO: 应用启动
2025-05-08 08:49:44 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-08 08:54:37 INFO: 应用启动
2025-05-08 09:13:33 INFO: 应用启动
2025-05-08 09:13:47 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-08 09:13:49 INFO: 用户 韦矍森 访问历史记录页面
2025-05-08 09:19:29 INFO: 用户 韦矍森 访问历史记录页面
2025-05-08 09:19:31 INFO: 用户 韦矍森 访问统计页面
2025-05-08 09:20:48 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-08 09:30:46 INFO: 应用启动
2025-05-08 09:30:54 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-12 14:59:10 INFO: 用户 覃建(18174938515) 登录成功
2025-05-12 15:06:39 INFO: 用户 覃建 访问统计页面
2025-05-12 15:06:57 INFO: 用户 覃建 访问历史记录页面
2025-05-12 15:07:04 INFO: 用户 覃建 访问统计页面
2025-05-12 15:08:43 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-12 15:14:27 INFO: 用户 韦矍森 开始同步离线设备
2025-05-12 15:14:27 INFO: 同步完成:创建了 4 个工单,恢复了 1 个设备
2025-05-12 15:14:30 INFO: 用户请求验证码
2025-05-12 15:14:30 INFO: 验证码获取成功
2025-05-12 15:14:34 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-05-12 15:14:34 INFO: 尝试登录中兴平台
2025-05-12 15:14:34 INFO: 登录成功,开始获取在线信息
2025-05-12 15:14:34 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-05-12 15:14:35 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 585 个
2025-05-12 15:14:47 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-05-12 15:14:47 INFO: 查询到 1107 个需要同步的设备
2025-05-12 15:14:48 INFO: 发现 21 个离线设备需要创建工单
2025-05-12 15:14:48 INFO: 成功创建 21 个工单
2025-05-12 15:15:49 INFO: 用户 覃建 访问统计页面
2025-05-12 18:04:43 INFO: 应用启动
2025-05-12 18:06:38 WARNING: 登录失败:用户不存在 (手机号: 199778990008)
2025-05-12 18:06:41 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-12 18:06:43 INFO: 用户 韦矍森 访问历史记录页面
2025-05-12 18:06:57 INFO: 用户 韦矍森 访问历史记录页面
2025-05-12 18:06:58 INFO: 用户 韦矍森 访问统计页面
2025-05-12 18:07:25 INFO: 用户 韦矍森 访问历史记录页面
2025-05-13 08:11:30 INFO: 用户 覃建(18174938515) 登录成功
2025-05-13 08:11:36 INFO: 用户 覃建 访问历史记录页面
2025-05-13 11:13:52 INFO: 用户 韦宗兴(15307786493) 登录成功
2025-05-13 11:32:42 INFO: 用户 韦矍森 访问统计页面
2025-05-13 11:57:15 INFO: 应用启动
2025-05-13 11:57:24 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-13 13:05:12 INFO: 用户 韦宗兴(15307786493) 登录成功
2025-05-14 17:45:07 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:45:35 INFO: 用户 韦矍森 检查设备工单状态,设备数量:4
2025-05-14 17:45:35 INFO: 发现 0 个设备存在故障工单
2025-05-14 17:45:54 INFO: 用户 韦矍森 检查设备工单状态,设备数量:3
2025-05-14 17:45:54 INFO: 发现 0 个设备存在故障工单
2025-05-14 17:46:34 INFO: 用户 易靖欢(18174939957) 登录成功
2025-05-14 17:49:50 INFO: 用户 易靖欢(18174939957) 登出
2025-05-14 17:50:06 INFO: 用户 黄建(19977818669) 登录成功
2025-05-14 17:51:52 INFO: 用户 黄建 访问历史记录页面
2025-05-14 17:51:54 INFO: 用户 黄建 访问统计页面
2025-05-14 17:51:58 INFO: 用户 黄建(19977818669) 登出
2025-05-14 17:52:02 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-14 17:52:04 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:52:05 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:53:39 INFO: 用户 韦矍森 导出月度统计报表
2025-05-14 17:54:13 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:54:18 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:54:59 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:57:08 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:57:46 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:57:50 INFO: 用户 韦矍森 查看工单 202505140007 的详情
2025-05-14 17:57:53 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:57:54 INFO: 用户 韦矍森 查看工单 202505140005 的详情
2025-05-14 17:57:54 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:57:55 INFO: 用户 韦矍森 查看工单 202505140001 的详情
2025-05-14 17:57:57 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:57:58 INFO: 用户 韦矍森 查看工单 202505120022 的详情
2025-05-14 17:58:13 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:58:18 INFO: 用户 韦矍森 查看工单 M202504240002 的详情
2025-05-14 17:58:24 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:58:26 INFO: 用户 韦矍森 查看工单 M202504220001 的详情
2025-05-14 17:58:28 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:58:29 INFO: 用户 韦矍森 查看工单 M202504220001 的详情
2025-05-14 17:58:32 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:58:33 INFO: 用户 韦矍森 查看工单 202504230324 的详情
2025-05-14 17:58:49 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:58:53 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:59:08 INFO: 用户 韦矍森 访问历史记录页面
2025-05-14 17:59:09 INFO: 用户 韦矍森 访问统计页面
2025-05-14 17:59:19 INFO: 用户 韦矍森 访问统计页面
2025-05-14 18:04:25 INFO: 用户 韦矍森 检查设备工单状态,设备数量:1
2025-05-14 18:04:25 INFO: 发现 0 个设备存在故障工单
2025-05-14 18:10:19 INFO: 应用启动
2025-05-14 18:10:30 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-15 14:44:38 INFO: 用户 韦矍森 访问统计页面
2025-05-19 21:47:13 INFO: 用户 韦矍森 访问统计页面
2025-05-19 21:47:33 INFO: 用户 韦矍森 访问历史记录页面
2025-05-19 21:47:39 INFO: 用户 韦矍森 访问统计页面
2025-05-19 21:49:07 INFO: 用户 韦矍森 导出月度统计报表
2025-05-19 23:58:35 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-19 23:58:40 INFO: 用户 韦矍森 访问历史记录页面
2025-05-19 23:58:42 INFO: 用户 韦矍森 访问统计页面
2025-05-19 23:58:51 INFO: 用户 韦矍森 导出月度统计报表
2025-05-20 00:07:56 INFO: 应用启动
2025-05-20 00:08:02 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-20 00:08:04 INFO: 用户 韦矍森 访问历史记录页面
2025-05-20 00:08:06 INFO: 用户 韦矍森 访问统计页面
2025-05-20 00:08:06 ERROR: Exception on /statistics [GET]
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/1panel/docker/compose/4an/app/views/statistics.py", line 179, in statistics
return render_template('statistics.html',
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/templating.py", line 150, in render_template
return _render(app, template, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/templating.py", line 131, in _render
rv = template.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/jinja2/environment.py", line 1304, in render
self.environment.handle_exception()
File "/mnt/nfs/.venv/lib/python3.12/site-packages/jinja2/environment.py", line 939, in handle_exception
raise rewrite_traceback_stack(source=source)
File "/opt/1panel/docker/compose/4an/app/templates/statistics.html", line 1, in top-level template code
{% extends "base.html" %}
File "/opt/1panel/docker/compose/4an/app/templates/base.html", line 108, in top-level template code
{% block content %}{% endblock %}
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/1panel/docker/compose/4an/app/templates/statistics.html", line 74, in block 'content'
<a href="{{ url_for('export_monthly_stats') }}" class="export_button">📥︎ 导出本月明细</a>
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1121, in url_for
return self.handle_url_build_error(error, endpoint, values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1110, in url_for
rv = url_adapter.build( # type: ignore[union-attr]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/werkzeug/routing/map.py", line 924, in build
raise BuildError(endpoint, values, method, self)
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'export_monthly_stats'. Did you forget to specify values ['month', 'year']?
2025-05-20 00:11:14 INFO: 应用启动
2025-05-20 00:11:17 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-20 00:11:19 INFO: 用户 韦矍森 访问统计页面
2025-05-20 00:11:24 INFO: 用户 韦矍森 导出 2025年1月 统计报表
2025-05-20 00:11:24 INFO: 用户 韦矍森 访问统计页面
2025-05-20 00:11:31 INFO: 用户 韦矍森 导出 2025年4月 统计报表
2025-05-20 00:13:12 INFO: 用户 韦矍森 访问统计页面
2025-05-20 00:13:13 INFO: 用户 韦矍森 访问历史记录页面
2025-05-20 15:21:10 INFO: 用户 韦矍森 开始同步离线设备
2025-05-20 15:21:11 INFO: 同步完成:创建了 71 个工单,恢复了 0 个设备
2025-05-20 15:25:19 INFO: 用户 唐运莹(18176136697) 登录成功
2025-05-20 17:17:14 INFO: 用户 韦矍森 访问历史记录页面
2025-05-22 08:40:07 INFO: 用户 韦矍森 访问历史记录页面
2025-05-22 08:40:12 INFO: 用户 韦矍森 访问统计页面
2025-05-27 08:48:59 INFO: 用户 韦矍森(19977899008) 登录成功
2025-05-27 08:53:47 INFO: 用户 韦矍森 开始同步离线设备
2025-05-27 08:53:47 INFO: 同步完成:创建了 6 个工单,恢复了 3 个设备
2025-05-27 08:53:59 INFO: 用户 韦矍森 访问统计页面
2025-05-27 08:54:00 INFO: 用户 韦矍森 访问历史记录页面
2025-05-27 14:49:09 INFO: 用户 韦矍森 访问历史记录页面
2025-05-28 09:48:26 INFO: 用户 韦矍森 访问历史记录页面
2025-05-29 08:39:59 INFO: 用户 韦矍森 访问历史记录页面
2025-06-03 01:30:03 INFO: 应用启动
2025-06-15 15:48:55 INFO: 应用启动
2025-06-17 09:43:15 INFO: 用户 韦矍森(19977899008) 登录成功
2025-06-17 09:43:30 INFO: 用户 韦矍森 访问历史记录页面
2025-06-17 09:43:31 INFO: 用户 韦矍森 访问统计页面
2025-06-17 09:43:52 INFO: 用户 韦矍森 开始同步离线设备
2025-06-17 09:43:53 INFO: 同步完成:创建了 11 个工单,恢复了 4 个设备
2025-06-17 09:43:55 INFO: 用户请求验证码
2025-06-17 09:43:55 INFO: 验证码获取成功
2025-06-17 09:44:02 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-06-17 09:44:02 INFO: 尝试登录中兴平台
2025-06-17 09:44:02 INFO: 登录成功,开始获取在线信息
2025-06-17 09:44:03 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-06-17 09:44:03 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 605 个
2025-06-17 09:44:07 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-06-17 09:44:07 INFO: 查询到 1107 个需要同步的设备
2025-06-17 09:44:07 INFO: 发现 25 个离线设备需要创建工单
2025-06-17 09:44:08 INFO: 成功创建 25 个工单
2025-06-17 17:52:13 INFO: 应用启动
2025-06-17 17:54:03 INFO: 应用启动
2025-06-17 17:54:13 INFO: 应用启动
2025-06-17 17:55:25 INFO: 应用启动
2025-06-17 17:55:35 INFO: 应用启动
2025-06-17 17:55:52 INFO: 应用启动
2025-06-17 17:56:38 INFO: 应用启动
2025-06-17 17:58:50 INFO: 应用启动
2025-06-17 18:00:20 ERROR: Exception on /statistics [GET]
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/utils.py", line 285, in decorated_view
return current_app.login_manager.unauthorized()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/login_manager.py", line 187, in unauthorized
flash(self.login_message, category=self.login_message_category)
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/helpers.py", line 335, in flash
session["_flashes"] = flashes
~~~~~~~^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/sessions.py", line 104, in _fail
raise RuntimeError(
RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret.
2025-06-17 18:01:38 INFO: 应用启动
2025-06-17 18:02:09 ERROR: Exception on /statistics [GET]
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/utils.py", line 285, in decorated_view
return current_app.login_manager.unauthorized()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/login_manager.py", line 187, in unauthorized
flash(self.login_message, category=self.login_message_category)
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/helpers.py", line 335, in flash
session["_flashes"] = flashes
~~~~~~~^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/sessions.py", line 104, in _fail
raise RuntimeError(
RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret.
2025-06-17 18:02:12 ERROR: Exception on /statistics [GET]
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/utils.py", line 285, in decorated_view
return current_app.login_manager.unauthorized()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/login_manager.py", line 187, in unauthorized
flash(self.login_message, category=self.login_message_category)
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/helpers.py", line 335, in flash
session["_flashes"] = flashes
~~~~~~~^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/sessions.py", line 104, in _fail
raise RuntimeError(
RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret.
2025-06-17 18:03:23 INFO: 应用启动
2025-06-17 18:05:02 ERROR: Exception on /statistics [GET]
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/utils.py", line 285, in decorated_view
return current_app.login_manager.unauthorized()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask_login/login_manager.py", line 187, in unauthorized
flash(self.login_message, category=self.login_message_category)
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/helpers.py", line 335, in flash
session["_flashes"] = flashes
~~~~~~~^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/sessions.py", line 104, in _fail
raise RuntimeError(
RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret.
2025-06-17 18:05:57 INFO: 应用启动
2025-06-17 18:06:01 INFO: 应用启动
2025-06-17 18:07:16 INFO: 应用启动
2025-06-17 18:08:32 INFO: 应用启动
2025-06-17 18:08:46 INFO: 用户 韦矍森(19977899008) 登录成功
2025-06-18 06:51:33 INFO: 应用启动
2025-06-18 06:52:12 INFO: 应用启动
2025-06-19 21:16:20 INFO: 应用启动
2025-06-21 06:04:07 INFO: 应用启动
2025-06-21 06:04:47 INFO: 应用启动
2025-06-23 12:12:28 INFO: 应用启动
2025-07-16 06:19:45 INFO: 应用启动
2025-07-25 17:02:48 INFO: 用户 韦矍森(19977899008) 登录成功
2025-07-25 17:02:52 INFO: 用户 韦矍森 开始同步离线设备
2025-07-25 17:02:52 INFO: 同步完成:创建了 33 个工单,恢复了 7 个设备
2025-07-25 17:02:55 INFO: 用户请求验证码
2025-07-25 17:02:55 INFO: 验证码获取成功
2025-07-25 17:03:00 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-25 17:03:00 INFO: 尝试登录中兴平台
2025-07-25 17:03:00 INFO: 登录成功,开始获取在线信息
2025-07-25 17:03:00 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-07-25 17:03:01 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 674 个
2025-07-25 17:03:04 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-07-25 17:03:04 INFO: 查询到 1107 个需要同步的设备
2025-07-25 17:03:05 INFO: 发现 82 个离线设备需要创建工单
2025-07-25 17:03:05 INFO: 成功创建 82 个工单
2025-07-25 17:03:36 INFO: 用户 韦矍森 开始同步离线设备
2025-07-25 17:03:36 INFO: 同步完成:创建了 107 个工单,恢复了 0 个设备
2025-07-25 17:04:04 INFO: 用户请求验证码
2025-07-25 17:04:05 INFO: 验证码获取成功
2025-07-25 17:04:10 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-25 17:04:10 INFO: 尝试登录中兴平台
2025-07-25 17:04:10 WARNING: 登录中兴平台失败:验证码错误
2025-07-25 17:04:13 INFO: 用户请求验证码
2025-07-25 17:04:13 INFO: 验证码获取成功
2025-07-25 17:04:17 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-25 17:04:17 INFO: 尝试登录中兴平台
2025-07-25 17:04:17 WARNING: 登录中兴平台失败:验证码错误
2025-07-25 17:04:18 INFO: 用户请求验证码
2025-07-25 17:04:18 INFO: 验证码获取成功
2025-07-25 17:04:22 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-25 17:04:22 INFO: 尝试登录中兴平台
2025-07-25 17:04:23 INFO: 登录成功,开始获取在线信息
2025-07-25 17:04:23 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-07-25 17:04:23 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 674 个
2025-07-25 17:04:24 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-07-25 17:04:25 INFO: 查询到 1107 个需要同步的设备
2025-07-25 17:04:25 INFO: 发现 382 个离线设备需要创建工单
2025-07-25 17:04:26 INFO: 成功创建 382 个工单
2025-07-25 17:05:07 INFO: 用户 韦矍森 开始同步离线设备
2025-07-25 17:05:07 INFO: 同步完成:创建了 107 个工单,恢复了 0 个设备
2025-07-25 17:05:15 INFO: 用户 韦矍森 访问历史记录页面
2025-07-25 17:05:19 INFO: 用户 韦矍森 访问统计页面
2025-07-25 17:08:06 INFO: 用户 韦矍森 访问统计页面
2025-07-25 17:08:09 INFO: 用户 韦矍森 访问历史记录页面
2025-07-25 17:13:13 INFO: 用户 韦矍森(19977899008) 登录成功
2025-07-25 17:13:14 INFO: 用户 韦矍森 访问历史记录页面
2025-07-25 18:13:01 INFO: 应用启动
2025-07-25 18:13:01 INFO: 错误处理器初始化完成
2025-07-25 18:13:03 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-25 18:13:12 INFO: 用户 韦矍森 访问历史记录页面
2025-07-25 18:13:13 INFO: 用户 韦矍森 访问统计页面
2025-07-25 18:13:14 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 08:27:08 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 08:29:41 INFO: 用户 韦矍森 开始同步离线设备
2025-07-28 08:29:41 INFO: 同步完成:创建了 3 个工单,恢复了 0 个设备
2025-07-28 08:29:43 INFO: 用户请求验证码
2025-07-28 08:29:43 INFO: 验证码获取成功
2025-07-28 08:29:52 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-28 08:29:52 INFO: 尝试登录中兴平台
2025-07-28 08:29:52 WARNING: 登录中兴平台失败:验证码错误
2025-07-28 08:29:53 INFO: 用户请求验证码
2025-07-28 08:29:53 INFO: 验证码获取成功
2025-07-28 08:29:58 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-07-28 08:29:58 INFO: 尝试登录中兴平台
2025-07-28 08:29:59 INFO: 登录成功,开始获取在线信息
2025-07-28 08:30:00 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-07-28 08:30:00 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 678 个
2025-07-28 08:30:01 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-07-28 08:30:01 INFO: 查询到 1107 个需要同步的设备
2025-07-28 08:30:02 INFO: 发现 9 个离线设备需要创建工单
2025-07-28 08:30:02 INFO: 成功创建 9 个工单
2025-07-28 08:30:04 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 08:30:38 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 08:51:47 INFO: 用户 韦矍森 访问统计页面
2025-07-28 08:51:49 INFO: 用户 韦矍森 访问历史记录页面
2025-07-28 08:52:02 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 08:52:54 INFO: 用户 韦矍森 访问历史记录页面
2025-07-28 08:52:55 INFO: 用户 韦矍森 访问统计页面
2025-07-28 08:52:58 INFO: 用户 韦矍森 访问仪表盘页面
2025-07-28 23:35:22 INFO: 应用启动
2025-07-28 23:35:22 INFO: 错误处理器初始化完成
2025-07-28 23:49:56 INFO: 应用启动
2025-07-28 23:49:56 INFO: 错误处理器初始化完成
2025-07-30 07:37:35 ERROR: 未处理异常: 405 Method Not Allowed: The method is not allowed for the requested URL.
Traceback (most recent call last):
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 891, in dispatch_request
self.raise_routing_exception(req)
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/app.py", line 500, in raise_routing_exception
raise request.routing_exception # type: ignore[misc]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/flask/ctx.py", line 362, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/mnt/nfs/.venv/lib/python3.12/site-packages/werkzeug/routing/map.py", line 624, in match
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
2025-08-05 06:56:05 INFO: 应用启动
2025-08-05 06:56:05 INFO: 错误处理器初始化完成
2025-08-05 11:37:35 WARNING: 登录失败:用户不存在 (手机号: 199778999008)
2025-08-05 11:37:39 INFO: 用户 韦矍森(19977899008) 登录成功
2025-08-05 11:37:39 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-05 11:37:53 INFO: 用户 韦矍森 开始同步离线设备
2025-08-05 11:37:53 INFO: 同步完成:创建了 105 个工单,恢复了 0 个设备
2025-08-05 11:37:56 INFO: 用户请求验证码
2025-08-05 11:37:56 INFO: 验证码获取成功
2025-08-05 11:37:59 INFO: 用户 韦矍森 开始同步摄像头第一步
2025-08-05 11:37:59 INFO: 尝试登录中兴平台
2025-08-05 11:37:59 INFO: 登录成功,开始获取在线信息
2025-08-05 11:38:00 INFO: 获取到 1436 个设备信息,开始更新数据库
2025-08-05 11:38:00 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 665 个
2025-08-05 11:38:01 INFO: 用户 韦矍森 开始同步摄像头第二步
2025-08-05 11:38:01 INFO: 查询到 1107 个需要同步的设备
2025-08-05 11:38:02 INFO: 发现 378 个离线设备需要创建工单
2025-08-05 11:38:02 INFO: 成功创建 378 个工单
2025-08-05 11:38:05 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-05 11:38:42 INFO: 用户 韦矍森 访问历史记录页面
2025-08-05 11:38:44 INFO: 用户 韦矍森 访问统计页面
2025-08-06 08:31:05 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-06 12:46:40 INFO: 应用启动
2025-08-06 12:46:40 INFO: 错误处理器初始化完成
2025-08-07 16:39:57 INFO: 用户 韦矍森(19977899008) 登录成功
2025-08-07 16:39:57 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 09:54:17 INFO: 用户 韦矍森(19977899008) 登录成功
2025-08-08 09:54:18 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 09:54:30 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 09:54:43 INFO: 用户 韦矍森 访问历史记录页面
2025-08-08 10:12:48 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:13:00 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:13:04 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:13:13 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:17 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:13:17 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:20 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:28 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:28 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:30 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:30 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:31 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:13:31 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:24 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:24 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:26 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:26 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:28 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:28 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:29 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:29 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:17:39 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:17:41 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:17:57 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:18:06 INFO: 用户 韦矍森 访问历史记录页面
2025-08-08 10:18:20 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:21:25 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:26:58 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:27:02 INFO: 用户 韦矍森 访问历史记录页面
2025-08-08 10:27:08 INFO: 用户 韦矍森 访问仪表盘页面
2025-08-08 10:29:37 INFO: 应用启动
2025-08-08 10:29:37 INFO: 错误处理器初始化完成
2025-08-08 10:30:45 INFO: 应用启动
2025-08-08 10:30:45 INFO: 错误处理器初始化完成
2025-08-08 10:33:52 INFO: 应用启动
2025-08-08 10:33:52 INFO: 错误处理器初始化完成
2025-08-08 10:36:13 ERROR: 数据库操作错误: (pymysql.err.OperationalError) (1066, "Not unique table/alias: 'device'")
[SQL: SELECT work_order.order_id AS work_order_order_id, work_order.device_id AS work_order_device_id, work_order.dispatch_time AS work_order_dispatch_time, work_order.status AS work_order_status, work_order.branch AS work_order_branch
FROM work_order INNER JOIN device ON device.device_id = work_order.device_id INNER JOIN device ON device.device_id = work_order.device_id
WHERE work_order.status = %(status_1)s AND device.device_type = %(device_type_1)s AND device.project_type = %(project_type_1)s
LIMIT %(param_1)s, %(param_2)s]
[parameters: {'status_1': '待处理', 'device_type_1': '摄像头', 'project_type_1': '校园安防"5+N"项目', 'param_1': 0, 'param_2': 50}]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-08-08 10:36:13 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:36:17 ERROR: 数据库操作错误: (pymysql.err.OperationalError) (1066, "Not unique table/alias: 'device'")
[SQL: SELECT work_order.order_id AS work_order_order_id, work_order.device_id AS work_order_device_id, work_order.dispatch_time AS work_order_dispatch_time, work_order.status AS work_order_status, work_order.branch AS work_order_branch
FROM work_order INNER JOIN device ON device.device_id = work_order.device_id INNER JOIN device ON device.device_id = work_order.device_id
WHERE work_order.status = %(status_1)s AND device.device_type = %(device_type_1)s AND device.project_type = %(project_type_1)s
LIMIT %(param_1)s, %(param_2)s]
[parameters: {'status_1': '待处理', 'device_type_1': '摄像头', 'project_type_1': '校园安防"6+N"项目', 'param_1': 0, 'param_2': 50}]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-08-08 10:36:17 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:36:21 ERROR: 数据库操作错误: (pymysql.err.OperationalError) (1066, "Not unique table/alias: 'device'")
[SQL: SELECT work_order.order_id AS work_order_order_id, work_order.device_id AS work_order_device_id, work_order.dispatch_time AS work_order_dispatch_time, work_order.status AS work_order_status, work_order.branch AS work_order_branch
FROM work_order INNER JOIN device ON device.device_id = work_order.device_id INNER JOIN device ON device.device_id = work_order.device_id
WHERE work_order.status = %(status_1)s AND device.device_type = %(device_type_1)s AND device.project_type = %(project_type_1)s
LIMIT %(param_1)s, %(param_2)s]
[parameters: {'status_1': '待处理', 'device_type_1': '摄像头', 'project_type_1': '校园安防"4+N"项目', 'param_1': 0, 'param_2': 50}]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-08-08 10:36:21 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:36:27 ERROR: 数据库操作错误: (pymysql.err.OperationalError) (1066, "Not unique table/alias: 'device'")
[SQL: SELECT work_order.order_id AS work_order_order_id, work_order.device_id AS work_order_device_id, work_order.dispatch_time AS work_order_dispatch_time, work_order.status AS work_order_status, work_order.branch AS work_order_branch
FROM work_order INNER JOIN device ON device.device_id = work_order.device_id INNER JOIN device ON device.device_id = work_order.device_id
WHERE work_order.status = %(status_1)s AND device.device_type = %(device_type_1)s AND device.project_type = %(project_type_1)s
LIMIT %(param_1)s, %(param_2)s]
[parameters: {'status_1': '待处理', 'device_type_1': '摄像头', 'project_type_1': '校园安防"4+N"项目', 'param_1': 0, 'param_2': 50}]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-08-08 10:36:27 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:36:33 ERROR: 数据库操作错误: (pymysql.err.OperationalError) (1066, "Not unique table/alias: 'device'")
[SQL: SELECT work_order.order_id AS work_order_order_id, work_order.device_id AS work_order_device_id, work_order.dispatch_time AS work_order_dispatch_time, work_order.status AS work_order_status, work_order.branch AS work_order_branch
FROM work_order INNER JOIN device ON device.device_id = work_order.device_id INNER JOIN device ON device.device_id = work_order.device_id
WHERE work_order.status = %(status_1)s AND device.device_type = %(device_type_1)s AND device.project_type = %(project_type_1)s
LIMIT %(param_1)s, %(param_2)s]
[parameters: {'status_1': '待处理', 'device_type_1': '一键报警', 'project_type_1': '校园安防"4+N"项目', 'param_1': 0, 'param_2': 50}]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-08-08 10:36:33 INFO: 用户 韦矍森 访问统计页面
2025-08-08 10:36:35 INFO: 用户 韦矍森 访问统计页面
View File
+90
View File
@@ -0,0 +1,90 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime, timezone
db = SQLAlchemy()
# 用户表
class User(UserMixin, db.Model):
__tablename__ = 'user'
phone = db.Column(db.String(20), primary_key=True, comment="手机号码")
name = db.Column(db.String(50), comment="姓名")
branch = db.Column(db.String(50), comment="所属支局")
role = db.Column(db.String(20), comment="角色")
def get_id(self):
return self.phone # 使用手机号码作为唯一标识符
# 设备表
class Device(db.Model):
__tablename__ = 'device'
device_id = db.Column(db.String(100), primary_key=True, comment="设备编号")
town = db.Column(db.String(50), comment="所属乡镇")
branch = db.Column(db.String(50), comment="所属支局")
unit_name = db.Column(db.String(100), comment="单位名称")
project_type = db.Column(db.String(50), comment="项目类型")
account = db.Column(db.String(20), comment="关联宽带账号")
device_type = db.Column(db.String(50), comment="设备类型")
puid = db.Column(db.String(100), comment="通道标识") # 添加 puid 字段
remark = db.Column(db.String(200), comment="备注")
in_use = db.Column(db.Boolean, default=True, comment="是否在用")
is_normal = db.Column(db.Boolean, default=True, comment="是否正常")
# 工单表
class WorkOrder(db.Model):
__tablename__ = 'work_order'
order_id = db.Column(db.String(20), primary_key=True, comment="工单号(日期+编号)")
device_id = db.Column(db.String(100), db.ForeignKey('device.device_id'), comment="设备编号") # 从20改为100
dispatch_time = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), comment="派单时间")
status = db.Column(db.String(20), default='待处理', comment="工单状态")
branch = db.Column(db.String(50), comment="所属支局")
# 与设备表的关系
device = db.relationship('Device', backref='work_orders')
# 处理工单信息表
class WorkOrderHandling(db.Model):
__tablename__ = 'work_order_handling'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
order_id = db.Column(db.String(20), db.ForeignKey('work_order.order_id'), comment="工单号")
device_id = db.Column(db.String(100), db.ForeignKey('device.device_id'), comment="设备编号") # 从20改为100
branch = db.Column(db.String(50), comment="所属支局")
restore_time = db.Column(db.DateTime, comment="恢复时间")
fault_type = db.Column(db.String(100), comment="故障类型")
contact_person = db.Column(db.String(50), comment="故障对接人")
contact_phone = db.Column(db.String(20), comment="联系电话")
handler = db.Column(db.String(50), comment="故障处理人")
handle_description = db.Column(db.String(500), comment="处理描述")
photo_paths = db.Column(db.String(500), comment="现场图片路径")
distance_from_branch = db.Column(db.Float, comment="距离支局公里数")
remark = db.Column(db.String(500), comment="备注")
# 与工单表的关系
work_order = db.relationship('WorkOrder', backref='handling_info')
# 手工故障单表
class ManualWorkOrder(db.Model):
__tablename__ = 'manual_work_order'
order_id = db.Column(db.String(20), primary_key=True, comment="工单号")
project_type = db.Column(db.String(50), comment="项目类型")
device_id = db.Column(db.String(100), comment="故障设备")
device_location = db.Column(db.String(200), comment="设备位置")
fault_time = db.Column(db.DateTime, comment="故障时间")
handler = db.Column(db.String(50), comment="故障处理人")
fault_type = db.Column(db.String(100), comment="故障类型")
handle_description = db.Column(db.String(500), comment="处理描述")
photo_paths = db.Column(db.String(500), comment="现场图片路径")
contact_person = db.Column(db.String(50), comment="故障对接人")
contact_phone = db.Column(db.String(20), comment="联系电话")
distance_from_branch = db.Column(db.Float, comment="距离支局公里数")
remark = db.Column(db.String(500), comment="备注")
create_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
branch = db.Column(db.String(50), comment="所属支局")
class Channel(db.Model):
__tablename__ = 'channels'
puid = db.Column(db.String(50), primary_key=True)
pu_name = db.Column(db.String(200))
status = db.Column(db.String(20))
update_time = db.Column(db.String(50))
def __repr__(self):
return f'<Channel {self.puid}>'
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+269
View File
@@ -0,0 +1,269 @@
import requests
import os
import time
from datetime import datetime
import hashlib
import uuid
from sqlalchemy import select, create_engine, Table, Column, String, MetaData, text
from sqlalchemy.dialects.mysql import insert
class LoginClient:
def __init__(self):
self.base_url = "https://116.10.244.81:28001"
self.session = requests.Session()
# 禁用SSL验证警告
requests.packages.urllib3.disable_warnings()
def get_captcha(self):
"""获取验证码图片"""
# 生成随机cid
cid = uuid.uuid4().hex.encode()
cid = hashlib.sha256(cid).hexdigest()
url = f"{self.base_url}/api/vss-auth-center/v1/captcha"
headers = {
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
"Referer": f"{self.base_url}/vss-em-iui/"
}
params = {"cid": cid}
response = self.session.get(url, headers=headers, params=params, verify=False)
# 缓存验证码到本地
captcha_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
'static', 'captcha.png')
with open(captcha_path, "wb") as f:
f.write(response.content)
return cid
def login(self, account, password, validcode, cid):
"""执行登录"""
url = f"{self.base_url}/api/vss-auth-center/v1/user/login"
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
"Referer": f"{self.base_url}/vss-em-iui/",
"language": "zh-CN"
}
params = {
"cutype": "12"
}
data = {
"account": account,
"password": password,
"validcode": validcode,
"cid": cid
}
response = self.session.post(
url,
headers=headers,
params=params,
json=data,
verify=False
)
return response.json()
def get_online_info(self, token, page=1, page_size=1500):
"""获取在线信息"""
url = f"{self.base_url}/api/vss-user-business/v1/pus/queryPage/4109"
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Authoration": token,
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/json",
"Referer": f"{self.base_url}/vss-em-iui/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
"language": "zh-CN"
}
# 生成防伪标识
forgery_defense = uuid.uuid4().hex
params = {
"page_size": page_size,
"page": page,
"pu_name": "",
"puid": "",
"protocol": "",
"nodeid": "",
"popunitid": "",
"status": "",
"forgerydefense": forgery_defense,
"csrftoken": forgery_defense
}
response = self.session.get(
url,
headers=headers,
params=params,
verify=False
)
return response.json()
def get_existing_channels(engine, channels_table):
"""获取数据库中现有的通道状态"""
with engine.connect() as conn:
query = select(channels_table.c.puid, channels_table.c.status)
result = conn.execute(query)
return {row.puid: row.status for row in result}
def update_device_status(engine):
"""更新设备状态"""
with engine.connect() as conn:
# 获取channels表和device表的数据
channels_query = """
SELECT puid, status, pu_name
FROM channels
"""
channels_result = conn.execute(text(channels_query))
channels_data = {row.puid: {'status': row.status, 'pu_name': row.pu_name} for row in channels_result}
# 获取device表中的puid
device_query = """
SELECT puid
FROM device
"""
device_result = conn.execute(text(device_query))
# 更新设备状态
updates = []
for row in device_result:
puid = row.puid
if puid in channels_data:
# 更新在线状态
is_normal = 1 if channels_data[puid]['status'] == 'online' else 0
# 检查使用状态
pu_name = channels_data[puid]['pu_name']
in_use = 0 if any(keyword in pu_name for keyword in ['无学生', '丢失']) else 1
# 构建更新语句
update_query = """
UPDATE device
SET is_normal = :is_normal, in_use = :in_use
WHERE puid = :puid
"""
updates.append({
'puid': puid,
'is_normal': is_normal,
'in_use': in_use
})
if updates:
update_stmt = text("""
UPDATE device
SET is_normal = :is_normal, in_use = :in_use
WHERE puid = :puid
""")
conn.execute(update_stmt, updates)
conn.commit()
print(f"成功更新 {len(updates)} 个设备状态")
def main():
client = LoginClient()
while True:
cid = client.get_captcha()
print("验证码已保存为captcha.png")
validcode = input("请查看captcha.png并输入验证码: ")
password = "Root1234#"
hashed_password = hashlib.sha256(password.encode()).hexdigest()
result = client.login(
account="DHDX001",
password=hashed_password,
validcode=validcode,
cid=cid
)
if result.get("result") == 0:
token = result.get("token")
online_info = client.get_online_info(token)
if online_info.get("result") == 0:
rows = online_info.get("rows", [])
filtered_rows = [
row for row in rows
if "大化4+N摄像头" not in row.get("pu_name", "")
and "大化幼儿园监控" not in row.get("pu_name", "")
]
engine = create_engine('mysql+pymysql://4an:GtbJp6Ai5azntnTB@10.10.10.253/4an')
metadata = MetaData()
channels = Table(
'channels', metadata,
Column('puid', String(50), primary_key=True),
Column('pu_name', String(200)),
Column('status', String(20)),
Column('update_time', String(50))
)
metadata.create_all(engine)
existing_channels = get_existing_channels(engine, channels)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
update_data = []
for row in filtered_rows:
puid = row.get('puid')
current_status = row.get('status')
if puid not in existing_channels or existing_channels[puid] != current_status:
update_data.append({
'puid': puid,
'pu_name': row.get('pu_name'),
'status': current_status,
'update_time': current_time
})
# 执行更新
if update_data:
with engine.connect() as conn:
stmt = insert(channels).on_duplicate_key_update(
pu_name=channels.c.pu_name,
status=channels.c.status,
update_time=channels.c.update_time
)
conn.execute(stmt, update_data)
conn.commit()
print(f"\n成功更新 {len(update_data)} 条记录")
else:
print("\n没有需要更新的记录")
# 更新设备状态
print("\n开始更新设备状态...")
update_device_status(engine)
return
else:
print("获取通道信息失败!")
print(f"错误信息: {online_info.get('err_msg', '未知错误')}")
return # 获取信息失败也退出程序
else:
print("登录失败!")
print(result)
if result.get("result") == 12: # 验证码错误
retry = input("验证码错误,是否重新获取验证码?(y/n): ")
if retry.lower() != 'y':
return
else:
return # 其他错误直接退出
if __name__ == "__main__":
main()
+110
View File
@@ -0,0 +1,110 @@
from datetime import datetime, timedelta
from collections import defaultdict
import sys
import os
# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scripts.models import db, Device, WorkOrder, WorkOrderHandling, ManualWorkOrder
from utils.lzmx import send_weekly_stats_message
from flask import Flask
# 创建一个简单的 Flask 应用
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://4an:GtbJp6Ai5azntnTB@10.10.10.253/4an'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
LZMX_WEBHOOK_URL = "http://imtwo.zdxlz.com/im-external/v1/webhook/send?key=ef1dc605827f425e96491ce5725b5274"
# 初始化数据库
db.init_app(app)
def get_reward_by_distance(project_type, distance):
if project_type == '校园安防"4+N"项目':
if distance <= 5: return 20
elif distance <= 30: return 40
elif distance <= 50: return 50
else: return 60
elif project_type == '综治视联网':
if distance <= 10: return 30
elif distance <= 30: return 40
elif distance <= 50: return 50
else: return 60
elif project_type == '教育城域网':
if distance <= 5: return 20
elif distance <= 30: return 30
elif distance <= 50: return 40
else: return 50
elif project_type == '其他':
return 30
return 0
def weekly_stats():
with app.app_context():
# 计算本周的开始和结束时间
now = datetime.now()
week_start = now - timedelta(days=now.weekday())
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
week_end = now
# 统计工单数据
total_orders = WorkOrder.query.filter(
WorkOrder.dispatch_time >= week_start,
WorkOrder.dispatch_time < week_end
).count()
completed_orders = WorkOrder.query.filter(
WorkOrder.dispatch_time >= week_start,
WorkOrder.dispatch_time < week_end,
WorkOrder.status == '已处理'
).count()
manual_orders = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= week_start,
ManualWorkOrder.create_time < week_end
).count()
# 获取处理人统计
handler_stats = defaultdict(float)
# 统计普通工单奖励
handlings = WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= week_start,
WorkOrderHandling.restore_time < week_end
).all()
for handling in handlings:
device = db.session.get(Device, handling.device_id) # 更新为新的方法
if device:
distance = float(handling.distance_from_branch)
reward = get_reward_by_distance(device.project_type, distance)
handler_stats[handling.handler] += reward
# 统计手工故障单奖励
manual_orders_list = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= week_start,
ManualWorkOrder.create_time < week_end
).all()
for order in manual_orders_list:
distance = float(order.distance_from_branch)
reward = get_reward_by_distance(order.project_type, distance)
handler_stats[order.handler] += reward
# 获取前三名
top_handlers = sorted(handler_stats.items(), key=lambda x: x[1], reverse=True)[:3]
# 准备统计数据
stats = {
'total_orders': total_orders,
'completed_orders': completed_orders,
'pending_orders': total_orders - completed_orders,
'manual_orders': manual_orders,
'top_handlers': top_handlers
}
# 发送统计消息
send_weekly_stats_message(LZMX_WEBHOOK_URL, stats)
if __name__ == '__main__':
weekly_stats()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+121
View File
@@ -0,0 +1,121 @@
.quick-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin: 20px 0;
list-style: none;
padding: 0;
}
.quick-link-item {
background: white;
border-radius: 12px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
overflow: hidden;
}
.quick-link-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.quick-link {
display: block;
padding: 16px 20px;
color: #2c3e50;
text-decoration: none;
position: relative;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
.quick-link:hover {
color: #1a73e8;
}
.quick-link::after {
content: '→';
position: absolute;
right: 20px;
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s ease;
}
.quick-link:hover::after {
opacity: 1;
transform: translateX(0);
}
.about-container {
max-width: 800px;
margin: 30px auto;
padding: 40px;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.about-header {
text-align: center;
margin-bottom: 40px;
}
.about-header h1 {
color: #2c3e50;
font-size: 2.2em;
margin-bottom: 15px;
font-weight: 600;
}
.about-content {
line-height: 1.8;
}
.about-section {
margin-bottom: 35px;
}
.about-section h2 {
color: #34495e;
font-size: 1.5em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.feature-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-item {
background: #f8f9fa;
padding: 20px;
border-radius: 12px;
transition: transform 0.2s;
}
.feature-item:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
}
.dev-info {
background: #f8f9fa;
padding: 25px;
border-radius: 12px;
margin-top: 30px;
}
.dev-info p {
margin: 10px 0;
}
.footer {
margin-top: 40px;
text-align: center;
color: #7f8c8d;
font-size: 0.9em;
}
+7
View File
File diff suppressed because one or more lines are too long
+315
View File
@@ -0,0 +1,315 @@
/* 仪表盘容器 */
.dashboard_container {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
/* 仪表盘标题 */
.dashboard_title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
}
/* 仪表盘区块 */
.dashboard_section {
margin-bottom: 30px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
/* 区块标题 */
.section_title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
}
/* 图表容器 */
.chart_container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
/* 图表包装器 */
.chart_wrapper {
flex: 1;
min-width: 300px;
height: 300px;
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
/* 卡片网格布局 */
.card_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
/* 仪表盘卡片 */
.dashboard_card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: transform 0.2s, box-shadow 0.2s;
}
.dashboard_card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
/* 卡片头部 */
.card_header {
padding: 15px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
/* 卡片标题 */
.card_title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
/* 卡片内容 */
.card_content {
padding: 15px;
}
/* 进度条容器 */
.progress_container {
height: 24px;
background: #e9ecef;
border-radius: 12px;
margin-bottom: 15px;
overflow: hidden;
}
/* 进度条 */
.progress_bar {
height: 100%;
background: linear-gradient(90deg, #0061f2 0%, #00a3ff 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: width 0.5s ease;
min-width: 30px;
}
/* 进度条文本 */
.progress_text {
color: white;
font-weight: 600;
font-size: 14px;
}
/* 统计详情 */
.stats_details {
display: flex;
justify-content: space-between;
}
/* 统计项 */
.stats_item {
display: flex;
flex-direction: column;
align-items: center;
}
/* 统计标签 */
.stats_label {
font-size: 12px;
color: #6c757d;
margin-bottom: 5px;
}
/* 统计值 */
.stats_value {
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 表格容器 */
.table_container {
overflow-x: auto;
margin-top: 10px;
}
/* 仪表盘表格 */
.dashboard_table {
width: 100%;
border-collapse: collapse;
}
.dashboard_table th,
.dashboard_table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.dashboard_table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.dashboard_table tr:hover {
background: #f8f9fa;
}
/* 支局单元格 */
.branch_cell {
font-weight: 600;
background: #f8f9fa;
}
/* 状态标签 */
.status_badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status_badge.good {
background: #d4edda;
color: #155724;
}
.status_badge.normal {
background: #fff3cd;
color: #856404;
}
.status_badge.bad {
background: #f8d7da;
color: #721c24;
}
/* 手风琴容器 */
.accordion_container {
margin-top: 10px;
}
/* 手风琴项 */
.accordion_item {
margin-bottom: 10px;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
}
/* 手风琴头部 */
.accordion_header {
padding: 15px;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: background 0.2s;
}
.accordion_header:hover {
background: #e9ecef;
}
/* 手风琴标题 */
.accordion_title {
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 手风琴摘要 */
.accordion_summary {
display: flex;
align-items: center;
gap: 15px;
}
/* 手风琴图标 */
.accordion_icon {
font-size: 12px;
color: #6c757d;
transition: transform 0.2s;
}
/* 手风琴内容 */
.accordion_content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
/* 详情表格 */
.detail_table {
width: 100%;
border-collapse: collapse;
}
.detail_table th,
.detail_table td {
padding: 10px 15px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.detail_table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.detail_table tr:hover {
background: #f8f9fa;
}
/* 响应式调整 */
@media (max-width: 768px) {
.card_grid {
grid-template-columns: 1fr;
}
.dashboard_table {
min-width: 600px;
}
.accordion_summary {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.chart_wrapper {
min-width: 100%;
height: 250px;
}
.chart_container {
flex-direction: column;
}
}
+475
View File
@@ -0,0 +1,475 @@
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
width: 300px;
text-align: center;
}
.modal-buttons {
margin-top: 15px;
}
.modal-buttons button {
margin: 0 10px;
padding: 5px 15px;
}
/* 备注编辑样式 */
.remark-display {
cursor: pointer;
color: #666;
transition: color 0.2s;
min-height: 20px;
display: inline-block;
min-width: 80px;
}
.remark-display:hover {
color: #007bff;
}
.remark-input {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 120px;
transition: border-color 0.2s;
}
.remark-input:focus {
outline: none;
border-color: #007bff;
}
/* 模态框基础样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
position: relative;
background-color: #fff;
margin: 15vh auto;
padding: 25px;
width: 90%;
max-width: 500px;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
transform: translateY(-30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-content h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 1.5em;
font-weight: 600;
}
.modal-content p {
margin: 15px 0;
color: #666;
line-height: 1.5;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 25px;
}
.modal-buttons button {
padding: 8px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.modal-buttons button:first-child {
background-color: #4CAF50;
color: white;
}
.modal-buttons button:last-child {
background-color: #f5f5f5;
color: #333;
}
.modal-buttons button:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 验证码模态框特殊样式 */
#captchaModal .modal-content {
text-align: center;
}
#captchaImage {
margin: 15px 0;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
#captchaInput {
width: 200px;
padding: 8px 12px;
margin: 15px 0;
border: 2px solid #eee;
border-radius: 6px;
font-size: 16px;
text-align: center;
transition: border-color 0.2s ease;
}
#captchaInput:focus {
outline: none;
border-color: #4CAF50;
}
/* 结果显示样式 */
#updatedCount {
font-weight: bold;
color: #4CAF50;
}
#resultMessage {
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
text-align: center;
}
.dispatch_filter_group {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: nowrap;
gap: 10px;
}
.dispatch_search_form {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
max-width: 1000px;
width: 100%;
}
.dispatch_action_group {
display: flex;
gap: 10px;
}
.dispatch_action_bar {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
width: 100%;
}
.dispatch_btn {
min-width: 100px;
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.dispatch_btn_primary {
background-color: #007bff;
color: white;
}
.dispatch_btn_primary:hover {
background-color: #5677fc;
}
.dispatch_btn_secondary_qx {
background-color: #607d8b;
color: white;
}
.dispatch_btn_secondary_qx:hover {
background-color: #b0bec5;
}
.dispatch_btn_secondary_fx {
background-color: #9e9e9e;
color: white;
}
.dispatch_btn_secondary_fx:hover {
background-color: #e0e0e0;
}
.dispatch_btn_secondary_bj {
background-color: #ff5722;
color: white;
}
.dispatch_btn_secondary_bj:hover {
background-color: #ff8a65;
}
.dispatch_btn_secondary_sxt {
background-color: #259b24;
color: white;
}
.dispatch_btn_secondary_sxt:hover {
background-color: #72d572;
}
.dispatch_filter {
position: sticky;
top: 0;
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 10px;
z-index: 2;
display: flex;
justify-content: center;
}
.dispatch_search {
flex: 1;
min-width: 150px;
max-width: 250px;
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.dispatch_btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.dispatch_btn_secondary {
background: #6c757d;
color: white;
margin-left: 10px;
}
.dispatch_btn_secondary:hover {
background: #545b62;
}
.dispatch_table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
z-index: 1;
}
.dispatch_table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.dispatch_table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
color: #495057;
}
.dispatch_table tr:hover {
background-color: #f5f5f5;
}
.dispatch_checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.dispatch_pagination {
margin-top: 10px;
text-align: center;
clear: both;
position: relative;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
/* 移动端适配 */
@media (max-width: 991.98px) {
.dispatch_search_form {
flex-direction: column;
gap: 12px;
width: 100%;
}
.dispatch_action_group {
width: 100%;
}
.dispatch_search_form .dispatch_btn,
.dispatch_action_group .dispatch_btn {
flex: 1;
}
.dispatch_search {
width: 100%;
max-width: 100%;
margin: 0;
}
.dispatch_filter {
position: static; /* 移除固定定位 */
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
}
.dispatch_filter form {
display: flex;
flex-direction: column;
gap: 10px;
}
.dispatch_filter_group {
flex-direction: column;
gap: 15px;
}
.dispatch_search {
width: 100%;
margin: 0;
}
.dispatch_action_bar {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
width: 100%;
}
.dispatch_btn {
min-width: unset;
width: 100%;
padding: 12px 15px;
font-size: 14px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.dispatch_btn_primary {
grid-column: 1 / -1;
}
.dispatch_table {
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.dispatch_table th,
.dispatch_table td {
min-width: 120px;
padding: 8px;
}
.dispatch_table th:first-child,
.dispatch_table td:first-child {
position: sticky;
left: 0;
background: white;
z-index: 1;
min-width: 60px;
}
.dispatch_checkbox {
width: 20px;
height: 20px;
}
.dispatch_pagination {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10px;
flex-wrap: nowrap;
margin-top: 15px;
}
.dispatch_page_link {
white-space: nowrap;
padding: 8px 15px;
margin: 0;
}
.dispatch_page_info {
width: 100%;
text-align: center;
margin: 10px 0;
}
}
+400
View File
@@ -0,0 +1,400 @@
/* 筛选区域样式 */
.receive_filter {
margin: 20px 0;
padding: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.filter_form {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.filter_select {
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
background: white;
min-width: 150px;
outline: none;
transition: border-color 0.3s;
}
.filter_select:focus {
border-color: #4f46e5;
}
/* 统计卡片样式 */
.receive_stats {
margin: 20px 0;
padding: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.stats_item {
display: flex;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: linear-gradient(135deg, #6366f1, #4f46e5);
border-radius: 8px;
color: white;
}
.stats_label {
font-size: 1.1em;
opacity: 0.9;
}
.stats_value {
font-size: 1.8em;
font-weight: bold;
margin-left: auto;
}
.stats_branches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stats_branches_row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 15px 0;
}
.branch_stat {
background: #f8fafc;
padding: 8px 15px;
border-radius: 6px;
border: 2px solid #e2e8f0;
transition: all 0.2s;
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
gap: 8px;
}
.branch_stat:hover {
background: #f1f5f9;
transform: translateY(-1px);
}
.branch_stat.active {
background: #7eb6ff3b;
color: white;
border-color: #2563eb;
}
.branch_name {
font-size: 0.9em;
}
.branch_count {
font-weight: bold;
color: currentColor;
}
.branch_stat:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.branch_name {
color: #64748b;
font-size: 0.9em;
}
.branch_count {
display: block;
color: #1e293b;
font-size: 1.2em;
font-weight: bold;
margin-top: 5px;
}
.receive_actions {
display: flex;
gap: 12px;
margin-top: 15px;
}
.receive_btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.receive_btn_warning {
background-color: #ff9800;
color: white;
}
.receive_btn_warning:hover {
background-color: #ffc772;
color: white;
}
.receive_btn_secondary {
background-color: #008cff;
color: white;
text-decoration: none;
text-align: center;
}
.receive_btn_secondary:hover {
background-color: #43abff;
text-decoration: none;
color: white;
}
.receive_btn_primary {
background-color: #4CAF50;
color: white;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background-color: #fff;
margin: 15vh auto;
padding: 25px;
width: 90%;
max-width: 400px;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease-out;
}
.modal-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin: 20px 0;
}
.modal-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background-position: center;
background-repeat: no-repeat;
background-size: 24px;
}
.modal-icon.warning {
background-color: #fff3e0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ff5722' d='M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2'/%3E%3C/svg%3E");
}
.modal-icon.info {
background-color: #e8f5e9;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%234CAF50' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
@keyframes modalSlideIn {
from {
transform: translateY(-30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* 分页样式 */
.receive_pagination {
margin: 25px 0;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
.page_link {
padding: 8px 16px;
background: #f1f5f9;
border-radius: 6px;
text-decoration: none;
color: #475569;
transition: all 0.3s;
}
.page_link:hover {
background: #e2e8f0;
color: #1e293b;
}
.per_page_select {
padding: 8px;
border-radius: 6px;
border: 1px solid #e2e8f0;
outline: none;
}
.receive_card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
border: 1px solid #e9ecef;
}
.receive_card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.receive_card_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e9ecef;
}
.receive_card_title {
font-weight: 600;
color: #2c3e50;
}
.receive_card_duration {
color: #6c757d;
font-size: 0.9em;
}
.receive_card_content {
display: grid;
gap: 8px;
}
.receive_card_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.receive_card_label {
color: #6c757d;
font-size: 0.9em;
}
.receive_card_value {
color: #2c3e50;
font-weight: 500;
}
.receive_card_value[data-device-type="摄像头"] {
background-color: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
}
.receive_card_value[data-device-type="紫色"] {
background-color: #f3e5f5;
color: #7b1fa2;
padding: 4px 8px;
border-radius: 4px;
}
.receive_card_value[data-device-type="绿色"] {
background-color: #e8f5e9;
color: #388e3c;
padding: 4px 8px;
border-radius: 4px;
}
.receive_card_value[data-device-type="一键报警"] {
background-color: #fff3e0;
color: #f57c00;
padding: 4px 8px;
border-radius: 4px;
}
.receive_card_value[data-device-type="灰色"] {
background-color: #f5f5f5;
color: #616161;
padding: 4px 8px;
border-radius: 4px;
}
/* 移动端适配 */
@media (max-width: 768px) {
.receive_actions {
flex-direction: column;
width: 100%;
}
.receive_btn {
width: 100%;
padding: 12px 16px;
}
.modal-content {
margin: 10vh auto;
padding: 20px;
}
.receive_card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 20px;
}
.receive_card_header {
margin-bottom: 16px;
}
.receive_card_title {
font-size: 1.1em;
}
.receive_card_content {
gap: 12px;
}
.receive_card_item {
padding: 6px 0;
}
}
+46
View File
@@ -0,0 +1,46 @@
.form_tips {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.form_tips ul {
margin: 10px 0 0 20px;
padding: 0;
}
.form_tips li {
margin-bottom: 5px;
color: #666;
}
.required {
color: #dc3545;
margin-left: 3px;
}
.detail_form input::placeholder,
.detail_form textarea::placeholder {
color: #999;
font-size: 13px;
}
.detail_form select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
color: #333;
}
.detail_form select option:first-child {
color: #999;
}
.detail_form input[type="tel"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
+268
View File
@@ -0,0 +1,268 @@
/* 容器样式 */
.statistics_container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.statistics_section {
margin-bottom: 30px;
}
/* 标题和按钮样式 */
.statistics_title {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
font-size: 1.5em;
color: #1e293b;
}
/* 统一按钮基础样式 */
.button_base {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 0.9em;
cursor: pointer;
text-decoration: none;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
height: 36px;
}
.export_button {
float: right;
padding: 6px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
}
.export_button:hover {
background-color: #45a049;
color: #fff;
}
.incentive_button {
float: right;
padding: 6px 12px;
background-color: #dc2626;
color: white;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
}
.incentive_button:hover {
background-color: #b91c1c;
color: #fff;
}
/* 卡片样式 */
.statistics_card {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.statistics_card h3 {
margin: 0 0 15px 0;
color: #334155;
font-size: 1.2em;
}
/* 网格布局 */
.statistics_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.statistics_item {
display: flex;
flex-direction: column;
gap: 8px;
}
.statistics_label {
color: #64748b;
font-size: 0.9em;
}
.statistics_value {
color: #1e293b;
font-size: 1.5em;
font-weight: bold;
}
/* 总计奖励样式 */
.total_reward {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e2e8f0;
}
/* 表格样式 */
.handler_stats_table {
overflow-x: auto;
}
.handler_stats_table table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.handler_stats_table th,
.handler_stats_table td {
padding: 12px;
text-align: center;
border-bottom: 1px solid #e2e8f0;
}
.handler_stats_table th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
}
.handler_stats_table tr:hover {
background-color: #f8fafc;
}
/* 统计页面样式 */
.statistics_container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.statistics_section {
margin-bottom: 30px;
}
.statistics_title {
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.statistics_card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.statistics_card h3 {
color: #666;
margin-bottom: 15px;
font-size: 1.1em;
}
.statistics_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.statistics_item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
}
.statistics_label {
color: #666;
font-size: 0.9em;
margin-bottom: 5px;
}
.statistics_value {
color: #2c3e50;
font-size: 1.5em;
font-weight: bold;
}
/* 响应式设计 */
@media (max-width: 768px) {
.statistics_grid {
grid-template-columns: 1fr;
}
.statistics_card {
padding: 15px;
}
.statistics_value {
font-size: 1.2em;
}
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 300px;
}
.month-picker-form {
display: flex;
flex-direction: column;
gap: 15px;
margin-top: 15px;
}
.month-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.cancel_button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #6c757d;
color: white;
cursor: pointer;
}
.cancel_button:hover {
background-color: #5a6268;
}
+978
View File
@@ -0,0 +1,978 @@
.per_page_select {
padding: 6px 10px;
width: auto;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
color: #495057;
font-size: 14px;
cursor: pointer;
}
.per_page_select:focus {
border-color: #007bff;
outline: none;
}
.dispatch_page_link {
display: inline-block;
padding: 8px 16px;
margin: 0 5px;
border-radius: 4px;
background: white;
color: #007bff;
text-decoration: none;
transition: all 0.3s ease;
}
.dispatch_page_link:hover {
background: #007bff;
color: white;
}
.dispatch_page_info {
display: inline-block;
padding: 8px 16px;
color: #6c757d;
}
/* 导航栏样式 */
.nav_container {
background: linear-gradient(135deg, #0061f2 0%, #00a3ff 100%);
padding: 0.5rem 1rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.nav_brand {
font-size: 1.5rem;
font-weight: 600;
color: white !important;
text-decoration: none;
transition: opacity 0.3s;
}
.nav_brand:hover {
opacity: 0.9;
}
.nav_toggler {
border: none !important;
padding: 0.5rem;
border-radius: 4px;
background-color: rgba(255,255,255,0.1) !important;
}
.nav_toggler_icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important;
}
.nav_menu {
margin-left: 1rem;
}
/* 导航栏样式 */
.nav_link {
color: rgba(255,255,255,0.9) !important;
padding: 0.5rem 1rem !important;
border-radius: 4px;
transition: all 0.3s;
position: relative;
font-weight: 500;
display: block; /* 确保按钮独占一行 */
text-decoration: none !important; /* 移除默认下划线 */
}
.nav_link:hover {
color: white !important;
background: rgba(255,255,255,0.1);
}
.nav_link.active {
background: rgba(255,255,255,0.2);
color: white !important;
}
/* 接单平台样式 */
.receive_container {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.receive_filter {
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.receive_select {
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
min-width: 130px;
}
.receive_table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.receive_table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.receive_table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
color: #495057;
}
.receive_table tr:hover {
background-color: #f5f5f5;
}
.receive_action {
color: #007bff;
text-decoration: none;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.3s;
}
.receive_action:hover {
background: #007bff;
color: white;
}
.card_container {
display: none;
}
/* 移动端卡片样式 */
@media (max-width: 991.98px) {
.receive_table {
display: none;
}
.card_container {
display: flex;
flex-direction: column;
gap: 15px;
}
.receive_card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
}
.receive_card:active {
transform: scale(0.98);
}
.receive_card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.receive_card_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.receive_card_title {
font-weight: 600;
color: #333;
}
.receive_card_content {
display: flex;
flex-direction: column;
gap: 8px;
}
.receive_card_item {
display: flex;
justify-content: space-between;
align-items: center;
}
.receive_card_label {
color: #666;
font-size: 0.9em;
}
.receive_card_value {
color: #333;
font-weight: 500;
}
.receive_card_duration {
position: absolute;
top: 15px;
right: 15px;
color: #666;
font-size: 0.85em;
}
}
/* 登录页面样式 */
.login_container {
min-height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login_box {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
.login_title {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.login_subtitle {
color: #666;
font-size: 16px;
margin-bottom: 30px;
}
.login_form {
text-align: left;
}
.login_input_group {
margin-bottom: 25px;
}
.login_label {
display: block;
color: #555;
margin-bottom: 8px;
font-size: 14px;
}
.login_input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: all 0.3s;
}
.login_input:focus {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.2);
outline: none;
}
.login_button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #0061f2 0%, #00a3ff 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.login_button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,97,242,0.2);
}
.login_button:active {
transform: translateY(0);
}
/* 移动端适配 */
@media (max-width: 991.98px) {
.login_box {
padding: 30px 20px;
}
}
@media screen and (max-width: 768px) {
.receive_select {
width: 140px;
}
.receive_filter form {
flex-wrap: nowrap;
}
}
/* 历史工单列表页面样式 */
.history_container {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.history_title {
color: #333;
font-size: 24px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}
.history_table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.history_table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.history_table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
color: #495057;
}
.history_table tr:hover {
background-color: #f5f5f5;
}
.history_link {
color: #007bff;
text-decoration: none;
transition: all 0.3s;
}
.history_link:hover {
color: #0056b3;
text-decoration: underline;
}
.history_pagination {
margin-top: 20px;
text-align: center;
}
.history_page_link {
display: inline-block;
padding: 8px 16px;
margin: 0 5px;
border-radius: 4px;
background: white;
color: #007bff;
text-decoration: none;
transition: all 0.3s ease;
}
.history_page_link:hover {
background: #007bff;
color: white;
}
.history_page_info {
display: inline-block;
padding: 8px 16px;
color: #6c757d;
}
/* 历史工单详情页面样式 */
.history_detail_container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.detail_photos {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.photo_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 15px;
}
.photo_item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.photo_item img {
width: 100%;
height: 200px;
object-fit: cover;
transition: transform 0.3s ease;
}
.photo_item img:hover {
transform: scale(1.05);
}
/* 移动端适配 */
@media (max-width: 991.98px) {
.history_container {
padding: 10px;
}
.history_table {
display: none;
}
.history_card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
}
.history_card:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.history_card_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.history_card_title {
font-weight: 600;
color: #333;
}
.history_card_content {
display: flex;
flex-direction: column;
gap: 8px;
}
.history_card_item {
display: flex;
justify-content: space-between;
align-items: center;
}
.history_card_label {
color: #666;
font-size: 0.9em;
}
.history_card_value {
color: #333;
font-weight: 500;
}
.photo_grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.photo_item img {
height: 150px;
}
.manual-card {
border-left: 4px solid #ff9800; /* 手工申报工单使用橙色边框 */
}
.system-card {
border-left: 4px solid #2196f3; /* 系统派单工单使用蓝色边框 */
}
}
/* 历史工单详情页样式更新 */
.detail_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid #eee;
}
.detail_title {
font-size: 22px;
color: #2c3e50;
margin: 0;
}
.detail_status {
padding: 4px 10px;
background: #28a745;
color: white;
border-radius: 12px;
font-size: 14px;
}
.detail_section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.section_title {
font-size: 18px;
color: #2c3e50;
margin-bottom: 20px;
font-weight: 500;
}
.detail_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.detail_item {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail_label {
color: #6c757d;
font-size: 14px;
}
.detail_value {
color: #2c3e50;
font-weight: 500;
}
.detail_value.highlight {
color: #007bff;
}
.detail_value.badge {
display: inline-block;
padding: 6px 12px;
background: #e9ecef;
border-radius: 6px;
font-size: 15px;
}
.detail_description {
background: white;
padding: 20px;
border-radius: 8px;
margin-top: 10px;
}
.detail_remark {
margin-top: 20px;
padding: 15px;
background: #fff3cd;
border-radius: 8px;
}
.detail_remark h5 {
color: #856404;
margin-bottom: 10px;
font-size: 16px;
}
.photo_grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 15px;
}
.photo_item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
}
.photo_item img {
width: 100%;
height: 200px;
object-fit: cover;
transition: transform 0.3s ease;
}
.photo_item:hover img {
transform: scale(1.05);
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
padding-top: 50px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
}
.modal_content {
margin: auto;
display: block;
max-width: 90%;
max-height: 90vh;
}
.modal_close {
position: absolute;
right: 35px;
top: 15px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
/* 移动端适配 */
@media (max-width: 768px) {
.detail_grid {
grid-template-columns: 1fr;
}
.detail_section {
padding: 15px;
}
.photo_grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.photo_item img {
height: 150px;
}
}
/* 历史工单搜索框样式 */
.history_search {
margin: 20px 0;
}
.history_search_form {
display: flex;
gap: 10px;
max-width: 600px;
}
.history_search_input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.history_search_input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.1);
}
.history_search_button {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.history_search_button:hover {
background: #0056b3;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.history_search_form {
flex-direction: column;
gap: 8px;
}
}
/* 手工故障单样式 */
.manual_container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.manual_title {
margin-bottom: 30px;
color: #333;
text-align: center;
}
.manual_form {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.manual_form .form-group {
margin-bottom: 20px;
}
.manual_form label {
display: block;
margin-bottom: 8px;
color: #555;
}
.manual_form input,
.manual_form select,
.manual_form textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.manual_form textarea {
height: 100px;
resize: vertical;
}
.manual_submit {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.manual_submit:hover {
background: #0056b3;
}
@media screen and (max-width: 768px) {
.manual_form {
padding: 20px;
}
}
.receive_detail_container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.detail_card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 工单信息卡片样式 */
.detail_card:first-child {
background: #f8f9fa;
border-left: 4px solid #007bff;
}
/* 处理信息卡片样式 */
.detail_card:last-child {
background: white;
border-left: 4px solid #28a745;
}
.detail_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px; /* 减小行距 */
}
.detail_item {
padding: 5px 10px; /* 减小内边距 */
}
.detail_label {
color: #666;
font-size: 0.9em;
margin-right: 8px;
display: inline-block;
width: 80px; /* 固定标签宽度,使值对齐 */
}
.detail_value {
color: #333;
font-weight: 500;
}
.detail_form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #666;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
height: 100px;
resize: vertical;
}
.detail_file_input {
padding: 8px;
border: 1px dashed #ddd;
border-radius: 4px;
width: 100%;
}
.detail_actions {
grid-column: 1 / -1;
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.detail_btn {
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.detail_btn_primary {
background: #007bff;
color: white;
}
.detail_btn_secondary {
background: #6c757d;
color: white;
}
.detail_btn:hover {
opacity: 0.9;
}
.login_error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
font-size: 14px;
text-align: center;
}
@media (max-width: 768px) {
.detail_form {
grid-template-columns: 1fr;
}
.detail_grid {
grid-template-columns: 1fr;
}
}
.detail_btn_danger {
background: #dc3545;
color: white;
}
.detail_btn_danger:hover {
background: #c82333;
}
.total_reward {
background: #e3f2fd;
}
.total_reward .statistics_value {
color: #1976d2;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+7
View File
File diff suppressed because one or more lines are too long
+271
View File
@@ -0,0 +1,271 @@
function handleSelectAll() {
const checkboxes = document.querySelectorAll('input[name="device_ids"]');
const selectAllButton = document.getElementById('selectAllBtn');
const hasSelected = Array.from(checkboxes).some(checkbox => checkbox.checked);
const newState = !hasSelected;
checkboxes.forEach(checkbox => {
checkbox.checked = newState;
});
selectAllButton.textContent = newState ? '清空' : '全选';
}
function handleInvertSelect() {
const checkboxes = document.querySelectorAll('input[name="device_ids"]');
checkboxes.forEach(checkbox => {
checkbox.checked = !checkbox.checked;
});
updateSelectAllButtonText();
}
function updateSelectAllButtonText() {
const checkboxes = document.querySelectorAll('input[name="device_ids"]');
const selectAllButton = document.getElementById('selectAllBtn');
const hasSelected = Array.from(checkboxes).some(checkbox => checkbox.checked);
selectAllButton.textContent = hasSelected ? '清空' : '全选';
}
function changePerPage(value) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('per_page', value);
currentUrl.searchParams.set('page', '1');
window.location.href = currentUrl.toString();
}
async function handleSync(type) {
if (type === 'camera') {
try {
const response = await fetch('/get_captcha', {
method: 'GET'
});
if (response.ok) {
const blob = await response.blob();
document.getElementById('captchaImage').src = URL.createObjectURL(blob);
document.getElementById('captchaModal').style.display = 'block';
} else {
alert('获取验证码失败,请重试');
}
} catch (error) {
console.error('获取验证码失败:', error);
alert('获取验证码失败,请重试');
}
} else if (type === 'alarm') {
// 显示确认模态框
document.getElementById('alarmConfirmModal').style.display = 'block';
}
}
// 添加关闭确认模态框的函数
function closeAlarmConfirmModal() {
document.getElementById('alarmConfirmModal').style.display = 'none';
}
// 添加确认同步的函数
async function confirmAlarmSync() {
try {
// 关闭确认模态框
closeAlarmConfirmModal();
// 显示加载提示
document.getElementById('resultMessage').textContent = '正在同步数据,请稍候...';
document.getElementById('resultModal').style.display = 'block';
const response = await fetch('/sync_offline_devices', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
document.getElementById('resultMessage').textContent =
`同步成功!已为 ${data.count} 个离线设备创建工单。`;
} else {
document.getElementById('resultMessage').textContent = '同步失败:' + data.message;
}
document.getElementById('resultModal').style.display = 'block';
} catch (error) {
console.error('同步失败:', error);
document.getElementById('resultMessage').textContent = '同步失败,请重试';
document.getElementById('resultModal').style.display = 'block';
}
}
async function submitCaptcha() {
const captcha = document.getElementById('captchaInput').value;
if (!captcha) {
alert('请输入验证码');
return;
}
try {
const response = await fetch('/sync_camera_step1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ captcha: captcha })
});
const data = await response.json();
if (data.success) {
document.getElementById('captchaModal').style.display = 'none';
// 更新模态框中的数量
document.getElementById('updatedCount').textContent = data.updated_count || 0;
document.getElementById('confirmModal').style.display = 'block';
} else {
if (data.message === 'INVALID_CAPTCHA') {
alert('验证码错误,请重新输入');
handleSync('camera');
} else {
alert('同步失败:' + data.message);
}
}
} catch (error) {
console.error('同步失败:', error);
alert('同步失败,请重试');
}
}
async function handleDispatch(isCamera = false) {
try {
// 不立即关闭确认模态框,而是先更新其内容
const confirmModal = document.getElementById('confirmModal');
const modalContent = confirmModal.querySelector('.modal-content');
modalContent.innerHTML = `
<h3>正在处理</h3>
<p>正在为选中设备创建工单,请稍候...</p>
<div class="loading-spinner"></div>
`;
let response;
if (isCamera) {
// 如果是摄像头同步,调用第二步接口
response = await fetch('/sync_camera_step2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
} else {
// 普通派单
response = await fetch('/dispatch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
}
const result = await response.json();
// 处理完成后,平滑过渡到结果模态框
document.getElementById('confirmModal').style.display = 'none';
document.getElementById('resultMessage').textContent = `派单成功!已为 ${result.count} 个设备创建工单。`;
document.getElementById('resultModal').style.display = 'block';
} catch (error) {
console.error('派单失败:', error);
document.getElementById('resultMessage').textContent = '派单失败,请重试';
document.getElementById('resultModal').style.display = 'block';
}
}
function cancelDispatch() {
document.getElementById('confirmModal').style.display = 'none';
window.location.reload();
}
function closeCaptchaModal() {
document.getElementById('captchaModal').style.display = 'none';
document.getElementById('captchaInput').value = '';
}
// 备注编辑功能
function editRemark(deviceId, spanElement) {
const inputElement = spanElement.nextElementSibling;
spanElement.style.display = 'none';
inputElement.style.display = 'inline-block';
inputElement.focus();
}
async function saveRemark(deviceId, inputElement) {
const newRemark = inputElement.value.trim();
const spanElement = inputElement.previousElementSibling;
try {
const response = await fetch('/update_device_remark', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
device_id: deviceId,
remark: newRemark
})
});
const data = await response.json();
if (data.success) {
spanElement.textContent = newRemark || '点击添加备注';
spanElement.style.display = 'inline-block';
inputElement.style.display = 'none';
} else {
alert('保存失败:' + data.message);
inputElement.focus();
}
} catch (error) {
console.error('保存备注失败:', error);
alert('保存失败,请重试');
inputElement.focus();
}
}
document.addEventListener('DOMContentLoaded', function() {
// 监听复选框的变化
document.addEventListener('change', function(e) {
if (e.target.matches('input[name="device_ids"]')) {
updateSelectAllButtonText();
}
});
// 添加表单提交前的检查
document.getElementById('dispatchForm').addEventListener('submit', async function(e) {
e.preventDefault();
const selectedDevices = Array.from(document.querySelectorAll('input[name="device_ids"]:checked'))
.map(checkbox => checkbox.value);
if (selectedDevices.length === 0) {
alert('请至少选择一个设备');
return;
}
try {
const response = await fetch('/check_existing_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ device_ids: selectedDevices })
});
const data = await response.json();
if (data.existing_devices.length > 0) {
alert(`以下设备已有在途工单,请勿重复派单:\n${data.existing_devices.join('\n')}`);
return;
}
this.submit();
} catch (error) {
console.error('检查失败:', error);
alert('检查在途工单失败,请重试');
}
});
});
+53
View File
@@ -0,0 +1,53 @@
function changePerPage(value) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('per_page', value);
currentUrl.searchParams.set('page', '1');
window.location.href = currentUrl.toString();
}
function recallAllOrders() {
document.getElementById('confirmModal').style.display = 'block';
}
function closeModal() {
document.getElementById('confirmModal').style.display = 'none';
}
async function confirmRecall() {
try {
// 关闭确认模态框
closeModal();
const response = await fetch('/recall_all_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
document.getElementById('resultMessage').textContent = `成功追回 ${data.count} 个工单`;
document.getElementById('resultModal').style.display = 'block';
} else {
document.getElementById('resultMessage').textContent = '追回失败:' + data.message;
document.getElementById('resultModal').style.display = 'block';
}
} catch (error) {
document.getElementById('resultMessage').textContent = '操作失败,请重试';
document.getElementById('resultModal').style.display = 'block';
}
}
function closeResultModal() {
document.getElementById('resultModal').style.display = 'none';
window.location.reload();
}
// 添加点击模态框外部关闭功能
window.onclick = function(event) {
const modal = document.getElementById('confirmModal');
if (event.target == modal) {
closeModal();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

+66
View File
@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/about.css') }}">
<div class="about-container">
<div class="about-header">
<h1>关于本平台</h1>
</div>
<div class="about-content">
<div class="about-section">
<h2>平台简介</h2>
<p>本平台是一个专门用于管理和处理政企重点项目运维工单的平台。平台支持工单的派发、处理、统计等功能,旨在提高运维工作效率和管理透明度。</p>
</div>
<div class="about-section">
<ul style="list-style-type: disc; margin-left: 20px;">
<li>工单管理:支持普通工单和手工故障单的创建、派发和处理</li>
<li>数据统计:提供周期性的工单处理统计和运维奖励计算</li>
<li>历史记录:记录和查询历史工单信息</li>
<li>设备管理:支持设备信息的管理和维护</li>
</ul>
</div>
<div class="about-section">
<h2>常用功能</h2>
<ul class="quick-links">
<li class="quick-link-item">
<a href="https://web.wps.cn/wo/sl/v3Qs5r1?app_id=3SDuJ9CDIyuC7KvoKUM6uT" class="quick-link">"4+N"项目关联账号查询</a>
</li>
<li class="quick-link-item">
<a href="https://www.kdocs.cn/l/crjbtOrFLgoD" class="quick-link">"4+N"项目和数字校园设备安装明细</a>
</li>
<li class="quick-link-item">
<a href="https://www.kdocs.cn/l/cdnzqi5GvcHo" class="quick-link">"4+N"项目云监控平台安装指南</a>
</li>
<li class="quick-link-item">
<a href="https://www.kdocs.cn/l/cg1kMuthlS1j" class="quick-link">大化教育城域网故障排查指南</a>
</li>
<li class="quick-link-item">
<a href="https://onu.dhdx.fun/statistics" class="quick-link">大化教育城域网终端信息管理系统</a>
</li>
<li class="quick-link-item">
<a href="https://www.kdocs.cn/l/cgJAP7E065tK" class="quick-link">大化教育城域网OLT安装规划表</a>
</li>
</ul>
</div>
<div class="about-section">
<h2>信息</h2>
<div class="dev-info">
<p>当前平台处于试行阶段,如果遇到页面错误,功能不正常等问题请截图后与管理员联系;</p>
<p>如果您有任何建议或意见,请随时与我们联系。</p>
<p>感谢您的支持和使用!</p>
</div>
<div class="dev-info">
<p><strong>联系:</strong>韦矍森</p>
<p><strong>电话:</strong>19977899008</p>
<p><strong>部门:</strong>大化政企业务交付运营中心</p>
</div>
</div>
<div class="footer">
<p>© 2025 政企重点项目运维管理平台</p>
</div>
</div>
</div>
{% endblock %}
+125
View File
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大化重点项目工单管理平台</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg nav_container">
<div class="container-fluid">
<a class="nav_brand">
{% if request.endpoint == 'dispatch' %}
故障派单
{% elif request.endpoint == 'receive' %}
在途工单
{% elif request.endpoint == 'statistics' %}
工单统计
{% elif request.endpoint == 'history' %}
历史工单
{% elif request.endpoint == 'manual' %}
申报故障
{% elif request.endpoint == 'dashboard' %}
设备仪表盘
{% else %}
重点项目工单管理平台
{% endif %}
</a>
<button class="navbar-toggler nav_toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon nav_toggler_icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 nav_menu">
{% if current_user.is_authenticated %}
{% if current_user.role == '统计员' %}
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'dashboard' }}" href="{{ url_for('dashboard') }}">仪表盘</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'dispatch' }}" href="{{ url_for('dispatch') }}">派单</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'receive' }}" href="{{ url_for('receive') }}">在途</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'manual' }}" href="{{ url_for('manual') }}">申报</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'statistics' }}" href="{{ url_for('statistics') }}">统计</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'history' }}" href="{{ url_for('history') }}">历史</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'about' }}" href="{{ url_for('about') }}">关于</a>
</li>
{% elif current_user.role == '装维员' %}
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'dashboard' }}" href="{{ url_for('dashboard') }}">仪表盘</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'receive' }}" href="{{ url_for('receive') }}">在途</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'manual' }}" href="{{ url_for('manual') }}">申报</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'statistics' }}" href="{{ url_for('statistics') }}">统计</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'history' }}" href="{{ url_for('history') }}">历史</a>
</li>
<li class="nav-item">
<a class="nav_link {{ 'active' if request.endpoint == 'about' }}" href="{{ url_for('about') }}">关于</a>
</li>
{% endif %}
{% endif %}
</ul>
<ul class="navbar-nav nav_menu">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav_link" href="{{ url_for('logout') }}">退出</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% if not request.endpoint == 'login' %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="modal" id="flashModal" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered" style="z-index: 1060;">
<div class="modal-content">
<div class="modal-body">
{% for message in messages %}
<p class="mb-0">{{ message }}</p>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeFlashModal()">确定</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endwith %}
{% endif %}
{% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script>
function closeFlashModal() {
document.getElementById('flashModal').style.display = 'none';
}
</script>
</body>
</html>
+418
View File
@@ -0,0 +1,418 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
<div class="dashboard_container">
<h1 class="dashboard_title">设备在线率仪表盘</h1>
<!-- 图表区域 -->
<div class="dashboard_section">
<h2 class="section_title">设备在线率趋势图</h2>
<div class="chart_container">
<div class="chart_wrapper">
<canvas id="overallChart"></canvas>
</div>
<div class="chart_wrapper">
<canvas id="branchChart"></canvas>
</div>
</div>
<div class="chart_container">
<div class="chart_wrapper">
<canvas id="deviceTypeChart"></canvas>
</div>
<div class="chart_wrapper">
<canvas id="townChart"></canvas>
</div>
</div>
</div>
<!-- 总体设备在线率 -->
<div class="dashboard_section">
<h2 class="section_title">各类型设备总体在线率</h2>
<div class="card_grid">
{% for device_type, stats in overall_stats.items() %}
<div class="dashboard_card">
<div class="card_header">
<h3 class="card_title">{{ device_type }}</h3>
</div>
<div class="card_content">
<div class="progress_container">
<div class="progress_bar" style="width: {{ stats.online_rate }}%">
<span class="progress_text">{{ stats.online_rate }}%</span>
</div>
</div>
<div class="stats_details">
<div class="stats_item">
<span class="stats_label">设备总数</span>
<span class="stats_value">{{ stats.total }}</span>
</div>
<div class="stats_item">
<span class="stats_label">派单数</span>
<span class="stats_value">{{ stats.work_orders }}</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 各支局设备在线率 -->
<div class="dashboard_section">
<h2 class="section_title">各支局设备在线率</h2>
<div class="table_container">
<table class="dashboard_table">
<thead>
<tr>
<th>支局</th>
<th>设备类型</th>
<th>设备总数</th>
<th>派单数</th>
<th>在线率</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for branch, types in branch_stats.items() %}
{% for device_type, stats in types.items() %}
{% if stats.total > 0 %}
<tr>
{% if loop.first %}
<td rowspan="{{ types|length }}" class="branch_cell">{{ branch }}</td>
{% endif %}
<td>{{ device_type }}</td>
<td>{{ stats.total }}</td>
<td>{{ stats.work_orders }}</td>
<td>{{ stats.online_rate }}%</td>
<td>
{% if stats.online_rate >= 90 %}
<span class="status_badge good">良好</span>
{% elif stats.online_rate >= 75 %}
<span class="status_badge normal">一般</span>
{% else %}
<span class="status_badge bad">需关注</span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 各乡镇设备在线率 -->
<div class="dashboard_section">
<h2 class="section_title">各乡镇设备在线率</h2>
<div class="accordion_container">
{% for town, types in town_stats.items() %}
<div class="accordion_item">
<div class="accordion_header" onclick="toggleAccordion(this)">
<div class="accordion_title">{{ town }}</div>
<div class="accordion_summary">
<span>总设备数: {{ types['总计'].total }}</span>
<span>在线率: {{ types['总计'].online_rate }}%</span>
{% if types['总计'].online_rate >= 90 %}
<span class="status_badge good">良好</span>
{% elif types['总计'].online_rate >= 75 %}
<span class="status_badge normal">一般</span>
{% else %}
<span class="status_badge bad">需关注</span>
{% endif %}
</div>
<div class="accordion_icon"></div>
</div>
<div class="accordion_content">
<table class="detail_table">
<thead>
<tr>
<th>设备类型</th>
<th>设备总数</th>
<th>派单数</th>
<th>在线率</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for device_type, stats in types.items() %}
{% if device_type != '总计' and stats.total > 0 %}
<tr>
<td>{{ device_type }}</td>
<td>{{ stats.total }}</td>
<td>{{ stats.work_orders }}</td>
<td>{{ stats.online_rate }}%</td>
<td>
{% if stats.online_rate >= 90 %}
<span class="status_badge good">良好</span>
{% elif stats.online_rate >= 75 %}
<span class="status_badge normal">一般</span>
{% else %}
<span class="status_badge bad">需关注</span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- 引入Chart.js库 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
function toggleAccordion(element) {
const content = element.nextElementSibling;
const icon = element.querySelector('.accordion_icon');
if (content.style.maxHeight) {
content.style.maxHeight = null;
icon.textContent = '▼';
} else {
content.style.maxHeight = content.scrollHeight + "px";
icon.textContent = '▲';
}
}
// 图表数据准备
document.addEventListener('DOMContentLoaded', function() {
// 设备类型在线率图表
const deviceTypeCtx = document.getElementById('deviceTypeChart').getContext('2d');
const deviceTypeLabels = [];
const deviceTypeData = [];
const deviceTypeColors = [];
{% for device_type, stats in overall_stats.items() %}
deviceTypeLabels.push('{{ device_type }}');
deviceTypeData.push({{ stats.online_rate }});
// 根据在线率设置颜色
if ({{ stats.online_rate }} >= 90) {
deviceTypeColors.push('rgba(40, 167, 69, 0.7)');
} else if ({{ stats.online_rate }} >= 75) {
deviceTypeColors.push('rgba(255, 193, 7, 0.7)');
} else {
deviceTypeColors.push('rgba(220, 53, 69, 0.7)');
}
{% endfor %}
new Chart(deviceTypeCtx, {
type: 'bar',
data: {
labels: deviceTypeLabels,
datasets: [{
label: '设备类型在线率(%)',
data: deviceTypeData,
backgroundColor: deviceTypeColors,
borderColor: deviceTypeColors.map(color => color.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '各设备类型在线率对比'
},
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '在线率(%)'
}
}
}
}
});
// 支局在线率对比图
const branchCtx = document.getElementById('branchChart').getContext('2d');
const branchLabels = [];
const branchData = [];
const branchColors = [];
{% for branch, types in branch_stats.items() %}
branchLabels.push('{{ branch }}');
branchData.push({{ types['总计'].online_rate }});
// 根据在线率设置颜色
if ({{ types['总计'].online_rate }} >= 90) {
branchColors.push('rgba(40, 167, 69, 0.7)');
} else if ({{ types['总计'].online_rate }} >= 75) {
branchColors.push('rgba(255, 193, 7, 0.7)');
} else {
branchColors.push('rgba(220, 53, 69, 0.7)');
}
{% endfor %}
new Chart(branchCtx, {
type: 'bar',
data: {
labels: branchLabels,
datasets: [{
label: '支局在线率(%)',
data: branchData,
backgroundColor: branchColors,
borderColor: branchColors.map(color => color.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '各支局在线率对比'
},
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '在线率(%)'
}
}
}
}
});
// 乡镇在线率对比图
const townCtx = document.getElementById('townChart').getContext('2d');
const townLabels = [];
const townData = [];
const townColors = [];
{% for town, types in town_stats.items() %}
townLabels.push('{{ town }}');
townData.push({{ types['总计'].online_rate }});
// 根据在线率设置颜色
if ({{ types['总计'].online_rate }} >= 90) {
townColors.push('rgba(40, 167, 69, 0.7)');
} else if ({{ types['总计'].online_rate }} >= 75) {
townColors.push('rgba(255, 193, 7, 0.7)');
} else {
townColors.push('rgba(220, 53, 69, 0.7)');
}
{% endfor %}
new Chart(townCtx, {
type: 'bar',
data: {
labels: townLabels,
datasets: [{
label: '乡镇在线率(%)',
data: townData,
backgroundColor: townColors,
borderColor: townColors.map(color => color.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '各乡镇在线率对比'
},
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '在线率(%)'
}
}
}
}
});
// 总体在线率趋势图(模拟数据)
const overallCtx = document.getElementById('overallChart').getContext('2d');
// 模拟过去7天的数据
const dates = [];
const today = new Date();
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(today.getDate() - i);
dates.push(date.getMonth() + 1 + '/' + date.getDate());
}
// 生成模拟数据,假设在线率在当前值附近波动
const generateTrendData = (baseValue) => {
return Array.from({length: 7}, () => {
// 在基准值附近随机波动,但确保不超过100和不低于50
const fluctuation = Math.random() * 10 - 5; // -5 到 +5 之间的随机数
return Math.min(100, Math.max(50, baseValue + fluctuation));
});
};
// 假设当前总体在线率为所有设备类型在线率的平均值
let avgOnlineRate = 0;
let count = 0;
{% for device_type, stats in overall_stats.items() %}
avgOnlineRate += {{ stats.online_rate }};
count++;
{% endfor %}
avgOnlineRate = avgOnlineRate / (count || 1);
new Chart(overallCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '总体在线率趋势(%)',
data: generateTrendData(avgOnlineRate),
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '总体在线率7天趋势'
}
},
scales: {
y: {
beginAtZero: false,
min: Math.max(0, avgOnlineRate - 20),
max: Math.min(100, avgOnlineRate + 20),
title: {
display: true,
text: '在线率(%)'
}
}
}
}
});
});
</script>
{% endblock %}
+141
View File
@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/dispatch.css') }}">
<div class="dispatch_filter">
<form method="GET" action="{{ url_for('dispatch') }}" class="dispatch_search_form">
<input type="text" name="search" class="dispatch_search" placeholder="输入搜索词" value="{{ request.args.get('search', '') }}">
<select name="town" class="dispatch_search">
<option value="">所有乡镇</option>
{% for town in towns %}
<option value="{{ town }}" {% if town == request.args.get('town') %}selected{% endif %}>{{ town }}</option>
{% endfor %}
</select>
<select name="branch" class="dispatch_search">
<option value="">所有支局</option>
{% for branch in branches %}
<option value="{{ branch }}" {% if branch == request.args.get('branch') %}selected{% endif %}>{{ branch }}</option>
{% endfor %}
</select>
<button type="submit" class="dispatch_btn dispatch_btn_primary">搜索</button>
</form>
<div class="dispatch_action_bar">
<button type="button" class="dispatch_btn dispatch_btn_secondary_bj" onclick="handleSync('alarm')">同步报警盒</button>
<button type="button" class="dispatch_btn dispatch_btn_secondary_sxt" onclick="handleSync('camera')">同步摄像头</button>
<button type="button" class="dispatch_btn dispatch_btn_secondary_qx" id="selectAllBtn" onclick="handleSelectAll()">全选</button>
<button type="button" class="dispatch_btn dispatch_btn_secondary_fx" onclick="handleInvertSelect()">反选</button>
<button type="submit" form="dispatchForm" class="dispatch_btn dispatch_btn_primary">派单</button>
</div>
</div>
<!-- 派单表单 -->
<form id="dispatchForm" method="POST" action="{{ url_for('dispatch') }}">
<!-- 设备表格 -->
<table class="dispatch_table">
<thead>
<tr>
<th>选择</th>
<th>设备编号</th>
<th>所属乡镇</th>
<th>所属支局</th>
<th>单位名称</th>
<th>项目类型</th>
<th>设备类型</th>
<th>备注</th>
</tr>
</thead>
<tbody>
{% for device in devices.items %}
<tr>
<td><input type="checkbox" class="dispatch_checkbox" name="device_ids" value="{{ device.device_id }}"></td>
<td>{{ device.device_id }}</td>
<td>{{ device.town }}</td>
<td>{{ device.branch }}</td>
<td>{{ device.unit_name }}</td>
<td>{{ device.project_type }}</td>
<td>{{ device.device_type }}</td>
<td>
<span class="remark-display" onclick="editRemark('{{ device.device_id }}', this)">{{ device.remark or '点击添加备注' }}</span>
<input type="text" class="remark-input" style="display: none;" value="{{ device.remark or '' }}"
onblur="saveRemark('{{ device.device_id }}', this)"
onkeypress="if(event.key==='Enter') saveRemark('{{ device.device_id }}', this)">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
</div>
<!-- 分页导航部分 -->
<div class="dispatch_pagination">
{% if devices.has_prev %}
<a href="{{ url_for('dispatch', page=devices.prev_num, search=request.args.get('search', ''), town=request.args.get('town', ''), branch=request.args.get('branch', ''), per_page=request.args.get('per_page', '50')) }}" class="dispatch_page_link">上一页</a>
{% endif %}
<span class="dispatch_page_info">第 {{ devices.page }} 页,共 {{ devices.pages }} 页</span>
<select class="per_page_select" onchange="changePerPage(this.value)">
<option value="10" {% if request.args.get('per_page', '50') == '10' %}selected{% endif %}>10条/页</option>
<option value="20" {% if request.args.get('per_page', '50') == '20' %}selected{% endif %}>20条/页</option>
<option value="50" {% if request.args.get('per_page', '50') == '50' %}selected{% endif %}>50条/页</option>
<option value="100" {% if request.args.get('per_page', '50') == '100' %}selected{% endif %}>100条/页</option>
</select>
{% if devices.has_next %}
<a href="{{ url_for('dispatch', page=devices.next_num, search=request.args.get('search', ''), town=request.args.get('town', ''), branch=request.args.get('branch', ''), per_page=request.args.get('per_page', '50')) }}" class="dispatch_page_link">下一页</a>
{% endif %}
</div>
</div>
<!-- 添加验证码模态框 -->
<div id="captchaModal" class="modal">
<div class="modal-content">
<h3>请输入验证码</h3>
<img id="captchaImage" src="{{ url_for('static', filename='captcha.png') }}" alt="验证码">
<input type="text" id="captchaInput" placeholder="请输入验证码">
<div class="modal-buttons">
<button onclick="submitCaptcha()">确定</button>
<button onclick="closeCaptchaModal()">取消</button>
</div>
</div>
</div>
<!-- 添加一键报警确认模态框 -->
<div id="alarmConfirmModal" class="modal">
<div class="modal-content">
<h3>确认同步</h3>
<p>确定要同步一键报警设备状态吗?</p>
<div class="modal-buttons">
<button onclick="confirmAlarmSync()" class="dispatch_btn dispatch_btn_primary">确定</button>
<button onclick="closeAlarmConfirmModal()" class="dispatch_btn">取消</button>
</div>
</div>
</div>
<!-- 添加确认模态框 -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<h3>数据更新完成</h3>
<p>已更新 <span id="updatedCount">0</span> 个设备的状态</p>
<p>是否要进行派单操作?</p>
<div class="modal-buttons">
<button onclick="handleDispatch(true)"></button>
<button onclick="cancelDispatch()"></button>
</div>
</div>
</div>
<!-- 添加结果模态框 -->
<div id="resultModal" class="modal">
<div class="modal-content">
<h3>操作结果</h3>
<p id="resultMessage"></p>
<div class="modal-buttons">
<button onclick="window.location.reload()">确定</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/dispatch.js') }}"></script>
{% endblock %}
+88
View File
@@ -0,0 +1,88 @@
{% extends "base.html" %}
{% block content %}
<div class="history_container">
<!-- 添加搜索框 -->
<div class="history_search">
<form method="GET" action="{{ url_for('history') }}" class="history_search_form">
<input type="text" name="search" value="{{ search }}" placeholder="搜索工单号、设备、支局、处理人等..." class="history_search_input">
<button type="submit" class="history_search_button">搜索</button>
</form>
</div>
<!-- PC端表格视图 -->
<table class="history_table">
<thead>
<tr>
<th>工单号</th>
<th>所属支局</th>
<th>故障设备</th>
<th>处理人</th>
<th>处理时间</th>
<th>故障类型</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for handling in handlings.items %}
<tr>
<td>{{ handling['order_id'] }}</td>
<td>{{ handling['branch'] }}</td>
<td>{{ handling['device_id'] }}</td>
<td>{{ handling['handler'] }}</td>
<td>{{ handling['time'].strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ handling['fault_type'] }}</td>
<td>
<a href="{{ url_for('history_detail', order_id=handling['order_id']) }}" class="view_btn">查看</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 移动端卡片视图 -->
<div class="card_container">
{% for handling in handlings.items %}
<a href="{{ url_for('history_detail', order_id=handling['order_id']) }}" class="history_card {% if handling['type'] == 'manual' %}manual-card{% else %}system-card{% endif %}">
<div class="history_card_header">
<span class="history_card_title">工单号: {{ handling['order_id'] }}</span>
<span class="history_card_time">{{ handling['time'].strftime('%Y-%m-%d') }}</span>
</div>
<div class="history_card_content">
<div class="history_card_item">
<span class="history_card_label">所属支局</span>
<span class="history_card_value">{{ handling['branch'] }}</span>
</div>
<div class="history_card_item">
<span class="history_card_label">故障设备</span>
<span class="history_card_value">{{ handling['device_id'] }}</span>
</div>
<div class="history_card_item">
<span class="history_card_label">处理人</span>
<span class="history_card_value">{{ handling['handler'] }}</span>
</div>
<div class="history_card_item">
<span class="history_card_label">故障类型</span>
<span class="history_card_value">{{ handling['fault_type'] }}</span>
</div>
<div class="history_card_item">
<span class="history_card_label">工单类型</span>
<span class="history_card_value">{{ '手工申报' if handling['type'] == 'manual' else '系统派单' }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 分页导航 -->
<div class="history_pagination">
{% if handlings.has_prev %}
<a href="{{ url_for('history', page=handlings.prev_num) }}" class="history_page_link">上一页</a>
{% endif %}
<span class="history_page_info">第 {{ handlings.page }} 页,共 {{ handlings.pages }} 页</span>
{% if handlings.has_next %}
<a href="{{ url_for('history', page=handlings.next_num) }}" class="history_page_link">下一页</a>
{% endif %}
</div>
</div>
{% endblock %}
+132
View File
@@ -0,0 +1,132 @@
{% extends "base.html" %}
{% block content %}
<div class="history_detail_container">
<!-- 工单信息卡片 -->
<div class="detail_card">
<div class="detail_header">
<h3 class="detail_title">工单详情 #{{ handling.order_id }}</h3>
<span class="detail_status">{{ '申报' if order_type == 'manual' else '系统' }}</span>
</div>
<div class="detail_section">
<h4 class="section_title">基本信息</h4>
<div class="detail_grid">
<div class="detail_item">
<span class="detail_label">所属支局</span>
<span class="detail_value">{{ handling.branch }}</span>
</div>
<div class="detail_item">
<span class="detail_label">故障设备</span>
<span class="detail_value">{{ handling.device_id }}</span>
</div>
<div class="detail_item">
<span class="detail_label">处理人</span>
<span class="detail_value highlight">{{ handling.handler }}</span>
</div>
{% if order_type == 'manual' %}
<div class="detail_item">
<span class="detail_label">项目类型</span>
<span class="detail_value">{{ handling.project_type }}</span>
</div>
<div class="detail_item">
<span class="detail_label">设备位置</span>
<span class="detail_value">{{ handling.device_location }}</span>
</div>
<div class="detail_item">
<span class="detail_label">故障时间</span>
<span class="detail_value">{{ handling.fault_time.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div>
{% else %}
<div class="detail_item">
<span class="detail_label">处理时间</span>
<span class="detail_value">{{ handling.restore_time.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div>
{% endif %}
</div>
</div>
<div class="detail_section">
<h4 class="section_title">故障信息</h4>
<div class="detail_grid">
<div class="detail_item">
<span class="detail_label">故障类型</span>
<span class="detail_value badge">{{ handling.fault_type }}</span>
</div>
<div class="detail_item">
<span class="detail_label">故障对接人</span>
<span class="detail_value">{{ handling.contact_person }}</span>
</div>
<div class="detail_item">
<span class="detail_label">联系电话</span>
<span class="detail_value">{{ handling.contact_phone }}</span>
</div>
<div class="detail_item">
<span class="detail_label">距离支局</span>
<span class="detail_value">{{ handling.distance_from_branch }} 公里</span>
</div>
</div>
</div>
<div class="detail_section">
<h4 class="section_title">处理详情</h4>
<div class="detail_description">
<p>{{ handling.handle_description }}</p>
</div>
{% if handling.remark %}
<div class="detail_remark">
<h5>备注信息</h5>
<p>{{ handling.remark }}</p>
</div>
{% endif %}
</div>
{% if handling.photo_paths %}
<div class="detail_section">
<h4 class="section_title">现场图片</h4>
<div class="photo_grid">
{% for photo in handling.photo_paths.split(',') %}
<div class="photo_item">
<img src="{{ url_for('static', filename='uploads/' + photo) }}" alt="现场图片"
onclick="showFullImage(this.src)">
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<div class="detail_actions">
<a href="{{ url_for('history') }}" class="detail_btn detail_btn_secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<!-- 图片预览模态框 -->
<div id="imageModal" class="modal">
<span class="modal_close">&times;</span>
<img class="modal_content" id="modalImage">
</div>
<script>
function showFullImage(src) {
var modal = document.getElementById("imageModal");
var modalImg = document.getElementById("modalImage");
modal.style.display = "block";
modalImg.src = src;
}
var modal = document.getElementById("imageModal");
var span = document.getElementsByClassName("modal_close")[0];
span.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
</script>
{% endblock %}
+22
View File
@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="login_container">
<div class="login_box">
<h2 class="login_title">欢迎使用</h2>
<div class="login_subtitle">大化重点项目工单管理平台</div>
<form method="POST" class="login_form">
<div class="login_input_group">
<label for="phone" class="login_label">用户名</label>
<input type="text" id="phone" name="phone" class="login_input" required>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="login_error">请先登录以访问此页面</div>
{% endif %}
{% endwith %}
<button type="submit" class="login_button">登 录</button>
</form>
</div>
</div>
{% endblock %}
+130
View File
@@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block content %}
<div class="manual_container">
<div class="form_tips">
<p>注意事项</p>
<ul>
<li>本页面用于填写用户来电申报或者巡检途中发现的故障、移机,申报前请确保在途工单中没有重复故障</li>
<li>现场照片最多可上传5张,支持jpg、jpeg、png</li>
<li>距离支局公里数请填写支局办公室到故障地点实际距离</li>
<li><span class="required">*</span> 的字段为必填项</li>
</ul>
</div>
<form method="POST" enctype="multipart/form-data" class="manual_form">
<div class="form-group">
<label for="project_type">项目类型<span class="required">*</span></label>
<select id="project_type" name="project_type" required>
<option value="">请选择项目类型</option>
<option value='校园安防"4+N"项目'>校园安防"4+N"项目</option>
<option value="综治视联网">综治视联网</option>
<option value="教育城域网">教育城域网</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label for="device_id">施工设备类型<span class="required">*</span></label>
<input type="text" id="device_id" name="device_id" required placeholder="如:摄像头、一键报警盒、光猫等">
</div>
<div class="form-group">
<label for="device_location">施工单位名称<span class="required">*</span></label>
<input type="text" id="device_location" name="device_location" required placeholder="请填写前往施工的单位名称如:古河村委、都阳镇中心小学等">
</div>
<div class="form-group">
<label for="fault_time">施工完成时间<span class="required">*</span></label>
<input type="datetime-local" id="fault_time" name="fault_time" required>
</div>
<div class="form-group">
<label for="handler">施工处理人<span class="required">*</span></label>
<input type="text" id="handler" name="handler" value="{{ current_user.name }}" required>
</div>
<div class="form-group">
<label for="fault_type">故障类型<span class="required">*</span></label>
<select id="fault_type" name="fault_type" required>
<option value="">请选择故障类型</option>
<option value="光猫故障">光猫故障</option>
<option value="设备故障">设备故障</option>
<option value="电源故障">电源故障</option>
<option value="电路故障">电路故障</option>
<option value="网线故障">网线故障</option>
<option value="皮线故障">皮线故障</option>
<option value="数据问题">数据问题</option>
<option value="用户欠费">用户欠费</option>
<option value="移机">移机</option>
<option value="新增">新增</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label for="handle_description">处理描述<span class="required">*</span></label>
<textarea id="handle_description" name="handle_description" placeholder="请简述处理过程,如:光猫电源适配器坏,更好光猫电源适配器后恢复。" required></textarea>
</div>
<div class="form-group">
<label for="photo">现场照片(最多5张)</label>
<input type="file" id="photo" name="photo" multiple accept="image/*">
</div>
<div class="form-group">
<label for="contact_person">施工对接人<span class="required">*</span></label>
<input type="text" id="contact_person" name="contact_person" placeholder="请输入本次施工单位对接人" required>
</div>
<div class="form-group">
<label for="contact_phone">对接人联系电话</label>
<input type="tel" id="contact_phone" name="contact_phone" placeholder="请输入本次施工单位对接人联系电话">
</div>
<div class="form-group">
<label for="distance_from_branch">距离支局公里数<span class="required">*</span></label>
<input type="number" step="0.1" id="distance_from_branch" name="distance_from_branch" required placeholder="请输入实际路程距离,如:5.5、50">
</div>
<div class="form-group">
<label for="remark">备注</label>
<textarea id="remark" name="remark" placeholder="如有其他需要说明的信息,请在此填写"></textarea>
</div>
<button type="submit" class="manual_submit">提交</button>
</form>
</div>
<style>
.form_tips {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.form_tips ul {
margin: 10px 0 0 20px;
padding: 0;
}
.form_tips li {
margin-bottom: 5px;
color: #666;
}
.required {
color: #dc3545;
margin-left: 3px;
}
.manual_form input::placeholder,
.manual_form textarea::placeholder {
color: #999;
font-size: 13px;
}
.manual_form select option:first-child {
color: #999;
}
</style>
{% endblock %}
+162
View File
@@ -0,0 +1,162 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/receive.css') }}">
<!-- 统计信息 -->
<div class="receive_stats">
<div class="stats_item">
<span class="stats_label">在途工单总数</span>
<span class="stats_value">{{ total_orders }}</span>
</div>
{% if current_user.branch == '交付中心' %}
<div class="stats_branches_row">
{% for branch, count in branch_stats.items() %}
<a href="{{ url_for('receive', branch=branch) }}" class="branch_stat {% if branch == selected_branch %}active{% endif %}">
<span class="branch_name">{{ branch }}</span>
<span class="branch_count">{{ count }}</span>
</a>
{% endfor %}
</div>
<div class="receive_actions">
<button onclick="recallAllOrders()" class="receive_btn receive_btn_warning">一键追回所有工单</button>
{% if selected_branch %}
<a href="{{ url_for('receive') }}" class="receive_btn receive_btn_secondary">清除筛选</a>
{% endif %}
</div>
{% endif %}
</div>
<!-- 筛选区域 -->
<div class="receive_filter">
<form method="GET" action="{{ url_for('receive') }}" class="filter_form">
<select name="device_type" class="filter_select">
<option value="">所有设备类型</option>
{% for device_type in device_types %}
<option value="{{ device_type }}" {% if device_type == request.args.get('device_type') %}selected{% endif %}>{{ device_type }}</option>
{% endfor %}
</select>
<select name="project_type" class="filter_select">
<option value="">所有项目类型</option>
{% for project_type in project_types %}
<option value="{{ project_type }}" {% if project_type == request.args.get('project_type') %}selected{% endif %}>{{ project_type }}</option>
{% endfor %}
</select>
<input type="hidden" name="branch" value="{{ selected_branch }}">
<input type="hidden" name="page" value="1">
<input type="hidden" name="per_page" value="{{ request.args.get('per_page', '50') }}">
<button type="submit" class="receive_btn receive_btn_primary">筛选</button>
{% if request.args.get('device_type') or request.args.get('project_type') %}
<a href="{{ url_for('receive', branch=selected_branch, per_page=request.args.get('per_page', '50')) }}" class="receive_btn receive_btn_secondary">清除筛选</a>
{% endif %}
</form>
</div>
<table class="receive_table">
<thead>
<tr>
<th>工单号</th>
<th>所属支局</th>
<th>故障设备</th>
<th>设备类型</th>
<th>项目类型</th>
<th>派单时间</th>
<th>派单时长</th>
</tr>
</thead>
<tbody>
{% for work_order in work_orders %}
<tr>
<td>{{ work_order.order_id }}</td>
<td>{{ work_order.device.branch }}</td>
<td><a href="{{ url_for('receive_detail', work_order_id=work_order.order_id) }}" class="receive_action">{{ work_order.device.device_id }}</a></td>
<td>{{ work_order.device.device_type }}</td>
<td>{{ work_order.device.project_type }}</td>
<td>{{ work_order.dispatch_time }}</td>
<td>{{ work_order.duration }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 移动端卡片视图 -->
<div class="card_container">
{% for work_order in work_orders %}
<a href="{{ url_for('receive_detail', work_order_id=work_order.order_id) }}" class="receive_card">
<div class="receive_card_header">
<span class="receive_card_title">工单号: {{ work_order.order_id }}</span>
<span class="receive_card_duration">{{ work_order.duration }}</span>
</div>
<div class="receive_card_content">
<div class="receive_card_item">
<span class="receive_card_label">故障设备</span>
<span class="receive_card_value">{{ work_order.device.device_id }}</span>
</div>
<div class="receive_card_item">
<span class="receive_card_label">设备类型</span>
<span class="receive_card_value" data-device-type="{{ work_order.device.device_type }}">{{ work_order.device.device_type }}</span>
</div>
<div class="receive_card_item">
<span class="receive_card_label">所属支局</span>
<span class="receive_card_value">{{ work_order.device.branch }}</span>
</div>
<div class="receive_card_item">
<span class="receive_card_label">项目类型</span>
<span class="receive_card_value">{{ work_order.device.project_type }}</span>
</div>
<div class="receive_card_item">
<span class="receive_card_label">派单时间</span>
<span class="receive_card_value">{{ work_order.dispatch_time }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- 添加分页 -->
<div class="receive_pagination">
{% if work_orders.has_prev %}
<a href="{{ url_for('receive', page=work_orders.prev_num, branch=selected_branch, device_type=request.args.get('device_type', ''), project_type=request.args.get('project_type', ''), per_page=request.args.get('per_page', '50')) }}" class="page_link">上一页</a>
{% endif %}
<span class="page_info">第 {{ work_orders.page }} 页,共 {{ work_orders.pages }} 页</span>
<select class="per_page_select" onchange="changePerPage(this.value)">
<option value="10" {% if request.args.get('per_page', '50') == '10' %}selected{% endif %}>10条/页</option>
<option value="20" {% if request.args.get('per_page', '50') == '20' %}selected{% endif %}>20条/页</option>
<option value="50" {% if request.args.get('per_page', '50') == '50' %}selected{% endif %}>50条/页</option>
<option value="100" {% if request.args.get('per_page', '50') == '100' %}selected{% endif %}>100条/页</option>
</select>
{% if work_orders.has_next %}
<a href="{{ url_for('receive', page=work_orders.next_num, branch=selected_branch, device_type=request.args.get('device_type', ''), project_type=request.args.get('project_type', ''), per_page=request.args.get('per_page', '50')) }}" class="page_link">下一页</a>
{% endif %}
</div>
<!-- 确认追回弹窗 -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<h3>确认追回</h3>
<div class="modal-body">
<i class="modal-icon warning"></i>
<p>确定要追回所有在途工单吗?</p>
</div>
<div class="modal-buttons">
<button onclick="confirmRecall()" class="receive_btn receive_btn_warning">确定</button>
<button onclick="closeModal()" class="receive_btn receive_btn_secondary">取消</button>
</div>
</div>
</div>
<div id="resultModal" class="modal">
<div class="modal-content">
<h3>操作结果</h3>
<div class="modal-body">
<i class="modal-icon info"></i>
<p id="resultMessage"></p>
</div>
<div class="modal-buttons">
<button onclick="closeResultModal()" class="receive_btn receive_btn_primary">确定</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/receive.js') }}"></script>
{% endblock %}
+141
View File
@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/receive_detail.css') }}">
<div class="receive_detail_container">
<!-- 工单信息卡片 -->
<div class="detail_card">
<h3 class="detail_title">工单信息</h3>
<div class="detail_grid">
<div class="detail_item">
<span class="detail_label">工单号:</span>
<span class="detail_value">{{ work_order.order_id }}</span>
</div>
<div class="detail_item">
<span class="detail_label">所属支局:</span>
<span class="detail_value">{{ work_order.device.branch }}</span>
</div>
<div class="detail_item">
<span class="detail_label">故障设备:</span>
<span class="detail_value">{{ work_order.device.device_id }}</span>
</div>
<div class="detail_item">
<span class="detail_label">设备类型:</span>
<span class="detail_value">{{ work_order.device.device_type }}</span>
</div>
<div class="detail_item">
<span class="detail_label">项目类型:</span>
<span class="detail_value">{{ work_order.device.project_type }}</span>
</div>
<div class="detail_item">
<span class="detail_label">派单时间:</span>
<span class="detail_value">{{ work_order.dispatch_time }}</span>
</div>
<div class="detail_item">
<span class="detail_label">关联账号:</span>
<span class="detail_value">{{ work_order.device.account }}</span>
</div>
</div>
</div>
<!-- 处理信息表单 -->
<div class="detail_card">
<h3 class="detail_title">处理信息</h3>
<form method="POST" enctype="multipart/form-data" class="detail_form">
<div class="form-group">
<label for="handler">故障处理人<span class="required">*</span></label>
<input type="text" id="handler" name="handler" value="{{ current_user.name }}" required>
</div>
<div class="form-group">
<label for="fault_type">故障类型<span class="required">*</span></label>
<select id="fault_type" name="fault_type" required>
<option value="">请选择故障类型</option>
<option value="光猫故障">光猫故障</option>
<option value="设备故障">设备故障</option>
<option value="电源故障">电源故障</option>
<option value="电路故障">电路故障</option>
<option value="网线故障">网线故障</option>
<option value="皮线故障">皮线故障</option>
<option value="数据问题">数据问题</option>
<option value="用户欠费">用户欠费</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label for="handle_description">处理描述<span class="required">*</span></label>
<textarea id="handle_description" name="handle_description"
placeholder="请简述处理过程,如:光猫电源适配器坏,更换光猫电源适配器后恢复。" required></textarea>
</div>
<div class="form-group">
<label for="photo">现场图片(最多5张)</label>
<input type="file" id="photo" name="photo" multiple accept="image/*" class="detail_file_input">
</div>
<div class="form-group">
<label for="contact_person">故障对接人<span class="required">*</span></label>
<input type="text" id="contact_person" name="contact_person"
value="{{ work_order.contact_person }}"
placeholder="请输入本次故障单位对接人" required>
</div>
<div class="form-group">
<label for="contact_phone">联系电话</label>
<input type="tel" id="contact_phone" name="contact_phone"
value="{{ work_order.contact_phone }}"
placeholder="请输入本次故障单位对接人联系电话">
</div>
<div class="form-group">
<label for="distance_from_branch">距离支局公里数<span class="required">*</span></label>
<input type="number" id="distance_from_branch" name="distance_from_branch"
step="0.1" placeholder="请输入支局办公室到故障地点实际距离" required>
</div>
<div class="form-group">
<label for="remark">备注</label>
<textarea id="remark" name="remark"
placeholder="如有其他需要说明的信息请在此备注"></textarea>
</div>
<div class="form_tips">
<p>注意事项</p>
<ul>
<li>现场照片最多可上传5张,支持jpg、jpeg、png</li>
<li>距离支局公里数请填写支局办公室到故障地点实际距离</li>
<li><span class="required">*</span> 的字段为必填项</li>
</ul>
</div>
<div class="detail_actions">
<button type="submit" class="detail_btn detail_btn_primary">提交</button>
{% if current_user.role == '统计员' %}
<a href="{{ url_for('recall_order', order_id=work_order.order_id) }}"
class="detail_btn detail_btn_danger"
onclick="return confirm('确定要追回这张工单吗?')">追回</a>
{% endif %}
<a href="{{ url_for('receive') }}" class="detail_btn detail_btn_secondary">返回</a>
</div>
</form>
</div>
</div>
<script>
document.getElementById('fault_type').addEventListener('change', function() {
const faultType = this.value;
const descriptionField = document.getElementById('handle_description');
// 根据故障类型设置默认描述文本
const descriptions = {
'光猫故障': '检查发现光猫故障,更换光猫后恢复正常。',
'设备故障': '检查发现设备故障,重启设备后恢复正常。',
'电源故障': '检查发现电源适配器故障,更换电源适配器后恢复正常。',
'电路故障': '检查发现电路故障,修复电路后恢复正常。',
'网线故障': '检查发现网线故障,更换网线后恢复正常。',
'皮线故障': '检查发现皮线故障,更换皮线后恢复正常。',
'数据问题': '检查发现数据配置异常,修改配置后恢复正常。',
'用户欠费': '检查发现用户已欠费,通知用户缴费后恢复正常。',
'其他': ''
};
// 如果选择了故障类型,则填入对应的描述文本
if (faultType && descriptions[faultType]) {
descriptionField.value = descriptions[faultType];
} else {
descriptionField.value = '';
}
});
</script>
{% endblock %}
+255
View File
@@ -0,0 +1,255 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<div class="statistics_container">
<!-- 本周统计 -->
<div class="statistics_section">
<h2 class="statistics_title">本周统计
<a href="{{ url_for('static', filename='file/关于政企交付运营中心2025年项目运维及交付实施激励方案.pdf') }}"
class="incentive_button" target="_blank">📄 关于政企交付运营中心2025年项目运维及交付实施激励方案</a>
</h2>
<!-- 普通工单统计 -->
<div class="statistics_card">
<h3>普通工单</h3>
{% if current_user.role == '统计员' %}
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">派单总数</span>
<span class="statistics_value">{{ week_work_stats.total }}</span>
</div>
<div class="statistics_item">
<span class="statistics_label">已处理</span>
<span class="statistics_value">{{ week_work_stats.completed }}</span>
</div>
<div class="statistics_item">
<span class="statistics_label">未处理</span>
<span class="statistics_value">{{ week_work_stats.pending }}</span>
</div>
</div>
{% else %}
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">已处理工单</span>
<span class="statistics_value">{{ week_work_stats.completed }}</span>
</div>
</div>
{% endif %}
</div>
<!-- 手工故障单统计 -->
<div class="statistics_card">
<h3>手工故障单</h3>
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">处理完成数量</span>
<span class="statistics_value">{{ week_manual_stats }}</span>
</div>
</div>
</div>
<!-- 运维奖励统计 -->
<div class="statistics_card">
<h3>运维奖励(元)</h3>
<div class="statistics_grid">
{% if week_rewards is mapping %}
{% for project_type, reward in week_rewards.items() %}
<div class="statistics_item">
<span class="statistics_label">{{ project_type }}</span>
<span class="statistics_value">{{ "%.2f"|format(reward) }}</span>
</div>
{% endfor %}
{% endif %}
<div class="statistics_item total_reward">
<span class="statistics_label">总计</span>
<span class="statistics_value">{{ "%.2f"|format(week_rewards.values()|sum) if week_rewards is mapping else "%.2f"|format(week_rewards|default(0)) }}</span>
</div>
</div>
</div>
</div>
<!-- 本月统计 -->
<div class="statistics_section">
<h2 class="statistics_title">本月统计
{% if current_user.role == '统计员' %}
<button onclick="showMonthPicker()" class="export_button">📥︎ 导出往月明细</button>
{% endif %}
</h2>
<!-- 普通工单统计 -->
<div class="statistics_card">
<h3>普通工单</h3>
{% if current_user.role == '统计员' %}
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">派单总数</span>
<span class="statistics_value">{{ month_work_stats.total }}</span>
</div>
<div class="statistics_item">
<span class="statistics_label">已处理</span>
<span class="statistics_value">{{ month_work_stats.completed }}</span>
</div>
<div class="statistics_item">
<span class="statistics_label">未处理</span>
<span class="statistics_value">{{ month_work_stats.pending }}</span>
</div>
</div>
{% else %}
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">已处理工单</span>
<span class="statistics_value">{{ month_work_stats.completed }}</span>
</div>
</div>
{% endif %}
</div>
<!-- 手工故障单统计 -->
<div class="statistics_card">
<h3>手工故障单</h3>
<div class="statistics_grid">
<div class="statistics_item">
<span class="statistics_label">处理完成数量</span>
<span class="statistics_value">{{ month_manual_stats }}</span>
</div>
</div>
</div>
<!-- 运维奖励统计 -->
<div class="statistics_card">
<h3>运维奖励(元)</h3>
<div class="statistics_grid">
{% if month_rewards is mapping %}
{% for project_type, reward in month_rewards.items() %}
<div class="statistics_item">
<span class="statistics_label">{{ project_type }}</span>
<span class="statistics_value">{{ "%.2f"|format(reward) }}</span>
</div>
{% endfor %}
{% endif %}
<div class="statistics_item total_reward">
<span class="statistics_label">总计</span>
<span class="statistics_value">{{ "%.2f"|format(month_rewards.values()|sum) if month_rewards is mapping else "%.2f"|format(month_rewards|default(0)) }}</span>
</div>
</div>
</div>
{% if current_user.role == '统计员' %}
<div class="statistics_section">
<h2 class="statistics_title">本周奖励统计</h2>
<div class="statistics_card">
<div class="handler_stats_table">
<table>
<thead>
<tr>
<th>处理人</th>
<th>普通工单</th>
<th>手工工单</th>
<th>校园安防"4+N"项目</th>
<th>综治视联网</th>
<th>教育城域网</th>
<th>其他</th>
<th>预计奖励</th>
</tr>
</thead>
<tbody>
{% for handler, stats in week_handler_stats.items() %}
<tr>
<td>{{ handler }}</td>
<td>{{ stats.work_orders }}</td>
<td>{{ stats.manual_orders }}</td>
<td>{{ "%.2f"|format(stats.rewards['校园安防"4+N"项目']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['综治视联网']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['教育城域网']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['其他']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards.values()|sum) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="statistics_section">
<h2 class="statistics_title">本月奖励统计</h2>
<div class="statistics_card">
<div class="handler_stats_table">
<table>
<thead>
<tr>
<th>处理人</th>
<th>普通工单</th>
<th>手工工单</th>
<th>校园安防"4+N"项目</th>
<th>综治视联网</th>
<th>教育城域网</th>
<th>其他</th>
<th>预计奖励</th>
</tr>
</thead>
<tbody>
{% for handler, stats in month_handler_stats.items() %}
<tr>
<td>{{ handler }}</td>
<td>{{ stats.work_orders }}</td>
<td>{{ stats.manual_orders }}</td>
<td>{{ "%.2f"|format(stats.rewards['校园安防"4+N"项目']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['综治视联网']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['教育城域网']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards['其他']|default(0)) }}</td>
<td>{{ "%.2f"|format(stats.rewards.values()|sum) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- 月份选择器模态框 -->
<div id="monthPickerModal" class="modal" style="display: none;">
<div class="modal-content">
<h3>选择导出月份</h3>
<div class="month-picker-form">
<input type="month" id="monthPicker" class="month-input">
<div class="modal-buttons">
<button onclick="exportMonthlyStats()" class="export_button">导出</button>
<button onclick="closeModal()" class="cancel_button">取消</button>
</div>
</div>
</div>
</div>
<script>
function showMonthPicker() {
document.getElementById('monthPickerModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('monthPickerModal').style.display = 'none';
}
function exportMonthlyStats() {
const monthPicker = document.getElementById('monthPicker');
const selectedMonth = monthPicker.value; // 格式:YYYY-MM
if (!selectedMonth) {
alert('请选择月份');
return;
}
// 将 YYYY-MM 格式拆分为年和月
const [year, month] = selectedMonth.split('-');
window.location.href = `/export_monthly_stats/${year}/${month}`;
closeModal();
}
// 点击模态框外部关闭
document.getElementById('monthPickerModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
</script>
{% endblock %}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+136
View File
@@ -0,0 +1,136 @@
from functools import wraps
import time
from flask import current_app
import logging
from sqlalchemy.exc import SQLAlchemyError
from scripts.models import db
from collections import defaultdict
# 简单的内存缓存实现
class QueryCache:
def __init__(self, max_size=100, ttl=300): # 默认缓存300秒
self.cache = {}
self.max_size = max_size
self.ttl = ttl
self.access_times = defaultdict(int)
def get(self, key):
if key in self.cache:
item = self.cache[key]
if time.time() - item['time'] < self.ttl:
self.access_times[key] += 1
return item['value']
else:
# 缓存过期,删除
del self.cache[key]
if key in self.access_times:
del self.access_times[key]
return None
def set(self, key, value):
# 如果缓存满了,删除最少访问的项
if len(self.cache) >= self.max_size:
# 找出访问次数最少的键
if self.access_times:
min_key = min(self.access_times, key=self.access_times.get)
if min_key in self.cache:
del self.cache[min_key]
del self.access_times[min_key]
self.cache[key] = {
'value': value,
'time': time.time()
}
self.access_times[key] = 1
return value
# 创建全局缓存实例
query_cache = QueryCache()
# 数据库操作装饰器 - 用于缓存结果
def cache_query(ttl=300):
"""
缓存查询结果的装饰器
:param ttl: 缓存有效期(秒)
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}"
# 尝试从缓存获取
cached_result = query_cache.get(cache_key)
if cached_result is not None:
return cached_result
# 执行查询
result = func(*args, **kwargs)
# 设置缓存
query_cache.set(cache_key, result)
return result
return wrapper
return decorator
# 数据库操作装饰器 - 用于异常处理
def db_operation(retries=3, retry_delay=0.5):
"""
数据库操作的装饰器,提供异常处理和重试功能
:param retries: 重试次数
:param retry_delay: 重试延迟(秒)
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(retries):
try:
return func(*args, **kwargs)
except SQLAlchemyError as e:
last_error = e
current_app.logger.warning(f"数据库操作失败: {str(e)},尝试重试 ({attempt+1}/{retries})")
# 回滚会话
db.session.rollback()
# 如果不是最后一次尝试,则等待后重试
if attempt < retries - 1:
time.sleep(retry_delay)
# 所有重试都失败
current_app.logger.error(f"数据库操作失败(已重试{retries}次): {str(last_error)}")
raise last_error
return wrapper
return decorator
# 辅助函数:清除特定表相关的所有缓存
def clear_table_cache(table_name):
"""
清除与特定表相关的所有缓存
:param table_name: 表名
"""
keys_to_delete = []
for key in list(query_cache.cache.keys()):
if table_name.lower() in key.lower():
keys_to_delete.append(key)
for key in keys_to_delete:
if key in query_cache.cache:
del query_cache.cache[key]
if key in query_cache.access_times:
del query_cache.access_times[key]
# 通用查询函数,包含异常处理和缓存
@db_operation()
@cache_query()
def get_all_distinct_values(model, column_name):
"""
获取指定模型指定列的所有不同值
:param model: 模型类
:param column_name: 列名
:return: 所有不同值的列表
"""
column = getattr(model, column_name)
results = db.session.query(column).distinct().all()
return [result[0] for result in results if result[0]]
+150
View File
@@ -0,0 +1,150 @@
from flask import render_template, jsonify, request, flash, redirect, url_for
from sqlalchemy.exc import SQLAlchemyError, IntegrityError, OperationalError
from werkzeug.exceptions import HTTPException, NotFound, Forbidden, BadRequest
import traceback
class ErrorHandler:
"""
全局异常处理器
用于注册Flask应用程序的错误处理函数
"""
def __init__(self, app=None):
if app:
self.init_app(app)
def init_app(self, app):
"""
初始化应用程序的错误处理器
:param app: Flask应用实例
"""
# 处理SQLAlchemy错误
app.register_error_handler(SQLAlchemyError, self.handle_db_error)
app.register_error_handler(IntegrityError, self.handle_integrity_error)
app.register_error_handler(OperationalError, self.handle_operational_error)
# 处理HTTP错误
app.register_error_handler(NotFound, self.handle_not_found)
app.register_error_handler(Forbidden, self.handle_forbidden)
app.register_error_handler(BadRequest, self.handle_bad_request)
# 处理通用错误
app.register_error_handler(Exception, self.handle_generic_error)
# 记录初始化完成
app.logger.info("错误处理器初始化完成")
def handle_db_error(self, error):
"""
处理一般数据库错误
"""
error_details = str(error)
traceback_details = traceback.format_exc()
current_app = self._get_current_app()
current_app.logger.error(f"数据库错误: {error_details}\n{traceback_details}")
return self._handle_error("数据库操作错误",
"抱歉,数据库操作失败,请稍后重试。",
500)
def handle_integrity_error(self, error):
"""
处理数据完整性错误(如违反唯一约束)
"""
error_details = str(error)
current_app = self._get_current_app()
current_app.logger.error(f"数据完整性错误: {error_details}")
# 尝试从错误消息中提取更友好的错误消息
friendly_message = "数据冲突,请检查您的输入。"
if "Duplicate entry" in error_details:
friendly_message = "数据已存在,请检查您的输入。"
return self._handle_error("数据完整性错误", friendly_message, 400)
def handle_operational_error(self, error):
"""
处理数据库操作错误(如连接问题)
"""
error_details = str(error)
current_app = self._get_current_app()
current_app.logger.error(f"数据库操作错误: {error_details}")
return self._handle_error("数据库连接错误",
"抱歉,无法连接到数据库,请稍后重试。",
503)
def handle_not_found(self, error):
"""
处理404错误
"""
return self._handle_error("页面未找到",
"抱歉,您请求的页面不存在。",
404)
def handle_forbidden(self, error):
"""
处理403错误
"""
return self._handle_error("访问被拒绝",
"抱歉,您没有权限访问此页面。",
403)
def handle_bad_request(self, error):
"""
处理400错误
"""
return self._handle_error("请求错误",
"抱歉,服务器无法理解您的请求。",
400)
def handle_generic_error(self, error):
"""
处理未被其他处理程序捕获的通用错误
"""
error_details = str(error)
traceback_details = traceback.format_exc()
current_app = self._get_current_app()
current_app.logger.error(f"未处理异常: {error_details}\n{traceback_details}")
if isinstance(error, HTTPException):
return self._handle_error("请求错误", error.description, error.code)
return self._handle_error("服务器错误",
"抱歉,服务器遇到了未知错误,请稍后重试。",
500)
def _handle_error(self, title, message, status_code):
"""
根据请求类型返回适当的错误响应
:param title: 错误标题
:param message: 错误消息
:param status_code: HTTP状态码
"""
# 根据接受的内容类型决定返回JSON还是HTML
# 检查是否是AJAX请求,is_xhr在新版Flask中已被移除
is_ajax_request = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
if is_ajax_request or request.headers.get('Accept') == 'application/json':
# 返回JSON格式的错误
return jsonify({
'error': title,
'message': message
}), status_code
else:
# 使用Flash消息并重定向到统计页面
from flask import current_app
flash(message)
# 根据错误类型决定重定向页面
if status_code == 404:
return redirect(url_for('statistics'))
elif status_code == 403:
return redirect(url_for('statistics'))
else:
return redirect(url_for('statistics'))
def _get_current_app(self):
"""
获取当前Flask应用实例
"""
from flask import current_app
return current_app
+10
View File
@@ -0,0 +1,10 @@
from PIL import Image
def allowed_file(filename, allowed_extensions):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
def compress_image(image_path, max_size=(1500, 1500), quality=85):
"""压缩图片"""
img = Image.open(image_path)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
img.save(image_path, quality=quality)
+102
View File
@@ -0,0 +1,102 @@
# utils\lzmx.py
import requests
import json
from datetime import datetime
# 推送系统工单处理信息到量子密信
def send_lzmx_message(webhook_url, handler, device_id, device_type, distance_from_branch, restore_time):
headers = {'Content-Type': 'application/json'}
message = {
"type": "text",
"textMsg": {
"content":
f"📢 故障处理通知\n"
f"故障设备:{device_id}\n"
f"处理时间:{restore_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
f"故障类型:{device_type}\n"
f"距离支局:{distance_from_branch}公里\n"
f"处理人:{handler}"
}
}
try:
response = requests.post(
webhook_url,
headers=headers,
data=json.dumps(message)
)
response.raise_for_status()
return True
except Exception as e:
print(f"发送量子密信消息失败: {str(e)}")
return False
# 推送手工申报工单信息到量子密信
def send_manual_lzmx_message(webhook_url, handler, device_id, fault_type, distance_from_branch, fault_time, project_type, device_location):
headers = {'Content-Type': 'application/json'}
message = {
"type": "text",
"textMsg": {
"content":
f"📝 故障处理通知\n"
f"项目类型:{project_type}\n"
f"故障设备:{device_id}\n"
f"设备位置:{device_location}\n"
f"故障时间:{fault_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
f"故障类型:{fault_type}\n"
f"距离支局:{distance_from_branch}公里\n"
f"处理人:{handler}"
}
}
try:
response = requests.post(
webhook_url,
headers=headers,
data=json.dumps(message)
)
response.raise_for_status()
return True
except Exception as e:
print(f"发送量子密信消息失败: {str(e)}")
return False
# 每周五统计本周情况推送量子密信
def send_weekly_stats_message(webhook_url, stats):
headers = {'Content-Type': 'application/json'}
# 格式化前三名处理人信息
top_handlers = []
for handler, amount in stats['top_handlers']:
top_handlers.append(f"👤 {handler}: {amount}")
message = {
"type": "text",
"textMsg": {
"content":
f"📊 本周工单统计报告\n"
f"—————————————————\n"
f" 本周派单:{stats['total_orders']}\n"
f" 已处理数:{stats['completed_orders']}\n"
f" 未处理数:{stats['pending_orders']}\n"
f" 手工故障:{stats['manual_orders']}\n\n"
f"🏆 前三名预计奖励:\n"
f"{chr(10).join(top_handlers)}\n"
f"—————————————————\n"
f"统计时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
}
}
try:
response = requests.post(
webhook_url,
headers=headers,
data=json.dumps(message)
)
response.raise_for_status()
return True
except Exception as e:
print(f"发送量子密信消息失败: {str(e)}")
return False
+11
View File
@@ -0,0 +1,11 @@
class Pagination:
def __init__(self, items, page, per_page, total):
self.items = items
self.page = page
self.per_page = per_page
self.total = total
self.pages = (total + per_page - 1) // per_page
self.has_prev = page > 1
self.has_next = page < self.pages
self.prev_num = page - 1
self.next_num = page + 1
+66
View File
@@ -0,0 +1,66 @@
def get_reward_by_distance(project_type, distance):
"""
根据项目类型和距离计算奖励金额
Args:
project_type (str): 项目类型
distance (float): 距离(公里)
Returns:
float: 奖励金额(元)
"""
# 2025.04.21 by v6ole
# 奖励配置说明:
# 格式为: '项目类型': [(距离上限1, 奖励金额1), (距离上限2, 奖励金额2), ...]
# 距离单位: 公里, 金额单位: 元
# 配置按照距离从小到大排序,系统会自动匹配第一个符合条件的奖励金额
rewards_config = {
# 校园安防"4+N"项目奖励标准:
# - 0-5公里: 20元/台
# - 5-30公里: 40元/台
# - 30-50公里: 50元/台
# - 50公里以上: 60元/台
'校园安防"4+N"项目': [
(5, 20),
(30, 40),
(50, 50),
(float('inf'), 60)
],
# 综治视联网奖励标准:
# - 0-10公里: 30元/台
# - 10-30公里: 40元/台
# - 30-50公里: 50元/台
# - 50公里以上: 60元/台
'综治视联网': [
(10, 30),
(30, 40),
(50, 50),
(float('inf'), 60)
],
# 教育城域网奖励标准:
# - 0-5公里: 20元/台
# - 5-30公里: 30元/台
# - 30-50公里: 40元/台
# - 50公里以上: 50元/台
'教育城域网': [
(5, 20),
(30, 30),
(50, 40),
(float('inf'), 50)
],
# 其他项目统一标准:
# - 任何距离: 30元/台
'其他': [(float('inf'), 30)]
}
if project_type not in rewards_config:
return 0
for max_distance, reward in rewards_config[project_type]:
if distance <= max_distance:
return reward
return 0
Executable
+99
View File
@@ -0,0 +1,99 @@
[uwsgi]
# 指定 WSGI 应用的入口模块和对象
# 格式为 module:callable,例如 app:app 表示模块名为 app.py,应用对象为 app
module = app:app
# 指定 uWSGI 监听的 HTTP 地址和端口
# 如果使用 Nginx 反向代理,可以注释掉此行
http = 0.0.0.0:18019
# 设置 uWSGI 启动的进程数
# 根据服务器性能和需求调整进程数
processes = 4
# 设置每个进程的线程数
# 线程数和进程数共同影响并发处理能力
threads = 2
# 启用主进程
master = true
# 优雅地重启工作进程
# 当收到SIGINT信号时,等待当前请求完成后再重启
reload-mercy = 10
# 进程超时时间(秒),如果一个请求处理时间超过这个值,工作进程会被杀死
harakiri = 30
# 接收到请求的队列最大长度,如果队列已满,新的请求将被拒绝
listen = 100
# 最大并发请求数
max-requests = 1000
# 每个工作进程处理完指定数量的请求后自动重启,防止内存泄漏
max-worker-lifetime = 3600
# 缓冲区大小设置
buffer-size = 32768
# 指定日志文件的路径
# uWSGI 的运行日志将写入此文件
logto = /opt/1panel/docker/compose/4an/uwsgi.log
# 指定虚拟环境的路径
# uWSGI 将使用此虚拟环境中的 Python 解释器和依赖库
virtualenv = /mnt/nfs/.venv
# 设置应用的根目录
# uWSGI 将切换到此目录运行,确保应用的相对路径正确
chdir = /opt/1panel/docker/compose/4an/app
# 设置 PID 文件的路径
# PID 文件用于管理 uWSGI 进程(如停止、重启)
pidfile = /opt/1panel/docker/compose/4an/uwsgi.pid
# 设置 Unix Socket 文件的路径
# 用于与 Nginx 或其他代理服务器通信
# 如果使用 HTTP 模式,可以注释掉此行
socket = /opt/1panel/docker/compose/4an/uwsgi.sock
# 设置 Socket 文件的权限
# 660 表示只有所有者和所属组有读写权限
chmod-socket = 660
# 当 uWSGI 退出时,自动清理其创建的套接字和 PID 文件
# 保持环境整洁,避免遗留文件
vacuum = true
# 设置日志文件的最大大小(单位为字节)
# 当日志文件达到此大小时,uWSGI 将自动轮转日志
# 示例值为 10MB
log-maxsize = 10000000
# 禁用请求日志,减少IO负担
disable-logging = true
log-slow = 1000 # 只记录处理时间超过1000ms的请求
# 紧急情况下尝试恢复的时间(秒)
worker-reload-mercy = 60
# 自动重载配置
py-autoreload = 1
# 内存报告
memory-report = true
# 启用统计服务器
stats = 127.0.0.1:9191
# 平滑重启
touch-chain-reload = true
touch-reload = uwsgi.reload
reload-on-exception = true # 当出现异常时自动重启
# 启用线程支持
enable-threads = true
# 设置自动异常处理
catch-exceptions = true
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More