开始
@@ -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):** 解释你做了哪些修改以及为什么。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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: 用户 韦矍森 访问统计页面
|
||||
@@ -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}>'
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -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('检查在途工单失败,请重试');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 167 KiB |
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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">×</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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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]]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||