打包上传的2.0版本

This commit is contained in:
YueL1331
2026-01-09 12:48:50 +08:00
parent ca895af384
commit ffbd494b7b
14 changed files with 240 additions and 162 deletions

View File

@ -1,149 +0,0 @@
import os
import sys
import json
from datetime import datetime
from flask import Flask
from flask_apscheduler import APScheduler # ✅ 新增:定时任务控制器
from extensions import db, cors
from models import Device, DeviceHistory, MaintenanceLog
from routes.api import api_bp
# 尝试导入爬虫核心逻辑,以便定时任务调用
try:
from services.core import execute_monitor_task
except ImportError:
execute_monitor_task = None
# 解决 Windows 下控制台输出乱码问题
sys.stdout.reconfigure(encoding='utf-8')
# 初始化调度器
scheduler = APScheduler()
def auto_monitor_job(app):
"""定时任务执行逻辑"""
with app.app_context():
print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if not execute_monitor_task:
print("❌ 错误: 找不到 services.core.execute_monitor_task 模块")
return
try:
# 1. 执行爬取任务
task_result = execute_monitor_task()
if not task_result:
print("⚠️ [定时任务] 爬虫未返回数据,跳过更新")
return
scraped_list = task_result.get('device_list', [])
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 2. 这里的逻辑需要和 api.py 保持高度一致(复用更新逻辑)
# 为了防止代码重复,建议以后将此逻辑封装在 services 里的一个独立函数中
from routes.api import calculate_offset
count = 0
for item in scraped_list:
d_name = item.get('name')
if not d_name: continue
device = Device.query.filter_by(name=d_name).first()
if not device:
device = Device(name=d_name, source=item.get('source'), install_site="")
db.session.add(device)
db.session.flush()
# 更新动态字段
device.status = item.get('status')
device.current_value = item.get('value')
device.latest_time = item.get('target_time')
device.check_time = current_time
device.json_data = json.dumps(item.get('raw_json', {}), ensure_ascii=False)
device.offset = calculate_offset(item.get('target_time'))
# 记录历史
db.session.add(DeviceHistory(
device_id=device.id,
status=device.status,
result_data=device.current_value,
data_time=item.get('target_time'),
json_data=device.json_data
))
count += 1
db.session.commit()
print(f"✅ [定时任务] 成功自动更新 {count} 台设备数据")
except Exception as e:
db.session.rollback()
print(f"❌ [定时任务] 运行出错: {str(e)}")
def create_app():
app = Flask(__name__)
# 1. 配置路径与数据库
basedir = os.path.abspath(os.path.dirname(__file__))
instance_path = os.path.join(basedir, 'instance')
if not os.path.exists(instance_path):
os.makedirs(instance_path)
db_path = os.path.join(instance_path, 'devices.db')
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['JSON_AS_ASCII'] = False
# 定时任务配置
app.config['SCHEDULER_API_ENABLED'] = True
app.config['SCHEDULER_TIMEZONE'] = "Asia/Shanghai" # 设置时区
# 2. 初始化插件
cors.init_app(app)
db.init_app(app)
# 3. 初始化并启动调度器
scheduler.init_app(app)
# 4. 注册定时任务:每天 10:00 运行
# 如果你想测试是否生效,可以暂时把 hour=10 改为每隔一分钟运行一次trigger='interval', minutes=1
scheduler.add_job(
id='daily_crawl_task',
func=auto_monitor_job,
args=[app],
trigger='cron',
hour=10,
minute=0
)
scheduler.start()
# 5. 注册蓝图
app.register_blueprint(api_bp)
# 6. 初始化数据库表
with app.app_context():
db.create_all()
return app
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {
'db': db,
'Device': Device,
'DeviceHistory': DeviceHistory,
'MaintenanceLog': MaintenanceLog
}
if __name__ == '__main__':
print("🚀 服务正在启动: http://127.0.0.1:5000")
print("⏰ 定时任务已就绪:每天 10:00 自动执行爬取")
# 注意:在生产环境中使用 debug=True 会导致调度器运行两次,建议生产环境设为 False
app.run(debug=False, host='0.0.0.0', port=5000)

