这是使用PyQT5作为UI的首次提交,将后端和UI合并到1个工程中,统一使用了Python,没有使用JS和HTML
This commit is contained in:
4
UI/ui_utils/__init__.py
Normal file
4
UI/ui_utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Utils module
|
||||
from .user_session import UserSession
|
||||
|
||||
__all__ = ['UserSession']
|
||||
220
UI/ui_utils/splash_window.py
Normal file
220
UI/ui_utils/splash_window.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
启动和关闭提示窗口
|
||||
使用 tkinter 实现轻量级的提示界面
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class SplashWindow:
|
||||
"""启动提示窗口"""
|
||||
|
||||
def __init__(self, title="正在启动", message="系统正在加载,请稍候..."):
|
||||
self.window = None
|
||||
self.title = title
|
||||
self.message = message
|
||||
self._closed = False
|
||||
|
||||
def show(self):
|
||||
"""显示启动窗口(在单独线程中)"""
|
||||
self._window_ready = threading.Event()
|
||||
|
||||
def _create_window():
|
||||
self.window = tk.Tk()
|
||||
self.window.title(self.title)
|
||||
|
||||
# 窗口大小和位置
|
||||
width = 400
|
||||
height = 150
|
||||
screen_width = self.window.winfo_screenwidth()
|
||||
screen_height = self.window.winfo_screenheight()
|
||||
x = (screen_width - width) // 2
|
||||
y = (screen_height - height) // 2
|
||||
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
# 无边框、置顶
|
||||
self.window.overrideredirect(True)
|
||||
self.window.attributes('-topmost', True)
|
||||
|
||||
# 背景色
|
||||
self.window.configure(bg='#f0f0f0')
|
||||
|
||||
# 创建主框架
|
||||
main_frame = tk.Frame(self.window, bg='#f0f0f0', padx=20, pady=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 标题
|
||||
title_label = tk.Label(
|
||||
main_frame,
|
||||
text="跌落试验管理系统",
|
||||
font=('Microsoft YaHei', 16, 'bold'),
|
||||
bg='#f0f0f0',
|
||||
fg='#333333'
|
||||
)
|
||||
title_label.pack(pady=(0, 10))
|
||||
|
||||
# 提示信息
|
||||
message_label = tk.Label(
|
||||
main_frame,
|
||||
text=self.message,
|
||||
font=('Microsoft YaHei', 11),
|
||||
bg='#f0f0f0',
|
||||
fg='#666666'
|
||||
)
|
||||
message_label.pack(pady=(0, 15))
|
||||
|
||||
# 进度条
|
||||
self.progress = ttk.Progressbar(
|
||||
main_frame,
|
||||
mode='indeterminate',
|
||||
length=350
|
||||
)
|
||||
self.progress.pack()
|
||||
self.progress.start(10) # 开始动画
|
||||
|
||||
# 边框
|
||||
border_frame = tk.Frame(self.window, bg='#3A84FF', bd=0)
|
||||
border_frame.place(x=0, y=0, relwidth=1, height=3)
|
||||
|
||||
# 强制刷新显示
|
||||
self.window.update_idletasks()
|
||||
self.window.update()
|
||||
|
||||
# 标记窗口已准备好
|
||||
self._window_ready.set()
|
||||
|
||||
# 运行主循环
|
||||
self.window.mainloop()
|
||||
|
||||
# 在单独线程中创建窗口
|
||||
self.thread = threading.Thread(target=_create_window, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
# 等待窗口创建完成(最多等待2秒)
|
||||
self._window_ready.wait(timeout=2.0)
|
||||
|
||||
def close(self):
|
||||
"""关闭启动窗口"""
|
||||
if self.window and not self._closed:
|
||||
try:
|
||||
self._closed = True
|
||||
# 在 tkinter 线程中关闭窗口
|
||||
if self.window:
|
||||
self.window.after(0, self._safe_close)
|
||||
# 等待线程结束(最多0.5秒)
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join(timeout=0.5)
|
||||
except Exception as e:
|
||||
print(f"关闭启动窗口时出错: {e}")
|
||||
|
||||
def _safe_close(self):
|
||||
"""安全关闭窗口(在 tkinter 线程中执行)"""
|
||||
try:
|
||||
if self.window:
|
||||
self.window.quit()
|
||||
self.window.destroy()
|
||||
except Exception as e:
|
||||
print(f"关闭窗口时出错: {e}")
|
||||
|
||||
|
||||
class ClosingWindow:
|
||||
"""关闭提示窗口"""
|
||||
|
||||
def __init__(self, title="正在关闭", message="系统正在关闭,请稍候..."):
|
||||
self.window = None
|
||||
self.title = title
|
||||
self.message = message
|
||||
self._closed = False
|
||||
|
||||
def show(self):
|
||||
"""显示关闭窗口(在主线程中)"""
|
||||
self.window = tk.Tk()
|
||||
self.window.title(self.title)
|
||||
|
||||
# 窗口大小和位置
|
||||
width = 400
|
||||
height = 150
|
||||
screen_width = self.window.winfo_screenwidth()
|
||||
screen_height = self.window.winfo_screenheight()
|
||||
x = (screen_width - width) // 2
|
||||
y = (screen_height - height) // 2
|
||||
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
# 无边框、置顶
|
||||
self.window.overrideredirect(True)
|
||||
self.window.attributes('-topmost', True)
|
||||
|
||||
# 背景色
|
||||
self.window.configure(bg='#f0f0f0')
|
||||
|
||||
# 创建主框架
|
||||
main_frame = tk.Frame(self.window, bg='#f0f0f0', padx=20, pady=20)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 标题
|
||||
title_label = tk.Label(
|
||||
main_frame,
|
||||
text="跌落试验管理系统",
|
||||
font=('Microsoft YaHei', 16, 'bold'),
|
||||
bg='#f0f0f0',
|
||||
fg='#333333'
|
||||
)
|
||||
title_label.pack(pady=(0, 10))
|
||||
|
||||
# 提示信息
|
||||
message_label = tk.Label(
|
||||
main_frame,
|
||||
text=self.message,
|
||||
font=('Microsoft YaHei', 11),
|
||||
bg='#f0f0f0',
|
||||
fg='#666666'
|
||||
)
|
||||
message_label.pack(pady=(0, 15))
|
||||
|
||||
# 进度条
|
||||
"""
|
||||
self.progress = ttk.Progressbar(
|
||||
main_frame,
|
||||
mode='indeterminate',
|
||||
length=350
|
||||
)
|
||||
self.progress.pack()
|
||||
self.progress.start(10) # 开始动画
|
||||
"""
|
||||
# 边框
|
||||
border_frame = tk.Frame(self.window, bg='#ff6b6b', bd=0)
|
||||
border_frame.place(x=0, y=0, relwidth=1, height=3)
|
||||
|
||||
# 刷新窗口
|
||||
self.window.update()
|
||||
|
||||
def close(self):
|
||||
"""关闭窗口"""
|
||||
if self.window and not self._closed:
|
||||
try:
|
||||
self._closed = True
|
||||
self.window.quit()
|
||||
self.window.destroy()
|
||||
except Exception as e:
|
||||
print(f"关闭提示窗口时出错: {e}")
|
||||
|
||||
def update_message(self, message):
|
||||
"""更新提示信息"""
|
||||
if self.window:
|
||||
try:
|
||||
# 查找message_label并更新
|
||||
for widget in self.window.winfo_children():
|
||||
if isinstance(widget, tk.Frame):
|
||||
for child in widget.winfo_children():
|
||||
if isinstance(child, tk.Label) and child.cget('font')[1] == 11:
|
||||
child.config(text=message)
|
||||
break
|
||||
self.window.update()
|
||||
except Exception as e:
|
||||
print(f"更新提示信息时出错: {e}")
|
||||
88
UI/ui_utils/user_session.py
Normal file
88
UI/ui_utils/user_session.py
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
用户会话管理模块
|
||||
用于存储和管理当前登录用户的信息
|
||||
"""
|
||||
|
||||
|
||||
class UserSession:
|
||||
"""单例模式的用户会话管理器"""
|
||||
|
||||
_instance = None
|
||||
_user_info = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(UserSession, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def set_user_info(cls, user_info):
|
||||
"""
|
||||
设置当前登录用户信息
|
||||
|
||||
Args:
|
||||
user_info (dict): 用户信息,包含 username, userid, role, auth 等字段
|
||||
"""
|
||||
cls._user_info = user_info
|
||||
print(f"[UserSession] 用户登录成功: {user_info.get('username')} (角色: {user_info.get('role')})")
|
||||
|
||||
@classmethod
|
||||
def get_user_info(cls):
|
||||
"""
|
||||
获取当前登录用户信息
|
||||
|
||||
Returns:
|
||||
dict: 用户信息,如果未登录则返回 None
|
||||
"""
|
||||
return cls._user_info
|
||||
|
||||
@classmethod
|
||||
def get_username(cls):
|
||||
"""获取当前用户名"""
|
||||
return cls._user_info.get('username') if cls._user_info else None
|
||||
|
||||
@classmethod
|
||||
def get_userid(cls):
|
||||
"""获取当前用户ID"""
|
||||
return cls._user_info.get('userid') if cls._user_info else None
|
||||
|
||||
@classmethod
|
||||
def get_role(cls):
|
||||
"""获取当前用户角色"""
|
||||
return cls._user_info.get('role') if cls._user_info else None
|
||||
|
||||
@classmethod
|
||||
def get_auth(cls):
|
||||
"""获取当前用户权限"""
|
||||
return cls._user_info.get('auth') if cls._user_info else None
|
||||
|
||||
@classmethod
|
||||
def is_logged_in(cls):
|
||||
"""检查是否已登录"""
|
||||
return cls._user_info is not None
|
||||
|
||||
@classmethod
|
||||
def is_super(cls):
|
||||
"""检查是否为超级管理员"""
|
||||
return cls.get_role() == 'super'
|
||||
|
||||
@classmethod
|
||||
def is_admin(cls):
|
||||
"""检查是否为管理员"""
|
||||
role = cls.get_role()
|
||||
return role in ['super', 'admin']
|
||||
|
||||
@classmethod
|
||||
def is_engineer(cls):
|
||||
"""检查是否为工程师"""
|
||||
role = cls.get_role()
|
||||
return role in ['super', 'admin', 'engineer']
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
"""清除用户会话(登出)"""
|
||||
cls._user_info = None
|
||||
print("[UserSession] 用户已登出")
|
||||
212
UI/ui_utils/websocket_client.py
Normal file
212
UI/ui_utils/websocket_client.py
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
WebSocket 客户端,用于接收后台推送的机台和工位实时数据
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QThread
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import traceback
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class WebSocketClientThread(QThread):
|
||||
"""WebSocket 客户端线程"""
|
||||
|
||||
# 信号定义
|
||||
message_received = pyqtSignal(dict) # 接收到消息
|
||||
connection_status_changed = pyqtSignal(bool) # 连接状态变化
|
||||
error_occurred = pyqtSignal(str) # 发生错误
|
||||
|
||||
def __init__(self, url="ws://localhost:5080", parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self._running = False
|
||||
self._websocket = None
|
||||
self._loop = None
|
||||
|
||||
def run(self):
|
||||
"""线程运行主函数"""
|
||||
self._running = True
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
try:
|
||||
self._loop.run_until_complete(self._connect_and_listen())
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(f"WebSocket 线程异常: {str(e)}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self._loop.close()
|
||||
|
||||
async def _connect_and_listen(self):
|
||||
"""连接 WebSocket 服务器并监听消息"""
|
||||
retry_count = 0
|
||||
max_retries = 5
|
||||
retry_delay = 5 # 秒
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
print(f"正在连接 WebSocket 服务器: {self.url}")
|
||||
async with websockets.connect(self.url) as websocket:
|
||||
self._websocket = websocket
|
||||
self.connection_status_changed.emit(True)
|
||||
print(f"WebSocket 连接成功: {self.url}")
|
||||
retry_count = 0 # 重置重试计数
|
||||
|
||||
# 持续监听消息
|
||||
async for message in websocket:
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
try:
|
||||
# 解析 JSON 消息
|
||||
data = json.loads(message)
|
||||
# 发射信号通知主线程
|
||||
self.message_received.emit(data)
|
||||
except json.JSONDecodeError as e:
|
||||
self.error_occurred.emit(f"JSON 解析失败: {str(e)}")
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(f"处理消息失败: {str(e)}")
|
||||
|
||||
except websockets.exceptions.ConnectionClosedError:
|
||||
self.connection_status_changed.emit(False)
|
||||
print("WebSocket 连接已关闭")
|
||||
if self._running:
|
||||
retry_count += 1
|
||||
if retry_count <= max_retries:
|
||||
print(f"将在 {retry_delay} 秒后重试连接 ({retry_count}/{max_retries})...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
self.error_occurred.emit(f"WebSocket 重连失败,已达到最大重试次数 ({max_retries})")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.connection_status_changed.emit(False)
|
||||
self.error_occurred.emit(f"WebSocket 连接错误: {str(e)}")
|
||||
traceback.print_exc()
|
||||
if self._running:
|
||||
retry_count += 1
|
||||
if retry_count <= max_retries:
|
||||
print(f"将在 {retry_delay} 秒后重试连接 ({retry_count}/{max_retries})...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
break
|
||||
|
||||
def stop(self):
|
||||
"""停止 WebSocket 客户端"""
|
||||
self._running = False
|
||||
if self._websocket:
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(self._websocket.close(), self._loop)
|
||||
except Exception as e:
|
||||
print(f"关闭 WebSocket 连接失败: {e}")
|
||||
self.quit()
|
||||
self.wait()
|
||||
|
||||
|
||||
class WebSocketClient(QObject):
|
||||
"""WebSocket 客户端管理器"""
|
||||
|
||||
# 信号定义
|
||||
machine_data_changed = pyqtSignal(dict) # 机台数据变化
|
||||
station_data_changed = pyqtSignal(dict) # 工位数据变化(预留,待后续完善)
|
||||
connection_status_changed = pyqtSignal(bool) # 连接状态
|
||||
error_occurred = pyqtSignal(str) # 错误
|
||||
|
||||
def __init__(self, url="ws://localhost:5080", parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self._thread = None
|
||||
self._connected = False
|
||||
|
||||
# 节流控制参数
|
||||
self._message_buffer = defaultdict(dict) # 消息缓存
|
||||
self._last_emit_time = defaultdict(float) # 上次发送时间
|
||||
self._throttle_interval = 0.1 # 节流间隔 100ms,每秒最多 5 次更新
|
||||
|
||||
def start(self):
|
||||
"""启动 WebSocket 客户端"""
|
||||
if self._thread and self._thread.isRunning():
|
||||
print("WebSocket 客户端已在运行")
|
||||
return
|
||||
|
||||
self._thread = WebSocketClientThread(self.url)
|
||||
self._thread.message_received.connect(self._handle_message)
|
||||
self._thread.connection_status_changed.connect(self._on_connection_status_changed)
|
||||
self._thread.error_occurred.connect(self.error_occurred.emit)
|
||||
self._thread.start()
|
||||
print("WebSocket 客户端已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止 WebSocket 客户端"""
|
||||
if self._thread:
|
||||
self._thread.stop()
|
||||
self._thread = None
|
||||
print("WebSocket 客户端已停止")
|
||||
|
||||
def _handle_message(self, data):
|
||||
"""处理接收到的消息(带节流控制)"""
|
||||
try:
|
||||
command = data.get('command', '')
|
||||
|
||||
if command == 'machine_data_change':
|
||||
# 机台数据变化 - 使用节流机制
|
||||
machine_sn = data.get('machine_sn') or data.get('dtMachineSN') or 'unknown'
|
||||
self._throttle_emit('machine', machine_sn, data)
|
||||
elif command in ('station_data_change', 'station_drop_result_change'):
|
||||
# 工位(通道)数据变化或跌落结果变化 - 使用节流机制
|
||||
msn = data.get('machine_sn') or data.get('dtMachineSN') or ''
|
||||
ssn = data.get('station_sn') or data.get('SN') or ''
|
||||
station_key = f"{msn}_{ssn}"
|
||||
self._throttle_emit('station', station_key, data)
|
||||
else:
|
||||
print(f"收到未知命令: {command}")
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(f"处理消息失败: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
def _throttle_emit(self, msg_type, key, data):
|
||||
"""节流发送信号,避免高频更新阻塞UI
|
||||
|
||||
策略:
|
||||
1. 缓存最新数据
|
||||
2. 如果距离上次发送时间 < 节流间隔,仅更新缓存
|
||||
3. 如果距离上次发送时间 >= 节流间隔,立即发送并清空缓存
|
||||
"""
|
||||
current_time = time.time()
|
||||
buffer_key = f"{msg_type}_{key}"
|
||||
|
||||
# 更新缓存为最新数据
|
||||
self._message_buffer[buffer_key] = data
|
||||
|
||||
# 检查是否需要发送
|
||||
last_time = self._last_emit_time.get(buffer_key, 0)
|
||||
time_since_last = current_time - last_time
|
||||
|
||||
if time_since_last >= self._throttle_interval:
|
||||
# 可以发送:发送缓存的最新数据
|
||||
buffered_data = self._message_buffer.pop(buffer_key)
|
||||
self._last_emit_time[buffer_key] = current_time
|
||||
|
||||
if msg_type == 'machine':
|
||||
self.machine_data_changed.emit(buffered_data)
|
||||
elif msg_type == 'station':
|
||||
self.station_data_changed.emit(buffered_data)
|
||||
# 否则:仅缓存,不发送(等待下次节流窗口)
|
||||
|
||||
def _on_connection_status_changed(self, connected):
|
||||
"""连接状态变化"""
|
||||
self._connected = connected
|
||||
self.connection_status_changed.emit(connected)
|
||||
print(f"WebSocket 连接状态: {'已连接' if connected else '已断开'}")
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""是否已连接"""
|
||||
return self._connected
|
||||
Reference in New Issue
Block a user