init version

This commit is contained in:
Julian Freeman
2025-10-18 20:39:34 -04:00
parent f238c7c8d6
commit 4933f233a1
6 changed files with 596 additions and 149 deletions

View File

@@ -1,26 +1,74 @@
import sys
import requests
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton, QMessageBox, QDialog,
QFormLayout, QLineEdit, QComboBox, QTextEdit, QDialogButtonBox,
QAbstractItemView
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QAbstractItemView
)
from PySide6.QtCore import Qt, QThread, Signal, QObject
from typing import Dict, Any, Optional
from PySide6.QtCore import (
Qt, QThread, Signal, QObject, QAbstractTableModel, QModelIndex, QLocale, QAbstractListModel
)
from PySide6.QtGui import QIcon
from qfluentwidgets import (
MSFluentWindow, setTheme, Theme, TableView, PushButton, MessageBox, MessageBoxBase,
LineEdit, InfoBar, TextEdit, InfoBarIcon, FluentTranslator, TeachingTip, TeachingTipTailPosition,
ModelComboBox,
)
from qfluentwidgets import FluentIcon as Fi
import resources
# dict[str, str | int] 就是
# {
# "ID": "xxx",
# "NAME": "zzz",
# "SAFE": -2,
# "UPDATE_DATA": "aaa",
# "NOTES": ""
# }
# --- API 客户端配置 ---
BASE_URL = "http://127.0.0.1:17701/api/v1/ext"
BASE_URL = "https://safe-marks.oranj.work/api/v1/ext"
# 安全状态与中文的映射 (根据新要求)
SAFE = [1, 0, -1, -2]
SAFE_MAP = {
1: "安全 (safe)",
0: "未知 (unsure)",
-1: "不安全 (unsafe)",
-2: "不详 (unknown)"
1: "安全",
0: "未知",
-1: "不安全",
-2: "未记录"
}
# 反向映射,用于 QComboBox
SAFE_MAP_INV = {v: k for k, v in SAFE_MAP.items()}
SAFE_MAP_ICON = {
1: InfoBarIcon.SUCCESS.icon(),
0: InfoBarIcon.WARNING.icon(),
-1: InfoBarIcon.ERROR.icon(),
-2: InfoBarIcon.INFORMATION.icon(),
}
class APIException(Exception):
pass
def show_quick_tip(widget: QWidget, caption: str, text: str):
mb = MessageBox(caption, text, widget)
mb.cancelButton.setHidden(True)
mb.buttonLayout.insertStretch(0, 1)
mb.buttonLayout.setStretch(1, 0)
mb.yesButton.setMinimumWidth(100)
mb.setClosableOnMaskClicked(True)
mb.exec()
def accept_warning(widget: QWidget, condition: bool,
caption: str = "警告", text: str = "你确定要继续吗?") -> bool:
if condition:
mb = MessageBox(caption, text, widget)
if not mb.exec():
return True
return False
# --- API 工作线程 ---
@@ -43,9 +91,9 @@ class ApiWorker(QObject):
if not response.ok:
try:
detail = response.json().get("detail", response.text)
raise Exception(f"API 错误 (状态 {response.status_code}): {detail}")
raise APIException(f"API 错误 (状态 {response.status_code}): {detail}")
except requests.JSONDecodeError:
raise Exception(f"API 错误 (状态 {response.status_code}): {response.text}")
raise APIException(f"API 错误 (状态 {response.status_code}): {response.text}")
return response.json()
def do_query_all(self):
@@ -57,7 +105,7 @@ class ApiWorker(QObject):
except Exception as e:
self.error.emit(str(e))
def do_add_one(self, extension: Dict[str, Any]):
def do_add_one(self, extension: dict[str, str | int]):
"""执行 add_one 操作 (dict)"""
try:
response = self.session.post(f"{BASE_URL}/add_one", json=extension)
@@ -66,7 +114,7 @@ class ApiWorker(QObject):
except Exception as e:
self.error.emit(str(e))
def do_update_one(self, item_id: str, payload: Dict[str, Any]):
def do_update_one(self, item_id: str, payload: dict[str, str | int]):
"""执行 update_one 操作 (dict)"""
try:
response = self.session.put(f"{BASE_URL}/update_one/{item_id}", json=payload)
@@ -85,127 +133,241 @@ class ApiWorker(QObject):
self.error.emit(str(e))
class SafeStatusListModel(QAbstractListModel):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.safe_status = SAFE
def rowCount(self, /, parent = ...):
return len(self.safe_status)
def columnCount(self, parent, /):
return 1
def data(self, index: QModelIndex, /, role: int = ...):
row = index.row()
if role == Qt.ItemDataRole.EditRole:
return SAFE_MAP[self.safe_status[row]]
elif role == Qt.ItemDataRole.DecorationRole:
return SAFE_MAP_ICON[self.safe_status[row]]
elif role == Qt.ItemDataRole.UserRole:
return self.safe_status[row]
return None
# --- 添加/编辑 对话框 ---
class ExtensionDialog(QDialog):
class ExtensionDialog(MessageBoxBase):
"""用于添加或编辑插件条目的对话框。"""
def __init__(self, parent=None, extension_data: Optional[Dict] = None):
def __init__(self, extension_data: dict[str, str | int] = None, parent=None):
super().__init__(parent)
self.is_edit_mode = extension_data is not None
self.setWindowTitle("编辑插件" if self.is_edit_mode else "添加插件")
self.setMinimumWidth(400)
# 创建控件
self.id_edit = QLineEdit()
self.name_edit = QLineEdit()
self.safe_combo = QComboBox()
self.notes_edit = QTextEdit()
self.lne_id = LineEdit(self)
self.lne_name = LineEdit(self)
self.safe_combo = ModelComboBox(self)
self.notes_edit = TextEdit(self)
# 填充 SAFE 下拉框 (使用新的 MAP)
for text in SAFE_MAP_INV.keys():
self.safe_combo.addItem(text)
safe_list_model = SafeStatusListModel(self)
self.safe_combo.setModel(safe_list_model)
# 布局
form_layout = QFormLayout()
form_layout.addRow("ID:", self.id_edit)
form_layout.addRow("名称 (NAME):", self.name_edit)
form_layout.addRow("安全状态 (SAFE):", self.safe_combo)
form_layout.addRow("备注 (NOTES):", self.notes_edit)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
form_layout.addRow("ID:", self.lne_id)
form_layout.addRow("名称:", self.lne_name)
form_layout.addRow("安全状态:", self.safe_combo)
form_layout.addRow("备注:", self.notes_edit)
self.cw = QWidget(self)
main_layout = QVBoxLayout()
main_layout.addLayout(form_layout)
main_layout.addWidget(self.button_box)
self.setLayout(main_layout)
self.cw.setLayout(main_layout)
self.viewLayout.addWidget(self.cw)
if self.is_edit_mode:
self.id_edit.setText(extension_data.get("ID", ""))
self.id_edit.setReadOnly(True) # ID 不允许修改
self.name_edit.setText(extension_data.get("NAME", ""))
safe_status = extension_data.get("SAFE", 0)
self.safe_combo.setCurrentText(SAFE_MAP.get(safe_status, "未知 (unsure)"))
self.lne_id.setText(extension_data.get("ID", ""))
self.lne_id.setReadOnly(True) # ID 不允许修改
self.lne_name.setText(extension_data.get("NAME", ""))
safe_status: int = extension_data.get("SAFE", -2)
self.safe_combo.setCurrentIndex(SAFE.index(safe_status))
self.notes_edit.setText(extension_data.get("NOTES", ""))
else:
self.safe_combo.setCurrentIndex(SAFE.index(-2))
def get_data(self) -> Dict[str, Any]:
def validate(self) -> bool:
if len(self.lne_id.text()) == 0:
TeachingTip.create(
target=self.lne_id,
title="错误",
content="ID 不能为空!",
icon=InfoBarIcon.ERROR,
isClosable=True,
duration=2000,
tailPosition=TeachingTipTailPosition.BOTTOM,
parent=self
)
return False
if len(self.lne_name.text()) == 0:
TeachingTip.create(
target=self.lne_name,
title="错误",
content="名称不能为空!",
icon=InfoBarIcon.ERROR,
isClosable=True,
duration=2000,
tailPosition=TeachingTipTailPosition.BOTTOM,
parent=self,
)
return False
return True
def get_data(self) -> dict[str, str | int]:
"""获取对话框中用于 'add_one' 的数据"""
return {
"ID": self.id_edit.text(),
"NAME": self.name_edit.text(),
"SAFE": SAFE_MAP_INV.get(self.safe_combo.currentText(), 0),
"ID": self.lne_id.text(),
"NAME": self.lne_name.text(),
"SAFE": self.safe_combo.currentData(),
"NOTES": self.notes_edit.toPlainText()
}
def get_update_payload(self) -> Dict[str, Any]:
def get_update_payload(self) -> dict[str, str | int]:
"""获取对话框中用于 'update_one' 的数据 (不含ID)"""
return {
"NAME": self.name_edit.text(),
"SAFE": SAFE_MAP_INV.get(self.safe_combo.currentText(), 0),
"NAME": self.lne_name.text(),
"SAFE": self.safe_combo.currentData(),
"NOTES": self.notes_edit.toPlainText()
}
class ExtensionsDataTable(QAbstractTableModel):
def __init__(self, ext_data: list[dict[str, str | int]], parent=None):
super().__init__(parent)
self.ext_data = ext_data or []
self.headers = ["ID", "名称", "安全性", "更新日期", "备注"]
self.col_map = {
0: "ID",
1: "NAME",
2: "SAFE",
3: "UPDATE_DATE",
4: "NOTES",
}
def rowCount(self, /, parent = ...):
return len(self.ext_data)
def columnCount(self, /, parent = ...):
return len(self.headers)
def headerData(self, section: int, orientation: Qt.Orientation, /, role: int = ...):
if orientation == Qt.Orientation.Horizontal:
if role == Qt.ItemDataRole.DisplayRole:
return self.headers[section]
return None
def data(self, index: QModelIndex, /, role: int = ...):
row = index.row()
col = index.column()
ext = self.ext_data[row]
col_val = ext[self.col_map[col]]
if role == Qt.ItemDataRole.DisplayRole:
if col == 2:
return SAFE_MAP[col_val]
elif col == 3:
return self.iso2custom(col_val)
else:
return col_val
if role == Qt.ItemDataRole.DecorationRole:
if col == 2:
return SAFE_MAP_ICON[col_val]
# 一行的每一个列都返回所有数据,这样不管双击一行的哪一个位置,都可以获取到所有数据
if role == Qt.ItemDataRole.UserRole:
return ext
return None
def update_data(self, ext_data: list[dict[str, str | int]]):
self.beginResetModel()
self.ext_data.clear()
self.ext_data.extend(ext_data)
self.endResetModel()
@staticmethod
def iso2custom(iso_time: str) -> str:
dt = datetime.fromisoformat(iso_time)
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.astimezone(ZoneInfo("America/New_York"))
custom_format = "%Y-%m-%d %H:%M:%S"
return dt.strftime(custom_format)
class MainInterface(QWidget):
def __init__(self, ext_data: list[dict[str, str | int]] = None, parent=None):
super().__init__(parent)
self.setObjectName("main")
self.vly_m = QVBoxLayout()
self.setLayout(self.vly_m)
self.hly_top = QHBoxLayout()
self.pbn_refresh = PushButton(Fi.SYNC, "刷新", self)
self.pbn_add = PushButton(Fi.ADD, "添加", self)
self.pbn_delete = PushButton(Fi.DELETE, "删除", self)
self.hly_top.addWidget(self.pbn_refresh)
self.hly_top.addWidget(self.pbn_add)
self.hly_top.addWidget(self.pbn_delete)
self.hly_top.addStretch(1)
self.vly_m.addLayout(self.hly_top)
self.tbv_m = TableView(self)
self.ext_model = ExtensionsDataTable(ext_data, self)
self.tbv_m.setModel(self.ext_model)
self.tbv_m.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.tbv_m.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # 整行选中
self.tbv_m.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) # 只允许单选
self.tbv_m.verticalHeader().hide()
self.tbv_m.setBorderVisible(True)
self.tbv_m.setBorderRadius(4)
self.tbv_m.horizontalHeader().setStretchLastSection(True)
self.tbv_m.setColumnWidth(0, 250)
self.tbv_m.setColumnWidth(1, 200)
self.tbv_m.setColumnWidth(3, 180)
self.vly_m.addWidget(self.tbv_m)
# --- 主窗口 ---
class MainWindow(QMainWindow):
class MainWindow(MSFluentWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("插件管理器")
self.setGeometry(100, 100, 800, 600)
self.setWindowTitle("插件安全标记")
self.setWindowIcon(QIcon(":/logo.png"))
# self.navigationInterface.hide()
self.resize(1000, 760)
desktop = QApplication.screens()[0].availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
setTheme(Theme.LIGHT)
self.table_widget = QTableWidget()
self.setup_table()
# --- UI ---
self.main_interface = MainInterface(parent=self)
self.addSubInterface(self.main_interface, Fi.HOME, "主页", Fi.HOME_FILL)
self.refresh_button = QPushButton("刷新 (Query All)")
self.add_button = QPushButton("添加 (Add One)")
self.delete_button = QPushButton("删除选定 (Delete One)")
button_layout = QHBoxLayout()
button_layout.addWidget(self.refresh_button)
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
button_layout.addStretch()
main_layout = QVBoxLayout()
main_layout.addLayout(button_layout)
main_layout.addWidget(self.table_widget)
central_widget = QWidget()
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
self.setup_worker_thread()
# 连接信号和槽
self.refresh_button.clicked.connect(self.worker.do_query_all)
self.add_button.clicked.connect(self.on_add)
self.delete_button.clicked.connect(self.on_delete)
self.table_widget.doubleClicked.connect(self.on_edit)
# 初始加载数据
self.worker.do_query_all()
def setup_table(self):
"""初始化表格"""
self.table_widget.setColumnCount(5)
self.table_widget.setHorizontalHeaderLabels(
["ID", "名称 (NAME)", "安全 (SAFE)", "更新日期 (UPDATE_DATE)", "备注 (NOTES)"])
self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows) # 整行选中
self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) # 只允许单选
self.table_widget.horizontalHeader().setStretchLastSection(True)
self.table_widget.resizeColumnsToContents()
def setup_worker_thread(self):
"""初始化后台工作线程和 QObject"""
# --- 初始化后台工作线程和 QObject ---
self.thread = QThread()
self.worker = ApiWorker()
self.worker.moveToThread(self.thread)
# 连接 Worker 的新信号
self.worker.queryAllFinished.connect(self.populate_table)
self.worker.queryAllFinished.connect(self.main_interface.ext_model.update_data)
self.worker.addOneFinished.connect(self.handle_operation_finished)
self.worker.updateOneFinished.connect(self.handle_operation_finished)
self.worker.deleteOneFinished.connect(self.handle_operation_finished)
@@ -213,6 +375,15 @@ class MainWindow(QMainWindow):
self.thread.start()
# 连接信号和槽
self.main_interface.pbn_refresh.clicked.connect(self.worker.do_query_all)
self.main_interface.pbn_add.clicked.connect(self.on_add)
self.main_interface.pbn_delete.clicked.connect(self.on_delete)
self.main_interface.tbv_m.doubleClicked.connect(self.on_edit)
# 初始加载数据
self.worker.do_query_all()
def closeEvent(self, event):
"""关闭窗口时,确保线程也退出"""
self.thread.quit()
@@ -221,88 +392,61 @@ class MainWindow(QMainWindow):
# --- GUI 槽函数 (在主线程中运行) ---
def populate_table(self, data: list):
"""使用从 API 获取的数据填充表格"""
self.table_widget.setRowCount(0)
for row, item in enumerate(data):
self.table_widget.insertRow(row)
id_item = QTableWidgetItem(item.get("ID"))
name_item = QTableWidgetItem(item.get("NAME"))
safe_val = item.get("SAFE", 0)
safe_item = QTableWidgetItem(SAFE_MAP.get(safe_val, "未知 (unsure)")) # 使用新 MAP
date_item = QTableWidgetItem(item.get("UPDATE_DATE"))
notes_item = QTableWidgetItem(item.get("NOTES"))
# 存储原始数据到 ID 项中,以便编辑时检索
id_item.setData(Qt.UserRole, item)
self.table_widget.setItem(row, 0, id_item)
self.table_widget.setItem(row, 1, name_item)
self.table_widget.setItem(row, 2, safe_item)
self.table_widget.setItem(row, 3, date_item)
self.table_widget.setItem(row, 4, notes_item)
self.table_widget.resizeColumnsToContents()
def handle_operation_finished(self, result: dict):
"""添加/更新/删除成功后的通用处理器"""
QMessageBox.information(self, "操作成功", f"操作已成功完成。\n{result}")
InfoBar.success("", "操作已成功完成。", duration=2000, parent=self.window())
self.worker.do_query_all() # 重新加载数据
def handle_error(self, error_message: str):
"""显示来自工作线程的错误消息"""
QMessageBox.critical(self, "API 错误", f"与服务器通信时发生错误:\n{error_message}")
show_quick_tip(self, "API 错误", f"与服务器通信时发生错误:\n{error_message}")
# --- 按钮点击处理 ---
def on_add(self):
"""打开“添加”对话框 (add_one)"""
dialog = ExtensionDialog(self)
dialog = ExtensionDialog(parent=self)
if dialog.exec():
data = dialog.get_data()
if not data.get("ID") or not data.get("NAME"):
self.handle_error("ID 和 名称 (NAME) 不能为空。")
return
self.worker.do_add_one(data)
def on_edit(self, index):
def on_edit(self, index: QModelIndex):
"""打开“编辑”对话框(通过双击)(update_one)"""
row = index.row()
item_data = self.table_widget.item(row, 0).data(Qt.UserRole)
item_data = index.data(Qt.ItemDataRole.UserRole)
item_id = item_data.get("ID")
dialog = ExtensionDialog(self, extension_data=item_data)
dialog = ExtensionDialog(extension_data=item_data, parent=self)
if dialog.exec():
update_payload = dialog.get_update_payload()
self.worker.do_update_one(item_id, update_payload)
def on_delete(self):
"""删除当前选定的行 (delete_one)"""
current_row = self.table_widget.currentRow()
if current_row < 0:
QMessageBox.warning(self, "未选择", "请选择一行进行删除。")
# 前面设定了只能选中一行
ext_data: list[dict] = [index.data(Qt.ItemDataRole.UserRole)
for index in self.main_interface.tbv_m.selectedIndexes()
if index.column() == 0]
if len(ext_data) == 0:
show_quick_tip(self, "警告", "你没有选中任何插件。")
return
id_item = self.table_widget.item(current_row, 0)
item_id = id_item.text()
ext_d: dict = ext_data[0]
if accept_warning(self, True, "警告",
f"你确定要删除插件【{ext_d.get('NAME')}】吗?"):
return
reply = QMessageBox.question(
self,
"确认删除",
f"你确定要删除插件 (ID: {item_id}) 吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.worker.do_delete_one(item_id)
self.worker.do_delete_one(ext_d.get("ID"))
# --- 运行 GUI ---
if __name__ == "__main__":
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
translator = FluentTranslator(QLocale(QLocale.Language.Chinese, QLocale.Country.China))
app.installTranslator(translator)
win = MainWindow()
win.show()
return app.exec()
if __name__ == '__main__':
main()