修改新增加文件数量的查询功能
This commit is contained in:
@ -26,17 +26,17 @@ def get_base_path():
|
|||||||
|
|
||||||
|
|
||||||
def get_static_path():
|
def get_static_path():
|
||||||
"""获取 Vue 静态资源 dist 的路径"""
|
"""获取 Vue 静态资源 web_dist 的路径"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# PyInstaller 打包时,资源文件会被解压到 sys._MEIPASS 临时目录
|
# PyInstaller 打包时,资源文件会被解压到 sys._MEIPASS 临时目录
|
||||||
# 我们需要在打包命令中指定 --add-data "dist;dist"
|
# 我们需要在打包命令中指定 --add-data "web_dist;web_dist"
|
||||||
return os.path.join(sys._MEIPASS, 'dist')
|
return os.path.join(sys._MEIPASS, 'web_dist')
|
||||||
# 开发环境
|
# 开发环境
|
||||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dist')
|
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_dist')
|
||||||
|
|
||||||
|
|
||||||
# --- Flask 初始化 ---
|
# --- Flask 初始化 ---
|
||||||
# static_folder 指向 Vue 打包后的 dist 目录
|
# static_folder 指向 Vue 打包后的 web_dist 目录
|
||||||
# static_url_path='' 表示静态文件不需要 /static 前缀
|
# static_url_path='' 表示静态文件不需要 /static 前缀
|
||||||
dist_folder = get_static_path()
|
dist_folder = get_static_path()
|
||||||
app = Flask(__name__, static_folder=dist_folder, static_url_path='')
|
app = Flask(__name__, static_folder=dist_folder, static_url_path='')
|
||||||
@ -303,7 +303,7 @@ def serve_index():
|
|||||||
|
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
def serve_static_files(path):
|
def serve_static_files(path):
|
||||||
# 尝试在 dist 目录寻找文件 (css, js, icons)
|
# 尝试在 web_dist 目录寻找文件 (css, js, icons)
|
||||||
file_path = os.path.join(app.static_folder, path)
|
file_path = os.path.join(app.static_folder, path)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
return send_from_directory(app.static_folder, path)
|
return send_from_directory(app.static_folder, path)
|
||||||
@ -323,5 +323,5 @@ if __name__ == "__main__":
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
# Host='0.0.0.0' 允许外部IP访问
|
# Host='0.0.0.0' 允许外部IP访问
|
||||||
# Port=5000 (确保 Windows 防火墙开放了此端口)
|
# Port=5000 (确保 Windows 防火墙开放了此端口)
|
||||||
print("应用正在启动... 请确保 dist 文件夹与脚本/exe 同级或已被打包")
|
print("应用正在启动... 请确保 web_dist 文件夹与脚本/exe 同级或已被打包")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
|
app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
|
||||||
|
|||||||
14
1.1/test1.py
14
1.1/test1.py
@ -26,17 +26,17 @@ def get_base_path():
|
|||||||
|
|
||||||
|
|
||||||
def get_static_path():
|
def get_static_path():
|
||||||
"""获取 Vue 静态资源 dist 的路径"""
|
"""获取 Vue 静态资源 web_dist 的路径"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# PyInstaller 打包时,资源文件会被解压到 sys._MEIPASS 临时目录
|
# PyInstaller 打包时,资源文件会被解压到 sys._MEIPASS 临时目录
|
||||||
# 我们需要在打包命令中指定 --add-data "dist;dist"
|
# 我们需要在打包命令中指定 --add-data "web_dist;web_dist"
|
||||||
return os.path.join(sys._MEIPASS, 'dist')
|
return os.path.join(sys._MEIPASS, 'web_dist')
|
||||||
# 开发环境
|
# 开发环境
|
||||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dist')
|
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_dist')
|
||||||
|
|
||||||
|
|
||||||
# --- Flask 初始化 ---
|
# --- Flask 初始化 ---
|
||||||
# static_folder 指向 Vue 打包后的 dist 目录
|
# static_folder 指向 Vue 打包后的 web_dist 目录
|
||||||
# static_url_path='' 表示静态文件不需要 /static 前缀
|
# static_url_path='' 表示静态文件不需要 /static 前缀
|
||||||
dist_folder = get_static_path()
|
dist_folder = get_static_path()
|
||||||
app = Flask(__name__, static_folder=dist_folder, static_url_path='')
|
app = Flask(__name__, static_folder=dist_folder, static_url_path='')
|
||||||
@ -303,7 +303,7 @@ def serve_index():
|
|||||||
|
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
def serve_static_files(path):
|
def serve_static_files(path):
|
||||||
# 尝试在 dist 目录寻找文件 (css, js, icons)
|
# 尝试在 web_dist 目录寻找文件 (css, js, icons)
|
||||||
file_path = os.path.join(app.static_folder, path)
|
file_path = os.path.join(app.static_folder, path)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
return send_from_directory(app.static_folder, path)
|
return send_from_directory(app.static_folder, path)
|
||||||
@ -323,5 +323,5 @@ if __name__ == "__main__":
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
# Host='0.0.0.0' 允许外部IP访问
|
# Host='0.0.0.0' 允许外部IP访问
|
||||||
# Port=5000 (确保 Windows 防火墙开放了此端口)
|
# Port=5000 (确保 Windows 防火墙开放了此端口)
|
||||||
print("应用正在启动... 请确保 dist 文件夹与脚本/exe 同级或已被打包")
|
print("应用正在启动... 请确保 web_dist 文件夹与脚本/exe 同级或已被打包")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
|
app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
|
||||||
|
|||||||
184
2_1banben/app.py
184
2_1banben/app.py
@ -2,7 +2,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import pytz # ✅ 必须引入:用于强制指定北京时间
|
||||||
|
|
||||||
from flask import Flask, send_from_directory, jsonify
|
from flask import Flask, send_from_directory, jsonify
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_apscheduler import APScheduler
|
from flask_apscheduler import APScheduler
|
||||||
@ -11,142 +14,141 @@ from flask_apscheduler import APScheduler
|
|||||||
# ✅ 1. 核心模块引用
|
# ✅ 1. 核心模块引用
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
try:
|
try:
|
||||||
# 导入配置类
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
# 数据库实例
|
|
||||||
from extensions import db
|
from extensions import db
|
||||||
|
|
||||||
# 数据模型
|
|
||||||
from models import Device, DeviceHistory
|
from models import Device, DeviceHistory
|
||||||
|
|
||||||
# 核心业务逻辑 (爬虫)
|
|
||||||
from services.core import execute_monitor_task
|
from services.core import execute_monitor_task
|
||||||
|
|
||||||
# 核心业务逻辑 (IoT) - 用于定时任务
|
# from services.iot_api import sync_iot_data_service # 如果不需要IoT可以注释
|
||||||
from services.iot_api import sync_iot_data_service
|
|
||||||
|
|
||||||
# 路由蓝图
|
|
||||||
try:
|
try:
|
||||||
from routes.api import api_bp as device_bp
|
from routes.api import api_bp as device_bp
|
||||||
# 导入保存逻辑,供定时任务复用
|
from routes.api import calculate_offset
|
||||||
from routes.api import save_iot_cards_to_db, calculate_offset
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from routes.api import device_bp, save_iot_cards_to_db, calculate_offset
|
from routes.api import device_bp, calculate_offset
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}")
|
print(f"❌ 严重错误: 模块导入失败。详细信息: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 2. 路径计算 (核心修复:区分资源路径和数据路径)
|
# 2. 路径配置
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
def get_paths():
|
def get_paths():
|
||||||
"""
|
|
||||||
计算关键路径:
|
|
||||||
1. resource_base: 用于存放 web_dist (打包后在临时目录)
|
|
||||||
2. data_base: 用于存放数据库 (打包后在 exe 旁边)
|
|
||||||
"""
|
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# --- 打包环境 (PyInstaller) ---
|
|
||||||
|
|
||||||
# 资源文件在临时解压目录 (sys._MEIPASS)
|
|
||||||
resource_base = sys._MEIPASS
|
resource_base = sys._MEIPASS
|
||||||
|
|
||||||
# 数据文件(数据库)在 exe 所在目录 (sys.executable 的父目录)
|
|
||||||
data_base = os.path.dirname(sys.executable)
|
data_base = os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# --- 开发环境 ---
|
|
||||||
base = os.path.abspath(os.path.dirname(__file__))
|
base = os.path.abspath(os.path.dirname(__file__))
|
||||||
resource_base = base
|
resource_base = base
|
||||||
data_base = base
|
data_base = base
|
||||||
|
|
||||||
return resource_base, data_base
|
return resource_base, data_base
|
||||||
|
|
||||||
|
|
||||||
# 获取路径
|
|
||||||
RESOURCE_BASE, DATA_BASE = get_paths()
|
RESOURCE_BASE, DATA_BASE = get_paths()
|
||||||
|
|
||||||
# 定义具体文件夹路径
|
|
||||||
STATIC_FOLDER = os.path.join(RESOURCE_BASE, 'web_dist')
|
STATIC_FOLDER = os.path.join(RESOURCE_BASE, 'web_dist')
|
||||||
# ⚠️ 关键:强制将 instance 文件夹定位到数据目录 (exe旁边),而不是临时目录
|
|
||||||
INSTANCE_PATH = os.path.join(DATA_BASE, 'instance')
|
INSTANCE_PATH = os.path.join(DATA_BASE, 'instance')
|
||||||
|
|
||||||
# 修复 Windows MIME 类型
|
|
||||||
mimetypes.add_type('application/javascript', '.js')
|
mimetypes.add_type('application/javascript', '.js')
|
||||||
mimetypes.add_type('text/css', '.css')
|
mimetypes.add_type('text/css', '.css')
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 3. 定时任务逻辑 (保持不变)
|
# 3. 核心定时任务逻辑 (加强版)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
def auto_monitor_job(app):
|
def auto_monitor_job(app):
|
||||||
"""定时任务具体执行逻辑"""
|
"""
|
||||||
|
每天 12:00 触发的爬虫任务
|
||||||
|
"""
|
||||||
|
# ✅ 强制使用应用上下文,确保数据库连接有效
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
# 获取当前北京时间用于日志
|
||||||
|
tz = pytz.timezone('Asia/Shanghai')
|
||||||
|
now_str = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
print(f"\n{'=' * 50}")
|
||||||
|
print(f"⏰ [定时任务触发] 北京时间: {now_str}")
|
||||||
|
print(f"🚀 正在开始执行爬虫逻辑...")
|
||||||
|
|
||||||
|
if not execute_monitor_task:
|
||||||
|
print("❌ 错误: 未找到爬虫执行函数 (execute_monitor_task)")
|
||||||
|
return
|
||||||
|
|
||||||
# --- 任务 A: 爬虫更新 ---
|
|
||||||
if execute_monitor_task:
|
|
||||||
try:
|
try:
|
||||||
|
# 1. 执行爬取
|
||||||
task_result = execute_monitor_task()
|
task_result = execute_monitor_task()
|
||||||
if task_result:
|
|
||||||
|
if not task_result:
|
||||||
|
print("⚠️ [警告] 爬虫执行完毕,但返回空数据 (None)")
|
||||||
|
return
|
||||||
|
|
||||||
scraped_list = task_result.get('device_list', [])
|
scraped_list = task_result.get('device_list', [])
|
||||||
|
print(f"📦 [数据获取] 爬虫返回了 {len(scraped_list)} 条设备数据")
|
||||||
|
|
||||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
count = 0
|
success_count = 0
|
||||||
|
|
||||||
|
# 2. 遍历入库
|
||||||
for item in scraped_list:
|
for item in scraped_list:
|
||||||
d_name = item.get('name')
|
d_name = item.get('name')
|
||||||
if not d_name: continue
|
if not d_name: continue
|
||||||
|
|
||||||
|
# 查找或新建设备
|
||||||
device = Device.query.filter_by(name=d_name).first()
|
device = Device.query.filter_by(name=d_name).first()
|
||||||
if not device:
|
if not device:
|
||||||
|
print(f"🆕 发现新设备: {d_name},正在创建...")
|
||||||
device = Device(name=d_name, source=item.get('source'), install_site="")
|
device = Device(name=d_name, source=item.get('source'), install_site="")
|
||||||
db.session.add(device)
|
db.session.add(device)
|
||||||
db.session.flush()
|
db.session.flush() # 立即获取 ID
|
||||||
|
|
||||||
|
# 更新设备状态表
|
||||||
device.status = item.get('status')
|
device.status = item.get('status')
|
||||||
device.current_value = item.get('value')
|
device.current_value = item.get('value')
|
||||||
device.latest_time = item.get('target_time')
|
device.latest_time = item.get('target_time')
|
||||||
device.check_time = current_time
|
device.check_time = current_time # 更新检查时间证明爬过了
|
||||||
|
|
||||||
# =========== ✅ 核心修复开始:防止丢失 bound_iccid ===========
|
f_count = item.get('num_files', 0)
|
||||||
|
device.file_count = f_count
|
||||||
|
|
||||||
# 1. 准备容器:先读取数据库里现有的 JSON 数据
|
# 计算 offset
|
||||||
|
device.offset = calculate_offset(item.get('target_time'))
|
||||||
|
|
||||||
|
# JSON 字段合并逻辑
|
||||||
old_json = {}
|
old_json = {}
|
||||||
try:
|
try:
|
||||||
if device.json_data:
|
if device.json_data:
|
||||||
old_json = json.loads(device.json_data)
|
old_json = json.loads(device.json_data)
|
||||||
except Exception:
|
except:
|
||||||
old_json = {}
|
old_json = {}
|
||||||
|
|
||||||
# 2. 获取爬虫新数据
|
|
||||||
new_json = item.get('raw_json', {})
|
new_json = item.get('raw_json', {})
|
||||||
|
|
||||||
# 3. 合并数据:只更新新爬取到的字段,保留 old_json 里的 bound_iccid
|
|
||||||
if isinstance(new_json, dict):
|
if isinstance(new_json, dict):
|
||||||
old_json.update(new_json)
|
old_json.update(new_json)
|
||||||
|
|
||||||
# 4. 存回数据库
|
|
||||||
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||||
|
|
||||||
# =========== ✅ 核心修复结束 ===========
|
# ✅ 3. 写入历史记录 (这是数据留存的关键)
|
||||||
|
history_entry = DeviceHistory(
|
||||||
device.offset = calculate_offset(item.get('target_time'))
|
|
||||||
|
|
||||||
db.session.add(DeviceHistory(
|
|
||||||
device_id=device.id,
|
device_id=device.id,
|
||||||
status=device.status,
|
status=device.status,
|
||||||
result_data=device.current_value,
|
result_data=device.current_value,
|
||||||
data_time=item.get('target_time'),
|
data_time=item.get('target_time'), # 文件的时间
|
||||||
json_data=device.json_data
|
json_data=device.json_data,
|
||||||
))
|
file_count=f_count,
|
||||||
count += 1
|
create_time=datetime.now() # 记录入库时的系统时间
|
||||||
print(f"✅ [定时任务-爬虫] 更新 {count} 台")
|
)
|
||||||
else:
|
db.session.add(history_entry)
|
||||||
print("⚠️ [定时任务-爬虫] 未获取到数据")
|
success_count += 1
|
||||||
|
|
||||||
|
# ✅ 4. 显式提交事务
|
||||||
|
print(f"💾 正在提交事务到数据库...")
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ [成功] 已更新 {success_count} 台设备,并写入历史记录。")
|
||||||
|
print(f"{'=' * 50}\n")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ [定时任务-爬虫] 异常: {e}")
|
db.session.rollback() # 出错回滚
|
||||||
|
print(f"❌ [严重异常] 定时任务执行失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
@ -155,27 +157,18 @@ def auto_monitor_job(app):
|
|||||||
# 4. Flask 应用工厂
|
# 4. Flask 应用工厂
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
def create_app():
|
def create_app():
|
||||||
# ⚠️ 关键修改:显式传入 instance_path,告诉 Flask 去哪里找/存 数据库文件
|
|
||||||
app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH)
|
app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# 1. 确保 instance 目录存在 (在 exe 旁边创建文件夹)
|
# 数据库路径配置
|
||||||
if not os.path.exists(app.instance_path):
|
if not os.path.exists(app.instance_path):
|
||||||
try:
|
|
||||||
os.makedirs(app.instance_path, exist_ok=True)
|
os.makedirs(app.instance_path, exist_ok=True)
|
||||||
print(f"📁 已创建数据目录: {app.instance_path}")
|
|
||||||
except OSError as e:
|
|
||||||
print(f"❌ 无法创建数据目录 {app.instance_path}: {e}")
|
|
||||||
|
|
||||||
# 2. 加载配置
|
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
# ⚠️ 关键修改:强制重写数据库 URI,确保使用绝对路径
|
db_name = 'monitor_data.db'
|
||||||
# 即使 Config 里写了,这里也要确保它指向我们刚才计算出的 INSTANCE_PATH
|
|
||||||
db_name = 'monitor_data.db' # 你的数据库文件名
|
|
||||||
db_path = os.path.join(app.instance_path, db_name)
|
db_path = os.path.join(app.instance_path, db_name)
|
||||||
|
|
||||||
# Windows下路径分隔符处理,防止报错
|
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||||
else:
|
else:
|
||||||
@ -183,36 +176,43 @@ def create_app():
|
|||||||
|
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
print(f"💾 数据库路径锁定为: {db_path}")
|
# ✅ APScheduler 配置
|
||||||
|
app.config['SCHEDULER_API_ENABLED'] = True
|
||||||
|
app.config['SCHEDULER_TIMEZONE'] = "Asia/Shanghai" # 全局时区设置
|
||||||
|
|
||||||
# 3. 初始化扩展
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
scheduler = APScheduler()
|
scheduler = APScheduler()
|
||||||
scheduler.init_app(app)
|
scheduler.init_app(app)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
# 4. 添加定时任务 (每天 12:00)
|
# ✅ 添加定时任务 (针对常开机环境的最稳配置)
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
id='daily_monitor_task',
|
id='daily_monitor_task',
|
||||||
func=auto_monitor_job,
|
func=auto_monitor_job,
|
||||||
args=[app],
|
args=[app],
|
||||||
trigger='cron',
|
trigger='cron',
|
||||||
hour=12,
|
hour=12, # 每天 12 点
|
||||||
minute=0
|
minute=0,
|
||||||
|
second=0,
|
||||||
|
misfire_grace_time=3600, # 允许延迟1小时执行
|
||||||
|
timezone=pytz.timezone('Asia/Shanghai') # 再次强制指定时区
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5. 注册路由蓝图
|
# 打印一下确认任务已添加
|
||||||
|
print(f"📅 定时任务已锁定: 每天北京时间 12:00 执行")
|
||||||
|
|
||||||
app.register_blueprint(device_bp)
|
app.register_blueprint(device_bp)
|
||||||
|
|
||||||
# -------------------------------------------------
|
# 手动触发测试接口 (保留以备不时之需)
|
||||||
# 前端路由支持
|
@app.route('/api/force_run')
|
||||||
# -------------------------------------------------
|
def force_run_task():
|
||||||
|
auto_monitor_job(app)
|
||||||
|
return jsonify({'code': 200, 'msg': '手动触发成功'})
|
||||||
|
|
||||||
|
# 前端路由
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def serve_index():
|
def serve_index():
|
||||||
index_path = os.path.join(app.static_folder, 'index.html')
|
|
||||||
if not os.path.exists(index_path):
|
|
||||||
return f"❌ 错误: 前端文件丢失 ({index_path})", 404
|
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
@ -220,14 +220,11 @@ def create_app():
|
|||||||
file_path = os.path.join(app.static_folder, path)
|
file_path = os.path.join(app.static_folder, path)
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
return send_from_directory(app.static_folder, path)
|
return send_from_directory(app.static_folder, path)
|
||||||
|
if path.startswith('api'):
|
||||||
if path.startswith('api') or path.startswith('static'):
|
|
||||||
return jsonify({'code': 404, 'message': 'Not Found'}), 404
|
return jsonify({'code': 404, 'message': 'Not Found'}), 404
|
||||||
|
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# 自动创建表结构
|
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
@ -236,6 +233,9 @@ def create_app():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = create_app()
|
app = create_app()
|
||||||
debug_mode = not getattr(sys, 'frozen', False)
|
debug_mode = not getattr(sys, 'frozen', False)
|
||||||
print("🚀 服务启动中...")
|
|
||||||
# use_reloader=False 防止定时任务执行两次
|
print("🚀 服务启动中 (24小时常驻模式)...")
|
||||||
|
|
||||||
|
# ✅ 关键设置: use_reloader=False
|
||||||
|
# 防止 Flask 的热重载功能启动两个进程,导致定时任务跑两遍或者被意外杀掉
|
||||||
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)
|
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)
|
||||||
@ -10,10 +10,10 @@ def get_base_path():
|
|||||||
|
|
||||||
|
|
||||||
def get_static_path():
|
def get_static_path():
|
||||||
"""获取 dist 静态资源路径"""
|
"""获取 web_dist 静态资源路径"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
return os.path.join(sys._MEIPASS, 'dist')
|
return os.path.join(sys._MEIPASS, 'web_dist')
|
||||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dist')
|
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_dist')
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
|
# models.py
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
|
||||||
from extensions import db
|
from extensions import db
|
||||||
|
|
||||||
|
|
||||||
class Device(db.Model):
|
class Device(db.Model):
|
||||||
__tablename__ = 'devices'
|
__tablename__ = 'devices'
|
||||||
|
|
||||||
@ -18,11 +19,17 @@ class Device(db.Model):
|
|||||||
reason = db.Column(db.String(255))
|
reason = db.Column(db.String(255))
|
||||||
offset = db.Column(db.String(50))
|
offset = db.Column(db.String(50))
|
||||||
|
|
||||||
|
# ✅ 新增字段:文件数量
|
||||||
|
file_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
# 手动录入字段(受保护,run_monitor 不主动覆盖)
|
# 手动录入字段(受保护,run_monitor 不主动覆盖)
|
||||||
install_site = db.Column(db.String(100), default="")
|
install_site = db.Column(db.String(100), default="")
|
||||||
is_maintaining = db.Column(db.Boolean, default=False)
|
is_maintaining = db.Column(db.Boolean, default=False)
|
||||||
is_hidden = db.Column(db.Boolean, default=False)
|
is_hidden = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
# 白名单字段 (根据上下文可能存在,补全以防万一)
|
||||||
|
is_whitelist = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
# 统一状态映射逻辑
|
# 统一状态映射逻辑
|
||||||
api_status = 'offline' if self.status in ['离线', '异常', '已离线'] else 'online'
|
api_status = 'offline' if self.status in ['离线', '异常', '已离线'] else 'online'
|
||||||
@ -38,9 +45,12 @@ class Device(db.Model):
|
|||||||
'install_site': self.install_site or '',
|
'install_site': self.install_site or '',
|
||||||
'is_maintaining': self.is_maintaining,
|
'is_maintaining': self.is_maintaining,
|
||||||
'is_hidden': self.is_hidden,
|
'is_hidden': self.is_hidden,
|
||||||
'offset': self.offset
|
'is_whitelist': self.is_whitelist,
|
||||||
|
'offset': self.offset,
|
||||||
|
'file_count': self.file_count # ✅ 返回给前端
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceHistory(db.Model):
|
class DeviceHistory(db.Model):
|
||||||
__tablename__ = 'device_history'
|
__tablename__ = 'device_history'
|
||||||
|
|
||||||
@ -53,8 +63,12 @@ class DeviceHistory(db.Model):
|
|||||||
json_data = db.Column(db.Text)
|
json_data = db.Column(db.Text)
|
||||||
file_path = db.Column(db.String(255))
|
file_path = db.Column(db.String(255))
|
||||||
|
|
||||||
|
# ✅ 新增字段:历史记录文件数量
|
||||||
|
file_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
recorded_at = db.Column(db.DateTime, default=datetime.now)
|
recorded_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceLog(db.Model):
|
class MaintenanceLog(db.Model):
|
||||||
__tablename__ = 'maintenance_logs'
|
__tablename__ = 'maintenance_logs'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|||||||
@ -255,6 +255,7 @@ def devices_overview():
|
|||||||
data_list = []
|
data_list = []
|
||||||
|
|
||||||
for d in devices:
|
for d in devices:
|
||||||
|
# 关键:d.to_dict() 在 models.py 中应包含 file_count
|
||||||
item = d.to_dict()
|
item = d.to_dict()
|
||||||
|
|
||||||
# 强制格式化时间
|
# 强制格式化时间
|
||||||
@ -422,6 +423,10 @@ def run_monitor():
|
|||||||
device.latest_time = target_time
|
device.latest_time = target_time
|
||||||
device.check_time = current_time
|
device.check_time = current_time
|
||||||
|
|
||||||
|
# ✅ [核心修改] 获取爬虫返回的文件数量并保存
|
||||||
|
f_count = item.get('num_files', 0)
|
||||||
|
device.file_count = f_count
|
||||||
|
|
||||||
old_json = {}
|
old_json = {}
|
||||||
try:
|
try:
|
||||||
if device.json_data:
|
if device.json_data:
|
||||||
@ -436,12 +441,14 @@ def run_monitor():
|
|||||||
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||||
device.offset = calculate_offset(device.latest_time)
|
device.offset = calculate_offset(device.latest_time)
|
||||||
|
|
||||||
|
# ✅ [核心修改] 写入历史记录时包含 file_count
|
||||||
new_history = DeviceHistory(
|
new_history = DeviceHistory(
|
||||||
device_id=device.id,
|
device_id=device.id,
|
||||||
status=item.get('status'),
|
status=item.get('status'),
|
||||||
result_data=item.get('value'),
|
result_data=item.get('value'),
|
||||||
data_time=target_time,
|
data_time=target_time,
|
||||||
json_data=device.json_data
|
json_data=device.json_data,
|
||||||
|
file_count=f_count # 确保历史数据也记录文件数
|
||||||
)
|
)
|
||||||
db.session.add(new_history)
|
db.session.add(new_history)
|
||||||
count_crawler += 1
|
count_crawler += 1
|
||||||
@ -637,3 +644,53 @@ def delete_log_entry():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'code': 200})
|
return jsonify({'code': 200})
|
||||||
return jsonify({'code': 404})
|
return jsonify({'code': 404})
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/device_history_list', methods=['GET'])
|
||||||
|
def get_device_history_list():
|
||||||
|
try:
|
||||||
|
name = request.args.get('name')
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 10))
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({'code': 400, 'message': '缺少设备名称'})
|
||||||
|
|
||||||
|
# 1. 找到设备ID
|
||||||
|
device = Device.query.filter_by(name=name).first()
|
||||||
|
if not device:
|
||||||
|
return jsonify({'code': 404, 'message': '设备不存在'})
|
||||||
|
|
||||||
|
# 2. 查询历史记录 (按时间倒序)
|
||||||
|
query = DeviceHistory.query.filter_by(device_id=device.id).order_by(desc(DeviceHistory.data_time))
|
||||||
|
|
||||||
|
# 3. 获取总数
|
||||||
|
total = query.count()
|
||||||
|
|
||||||
|
# 4. 分页切片
|
||||||
|
history_list = query.offset((page - 1) * limit).limit(limit).all()
|
||||||
|
|
||||||
|
# 5. 格式化返回数据
|
||||||
|
data = []
|
||||||
|
for h in history_list:
|
||||||
|
# 简单处理日期格式,只取日期部分,或者保留完整时间视需求而定
|
||||||
|
# 这里假设 data_time 格式为 "YYYY-MM-DD HH:MM:SS" 或 "YYYY_MM_DD..."
|
||||||
|
date_str = h.data_time
|
||||||
|
if not date_str:
|
||||||
|
date_str = h.recorded_at.strftime("%Y-%m-%d %H:%M:%S") if h.recorded_at else "未知"
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
'date': date_str,
|
||||||
|
'count': h.file_count or 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'code': 200,
|
||||||
|
'data': data,
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'limit': limit
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'code': 500, 'message': str(e)})
|
||||||
@ -7,11 +7,11 @@ web_bp = Blueprint('web', __name__)
|
|||||||
|
|
||||||
@web_bp.route('/')
|
@web_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""访问根路径时,返回 dist/index.html"""
|
"""访问根路径时,返回 web_dist/index.html"""
|
||||||
try:
|
try:
|
||||||
return send_from_directory(get_static_path(), 'index.html')
|
return send_from_directory(get_static_path(), 'index.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"前端资源未找到,请确认 dist 文件夹是否存在。错误信息: {e}", 404
|
return f"前端资源未找到,请确认 web_dist 文件夹是否存在。错误信息: {e}", 404
|
||||||
|
|
||||||
@web_bp.route('/<path:path>')
|
@web_bp.route('/<path:path>')
|
||||||
def static_files(path):
|
def static_files(path):
|
||||||
|
|||||||
@ -1,8 +1,27 @@
|
|||||||
# services/core.py
|
# services/core.py
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from .crawler_106 import run_106_logic
|
import traceback
|
||||||
from .crawler_82 import run_82_logic
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 动态导入,防止文件缺失导致整个程序启动失败
|
||||||
|
try:
|
||||||
|
from .crawler_106 import run_106_logic
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ 警告: 未找到 crawler_106 模块")
|
||||||
|
|
||||||
|
|
||||||
|
def run_106_logic():
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .crawler_82 import run_82_logic
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ 警告: 未找到 crawler_82 模块")
|
||||||
|
|
||||||
|
|
||||||
|
def run_82_logic():
|
||||||
|
return []
|
||||||
|
|
||||||
task_lock = threading.Lock()
|
task_lock = threading.Lock()
|
||||||
|
|
||||||
@ -12,26 +31,65 @@ def execute_monitor_task():
|
|||||||
执行所有爬虫,返回一个大列表:
|
执行所有爬虫,返回一个大列表:
|
||||||
{'device_list': [item1, item2...], 'target_time': '...'}
|
{'device_list': [item1, item2...], 'target_time': '...'}
|
||||||
"""
|
"""
|
||||||
|
# 1. 锁机制:防止任务重复运行
|
||||||
if task_lock.locked():
|
if task_lock.locked():
|
||||||
logging.warning(">>> 任务正在运行中,跳过")
|
logging.warning(">>> 任务正在运行中,跳过")
|
||||||
|
print(">>> ⚠️ 任务正在运行中,本次请求跳过")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with task_lock:
|
with task_lock:
|
||||||
logging.info(">>> 开始执行监控任务...")
|
logging.info(">>> 开始执行监控任务...")
|
||||||
|
print(f"--- [任务开始] {datetime.now().strftime('%H:%M:%S')} ---")
|
||||||
|
|
||||||
# 1. 获取 106 数据列表
|
all_results = []
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 2. 执行 106 爬虫
|
||||||
|
# ==========================
|
||||||
|
try:
|
||||||
list_106 = run_106_logic()
|
list_106 = run_106_logic()
|
||||||
|
if list_106:
|
||||||
|
count = len(list_106)
|
||||||
|
print(f"✅ 106爬虫获取数据: {count} 条")
|
||||||
|
|
||||||
# 2. 获取 82 数据列表
|
# 🔍 [调试] 打印第一条数据,确认 num_files 是否存在
|
||||||
|
if count > 0:
|
||||||
|
first = list_106[0]
|
||||||
|
print(f" [调试检查] 106样本: {first.get('name')} | num_files={first.get('num_files')}")
|
||||||
|
|
||||||
|
all_results.extend(list_106)
|
||||||
|
else:
|
||||||
|
print("⚠️ 106爬虫未返回数据")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 106爬虫执行失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 3. 执行 82 爬虫
|
||||||
|
# ==========================
|
||||||
|
try:
|
||||||
list_82 = run_82_logic()
|
list_82 = run_82_logic()
|
||||||
|
if list_82:
|
||||||
|
print(f"✅ 82爬虫获取数据: {len(list_82)} 条")
|
||||||
|
|
||||||
# 3. 合并
|
# 🛠️ [补全] 82爬虫没有文件数概念,手动补0,防止入库报错
|
||||||
combined_list = list_106 + list_82
|
for item in list_82:
|
||||||
|
if 'num_files' not in item:
|
||||||
|
item['num_files'] = 0
|
||||||
|
|
||||||
logging.info(f">>> 任务完成,共获取 {len(combined_list)} 条数据")
|
all_results.extend(list_82)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 82爬虫执行失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 4. 汇总返回
|
||||||
|
# ==========================
|
||||||
|
logging.info(f">>> 任务完成,共获取 {len(all_results)} 条数据")
|
||||||
|
print(f"--- [任务结束] 总计获取: {len(all_results)} 台设备 ---")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'device_list': combined_list,
|
'device_list': all_results,
|
||||||
'target_time': None, # 具体时间已在 item 里
|
'target_time': None, # 具体时间已在 item['target_time'] 里
|
||||||
'temp_file_path': None # 废弃旧逻辑,文件路径已在 item 里
|
'temp_file_path': None # 废弃旧逻辑,文件路径已在 item['temp_file'] 里
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ def run_106_logic():
|
|||||||
"""返回 result_list, 每个元素是一个字典"""
|
"""返回 result_list, 每个元素是一个字典"""
|
||||||
results = []
|
results = []
|
||||||
print(">>> [106爬虫] 启动...")
|
print(">>> [106爬虫] 启动...")
|
||||||
today_str = datetime.now().strftime("%Y_%m_%d")
|
# today_str = datetime.now().strftime("%Y_%m_%d") # ❌ 移除严格的“今天”判断
|
||||||
main_headers = {"Authorization": CONFIG["primary_auth"], "User-Agent": "Mozilla/5.0"}
|
main_headers = {"Authorization": CONFIG["primary_auth"], "User-Agent": "Mozilla/5.0"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -75,7 +75,8 @@ def run_106_logic():
|
|||||||
'value': '',
|
'value': '',
|
||||||
'target_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
'target_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'raw_json': {},
|
'raw_json': {},
|
||||||
'temp_file': None
|
'temp_file': None,
|
||||||
|
'num_files': 0 # ✅ 默认值
|
||||||
}
|
}
|
||||||
|
|
||||||
if str(item.get('status')).lower() != 'online':
|
if str(item.get('status')).lower() != 'online':
|
||||||
@ -96,29 +97,40 @@ def run_106_logic():
|
|||||||
headers = {"Authorization": CONFIG["primary_auth"], "x-auth": token}
|
headers = {"Authorization": CONFIG["primary_auth"], "x-auth": token}
|
||||||
api_root = "/api/resources/Data/" if is_tower_underscore else "/api/resources/data/"
|
api_root = "/api/resources/Data/" if is_tower_underscore else "/api/resources/data/"
|
||||||
|
|
||||||
|
# 1. 获取日期列表
|
||||||
res1 = requests.get(f"http://106.75.72.40:{port}{api_root}", headers=headers, timeout=10)
|
res1 = requests.get(f"http://106.75.72.40:{port}{api_root}", headers=headers, timeout=10)
|
||||||
best_date = find_closest_item(res1.json().get('items', []), True)
|
best_date = find_closest_item(res1.json().get('items', []), True)
|
||||||
|
|
||||||
if not best_date or best_date[2] != today_str:
|
# ✅ 修改点:如果找不到任何日期文件夹,才报错。否则,即使是旧日期也继续往下走。
|
||||||
data_packet['value'] = "未找到今日文件夹"
|
if not best_date:
|
||||||
data_packet['target_time'] = best_date[2] if best_date else "N/A"
|
data_packet['value'] = "未找到任何日期文件夹"
|
||||||
results.append(data_packet)
|
results.append(data_packet)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data_packet['target_time'] = best_date[2] # 实际数据时间
|
data_packet['target_time'] = best_date[2] # 记录找到的那个日期 (比如 2026_02_02)
|
||||||
date_path = f"{api_root}{best_date[2]}/"
|
date_path = f"{api_root}{best_date[2]}/"
|
||||||
|
|
||||||
|
# 2. 请求具体日期的文件夹内容 (这一步能获取 numFiles)
|
||||||
res2 = requests.get(f"http://106.75.72.40:{port}{date_path}", headers=headers, timeout=10)
|
res2 = requests.get(f"http://106.75.72.40:{port}{date_path}", headers=headers, timeout=10)
|
||||||
best_file = find_closest_item(res2.json().get('items', []), False)
|
folder_data = res2.json() # 获取完整JSON
|
||||||
|
|
||||||
|
# ✅ 核心:提取 numFiles (只要请求成功,这里一定能拿到)
|
||||||
|
file_count = folder_data.get('numFiles', 0)
|
||||||
|
data_packet['num_files'] = file_count
|
||||||
|
print(f" -> {name}: 找到日期 {best_date[2]}, 文件数: {file_count}")
|
||||||
|
|
||||||
|
# 3. 找该文件夹里最新的文件
|
||||||
|
best_file = find_closest_item(folder_data.get('items', []), False)
|
||||||
|
|
||||||
if not best_file:
|
if not best_file:
|
||||||
data_packet['value'] = "今日文件夹为空"
|
data_packet['value'] = "文件夹为空" # 这种情况下 numFiles 应该是 0
|
||||||
results.append(data_packet)
|
results.append(data_packet)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_item = best_file[1]
|
file_item = best_file[1]
|
||||||
full_path = file_item.get('path') or f"{date_path}{file_item.get('name')}"
|
full_path = file_item.get('path') or f"{date_path}{file_item.get('name')}"
|
||||||
|
|
||||||
# 核心逻辑:获取内容
|
# 4. 下载/读取内容逻辑
|
||||||
if is_tower_i:
|
if is_tower_i:
|
||||||
# 下载二进制文件
|
# 下载二进制文件
|
||||||
download_url = f"http://106.75.72.40:{port}/api/raw{full_path}"
|
download_url = f"http://106.75.72.40:{port}/api/raw{full_path}"
|
||||||
@ -129,9 +141,9 @@ def run_106_logic():
|
|||||||
with open(temp_path, 'wb') as f:
|
with open(temp_path, 'wb') as f:
|
||||||
f.write(res3.content)
|
f.write(res3.content)
|
||||||
|
|
||||||
data_packet['temp_file'] = temp_path # 🔥 传递给API
|
data_packet['temp_file'] = temp_path
|
||||||
data_packet['value'] = f"Binary Downloaded: {len(res3.content)} bytes"
|
data_packet['value'] = f"Binary Downloaded: {len(res3.content)} bytes"
|
||||||
data_packet['raw_json'] = file_item # 用文件属性充当RawData
|
data_packet['raw_json'] = file_item # 借用 file_item 充当 raw_json
|
||||||
else:
|
else:
|
||||||
data_packet['status'] = '异常'
|
data_packet['status'] = '异常'
|
||||||
data_packet['value'] = f"下载失败: {res3.status_code}"
|
data_packet['value'] = f"下载失败: {res3.status_code}"
|
||||||
@ -141,7 +153,7 @@ def run_106_logic():
|
|||||||
res3 = requests.get(file_api_url, headers=headers, timeout=20)
|
res3 = requests.get(file_api_url, headers=headers, timeout=20)
|
||||||
try:
|
try:
|
||||||
json_content = res3.json()
|
json_content = res3.json()
|
||||||
data_packet['raw_json'] = json_content # 🔥 完整保存
|
data_packet['raw_json'] = json_content
|
||||||
data_packet['value'] = json_content.get('content', '')
|
data_packet['value'] = json_content.get('content', '')
|
||||||
except:
|
except:
|
||||||
data_packet['value'] = "JSON解析失败"
|
data_packet['value'] = "JSON解析失败"
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="version-footer">
|
<footer class="version-footer">
|
||||||
2.4版本 © 2026 Device Monitor
|
2.5版本(加入每日数据个数) © 2026 Device Monitor
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -103,6 +103,25 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="今日文件" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip content="点击查看历史文件趋势" placement="top">
|
||||||
|
<div
|
||||||
|
class="file-count-cell"
|
||||||
|
@click="openHistoryDialog(row)"
|
||||||
|
:class="{ 'has-data': row.file_count > 0 }"
|
||||||
|
>
|
||||||
|
<el-tag v-if="row.file_count > 0" type="primary" effect="plain" round size="small">
|
||||||
|
{{ row.file_count }} 个
|
||||||
|
</el-tag>
|
||||||
|
<span v-else style="color: #ccc; font-size: 12px;">--</span>
|
||||||
|
|
||||||
|
<el-icon v-if="!row.is_hidden" class="history-icon"><Histogram /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="安装地点" min-width="140">
|
<el-table-column label="安装地点" min-width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row.isEditingSite" class="editing-cell">
|
<div v-if="row.isEditingSite" class="editing-cell">
|
||||||
@ -235,6 +254,8 @@
|
|||||||
<MaintenanceLogs ref="maintenanceLogsRef" />
|
<MaintenanceLogs ref="maintenanceLogsRef" />
|
||||||
<IoTDeviceBinder ref="iotBinderRef" @update-success="fetchData" />
|
<IoTDeviceBinder ref="iotBinderRef" @update-success="fetchData" />
|
||||||
|
|
||||||
|
<FileHistoryDialog ref="fileHistoryRef" />
|
||||||
|
|
||||||
<el-dialog v-model="showAddDialog" title="手动添加设备" width="400px" align-center>
|
<el-dialog v-model="showAddDialog" title="手动添加设备" width="400px" align-center>
|
||||||
<el-form :model="newDeviceForm" label-width="80px">
|
<el-form :model="newDeviceForm" label-width="80px">
|
||||||
<el-form-item label="设备名称">
|
<el-form-item label="设备名称">
|
||||||
@ -262,11 +283,13 @@ import { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from 'v
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Clock, DataLine, Document, Refresh, EditPen, Search, Edit, RefreshRight, Delete, RefreshLeft, SwitchButton, Warning, WarningFilled, Plus, Odometer, Link } from '@element-plus/icons-vue'
|
import { Clock, DataLine, Document, Refresh, EditPen, Search, Edit, RefreshRight, Delete, RefreshLeft, SwitchButton, Warning, WarningFilled, Plus, Odometer, Link, Histogram } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import DataMonitor from './DataMonitor.vue'
|
import DataMonitor from './DataMonitor.vue'
|
||||||
import MaintenanceLogs from './MaintenanceLogs.vue'
|
import MaintenanceLogs from './MaintenanceLogs.vue'
|
||||||
import IoTDeviceBinder from './IoTDeviceBinder.vue'
|
import IoTDeviceBinder from './IoTDeviceBinder.vue'
|
||||||
|
// ✅ 引入新组件
|
||||||
|
import FileHistoryDialog from './FileHistoryDialog.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -288,6 +311,8 @@ const API_BASE = import.meta.env.DEV ? 'http://127.0.0.1:5000' : ''
|
|||||||
const dataMonitorRef = ref(null)
|
const dataMonitorRef = ref(null)
|
||||||
const maintenanceLogsRef = ref(null)
|
const maintenanceLogsRef = ref(null)
|
||||||
const iotBinderRef = ref(null)
|
const iotBinderRef = ref(null)
|
||||||
|
// ✅ 定义新的 ref
|
||||||
|
const fileHistoryRef = ref(null)
|
||||||
|
|
||||||
const showAddDialog = ref(false)
|
const showAddDialog = ref(false)
|
||||||
const isAdding = ref(false)
|
const isAdding = ref(false)
|
||||||
@ -422,10 +447,6 @@ const fetchData = async () => {
|
|||||||
} else if (diffHours > 24) {
|
} else if (diffHours > 24) {
|
||||||
statusLabel = '滞后'; statusColor = '#E6A23C'; statusType = 'warning';
|
statusLabel = '滞后'; statusColor = '#E6A23C'; statusType = 'warning';
|
||||||
statusReason = `滞后 ${Math.floor(diffDays)} 天`;
|
statusReason = `滞后 ${Math.floor(diffDays)} 天`;
|
||||||
|
|
||||||
// === 注意:这里没有把 trafficWarning 加入到 sortWeight 或 statusType 的改变逻辑中 ===
|
|
||||||
// 从而实现了“只标黄文字,不改变行状态,不置顶”
|
|
||||||
|
|
||||||
} else if (expireWarning) {
|
} else if (expireWarning) {
|
||||||
statusLabel = '即将过期'; statusColor = '#E6A23C'; statusType = 'warning';
|
statusLabel = '即将过期'; statusColor = '#E6A23C'; statusType = 'warning';
|
||||||
statusReason = `即将过期`;
|
statusReason = `即将过期`;
|
||||||
@ -451,7 +472,8 @@ const fetchData = async () => {
|
|||||||
currentValueNum,
|
currentValueNum,
|
||||||
trafficNum,
|
trafficNum,
|
||||||
trafficWarning,
|
trafficWarning,
|
||||||
expireWarning
|
expireWarning,
|
||||||
|
file_count: item.file_count || 0 // ✅ 绑定后端返回的文件数字段
|
||||||
}
|
}
|
||||||
}).sort((a, b) => b.sortWeight - a.sortWeight)
|
}).sort((a, b) => b.sortWeight - a.sortWeight)
|
||||||
|
|
||||||
@ -500,6 +522,14 @@ const totalUsageSum = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// === 交互函数 ===
|
// === 交互函数 ===
|
||||||
|
// ✅ 新增:打开历史记录弹窗
|
||||||
|
const openHistoryDialog = (row) => {
|
||||||
|
if (row.is_hidden) return
|
||||||
|
if (fileHistoryRef.value) {
|
||||||
|
fileHistoryRef.value.open(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddDeviceSubmit = async () => { if (!newDeviceForm.name) return; await axios.post(`${API_BASE}/api/add_device`, newDeviceForm); showAddDialog.value=false; fetchData() }
|
const handleAddDeviceSubmit = async () => { if (!newDeviceForm.name) return; await axios.post(`${API_BASE}/api/add_device`, newDeviceForm); showAddDialog.value=false; fetchData() }
|
||||||
const handleDeviceClick = (row) => { if (!row.is_hidden && dataMonitorRef.value) dataMonitorRef.value.open(row) }
|
const handleDeviceClick = (row) => { if (!row.is_hidden && dataMonitorRef.value) dataMonitorRef.value.open(row) }
|
||||||
const openLogCenter = (row) => { if (maintenanceLogsRef.value) maintenanceLogsRef.value.open(row ? { deviceName: row.name } : null) }
|
const openLogCenter = (row) => { if (maintenanceLogsRef.value) maintenanceLogsRef.value.open(row ? { deviceName: row.name } : null) }
|
||||||
@ -567,6 +597,34 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateDimensions))
|
|||||||
:deep(.data-error-row) { background-color: #ffe6e6 !important; }
|
:deep(.data-error-row) { background-color: #ffe6e6 !important; }
|
||||||
:deep(.data-warning-row) { background-color: #fffbe6 !important; }
|
:deep(.data-warning-row) { background-color: #fffbe6 !important; }
|
||||||
|
|
||||||
|
/* ✅ 新增样式 */
|
||||||
|
.file-count-cell {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.file-count-cell:hover {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
.file-count-cell:hover .el-tag {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.history-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.file-count-cell:hover .history-icon {
|
||||||
|
opacity: 1;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.dashboard-container { padding: 5px; }
|
.dashboard-container { padding: 5px; }
|
||||||
.left-panel, .header-actions { width: 100%; justify-content: space-between; margin-bottom: 5px; }
|
.left-panel, .header-actions { width: 100%; justify-content: space-between; margin-bottom: 5px; }
|
||||||
|
|||||||
177
zhandianxinxi/光谱数据监控/src/views/FileHistoryDialog.vue
Normal file
177
zhandianxinxi/光谱数据监控/src/views/FileHistoryDialog.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="600px"
|
||||||
|
align-center
|
||||||
|
@closed="handleClose"
|
||||||
|
>
|
||||||
|
<div class="history-container">
|
||||||
|
<el-table
|
||||||
|
:data="paginatedData"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
height="400"
|
||||||
|
>
|
||||||
|
<el-table-column prop="date" label="数据日期" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-icon><Calendar /></el-icon> {{ row.dateDisplay }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="count" label="文件个数" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.count > 0 ? 'primary' : 'info'" effect="plain">
|
||||||
|
{{ row.count }} 个
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-config-provider :locale="zhCn">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
background
|
||||||
|
/>
|
||||||
|
</el-config-provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Calendar } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, ElConfigProvider } from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentDeviceName = ref('')
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
|
||||||
|
// 数据源
|
||||||
|
const allTableData = ref([]) // 存放去重、排序后的所有数据
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0) // 这里的 total 是去重后的总天数
|
||||||
|
|
||||||
|
const API_BASE = import.meta.env.DEV ? 'http://127.0.0.1:5000' : ''
|
||||||
|
|
||||||
|
// 格式化工具:提取 YYYY-MM-DD
|
||||||
|
const getDayKey = (raw) => {
|
||||||
|
if (!raw) return ''
|
||||||
|
// 兼容 2026_02_03 和 2026-02-03 格式,且去掉时分秒
|
||||||
|
return raw.replace(/_/g, '-').split(' ')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性:实现前端分页切片
|
||||||
|
// 这里的逻辑是:从“总数据”中,切出“当前页”需要显示的那几条
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return allTableData.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = (device) => {
|
||||||
|
if (!device || !device.name) return
|
||||||
|
currentDeviceName.value = device.name
|
||||||
|
dialogTitle.value = `📜 ${formatDisplayName(device.name)} - 历史文件记录`
|
||||||
|
visible.value = true
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 关键点:为了让前端能准确去重和排序,这里 limit 设得很大,意图拉取“全部”或“近期所有”数据
|
||||||
|
// 如果不拉全量数据,就无法保证跨页去重的准确性
|
||||||
|
const res = await axios.get(`${API_BASE}/api/device_history_list`, {
|
||||||
|
params: {
|
||||||
|
name: currentDeviceName.value,
|
||||||
|
page: 1,
|
||||||
|
limit: 1000 // 拉取足够多的数据在前端处理
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const rawList = res.data.data || []
|
||||||
|
|
||||||
|
// 1. 分组去重:同一天取 count 最大的
|
||||||
|
const dateMap = new Map()
|
||||||
|
|
||||||
|
rawList.forEach(item => {
|
||||||
|
const dayStr = getDayKey(item.date)
|
||||||
|
if (!dayStr) return
|
||||||
|
|
||||||
|
if (dateMap.has(dayStr)) {
|
||||||
|
const exist = dateMap.get(dayStr)
|
||||||
|
// 如果当前数据的 count 比已记录的大,就替换掉
|
||||||
|
if (item.count > exist.count) {
|
||||||
|
// 保留原始 item,并附加格式化好的日期方便展示
|
||||||
|
dateMap.set(dayStr, { ...item, dateDisplay: dayStr })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dateMap.set(dayStr, { ...item, dateDisplay: dayStr })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 转为数组
|
||||||
|
const processedList = Array.from(dateMap.values())
|
||||||
|
|
||||||
|
// 3. 强制排序:按日期字符串降序 (2026-02-03 -> 2026-02-01)
|
||||||
|
processedList.sort((a, b) => {
|
||||||
|
return b.dateDisplay.localeCompare(a.dateDisplay)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 赋值
|
||||||
|
allTableData.value = processedList
|
||||||
|
|
||||||
|
// 5. 修正 Total:现在的 Total 是“有多少个不同的日期”,而不是后端的 raw total
|
||||||
|
total.value = processedList.length
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '获取历史记录失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
ElMessage.error('网络请求异常')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
allTableData.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDisplayName = (name) => name ? name.toUpperCase().replace(/_/g, ' ') : ''
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.history-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.pagination-wrapper {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user