|
|
@@ -2,12 +2,13 @@
|
|
|
登录窗口
|
|
|
"""
|
|
|
from PyQt5.QtWidgets import (
|
|
|
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
- QStackedWidget, QTextEdit, QMessageBox, QDialog,
|
|
|
- QDialogButtonBox, QAction
|
|
|
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
+ QStackedWidget, QMessageBox, QDialog,
|
|
|
+ QDialogButtonBox, QAction, QListWidget, QListWidgetItem,
|
|
|
+ QAbstractItemView, QSizePolicy, QListView, QFrame
|
|
|
)
|
|
|
from PyQt5.QtWidgets import QLineEdit
|
|
|
-from PyQt5.QtCore import Qt, pyqtSignal, QThread, QPoint
|
|
|
+from PyQt5.QtCore import Qt, pyqtSignal, QThread, QPoint, QEvent, QTimer
|
|
|
from PyQt5.QtGui import QFont, QPixmap, QMouseEvent
|
|
|
from qfluentwidgets import (
|
|
|
FluentWindow, LineEdit, PasswordLineEdit,
|
|
|
@@ -21,6 +22,7 @@ from src.utils.logger import get_logger
|
|
|
from src.api.client import api_client
|
|
|
from src.utils.machine import get_machine_code
|
|
|
from src.utils.storage import get_storage
|
|
|
+from datetime import datetime
|
|
|
from PyQt5.QtWidgets import QWidget
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
@@ -34,7 +36,9 @@ class LoginWindow(QWidget):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.machine_code = get_machine_code()
|
|
|
- self.announcement_text = None
|
|
|
+ self.announcement_list = None
|
|
|
+ self.announcements = []
|
|
|
+ self.announcement_timer = None
|
|
|
self.storage = get_storage()
|
|
|
|
|
|
# 拖动相关变量
|
|
|
@@ -51,6 +55,7 @@ class LoginWindow(QWidget):
|
|
|
|
|
|
self.init_ui()
|
|
|
self.load_announcement()
|
|
|
+ self.start_announcement_refresh_timer()
|
|
|
self.load_public_key()
|
|
|
self.try_auto_login()
|
|
|
|
|
|
@@ -163,7 +168,7 @@ class LoginWindow(QWidget):
|
|
|
announcement_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
|
|
announcement_label.setStyleSheet("""
|
|
|
QLabel {
|
|
|
- color: white;
|
|
|
+ color: #000000;
|
|
|
font-size: 18px;
|
|
|
font-weight: bold;
|
|
|
margin-bottom: 10px;
|
|
|
@@ -171,23 +176,37 @@ class LoginWindow(QWidget):
|
|
|
""")
|
|
|
announcement_layout.addWidget(announcement_label)
|
|
|
|
|
|
- # 公告内容区域
|
|
|
- self.announcement_text = TextEdit(announcement_widget)
|
|
|
- self.announcement_text.setReadOnly(True)
|
|
|
- self.announcement_text.setPlaceholderText("暂无公告")
|
|
|
- # 设置公告列表高度为窗口高度,并启用纵向滚动
|
|
|
- self.announcement_text.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
|
- self.announcement_text.setStyleSheet("""
|
|
|
- TextEdit {
|
|
|
- background-color: rgba(255, 255, 255, 0.1);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ # 公告内容列表,可点击查看详情
|
|
|
+ self.announcement_list = QListWidget(announcement_widget)
|
|
|
+ self.announcement_list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
|
+ self.announcement_list.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
|
+ self.announcement_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
+ self.announcement_list.setWordWrap(True)
|
|
|
+ self.announcement_list.setResizeMode(QListView.ResizeMode.Adjust)
|
|
|
+ self.announcement_list.setSpacing(0)
|
|
|
+ self.announcement_list.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
|
+ self.announcement_list.viewport().installEventFilter(self)
|
|
|
+ self.announcement_list.setStyleSheet("""
|
|
|
+ QListWidget {
|
|
|
+ background-color: rgba(255, 255, 255, 0.25);
|
|
|
+ border: 1px solid rgba(0, 0, 0, 0.15);
|
|
|
border-radius: 8px;
|
|
|
- color: #333333;
|
|
|
- font-size: 14px;
|
|
|
- padding: 15px;
|
|
|
+ padding: 15px 10px;
|
|
|
+ }
|
|
|
+ QListWidget::item {
|
|
|
+ margin: 0px;
|
|
|
+ }
|
|
|
+ QListWidget::item:selected {
|
|
|
+ background: transparent;
|
|
|
}
|
|
|
""")
|
|
|
- announcement_layout.addWidget(self.announcement_text, 1) # 使用拉伸因子,让公告列表占据剩余空间
|
|
|
+ self.announcement_list.itemClicked.connect(self.handle_announcement_click)
|
|
|
+ list_container = QWidget()
|
|
|
+ list_container_layout = QVBoxLayout()
|
|
|
+ list_container_layout.setContentsMargins(15, 0, 15, 0)
|
|
|
+ list_container_layout.addWidget(self.announcement_list)
|
|
|
+ list_container.setLayout(list_container_layout)
|
|
|
+ announcement_layout.addWidget(list_container, 1) # 使用拉伸因子,让公告列表占据剩余空间
|
|
|
announcement_widget.setLayout(announcement_layout)
|
|
|
|
|
|
# 右侧:登录卡片
|
|
|
@@ -248,7 +267,7 @@ class LoginWindow(QWidget):
|
|
|
|
|
|
# 切换按钮
|
|
|
switch_layout = QHBoxLayout()
|
|
|
- switch_layout.setSpacing(10)
|
|
|
+ switch_layout.setSpacing(15)
|
|
|
self.switch_to_register_btn = HyperlinkButton(card)
|
|
|
self.switch_to_register_btn.setText("切换到注册")
|
|
|
self.switch_to_login_btn = HyperlinkButton(card)
|
|
|
@@ -256,18 +275,17 @@ class LoginWindow(QWidget):
|
|
|
self.switch_to_register_btn.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(1))
|
|
|
self.switch_to_login_btn.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0))
|
|
|
|
|
|
+ unbind_btn = HyperlinkButton(card)
|
|
|
+ unbind_btn.setText("机器解绑")
|
|
|
+ unbind_btn.clicked.connect(self.show_unbind_dialog)
|
|
|
+
|
|
|
switch_layout.addStretch()
|
|
|
switch_layout.addWidget(self.switch_to_register_btn)
|
|
|
switch_layout.addWidget(self.switch_to_login_btn)
|
|
|
+ switch_layout.addWidget(unbind_btn)
|
|
|
switch_layout.addStretch()
|
|
|
card_layout.addLayout(switch_layout)
|
|
|
|
|
|
- # 机器解绑按钮
|
|
|
- unbind_btn = HyperlinkButton(card)
|
|
|
- unbind_btn.setText("机器解绑")
|
|
|
- unbind_btn.clicked.connect(self.show_unbind_dialog)
|
|
|
- card_layout.addWidget(unbind_btn)
|
|
|
-
|
|
|
card.setLayout(card_layout)
|
|
|
|
|
|
# 组装右侧布局
|
|
|
@@ -275,9 +293,10 @@ class LoginWindow(QWidget):
|
|
|
right_layout.addStretch()
|
|
|
|
|
|
# 版权信息
|
|
|
- copyright_label = QLabel("© 2024 游铛铛. All rights reserved.")
|
|
|
+ current_year = datetime.now().year
|
|
|
+ copyright_label = QLabel(f"© {current_year} 游铛铛. All rights reserved.")
|
|
|
copyright_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
- copyright_label.setStyleSheet("color: rgba(255, 255, 255, 0.8); font-size: 12px; margin-top: 20px;")
|
|
|
+ copyright_label.setStyleSheet("color: #000000; font-size: 12px; margin-top: 20px;")
|
|
|
right_layout.addWidget(copyright_label)
|
|
|
|
|
|
right_widget.setLayout(right_layout)
|
|
|
@@ -472,10 +491,12 @@ class LoginWindow(QWidget):
|
|
|
|
|
|
def show_unbind_dialog(self):
|
|
|
"""显示机器解绑对话框"""
|
|
|
- from qfluentwidgets import Dialog, LineEdit, PasswordLineEdit
|
|
|
+ from qfluentwidgets import LineEdit, PasswordLineEdit
|
|
|
|
|
|
- dialog = Dialog("机器解绑", self)
|
|
|
- dialog.resize(400, 200)
|
|
|
+ dialog = QDialog(self)
|
|
|
+ dialog.setWindowTitle("机器解绑")
|
|
|
+ dialog.setModal(True)
|
|
|
+ dialog.resize(400, 220)
|
|
|
|
|
|
layout = QVBoxLayout()
|
|
|
layout.setSpacing(12)
|
|
|
@@ -503,7 +524,7 @@ class LoginWindow(QWidget):
|
|
|
layout.addWidget(password_input)
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
- dialog.contentLayout.addLayout(layout)
|
|
|
+ dialog.setLayout(layout)
|
|
|
dialog.exec()
|
|
|
|
|
|
def handle_unbind(self, dialog: QDialog, username: str, password: str):
|
|
|
@@ -529,40 +550,159 @@ class LoginWindow(QWidget):
|
|
|
result = api_client.get_announcement()
|
|
|
if result.get("code") == 200:
|
|
|
announcements = result.get("data", [])
|
|
|
- if announcements:
|
|
|
- # 格式化公告显示:每个公告显示为:公告名称、公告主题、分隔线
|
|
|
- announcement_lines = []
|
|
|
- for i, ann in enumerate(announcements):
|
|
|
- title = ann.get("title", "")
|
|
|
- content = ann.get("content", "")
|
|
|
- if title or content:
|
|
|
- if title:
|
|
|
- announcement_lines.append(title)
|
|
|
- if content:
|
|
|
- announcement_lines.append(content)
|
|
|
- # 如果不是最后一个公告,添加分隔线
|
|
|
- if i < len(announcements) - 1:
|
|
|
- announcement_lines.append("————————————")
|
|
|
-
|
|
|
- if announcement_lines:
|
|
|
- self.announcement_text.setPlainText("\n".join(announcement_lines))
|
|
|
- else:
|
|
|
- self.announcement_text.setPlainText("暂无公告")
|
|
|
+ self.populate_announcement_list(announcements)
|
|
|
+ else:
|
|
|
+ self.populate_announcement_list([])
|
|
|
except Exception as e:
|
|
|
logger.error(f"加载公告失败: {e}")
|
|
|
- self.announcement_text.setPlainText("加载公告失败")
|
|
|
+ self.populate_announcement_list([], error_text="加载公告失败")
|
|
|
+
|
|
|
+ def start_announcement_refresh_timer(self):
|
|
|
+ """启动公告定时刷新"""
|
|
|
+ if not self.announcement_timer:
|
|
|
+ self.announcement_timer = QTimer(self)
|
|
|
+ self.announcement_timer.setTimerType(Qt.TimerType.CoarseTimer)
|
|
|
+ self.announcement_timer.timeout.connect(self.load_announcement)
|
|
|
+
|
|
|
+ # 避免重复启动
|
|
|
+ if self.announcement_timer.isActive():
|
|
|
+ self.announcement_timer.stop()
|
|
|
+ self.announcement_timer.start(60 * 1000)
|
|
|
+
|
|
|
+ def stop_announcement_refresh_timer(self):
|
|
|
+ """停止公告定时刷新"""
|
|
|
+ if self.announcement_timer and self.announcement_timer.isActive():
|
|
|
+ self.announcement_timer.stop()
|
|
|
+
|
|
|
+ def populate_announcement_list(self, announcements: list, error_text=None):
|
|
|
+ """填充公告列表"""
|
|
|
+ self.announcement_list.clear()
|
|
|
+ self.announcements = announcements or []
|
|
|
+
|
|
|
+ if not self.announcements:
|
|
|
+ placeholder = QListWidgetItem(error_text or "暂无公告")
|
|
|
+ placeholder.setFlags(Qt.ItemFlag.NoItemFlags)
|
|
|
+ self.announcement_list.addItem(placeholder)
|
|
|
+ return
|
|
|
+
|
|
|
+ total = len(self.announcements)
|
|
|
+ for idx, ann in enumerate(self.announcements):
|
|
|
+ item = QListWidgetItem()
|
|
|
+ item.setData(Qt.ItemDataRole.UserRole, ann)
|
|
|
+ widget = self.create_announcement_item_widget(ann, is_last=(idx == total - 1))
|
|
|
+ item.setSizeHint(widget.sizeHint())
|
|
|
+ self.announcement_list.addItem(item)
|
|
|
+ self.announcement_list.setItemWidget(item, widget)
|
|
|
+ self.update_announcement_item_width()
|
|
|
+
|
|
|
+ def create_announcement_item_widget(self, announcement: dict, is_last: bool = False) -> QWidget:
|
|
|
+ """创建公告列表项"""
|
|
|
+ widget = QWidget()
|
|
|
+ widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)
|
|
|
+ layout = QVBoxLayout()
|
|
|
+ layout.setContentsMargins(5, 8, 5, 8)
|
|
|
+ layout.setSpacing(4)
|
|
|
+
|
|
|
+ title = announcement.get("title") or "未命名公告"
|
|
|
+ title_label = QLabel(title)
|
|
|
+ title_label.setWordWrap(True)
|
|
|
+ title_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
|
+ title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #1a1a1a;")
|
|
|
+
|
|
|
+ summary_source = announcement.get("summary") or announcement.get("content", "") or ""
|
|
|
+ summary = summary_source.strip()
|
|
|
+ if len(summary) > 120:
|
|
|
+ summary = summary[:120] + "..."
|
|
|
+ content_label = QLabel(summary or "暂无简介")
|
|
|
+ content_label.setWordWrap(True)
|
|
|
+ content_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
|
+ content_label.setStyleSheet("font-size: 14px; color: #4a4a4a;")
|
|
|
+
|
|
|
+ layout.addWidget(title_label)
|
|
|
+ layout.addWidget(content_label)
|
|
|
+
|
|
|
+ if not is_last:
|
|
|
+ layout.addSpacing(4)
|
|
|
+ separator = QFrame()
|
|
|
+ separator.setFrameShape(QFrame.Shape.HLine)
|
|
|
+ separator.setFrameShadow(QFrame.Shadow.Plain)
|
|
|
+ separator.setFixedHeight(1)
|
|
|
+ separator.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
|
+ separator.setStyleSheet("color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);")
|
|
|
+ layout.addWidget(separator)
|
|
|
+ layout.addSpacing(6)
|
|
|
+
|
|
|
+ widget.setLayout(layout)
|
|
|
+ widget.setStyleSheet("background-color: transparent;")
|
|
|
+ return widget
|
|
|
+
|
|
|
+ def update_announcement_item_width(self):
|
|
|
+ """根据列表宽度调整公告条目内容,确保自动换行"""
|
|
|
+ if not self.announcement_list:
|
|
|
+ return
|
|
|
+ viewport_width = max(self.announcement_list.viewport().width() - 20, 100)
|
|
|
+ for i in range(self.announcement_list.count()):
|
|
|
+ item = self.announcement_list.item(i)
|
|
|
+ widget = self.announcement_list.itemWidget(item)
|
|
|
+ if widget:
|
|
|
+ widget.setFixedWidth(viewport_width)
|
|
|
+
|
|
|
+ def handle_announcement_click(self, item: QListWidgetItem):
|
|
|
+ """公告点击事件"""
|
|
|
+ announcement = item.data(Qt.ItemDataRole.UserRole)
|
|
|
+ if not announcement:
|
|
|
+ return
|
|
|
+ self.show_announcement_detail_dialog(announcement)
|
|
|
+
|
|
|
+ def show_announcement_detail_dialog(self, announcement: dict):
|
|
|
+ """弹出公告详情"""
|
|
|
+ dialog = QDialog(self)
|
|
|
+ dialog.setWindowTitle(announcement.get("title") or "公告详情")
|
|
|
+ dialog.setModal(True)
|
|
|
+ dialog.resize(500, 400)
|
|
|
+
|
|
|
+ layout = QVBoxLayout(dialog)
|
|
|
+ layout.setContentsMargins(20, 20, 20, 20)
|
|
|
+ layout.setSpacing(15)
|
|
|
+
|
|
|
+ title_label = QLabel(announcement.get("title") or "未命名公告")
|
|
|
+ title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #1f1f1f;")
|
|
|
+
|
|
|
+ meta = announcement.get("created_at") or announcement.get("publish_time") or ""
|
|
|
+ meta_label = QLabel(meta)
|
|
|
+ meta_label.setStyleSheet("font-size: 12px; color: #7a7a7a;")
|
|
|
+
|
|
|
+ content = announcement.get("content") or "暂无公告内容"
|
|
|
+ detail_text = TextEdit(dialog)
|
|
|
+ detail_text.setReadOnly(True)
|
|
|
+ detail_text.setPlainText(content)
|
|
|
+ detail_text.setStyleSheet("""
|
|
|
+ TextEdit {
|
|
|
+ background-color: rgba(245, 247, 250, 0.9);
|
|
|
+ border: 1px solid rgba(0, 0, 0, 0.05);
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #2d2d2d;
|
|
|
+ }
|
|
|
+ """)
|
|
|
+
|
|
|
+ button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok, parent=dialog)
|
|
|
+ button_box.accepted.connect(dialog.accept)
|
|
|
+
|
|
|
+ layout.addWidget(title_label)
|
|
|
+ if meta:
|
|
|
+ layout.addWidget(meta_label)
|
|
|
+ layout.addWidget(detail_text, 1)
|
|
|
+ layout.addWidget(button_box)
|
|
|
+
|
|
|
+ dialog.setLayout(layout)
|
|
|
+ dialog.exec()
|
|
|
|
|
|
def load_public_key(self):
|
|
|
"""加载公钥"""
|
|
|
try:
|
|
|
- result = api_client.get_public_key()
|
|
|
- if result.get("code") == 200:
|
|
|
- data = result.get("data", {})
|
|
|
- public_key = data.get("public_key")
|
|
|
- if public_key:
|
|
|
- from src.api.encrypt import encryptor
|
|
|
- encryptor.load_public_key(public_key)
|
|
|
- logger.info("公钥加载成功")
|
|
|
+ api_client.refresh_public_key()
|
|
|
+ logger.info("公钥加载成功")
|
|
|
except Exception as e:
|
|
|
logger.error(f"加载公钥失败: {e}")
|
|
|
|
|
|
@@ -652,7 +792,6 @@ class LoginWindow(QWidget):
|
|
|
if user_info:
|
|
|
expire_time = user_info.get("expire_time")
|
|
|
if expire_time:
|
|
|
- from datetime import datetime
|
|
|
try:
|
|
|
if isinstance(expire_time, str):
|
|
|
expire_dt = datetime.fromisoformat(expire_time.replace('Z', '+00:00'))
|
|
|
@@ -724,6 +863,21 @@ class LoginWindow(QWidget):
|
|
|
if 'title' in child.objectName().lower() or 'TitleBar' in type(child).__name__:
|
|
|
child.hide()
|
|
|
child.setFixedHeight(0)
|
|
|
+ self.update_announcement_item_width()
|
|
|
+
|
|
|
+ def eventFilter(self, obj, event):
|
|
|
+ if (
|
|
|
+ hasattr(self, "announcement_list")
|
|
|
+ and obj == self.announcement_list.viewport()
|
|
|
+ and event.type() == QEvent.Type.Resize
|
|
|
+ ):
|
|
|
+ self.update_announcement_item_width()
|
|
|
+ return super().eventFilter(obj, event)
|
|
|
+
|
|
|
+ def closeEvent(self, event):
|
|
|
+ """窗口关闭事件"""
|
|
|
+ self.stop_announcement_refresh_timer()
|
|
|
+ super().closeEvent(event)
|
|
|
|
|
|
|
|
|
class LoginThread(QThread):
|