from datetime import datetime, timezone from zoneinfo import ZoneInfo from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QAbstractItemView from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QSortFilterProxyModel from qfluentwidgets import ( TableView, FluentIcon as Fi, PillPushButton, PrimaryPushButton ) from common.utils import SAFE_MAP_ICON, SAFE_MAP from common.config import cfg # dict[str, str | int] 就是 # { # "ID": "xxx", # "NAME": "zzz", # "SAFE": -2, # "UPDATE_DATA": "aaa", # "NOTES": "" # } 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 SafeFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) self.accepted_status = set() def set_accepted_status(self, status: list[int]): self.accepted_status = set(status) self.invalidateFilter() def filterAcceptsRow(self, source_row: int, source_parent, /): index = self.sourceModel().index(source_row, 2, source_parent) ext_d: dict[str, str | int] = self.sourceModel().data(index, Qt.ItemDataRole.UserRole) return ext_d["SAFE"] in self.accepted_status 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 = PrimaryPushButton(Fi.SYNC, "刷新", self) self.pbn_add = PrimaryPushButton(Fi.ADD, "添加", self) self.pbn_delete = PrimaryPushButton(Fi.DELETE, "删除", self) safe_checks = [ ("安全", 1), ("未知", 0), ("不安全", -1), ("未记录", -2), ] self.safe_switches: list[PillPushButton] = [] for text, m in safe_checks: c = PillPushButton(self) c.setText(text) c.setIcon(SAFE_MAP_ICON[m]) c.setProperty("mark", m) c.toggled.connect(self.update_filter) self.safe_switches.append(c) 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) for c in self.safe_switches: self.hly_top.addWidget(c) self.vly_m.addLayout(self.hly_top) self.tbv_m = TableView(self) self.ext_model = ExtensionsDataTable(ext_data, self) self.fil_model = SafeFilterProxyModel(self) self.fil_model.setSourceModel(self.ext_model) self.tbv_m.setModel(self.fil_model) self.tbv_m.setSortingEnabled(True) 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.tbv_m.scrollDelagate.verticalSmoothScroll.setSmoothMode(cfg.get(cfg.smooth_mode)) self.vly_m.addWidget(self.tbv_m) # 放到最后 for c in self.safe_switches: c.setChecked(True) def update_filter(self, checked: bool): accepted_status = [] for c in self.safe_switches: if c.isChecked(): accepted_status.append(c.property("mark")) self.fil_model.set_accepted_status(accepted_status)