202
2_1banben/app.py Normal file
View File

@ -0,0 +1,202 @@
import os
import sys
import json
import logging
import mimetypes
from datetime import datetime
from flask import Flask, send_from_directory, jsonify
from flask_cors import CORS
from flask_apscheduler import APScheduler
# ==============================================================================
# ✅ 1. 核心模块引用
# ==============================================================================
try:
# 数据库实例 (在根目录 extensions.py 中)
from extensions import db
# 数据模型 (在根目录 models.py 中)
from models import Device, DeviceHistory, MaintenanceLog
# 核心业务逻辑 (在 services/core.py 中)
from services.core import execute_monitor_task
# 路由蓝图 (在 routes/api.py 中)
try:
from routes.api import api_bp as device_bp
except ImportError:
from routes.api import device_bp
# 工具函数 (在 routes/api.py 中)
from routes.api import calculate_offset
except ImportError as e:
print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}")
print(f"系统路径: {sys.path}")
sys.exit(1)
# ==============================================================================
# 2. 路径计算模块 (兼容 PyInstaller 打包)
# ==============================================================================
def get_base_path():
"""获取运行时基准路径,兼容开发环境和打包环境"""
if getattr(sys, 'frozen', False):
if hasattr(sys, '_MEIPASS'):
return sys._MEIPASS # --onefile 模式
else:
return os.path.dirname(os.path.abspath(sys.executable)) # --onedir 模式
else:
return os.path.abspath(os.path.dirname(__file__))
BASE_DIR = get_base_path()
STATIC_FOLDER = os.path.join(BASE_DIR, 'web_dist')
INSTANCE_FOLDER = os.path.join(BASE_DIR, 'instance')
DB_PATH = os.path.join(INSTANCE_FOLDER, 'devices.db')
# 修复 Windows 下注册表 MIME 类型缺失导致网页白屏的问题
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('text/css', '.css')
print(f"🚀 运行环境: {'Packaged' if getattr(sys, 'frozen', False) else 'Dev'}")
print(f"📂 基准路径: {BASE_DIR}")
print(f"💾 数据库路径: {DB_PATH}")
# ==============================================================================
# 3. 定时任务逻辑
# ==============================================================================
def auto_monitor_job(app):
"""定时任务具体执行逻辑"""
with app.app_context():
print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if not execute_monitor_task:
print("❌ 错误: 无法加载爬虫核心模块 (execute_monitor_task is Missing)")
return
try:
# 执行爬取
task_result = execute_monitor_task()
if not task_result:
print("⚠️ [定时任务] 爬虫未获取到数据")
return
scraped_list = task_result.get('device_list', [])
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
count = 0
for item in scraped_list:
d_name = item.get('name')
if not d_name: continue
# 查找或创建设备
device = Device.query.filter_by(name=d_name).first()
if not device:
device = Device(name=d_name, source=item.get('source'), install_site="")
db.session.add(device)
db.session.flush()
# 更新字段
device.status = item.get('status')
device.current_value = item.get('value')
device.latest_time = item.get('target_time')
device.check_time = current_time
device.json_data = json.dumps(item.get('raw_json', {}), ensure_ascii=False)
device.offset = calculate_offset(item.get('target_time'))
# 写入历史
db.session.add(DeviceHistory(
device_id=device.id,
status=device.status,
result_data=device.current_value,
data_time=item.get('target_time'),
json_data=device.json_data
))
count += 1
db.session.commit()
print(f"✅ [定时任务] 成功更新 {count} 台设备状态")
except Exception as e:
db.session.rollback()
print(f"❌ [定时任务] 异常: {str(e)}")
# ==============================================================================
# 4. Flask 应用工厂
# ==============================================================================
def create_app():
# 🔴 关键修复:移除了 static_url_path=''
# 这样 Flask 就不会强制拦截所有根路径请求,让下面的 serve_static 有机会处理 /dashboard
app = Flask(__name__, static_folder=STATIC_FOLDER)
CORS(app)
# 确保 instance 目录存在
if not os.path.exists(INSTANCE_FOLDER):
os.makedirs(INSTANCE_FOLDER, exist_ok=True)
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SCHEDULER_API_ENABLED'] = True
# 初始化数据库
db.init_app(app)
# 初始化定时任务
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
# 添加定时任务 (每天 10:00)
scheduler.add_job(
id='daily_monitor_task',
func=auto_monitor_job,
args=[app],
trigger='cron',
hour=10,
minute=0
)
# 注册蓝图
app.register_blueprint(device_bp)
# -------------------------------------------------
# 前端路由支持 (Vue History Mode)
# -------------------------------------------------
@app.route('/')
def serve_index():
if not os.path.exists(os.path.join(app.static_folder, 'index.html')):
return "❌ 错误: 前端文件丢失 (web_dist/index.html)", 404
return send_from_directory(app.static_folder, 'index.html')
@app.route('/<path:path>')
def serve_static(path):
# 1. 优先尝试直接返回实际存在的文件 (js, css, img等)
file_path = os.path.join(app.static_folder, path)
if os.path.exists(file_path):
return send_from_directory(app.static_folder, path)
# 2. 如果是 API 请求但没找到对应接口,返回 404 JSON (不返回 HTML)
if path.startswith('api') or path.startswith('static'):
return jsonify({'code': 404, 'message': 'Not Found'}), 404
# 3. 关键逻辑:
# 访问 /dashboard 等前端路由时,文件系统中并没有 dashboard 这个文件
# 所以会走到这里,返回 index.html让 Vue 及其 Router 接管页面渲染
return send_from_directory(app.static_folder, 'index.html')
with app.app_context():
db.create_all()
return app
if __name__ == '__main__':
app = create_app()
# 生产环境/打包环境通常设为 False
debug_mode = not getattr(sys, 'frozen', False)
print("🚀 服务启动中...")
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)

