1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-08-02 22:19:37 +08:00

feat(bulletin): 公告栏系统完整实现

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-24 10:18:30 +08:00
parent 588718f9c5
commit a716e5b945
10 changed files with 1938 additions and 332 deletions
+382
View File
@@ -0,0 +1,382 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import requests
from datetime import datetime, timedelta
from typing import Any
from PySide6.QtCore import (
Qt,
QThread,
Signal,
Slot,
QTimer
)
from PySide6.QtGui import (
QFont
)
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QListWidgetItem,
QMessageBox,
QVBoxLayout,
QWidget
)
from gui.ALStatusLabel import ALStatusLabel
from gui.resources.ui.Ui_ALBulletinDialog import Ui_ALBulletinDialog
from managers.bulletin.BulletinManager import instance as bulletinInstance
class ALBulletinFetchWorker(QThread):
"""
Worker thread for fetching bulletins from the server.
"""
fetchWorkerIsFinished = Signal(dict)
fetchWorkerFinishedWithError = Signal(str)
def __init__(
self,
parent=None,
request_url: str = "",
params: dict = None
):
super().__init__(parent)
self.__request_url = request_url.rstrip("/") if request_url else ""
self.__params = params or {}
def run(
self
):
try:
response = requests.get(
self.__request_url, params=self.__params, timeout=5
)
response.raise_for_status()
r = response.json()
if r.get("code") == 200:
data = r.get("data", {})
self.fetchWorkerIsFinished.emit(data)
else:
raise Exception(
f"服务器返回错误: [{r.get('code', '未知代码')}] {r.get('msg', '未知错误')}"
)
except requests.RequestException as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生网络错误: \n{e}")
except Exception as e:
self.fetchWorkerFinishedWithError.emit(f"获取公告数据时发生未知错误: \n{e}")
class ALBulletinItemWidget(QWidget):
"""
Single bulletin item widget for the bulletin list.
"""
def __init__(
self,
parent=None,
bulletin: dict[str, Any] = None
):
super().__init__(parent)
self.__bulletin = bulletin.copy() if bulletin else {}
self.modifyUi()
def modifyUi(
self
):
self.ItemWidgetLayout = QVBoxLayout()
self.ItemWidgetLayout.setSpacing(10)
self.ItemWidgetLayout.setContentsMargins(10, 5, 10, 5)
self.BulletinTitleLayout = QHBoxLayout()
self.BulletinTitleLabel = QLabel(self.__bulletin.get("title", "无标题"))
titleFont = QFont()
titleFont.setBold(True)
self.BulletinTitleLabel.setFont(titleFont)
self.BulletinTitleLayout.addWidget(self.BulletinTitleLabel)
if self.__bulletin.get("isNew", False):
self.NewIndicatorLabel = QLabel("新 !")
self.NewIndicatorLabel.setStyleSheet(
"color: #DC0000;"\
"font-size: 10px;"\
"font-weight: bold;"\
"font-style: italic;"
)
self.NewIndicatorLabel.setFixedSize(25, 25)
self.BulletinTitleLayout.addWidget(self.NewIndicatorLabel)
else:
self.NewIndicatorLabel = None
if self.__bulletin.get("isEdited", False):
self.BulletinIsEditedLabel = QLabel("(已编辑)")
self.BulletinIsEditedLabel.setStyleSheet(
"color: #FF9800;"\
"font-size: 10px;"
)
self.BulletinTitleLayout.addWidget(self.BulletinIsEditedLabel)
self.BulletinTitleLayout.addStretch()
self.ItemWidgetLayout.addLayout(self.BulletinTitleLayout)
self.BulletinInfoLayout = QHBoxLayout()
self.BulletinDateLabel = QLabel()
date_time = datetime.fromisoformat(self.__bulletin.get("dateTime", ""))
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
self.BulletinDateLabel.setStyleSheet("color: #969696; font-size: 11px;")
self.BulletinInfoLayout.addWidget(self.BulletinDateLabel)
self.BulletinAuthorLabel = QLabel(self.__bulletin.get("author", "未知"))
self.BulletinAuthorLabel.setStyleSheet("color: #969696; font-size: 11px;")
self.BulletinInfoLayout.addWidget(self.BulletinAuthorLabel)
self.BulletinInfoLayout.addStretch()
self.ItemWidgetLayout.addLayout(self.BulletinInfoLayout)
self.setLayout(self.ItemWidgetLayout)
self.__bulletin = None
def markAsRead(
self
):
if self.NewIndicatorLabel:
self.NewIndicatorLabel.hide()
class ALBulletinDialog(QDialog, Ui_ALBulletinDialog):
"""
Bulletin viewer dialog.
"""
def __init__(
self,
parent=None
):
super().__init__(parent)
self.__sync_timer: QTimer | None = None
self.__fetch_worker: ALBulletinFetchWorker | None = None
self.__bulletin_mgr = bulletinInstance()
self.setupUi(self)
self.modifyUi()
self.connectSignals()
self.setupTimer()
if self.shouldAutoSync():
self.syncBulletin()
def modifyUi(
self
):
self.ALSyncStatusLabel = ALStatusLabel(self)
self.ALSyncStatusLabel.setFixedSize(30, 30)
self.SyncLayout.replaceWidget(
self.SyncStatusPlaceholder, self.ALSyncStatusLabel
)
titleFont = QFont()
titleFont.setBold(True)
titleFont.setPointSize(15)
self.BulletinTitleLabel.setFont(titleFont)
self.BulletinDateLabel.setStyleSheet("color: #969696;")
self.BulletinAuthorLabel.setStyleSheet("color: #969696;")
self.BulletinIsEditedLabel.setStyleSheet("color: #FF9800;")
self.BulletinIsEditedLabel.hide()
last_time = self.__bulletin_mgr.lastSyncTime()
if last_time:
self.setLastSyncTime(last_time)
self.updateBulletinList(self.__bulletin_mgr.bulletins())
def shouldAutoSync(
self
) -> bool:
last = self.__bulletin_mgr.lastSyncTime()
if last is None:
return True
try:
last_dt = datetime.fromisoformat(last)
interval_min = max(self.__bulletin_mgr.syncInterval()//2, 1)
threshold = timedelta(minutes=interval_min)
return (datetime.now().astimezone() - last_dt) > threshold
except (ValueError, TypeError):
return True
def setLastSyncTime(
self,
iso_str: str
):
try:
dt = datetime.fromisoformat(iso_str)
self.LastSyncDateTimeEdit.setDateTime(dt)
except (ValueError, TypeError):
pass
def connectSignals(
self
):
self.BulletinListWidget.itemClicked.connect(self.onBulletinListWidgetItemClicked)
self.SyncButton.clicked.connect(self.syncBulletin)
def setupTimer(
self
):
self.__sync_timer = QTimer(self)
self.__sync_timer.timeout.connect(self.syncBulletin)
self.__sync_timer.start(self.__bulletin_mgr.syncInterval()*60*1000)
def updateBulletinList(
self,
bulletins: list[dict]
):
self.BulletinListWidget.clear()
sorted_list = sorted(
bulletins, key=lambda x: x.get("dateTime", ""), reverse=True
)
for bulletin in sorted_list:
item = QListWidgetItem()
item.setData(Qt.UserRole, bulletin)
widget = ALBulletinItemWidget(self, bulletin)
item.setSizeHint(widget.sizeHint())
self.BulletinListWidget.addItem(item)
self.BulletinListWidget.setItemWidget(item, widget)
def showBulletin(
self,
bulletin: dict
):
self.BulletinTitleLabel.setText(bulletin.get("title", "无标题"))
date_time = datetime.fromisoformat(bulletin.get("dateTime", ""))
self.BulletinDateLabel.setText(date_time.strftime("%Y-%m-%d %H:%M:%S"))
self.BulletinAuthorLabel.setText(bulletin.get("author", "未知"))
if bulletin.get("isEdited", False):
self.BulletinIsEditedLabel.show()
else:
self.BulletinIsEditedLabel.hide()
self.BulletinContentTextBrowser.setHtml(
"<div style='font-size: 14px;'>{content}</div>".format(
content=bulletin.get("content", "无内容")
)
)
def clearBulletin(
self
):
self.BulletinTitleLabel.setText("")
self.BulletinDateLabel.setText("")
self.BulletinAuthorLabel.setText("")
self.BulletinContentTextBrowser.setText("")
self.BulletinIsEditedLabel.hide()
def syncBulletin(
self
):
if self.__fetch_worker is not None:
return
self.SyncButton.setEnabled(False)
self.SyncButton.setText("同步中...")
self.ALSyncStatusLabel.status = ALStatusLabel.Status.RUNNING
self.SyncStatusDetailLabel.setText("")
self.SyncStatusDetailLabel.setStyleSheet("")
params = self.__bulletin_mgr.getSyncDateTimeAndRange()
self.__fetch_worker = ALBulletinFetchWorker(
self, self.__bulletin_mgr.apiUrl(), params
)
self.__fetch_worker.fetchWorkerIsFinished.connect(self.onBulletinsFetched)
self.__fetch_worker.fetchWorkerFinishedWithError.connect(self.onBulletinsFetchError)
self.__fetch_worker.start()
@Slot(dict)
def onBulletinsFetched(
self,
data: dict
):
if self.__fetch_worker:
self.__fetch_worker.wait(2000)
self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
self.__fetch_worker.deleteLater()
self.__fetch_worker = None
bulletins = data.get("bulletins", [])
delete_ids = data.get("delete_ids", [])
merged = self.__bulletin_mgr.updateAndMergeBulletins(bulletins, delete_ids)
self.__bulletin_mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
self.SyncButton.setEnabled(True)
self.SyncButton.setText("同步")
self.ALSyncStatusLabel.status = ALStatusLabel.Status.SUCCESS
self.SyncStatusDetailLabel.setText("同步成功")
self.SyncStatusDetailLabel.setStyleSheet("color: green;")
QTimer.singleShot(3000, self.clearSyncStatus)
self.setLastSyncTime(self.__bulletin_mgr.lastSyncTime())
self.updateBulletinList(merged)
self.clearBulletin()
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
if self.__sync_timer:
self.__sync_timer.start(interval_ms)
@Slot(str)
def onBulletinsFetchError(
self,
error_message: str
):
if self.__fetch_worker:
self.__fetch_worker.wait(2000)
self.__fetch_worker.fetchWorkerIsFinished.disconnect(self.onBulletinsFetched)
self.__fetch_worker.fetchWorkerFinishedWithError.disconnect(self.onBulletinsFetchError)
self.__fetch_worker.deleteLater()
self.__fetch_worker = None
self.SyncButton.setEnabled(True)
self.SyncButton.setText("重试")
self.ALSyncStatusLabel.status = ALStatusLabel.Status.FAILURE
self.SyncStatusDetailLabel.setText("同步失败,请检查网络连接")
self.SyncStatusDetailLabel.setStyleSheet("color: red;")
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"同步失败:{error_message}"
)
interval_ms = self.__bulletin_mgr.syncInterval()*60*1000
if self.__sync_timer:
self.__sync_timer.start(interval_ms)
def clearSyncStatus(
self
):
self.ALSyncStatusLabel.status = ALStatusLabel.Status.WAITING
self.SyncStatusDetailLabel.setText("")
self.SyncStatusDetailLabel.setStyleSheet("")
@Slot(QListWidgetItem)
def onBulletinListWidgetItemClicked(
self,
item: QListWidgetItem
):
if item is None or item.data(Qt.UserRole) is None:
return
bulletin = item.data(Qt.UserRole)
if bulletin.get("isNew", False):
bulletin["isNew"] = False
widget = self.BulletinListWidget.itemWidget(item)
widget.markAsRead()
item.setData(Qt.UserRole, bulletin)
self.__bulletin_mgr.markBulletinAsRead(bulletin["id"])
self.showBulletin(bulletin)
+170
View File
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from datetime import datetime
from PySide6.QtCore import (
QObject,
QTimer,
Signal,
Slot
)
from gui.ALBulletinDialog import ALBulletinFetchWorker
from managers.bulletin.BulletinManager import instance as bulletinInstance
class ALBulletinPoller(QObject):
"""
Background bulletin poller.
Owns the periodic poll timer and the ALBulletinFetchWorker
lifecycle. Emits signals when new bulletins are detected so
the owner can show tray notifications without knowing the
fetch / merge details.
"""
newBulletinsDetected = Signal(int)
def __init__(
self,
parent=None
):
super().__init__(parent)
self.__timer = QTimer(self)
self.__timer.timeout.connect(self.__poll)
self.__worker = None
self.__dialog_open = False
self.__mgr = bulletinInstance()
def start(
self
):
interval_ms = self.__mgr.syncInterval()*60*1000
self.__timer.start(interval_ms)
def stop(
self
):
self.__timer.stop()
self.__cleanupWorker()
def restart(
self
):
self.stop()
self.start()
def fetchNow(
self
):
if self.__worker is not None:
return
if self.__dialog_open:
return
self.__doFetch()
def isPolling(
self
) -> bool:
return self.__timer.isActive()
def setDialogOpen(
self,
open: bool
):
self.__dialog_open = open
def __doFetch(
self
):
params = self.__mgr.getSyncDateTimeAndRange()
self.__worker = ALBulletinFetchWorker(
self, self.__mgr.apiUrl(), params
)
self.__worker.fetchWorkerIsFinished.connect(self.__onFetched)
self.__worker.fetchWorkerFinishedWithError.connect(self.__onError)
self.__worker.start()
@Slot()
def __poll(
self
):
if self.__worker is not None:
return
if self.__dialog_open:
return
self.__doFetch()
@Slot(dict)
def __onFetched(
self,
data: dict
):
if self.__worker is None:
return
self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched)
self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError)
self.__worker.wait(2000)
self.__worker.deleteLater()
self.__worker = None
old_ids = {b["id"] for b in self.__mgr.bulletins()}
bulletins = data.get("bulletins", [])
delete_ids = data.get("delete_ids", [])
self.__mgr.updateAndMergeBulletins(bulletins, delete_ids)
self.__mgr.setLastSyncTime(datetime.now().astimezone().isoformat())
new_ids = {b["id"] for b in bulletins if b["id"] not in old_ids}
if new_ids:
self.newBulletinsDetected.emit(len(new_ids))
interval_ms = self.__mgr.syncInterval()*60*1000
self.__timer.start(interval_ms)
@Slot(str)
def __onError(
self,
error_message: str
):
if self.__worker is None:
return
self.__worker.fetchWorkerIsFinished.disconnect(self.__onFetched)
self.__worker.fetchWorkerFinishedWithError.disconnect(self.__onError)
self.__worker.wait(2000)
self.__worker.deleteLater()
self.__worker = None
interval_ms = self.__mgr.syncInterval()*60*1000
self.__timer.start(interval_ms)
def __cleanupWorker(
self
):
if self.__worker is None:
return
try:
self.__worker.fetchWorkerIsFinished.disconnect()
except (TypeError, RuntimeError):
pass
try:
self.__worker.fetchWorkerFinishedWithError.disconnect()
except (TypeError, RuntimeError):
pass
self.__worker.wait(2000)
self.__worker.deleteLater()
self.__worker = None
+63
View File
@@ -32,6 +32,8 @@ from PySide6.QtWidgets import (
from base.MsgBase import MsgBase
from gui.ALAboutDialog import ALAboutDialog
from gui.ALBulletinDialog import ALBulletinDialog
from gui.ALBulletinPoller import ALBulletinPoller
from gui.ALConfigWidget import ALConfigWidget
from gui.ALSettingsWidget import ALSettingsWidget
from gui.ALMainWorkers import (
@@ -41,6 +43,7 @@ from gui.ALMainWorkers import (
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.config.ConfigUtils import ConfigUtils
@@ -62,6 +65,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alTimerTaskManageWidget = None
self.__alConfigWidget = None
self.__alSettingsWidget = None
self.__alBulletinDialog = None
self.__bulletin_poller = ALBulletinPoller(self)
self.__auto_lib_thread = None
self.__current_timer_task_thread = None
self.__is_running_timer_task = False
@@ -72,6 +77,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.connectSignals()
self.startMsgPolling()
self.startTimerTaskPolling()
self.__bulletin_poller.start()
if bulletinInstance().autoFetch():
QTimer.singleShot(1000, self.__bulletin_poller.fetchNow)
self._showLog("主窗口初始化完成")
def modifyUi(
@@ -84,6 +94,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.ManualAction.triggered.connect(self.onManualActionTriggered)
self.AboutAction.triggered.connect(self.onAboutActionTriggered)
self.SettingsAction.triggered.connect(self.onSettingsActionTriggered)
if hasattr(self, 'BulletinAction'):
self.BulletinAction.triggered.connect(self.onBulletinActionTriggered)
# initialize timer task widget, but not show it
try:
@@ -131,12 +143,14 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
self.TrayMenu.addAction("公告栏", self.onBulletinActionTriggered)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.messageClicked.connect(self.onBulletinActionTriggered)
self.TrayIcon.show()
def hideToTray(
@@ -170,6 +184,10 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
self.__bulletin_poller.newBulletinsDetected.connect(
self._onBulletinPollerNewBulletins
)
def closeEvent(
self,
event: QCloseEvent
@@ -195,6 +213,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if self.__alSettingsWidget:
self.__alSettingsWidget.close()
# the settings widget is already deleted in the 'self.onSettingsWidgetClosed'
if self.__alBulletinDialog:
self.__alBulletinDialog.close()
# the bulletin dialog is already deleted in the 'self.onBulletinDialogClosed'
if self.__bulletin_poller:
self.__bulletin_poller.stop()
self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event)
@@ -318,6 +341,46 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alSettingsWidget = None
self.SettingsAction.setEnabled(True)
@Slot()
def onBulletinActionTriggered(
self
):
self.__bulletin_poller.setDialogOpen(True)
if self.__alBulletinDialog is None:
self.__alBulletinDialog = ALBulletinDialog(self)
self.__alBulletinDialog.finished.connect(self.onBulletinDialogClosed)
self.__alBulletinDialog.show()
self.__alBulletinDialog.raise_()
self.__alBulletinDialog.activateWindow()
self._showLog("打开公告栏窗口")
@Slot()
def onBulletinDialogClosed(
self
):
if self.__alBulletinDialog:
self.__alBulletinDialog.finished.disconnect(self.onBulletinDialogClosed)
self.__alBulletinDialog.deleteLater()
self.__alBulletinDialog = None
self.__bulletin_poller.setDialogOpen(False)
@Slot(int)
def _onBulletinPollerNewBulletins(
self,
count: int
):
if not hasattr(self, 'TrayIcon'):
return
self.TrayIcon.showMessage(
"公告栏 - AutoLibrary",
f"{count} 条新公告,点击查看详情。",
QSystemTrayIcon.MessageIcon.Information,
3000
)
@Slot()
def onSettingsActionTriggered(
self
+86 -7
View File
@@ -9,12 +9,15 @@ See the LICENSE file for details.
"""
import os
import sys
import time
import requests
import qtawesome as qta
from PySide6.QtCore import (
QProcess,
Qt,
QTimer,
Signal,
Slot
)
@@ -30,6 +33,7 @@ from PySide6.QtWidgets import (
)
import managers.config.ConfigManager as ConfigManager
from managers.bulletin.BulletinManager import instance as bulletinInstance
from managers.log.LogManager import instance as logInstance
from managers.theme.ThemeManager import(
getActiveStyle,
@@ -96,6 +100,9 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_theme: str = ""
self.__original_custom_theme: str = ""
self.__original_style: str = ""
self.__original_bulletin_url: str = ""
self.__original_bulletin_auto_fetch: bool = False
self.__original_bulletin_sync_interval: int = 10
self.setupUi(self)
self.modifyUi()
@@ -130,6 +137,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
"padding: 5px;"
)
self.NavigationList.setCurrentRow(0)
self.NavigationList.currentRowChanged.connect(self.PageStack.setCurrentIndex)
self.populateStyles()
self.populateCustomThemes()
@@ -139,9 +147,12 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
app : QApplication | None = QApplication.instance()
color = app.palette().color(app.palette().ColorRole.WindowText).name()
item = self.NavigationList.item(0)
if item:
item.setIcon(qta.icon("fa6s.palette", color=color))
item0 = self.NavigationList.item(0)
if item0:
item0.setIcon(qta.icon("fa6s.palette", color=color))
item1 = self.NavigationList.item(1)
if item1:
item1.setIcon(qta.icon("fa6s.bullhorn", color=color))
def populateStyles(
self
@@ -176,6 +187,7 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.RemoveCustomThemeButton.clicked.connect(self.onRemoveCustomThemeButtonClicked)
self.CustomThemeComboBox.currentIndexChanged.connect(self.onCustomThemeComboBoxChanged)
self.ResetCustomThemeButton.clicked.connect(self.onResetCustomThemeButtonClicked)
self.BulletinTestButton.clicked.connect(self.onBulletinTestButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ApplyButton.clicked.connect(self.onApplyButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
@@ -207,6 +219,14 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.updateCustomThemeInfo()
self.updateCustomThemeStatus()
bulletin_mgr = bulletinInstance()
self.__original_bulletin_url = bulletin_mgr.serverUrl()
self.__original_bulletin_auto_fetch = bulletin_mgr.autoFetch()
self.__original_bulletin_sync_interval = bulletin_mgr.syncInterval()
self.BulletinServerUrlEdit.setText(self.__original_bulletin_url)
self.BulletinAutoFetchCheck.setChecked(self.__original_bulletin_auto_fetch)
self.BulletinSyncIntervalSpin.setValue(self.__original_bulletin_sync_interval)
def updateCustomThemeInfo(
self
):
@@ -270,6 +290,29 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
custom_theme = ""
return theme, style, custom_theme
def clearBulletinTestStatus(
self
):
self.BulletinTestStatusLabel.setText("")
self.BulletinTestStatusLabel.setStyleSheet("")
def saveBulletinSettings(
self
):
url = self.BulletinServerUrlEdit.text().strip()
auto_fetch = self.BulletinAutoFetchCheck.isChecked()
sync_interval = self.BulletinSyncIntervalSpin.value()
if sync_interval < 1:
sync_interval = 5
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SERVER_URL, url)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.AUTO_FETCH, auto_fetch)
self.__cfg_mgr.set(CfgKey.GLOBAL.BULLETIN.SYNC_INTERVAL, sync_interval)
self.__original_bulletin_url = url
self.__original_bulletin_auto_fetch = auto_fetch
self.__original_bulletin_sync_interval = sync_interval
def saveAndApply(
self
):
@@ -281,8 +324,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
if not _applyCustomTheme(custom_theme, theme):
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.CUSTOM_THEME, "")
self.syncRadioFromNeedTheme(custom_theme)
# Re-read theme after syncRadioFromNeedTheme — the radio may have
# changed to match the custom theme's need_theme
theme, _, _ = self.collectSettings()
self.__cfg_mgr.set(CfgKey.GLOBAL.APPEARANCE.THEME, theme)
self.setNavigationIcons()
@@ -292,6 +333,8 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.__original_custom_theme = custom_theme if custom_theme else ""
self.__original_style = getActiveStyle()
self.saveBulletinSettings()
def maybeRestart(
self
) -> bool:
@@ -380,7 +423,6 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
):
self.updateCustomThemeInfo()
# no status update, because custom theme is not applied yet.
@Slot()
def onResetCustomThemeButtonClicked(
@@ -406,6 +448,43 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self.updateCustomThemeInfo()
self.updateCustomThemeStatus()
@Slot()
def onBulletinTestButtonClicked(
self
):
url = self.BulletinServerUrlEdit.text().strip()
if not url:
self.BulletinTestStatusLabel.setText("请先输入服务器地址。")
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
return
self.BulletinTestButton.setEnabled(False)
self.BulletinTestStatusLabel.setText("正在测试连接...")
self.BulletinTestStatusLabel.setStyleSheet("")
try:
api_url = url.rstrip("/") + "/bulletins"
t0 = time.monotonic()
response = requests.get(api_url, timeout=5)
elapsed_ms = (time.monotonic() - t0) * 1000
response.raise_for_status()
data = response.json()
if data.get("code") == 200:
self.BulletinTestStatusLabel.setText(
f"连接成功!响应延迟 {elapsed_ms:.0f} ms"
)
self.BulletinTestStatusLabel.setStyleSheet("color: green;")
else:
self.BulletinTestStatusLabel.setText(
f"服务器返回异常: [{data.get('code', '?')}] {data.get('msg', '未知错误')}"
)
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
except Exception as e:
self.BulletinTestStatusLabel.setText(f"连接失败:{e}")
self.BulletinTestStatusLabel.setStyleSheet("color: red;")
finally:
self.BulletinTestButton.setEnabled(True)
QTimer.singleShot(3000, self.clearBulletinTestStatus)
@Slot()
def onCancelButtonClicked(
self
@@ -429,5 +508,5 @@ class ALSettingsWidget(CenterOnParentMixin, QWidget, Ui_ALSettingsWidget):
self
):
self.onApplyButtonClicked() # virtually call apply button clicked
self.onApplyButtonClicked()
self.close()
+385
View File
@@ -0,0 +1,385 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALBulletinDialog</class>
<widget class="QDialog" name="ALBulletinDialog">
<property name="windowModality">
<enum>Qt::WindowModality::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>450</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>450</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>450</height>
</size>
</property>
<property name="windowTitle">
<string>公告栏 - AutoLibrary</string>
</property>
<layout class="QVBoxLayout" name="ALBulletinDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="SyncLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QPushButton" name="SyncButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::NoFocus</enum>
</property>
<property name="text">
<string>同步</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="SyncStatusPlaceholder">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="SyncStatusDetailLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="SyncSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>800</width>
<height>25</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="LastSyncTimeLabel">
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>上次同步:</string>
</property>
</widget>
</item>
<item>
<widget class="QDateTimeEdit" name="LastSyncDateTimeEdit">
<property name="minimumSize">
<size>
<width>130</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>130</width>
<height>25</height>
</size>
</property>
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="keyboardTracking">
<bool>false</bool>
</property>
<property name="displayFormat">
<string>yyyy/MM/dd HH:mm:ss</string>
</property>
<property name="timeSpec">
<enum>Qt::TimeSpec::LocalTime</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="BulletinSplitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QFrame" name="LeftFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<layout class="QVBoxLayout" name="LeftFrameLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="BulletinListWidget">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QFrame" name="RightFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<layout class="QVBoxLayout" name="RightFrameLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="BulletinTitleLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BulletinInfoLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinDateLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinAuthorLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinIsEditedLabel">
<property name="text">
<string>(已编辑)</string>
</property>
</widget>
</item>
<item>
<spacer name="BulletinInfoSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextBrowser" name="BulletinContentTextBrowser">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="BulletinDialogButtonBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>BulletinDialogButtonBox</sender>
<signal>accepted()</signal>
<receiver>ALBulletinDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>BulletinDialogButtonBox</sender>
<signal>rejected()</signal>
<receiver>ALBulletinDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+540 -325
View File
@@ -101,340 +101,555 @@
<iconset theme="preferences-desktop-color"/>
</property>
</item>
<item>
<property name="text">
<string>公告</string>
</property>
<property name="icon">
<iconset theme="preferences-desktop-color"/>
</property>
</item>
</widget>
</item>
<item>
<widget class="QScrollArea" name="AppearanceScrollArea">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="AppearancePageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>-51</y>
<width>397</width>
<height>434</height>
</rect>
<widget class="QStackedWidget" name="PageStack">
<widget class="QScrollArea" name="AppearanceScrollArea">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<layout class="QVBoxLayout" name="AppearancePageLayout">
<property name="spacing">
<number>5</number>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="AppearancePageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>434</height>
</rect>
</property>
<property name="leftMargin">
<number>3</number>
<layout class="QVBoxLayout" name="AppearancePageLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QGroupBox" name="AppearanceGroupBox">
<property name="title">
<string>主题模式</string>
</property>
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QRadioButton" name="LightThemeRadio">
<property name="text">
<string>浅色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="DarkThemeRadio">
<property name="text">
<string>深色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="SystemThemeRadio">
<property name="text">
<string>跟随系统</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="InterfaceGroupBox">
<property name="title">
<string>界面风格</string>
</property>
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="StyleSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="StyleSelectLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>应用程序样式:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="StyleComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="StyleHintLabel">
<property name="text">
<string>更改样式将在下次启动应用程序时生效。</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="CustomThemeGroupBox">
<property name="title">
<string>自定义外观</string>
</property>
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QLabel" name="CustomThemeHintLabel">
<property name="text">
<string>选择一个主题,或导入新的主题文件:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="CustomThemePathEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>选择或输入 QSS 样式表文件路径...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ImportCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="RemoveCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeInfoLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重置主题</string>
</property>
</widget>
</item>
<item>
<spacer name="CustomThemeActionSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeStatusLabel">
<property name="text">
<string>当前使用程序 默认 外观。</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="AppearancePageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<widget class="QScrollArea" name="BulletinScrollArea">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="BulletinPageContent">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>434</height>
</rect>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QGroupBox" name="AppearanceGroupBox">
<property name="title">
<string>主题模式</string>
</property>
<layout class="QVBoxLayout" name="AppearanceGroupBoxLayout">
<property name="spacing">
<number>5</number>
<layout class="QVBoxLayout" name="BulletinPageLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QGroupBox" name="BulletinGroupBox">
<property name="title">
<string>公告设置</string>
</property>
<property name="leftMargin">
<number>3</number>
<layout class="QVBoxLayout" name="BulletinGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="BulletinServerUrlLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinServerUrlLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>服务器地址:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="BulletinServerUrlEdit">
<property name="minimumSize">
<size>
<width>200</width>
<height>25</height>
</size>
</property>
<property name="placeholderText">
<string>https://api.autolibrary.kenanzhu.com</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="BulletinAutoFetchCheck">
<property name="text">
<string>软件启动时自动获取公告</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BulletinSyncIntervalLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="BulletinSyncIntervalLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>自动同步间隔:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="BulletinSyncIntervalSpin">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1440</number>
</property>
<property name="suffix">
<string> 分钟</string>
</property>
</widget>
</item>
<item>
<spacer name="BulletinSyncIntervalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="BulletinTestButton">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>测试连接</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="BulletinTestStatusLabel">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="BulletinPageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="topMargin">
<number>3</number>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QRadioButton" name="LightThemeRadio">
<property name="text">
<string>浅色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="DarkThemeRadio">
<property name="text">
<string>深色</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="SystemThemeRadio">
<property name="text">
<string>跟随系统</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="InterfaceGroupBox">
<property name="title">
<string>界面风格</string>
</property>
<layout class="QVBoxLayout" name="InterfaceGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="StyleSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="StyleSelectLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>应用程序样式:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="StyleComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="StyleHintLabel">
<property name="text">
<string>更改样式将在下次启动应用程序时生效。</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="CustomThemeGroupBox">
<property name="title">
<string>自定义外观</string>
</property>
<layout class="QVBoxLayout" name="CustomQssGroupBoxLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QLabel" name="CustomThemeHintLabel">
<property name="text">
<string>选择一个主题,或导入新的主题文件:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemePathLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QComboBox" name="CustomThemeComboBox">
<property name="minimumSize">
<size>
<width>160</width>
<height>25</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="CustomThemePathEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>选择或输入 QSS 样式表文件路径...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ImportCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="RemoveCustomThemeButton">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeInfoLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CustomThemeActionLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="ResetCustomThemeButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重置主题</string>
</property>
</widget>
</item>
<item>
<spacer name="CustomThemeActionSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="CustomThemeStatusLabel">
<property name="text">
<string>当前使用程序 默认 外观。</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="AppearancePageSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</spacer>
</item>
</layout>
</widget>
</widget>
</widget>
</item>