View File

@ -8,7 +8,7 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist web_dist
dist-ssr dist-ssr
*.local *.local

View File

@ -1,34 +1,59 @@
<!--App.vue-->
<template> <template>
<router-view></router-view> <div class="app-container">
<main class="main-content">
<router-view></router-view>
</main>
<footer class="version-footer">
2.0版本 © 2026 Device Monitor
</footer>
</div>
</template> </template>
<script setup> <script setup>
// App.vue 作为顶层入口,通常不需要写业务逻辑 // App.vue 保持简洁
// 逻辑都分散在各个 views (Dashboard.vue 等) 中了
</script> </script>
<style> <style>
/* --- 全局样式 --- */ /* --- 全局样式 --- */
/* 1. 去除浏览器默认的 8px 边距,确保页面贴边 */
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
/* 设置全局背景色,防止页面切换时出现白底闪烁 */
background-color: #f5f7fa; background-color: #f5f7fa;
/* 统一字体,防止不同系统显示差异过大 */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; overflow-x: hidden; /* 防止 iOS 橡皮筋效果 */
-webkit-font-smoothing: antialiased;
} }
/* 2. 确保 #app 容器也是全屏的 */
#app { #app {
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
} }
/* 3. 如果你也使用了 Element Plus 的暗黑模式或其他全局配置,可以在这里补充 */ .app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
max-width: 100vw; /* 强制不超过屏幕宽 */
}
/* ✅ 关键:内容区滚动控制 */
.main-content {
flex: 1;
width: 100%;
box-sizing: border-box;
overflow-x: auto; /* 允许横向滚动 */
-webkit-overflow-scrolling: touch; /* 移动端顺滑滚动 */
}
.version-footer {
text-align: center;
padding: 15px 0;
color: #c0c4cc;
font-size: 12px;
background-color: #f5f7fa;
flex-shrink: 0; /* 防止被压缩 */
}
</style> </style>