from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QLabel, QHBoxLayout, QListWidget, QStackedWidget, QPushButton, QLineEdit, QMessageBox, QFormLayout, QComboBox, QListWidgetItem, QFrame, QScrollArea, QAbstractItemView, QApplication, QCheckBox, ) from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, QSize, pyqtSignal from PyQt6.QtGui import QMouseEvent, QPixmap, QImage, QFont import codecs import io import os import subprocess import qrcode from utils.wifi_manager import WifiManager from utils.floating_keyboard import FloatingKeyboard class DragScrollArea(QScrollArea): """支持鼠标/触屏点击拖拽滑动的滚动区域""" def __init__(self, parent=None): super().__init__(parent) self._press_pos = None self._start_value = 0 self._dragging = False def viewportEvent(self, event): etype = event.type() if etype == QEvent.Type.MouseButtonPress: if event.button() == Qt.MouseButton.LeftButton: self._press_pos = event.position().toPoint() self._start_value = self.verticalScrollBar().value() self._dragging = False elif etype == QEvent.Type.MouseMove: if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton: pos = event.position().toPoint() delta = pos - self._press_pos # 移动超过15px阈值才触发拖拽,避免误触 if self._dragging or delta.manhattanLength() > 15: self._dragging = True new_val = self._start_value - delta.y() # 限制在有效范围内 self.verticalScrollBar().setValue( max(0, min(new_val, self.verticalScrollBar().maximum())) ) return True # 拖拽中拦截事件,不传递给子控件 elif etype == QEvent.Type.MouseButtonRelease: if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None: was_dragging = self._dragging self._press_pos = None self._dragging = False if was_dragging: return True # 拖拽结束,拦截释放事件,避免触发子控件点击 return super().viewportEvent(event) class DraggableListWidget(QListWidget): """支持鼠标/触屏点击拖拽滑动的列表控件""" def __init__(self, parent=None): super().__init__(parent) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self._press_pos = None self._start_value = 0 self._dragging = False def viewportEvent(self, event): etype = event.type() if etype == QEvent.Type.MouseButtonPress: if event.button() == Qt.MouseButton.LeftButton: self._press_pos = event.position().toPoint() self._start_value = self.verticalScrollBar().value() self._dragging = False elif etype == QEvent.Type.MouseMove: if self._press_pos and event.buttons() & Qt.MouseButton.LeftButton: pos = event.position().toPoint() delta = pos - self._press_pos # 移动超过15px阈值才触发拖拽,避免误触 if self._dragging or delta.manhattanLength() > 15: self._dragging = True new_val = self._start_value - int(delta.y()) # 限制在有效范围内 self.verticalScrollBar().setValue( max(0, min(new_val, self.verticalScrollBar().maximum())) ) return True # 拖拽中拦截事件,不传递给子控件 elif etype == QEvent.Type.MouseButtonRelease: if event.button() == Qt.MouseButton.LeftButton and self._press_pos is not None: was_dragging = self._dragging self._press_pos = None self._dragging = False if was_dragging: return True # 拖拽结束,拦截释放事件,避免触发子控件点击 return super().viewportEvent(event) class WifiScanWorker(QObject): """在后台线程中执行WiFi扫描,避免阻塞UI""" scan_finished = pyqtSignal(list) scan_error = pyqtSignal(str) def __init__(self, wifi_manager): super().__init__() self.wifi_manager = wifi_manager def run(self): try: networks = self.wifi_manager.scan_networks() self.scan_finished.emit(networks) except Exception as e: self.scan_error.emit(str(e)) class WifiConnectWorker(QObject): """在后台线程中连接新WiFi""" finished = pyqtSignal(bool, str) # (success, ssid) error = pyqtSignal(str) def __init__(self, wifi_manager, auth_mode, ssid, password=None, identity=None): super().__init__() self.wifi_manager = wifi_manager self.auth_mode = auth_mode self.ssid = ssid self.password = password self.identity = identity def run(self): try: if self.auth_mode == "open": ok = self.wifi_manager.connect_wifi(self.ssid, None) elif self.auth_mode == "psk": ok = self.wifi_manager.connect_wifi(self.ssid, self.password) else: ok = self.wifi_manager.connect_eap(self.ssid, self.identity, self.password) self.finished.emit(ok, self.ssid) except Exception as e: self.error.emit(str(e)) class WifiConnectSavedWorker(QObject): """在后台线程中连接已保存的网络""" finished = pyqtSignal(bool, str) error = pyqtSignal(str) def __init__(self, wifi_manager, network_id, ssid): super().__init__() self.wifi_manager = wifi_manager self.network_id = network_id self.ssid = ssid def run(self): try: ok = self.wifi_manager.connect_network_id(self.network_id) self.finished.emit(ok, self.ssid) except Exception as e: self.error.emit(str(e)) class WifiRemoveWorker(QObject): """在后台线程中删除已保存的网络""" finished = pyqtSignal(bool, str) error = pyqtSignal(str) def __init__(self, wifi_manager, network_id, ssid): super().__init__() self.wifi_manager = wifi_manager self.network_id = network_id self.ssid = ssid def run(self): try: self.wifi_manager.remove_network(self.network_id) self.finished.emit(True, self.ssid) except Exception as e: self.error.emit(str(e)) class WifiHotspotWorker(QObject): """在后台线程中开启/关闭热点""" finished = pyqtSignal(bool, str) error = pyqtSignal(str) def __init__(self, wifi_manager, action, ssid=None, password=None): super().__init__() self.wifi_manager = wifi_manager self.action = action self.ssid = ssid self.password = password def run(self): try: if self.action == "open": ret = self.wifi_manager.open_hotspot(self.ssid, self.password) self.finished.emit(bool(ret), self.ssid or "") else: self.wifi_manager.close_hotspot() self.finished.emit(True, "") except Exception as e: self.error.emit(str(e)) class SettingPage(QWidget): def __init__(self, api_client, parent=None): super().__init__(parent) self.api_client = api_client self.wifi_manager = WifiManager() self._saved_networks_cache = [] # 缓存上次的已保存网络列表 # 让页面自身能接收焦点,用于点击扫描按钮后转移焦点防止跳动 self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) # 创建悬浮键盘 self._keyboard = FloatingKeyboard() self._keyboard_attached = False # 监听全局焦点变化,可靠处理键盘消失 self._focus_changed_connected = False self.init_ui() self._start_status_timer() def _on_app_focus_changed(self, old_widget, new_widget): """全局焦点变化时检查是否需要关闭键盘""" if not self._keyboard_attached: return # 如果新焦点在输入控件上,什么都不做 if isinstance(new_widget, (QLineEdit, QComboBox, QPushButton)): return # 如果新焦点是键盘自身的子控件,也不关闭 if new_widget and self._keyboard.isAncestorOf(new_widget): return # 如果新焦点是键盘本身,不关闭 if new_widget is self._keyboard: return # 如果是热点开关按钮的 ON/OFF 文本区域,不关闭 if isinstance(new_widget, QWidget) and new_widget.parent() is self.hotspot_toggle: return # 其他情况 → 关闭键盘 self._dismiss_keyboard() def _connect_focus_signal(self): """连接全局焦点信号""" if not self._focus_changed_connected: QApplication.instance().focusChanged.connect(self._on_app_focus_changed) self._focus_changed_connected = True def _disconnect_focus_signal(self): """断开全局焦点信号""" if self._focus_changed_connected: QApplication.instance().focusChanged.disconnect(self._on_app_focus_changed) self._focus_changed_connected = False def _attach_keyboard(self, widget): """将悬浮键盘绑定到指定输入框并显示""" self._keyboard.attach(widget) self._keyboard.show_below(widget) self._keyboard_attached = True self._connect_focus_signal() def _dismiss_keyboard(self): """关闭悬浮键盘""" self._keyboard.hide() self._keyboard.detach() self._keyboard_attached = False self._disconnect_focus_signal() def _on_input_focus_in(self, widget): """输入框获得焦点时的处理""" if self._keyboard_attached: self._keyboard.attach(widget) self._keyboard.show_below(widget) else: self._attach_keyboard(widget) def eventFilter(self, obj, event): if event.type() == QEvent.Type.FocusIn: if obj in (self.ssid_input, self.identity_input, self.password_input, self.hotspot_ssid, self.hotspot_password): self._on_input_focus_in(obj) # 如果不是输入框获得焦点且键盘正显示,则关闭键盘 elif self._keyboard_attached and not isinstance(obj, (QLineEdit, QComboBox, QPushButton)): self._dismiss_keyboard() elif event.type() == QEvent.Type.FocusOut: # 延迟检查:如果焦点真的离开了所有输入框,就关闭键盘 if obj in (self.ssid_input, self.identity_input, self.password_input, self.hotspot_ssid, self.hotspot_password): QTimer.singleShot(100, self._check_dismiss_keyboard) return super().eventFilter(obj, event) def _check_dismiss_keyboard(self): """检查当前焦点是否还在任何输入控件上,不在则关闭键盘""" w = self.focusWidget() if w not in (self.ssid_input, self.identity_input, self.password_input, self.auth_combo, self.hotspot_ssid, self.hotspot_password): self._dismiss_keyboard() @staticmethod def _styled_message(icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok): """显示与整体风格一致的暗色主题消息框""" msg = QMessageBox(parent) msg.setIcon(icon) msg.setWindowTitle(title) msg.setText(text) msg.setStandardButtons(buttons) msg.setStyleSheet(""" QMessageBox { background-color: #3f3f3f; color: #f2f2f2; border: 2px solid #646464; border-radius: 12px; padding: 20px; } QMessageBox QLabel { color: #f2f2f2; font-size: 20px; padding: 10px; } QMessageBox QPushButton { min-width: 130px; min-height: 50px; font-size: 20px; font-weight: 600; color: #f8f8f8; background-color: #555555; border: 2px solid #888888; border-radius: 10px; padding: 8px 24px; } QMessageBox QPushButton:hover { background-color: #636363; border-color: #aaaaaa; } QMessageBox QPushButton:pressed { background-color: #3d3d3d; border-color: #5a9fcf; } """) return msg.exec() def init_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(14, 14, 14, 14) layout.setSpacing(14) left_panel = QFrame() left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;") left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(10, 10, 10, 10) title = QLabel("系统设置") title.setAlignment(Qt.AlignmentFlag.AlignCenter) title.setStyleSheet("color: #efefef; font-size: 24px; font-weight: 600;") left_layout.addWidget(title) # 左侧设置项列表(启用平滑/按像素滚动,增大触摸目标) self.item_list = QListWidget() self.item_list.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.item_list.setSpacing(6) self.item_list.setUniformItemSizes(False) self.item_list.setStyleSheet( """ QListWidget { background-color: #4a4a4a; color: #f2f2f2; border: 1px solid #646464; border-radius: 8px; font-size: 22px; outline: none; } QListWidget::item { height: 56px; padding-left: 10px; border-radius: 6px; } QListWidget::item:selected { background-color: #2f6f91; color: white; } """ ) self.item_list.addItem("WiFi设置") self.item_list.addItem("热点设置") self.item_list.addItem("电源设置") self.item_list.addItem("其它设置(待定)") self.item_list.currentRowChanged.connect(self.display_setting) left_layout.addWidget(self.item_list) layout.addWidget(left_panel, 1) # 右侧设置参数区域 self.settings_stack = QStackedWidget() self.settings_stack.setStyleSheet("background-color: #444444; border-radius: 10px;") # 每个 init 方法返回一个包裹了 DragScrollArea 的页面 self.init_wifi_settings() self.init_hotspot_settings() self.init_power_settings() self.init_todo_settings() layout.addWidget(self.settings_stack, 3) self.item_list.setCurrentRow(0) def init_wifi_settings(self): wifi_widget = QWidget() wifi_widget.setStyleSheet( """ /* ------ 通用标签 ------ */ QLabel { color: #e8e8e8; } /* ------ 大按钮:清晰边框、足够高度 ------ */ QPushButton { min-height: 50px; font-size: 20px; font-weight: 600; color: #f8f8f8; background-color: #555555; border: 2px solid #888888; border-radius: 10px; padding: 10px 24px; } QPushButton:hover { background-color: #636363; border-color: #aaaaaa; } QPushButton:pressed { background-color: #3d3d3d; border-color: #5a9fcf; } QPushButton:disabled { color: #808080; background-color: #404040; border-color: #555555; } /* ------ 输入框 ------ */ QLineEdit { min-height: 44px; font-size: 18px; color: #f2f2f2; background-color: #3a3a3a; border: 2px solid #707070; border-radius: 8px; padding: 4px 12px; } QLineEdit:focus { border-color: #5a9fcf; } /* ------ 下拉框 ------ */ QComboBox { min-height: 44px; font-size: 18px; color: #f2f2f2; background-color: #3a3a3a; border: 2px solid #707070; border-radius: 8px; padding: 4px 12px; } QComboBox:hover { border-color: #aaaaaa; } QComboBox QAbstractItemView { background-color: #3a3a3a; color: #f2f2f2; selection-background-color: #2f6f91; border: 1px solid #707070; font-size: 18px; } /* ------ 列表控件 ------ */ QListWidget { min-height: 300px; max-width: 500%; background-color: #3a3a3a; color: #f2f2f2; border: 2px solid #707070; border-radius: 8px; font-size: 18px; outline: none; } QListWidget::item { min-height: 40px; padding: 4px 10px; border-radius: 4px; } QListWidget::item:selected { background-color: #2f6f91; color: #ffffff; } QListWidget::item:selected QLabel { color: #ffffff; } QListWidget::item:hover { background-color: #505050; } """ ) wifi_layout = QVBoxLayout(wifi_widget) wifi_layout.setContentsMargins(16, 16, 16, 16) wifi_layout.setSpacing(14) wifi_title = QLabel("WiFi设置") wifi_title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;") wifi_layout.addWidget(wifi_title) self.current_status_label = QLabel("当前连接:未知") self.current_status_label.setStyleSheet("color: #cccccc; font-size: 19px; font-weight: 500;") wifi_layout.addWidget(self.current_status_label) # 显示保存的WiFi列表 saved_title = QLabel("已保存网络") saved_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;") wifi_layout.addWidget(saved_title) self.saved_wifi_list = DraggableListWidget() wifi_layout.addWidget(self.saved_wifi_list) saved_buttons_layout = QHBoxLayout() saved_buttons_layout.setSpacing(12) self.connect_saved_button = QPushButton("连接到此网络") self.connect_saved_button.clicked.connect(self.connect_to_saved_wifi) saved_buttons_layout.addWidget(self.connect_saved_button) self.remove_saved_button = QPushButton("删除选中") self.remove_saved_button.clicked.connect(self.remove_selected_saved_wifi) saved_buttons_layout.addWidget(self.remove_saved_button) wifi_layout.addLayout(saved_buttons_layout) nearby_title = QLabel("附近网络") nearby_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;") wifi_layout.addWidget(nearby_title) self.nearby_wifi_list = DraggableListWidget() self.nearby_wifi_list.itemClicked.connect(self.fill_ssid_from_scan) wifi_layout.addWidget(self.nearby_wifi_list) self.scan_button = QPushButton("扫描网络") self.scan_button.clicked.connect(self.scan_nearby_wifi) wifi_layout.addWidget(self.scan_button) # 连接新WiFi connect_title = QLabel("连接新网络") connect_title.setStyleSheet("color: #dcdcdc; font-size: 22px; font-weight: 600;") wifi_layout.addWidget(connect_title) form = QFormLayout() form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) form.setFormAlignment(Qt.AlignmentFlag.AlignLeft) form.setHorizontalSpacing(16) form.setVerticalSpacing(12) # 表单标签使用浅色 ssid_label = QLabel("SSID") ssid_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;") self.ssid_input = QLineEdit() self.ssid_input.setStyleSheet("max-width: 400%;") self.ssid_input.setPlaceholderText("输入WiFi名称") self.ssid_input.installEventFilter(self) self.ssid_input.textChanged.connect(self._on_ssid_text_changed) form.addRow(ssid_label, self.ssid_input) auth_label = QLabel("认证方式") auth_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;") self.auth_combo = QComboBox() self.auth_combo.setStyleSheet( """ QComboBox { max-width: 400%; font-size: 22px; min-height: 44px; } """ ) # 直接设置下拉列表视图的样式,避免被父级样式覆盖 self.auth_combo.view().setStyleSheet( """ QListView { font-size: 24px; min-height: 50px; padding: 6px 12px; } QListView::item { min-height: 50px; padding: 8px 12px; } """ ) self.auth_combo.addItem("开放网络(无密码)", "open") self.auth_combo.addItem("WPA/WPA2-PSK", "psk") self.auth_combo.addItem("WPA-EAP (PEAP/MSCHAPv2)", "eap") self.auth_combo.currentIndexChanged.connect(self.update_auth_fields) form.addRow(auth_label, self.auth_combo) identity_label = QLabel("身份") identity_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;") self.identity_label = identity_label self.identity_input = QLineEdit() self.identity_input.setStyleSheet("max-width: 400%;") self.identity_input.setPlaceholderText("企业网络用户名/身份") self.identity_input.installEventFilter(self) form.addRow(identity_label, self.identity_input) password_label = QLabel("密码") password_label.setStyleSheet("color: #d0d0d0; font-size: 19px; font-weight: 500;") self.password_label = password_label self.password_input = QLineEdit() self.password_input.setStyleSheet("max-width: 400%;") self.password_input.setPlaceholderText("输入WiFi密码") self.password_input.installEventFilter(self) self.password_input.setEchoMode(QLineEdit.EchoMode.Password) form.addRow(password_label, self.password_input) wifi_layout.addLayout(form) # 连接按钮使用醒目的强调色 self.connect_button = QPushButton("连接") self.connect_button.setStyleSheet( """ QPushButton { min-height: 52px; font-size: 22px; font-weight: 700; color: #ffffff; background-color: #2f6f91; border: 2px solid #4a9fc8; border-radius: 10px; padding: 10px 24px; } QPushButton:hover { background-color: #3a85b3; border-color: #6fb8dd; } QPushButton:pressed { background-color: #1e4d66; border-color: #2f6f91; } """ ) self.connect_button.clicked.connect(self.connect_to_wifi) wifi_layout.addWidget(self.connect_button) wifi_layout.addStretch() self.settings_stack.addWidget(self._wrap_scroll(wifi_widget)) self.update_auth_fields() self.refresh_saved_wifi() self.refresh_current_status() def init_hotspot_settings(self): hotspot_widget = QWidget() hotspot_layout = QVBoxLayout(hotspot_widget) hotspot_layout.setContentsMargins(20, 20, 20, 20) hotspot_layout.setSpacing(16) title = QLabel("热点设置") title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;") hotspot_layout.addWidget(title) # ── 左右分栏 ── split_row = QHBoxLayout() split_row.setSpacing(20) # ====== 左栏:信息设置 ====== left_col = QVBoxLayout() left_col.setSpacing(12) desc = QLabel("将设备设置为WiFi热点,其他设备可扫描连接") desc.setStyleSheet("color: #aaaaaa; font-size: 16px;") desc.setWordWrap(True) left_col.addWidget(desc) # SSID 输入 ssid_label = QLabel("热点名称 (SSID)") ssid_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;") left_col.addWidget(ssid_label) self.hotspot_ssid = QLineEdit("PrinterScreen-AP") self.hotspot_ssid.setPlaceholderText("输入热点名称") self.hotspot_ssid.setStyleSheet( "min-height: 44px; font-size: 20px; color: #f2f2f2; " "background-color: #3a3a3a; border: 2px solid #707070; " "border-radius: 8px; padding: 4px 12px;" ) self.hotspot_ssid.installEventFilter(self) left_col.addWidget(self.hotspot_ssid) # 密码输入 pw_label = QLabel("热点密码") pw_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;") left_col.addWidget(pw_label) self.hotspot_password = QLineEdit("12345678") self.hotspot_password.setPlaceholderText("至少8位") self.hotspot_password.setStyleSheet( "min-height: 44px; font-size: 20px; color: #f2f2f2; " "background-color: #3a3a3a; border: 2px solid #707070; " "border-radius: 8px; padding: 4px 12px;" ) self.hotspot_password.installEventFilter(self) left_col.addWidget(self.hotspot_password) # 热点开关行 toggle_row = QHBoxLayout() toggle_row.setSpacing(16) toggle_label = QLabel("热点开关") toggle_label.setStyleSheet("color: #d0d0d0; font-size: 20px; font-weight: 600;") toggle_row.addWidget(toggle_label) self.hotspot_toggle = QPushButton() self.hotspot_toggle.setCheckable(True) self.hotspot_toggle.setFixedSize(80, 40) self.hotspot_toggle.setCursor(Qt.CursorShape.PointingHandCursor) self._apply_toggle_style(False) self.hotspot_toggle.toggled.connect(self._on_hotspot_toggled) toggle_row.addWidget(self.hotspot_toggle) toggle_row.addStretch() left_col.addLayout(toggle_row) # 热点状态标签 self.hotspot_status = QLabel("热点状态:关闭") self.hotspot_status.setStyleSheet("color: #cccccc; font-size: 17px;") left_col.addWidget(self.hotspot_status) left_col.addStretch() split_row.addLayout(left_col, 1) # ====== 右栏:二维码 ====== right_col = QVBoxLayout() right_col.setSpacing(8) right_col.setAlignment(Qt.AlignmentFlag.AlignCenter) qr_title = QLabel("扫码连接") qr_title.setStyleSheet("color: #dcdcdc; font-size: 20px; font-weight: 600;") qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter) right_col.addWidget(qr_title) self.qr_label = QLabel() self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.qr_label.setFixedSize(240, 240) self.qr_label.setStyleSheet( "background-color: #ffffff; border: 2px solid #646464; border-radius: 8px;" ) right_col.addWidget(self.qr_label, alignment=Qt.AlignmentFlag.AlignCenter) self.qr_hint = QLabel("开启热点后自动生成二维码") self.qr_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) self.qr_hint.setStyleSheet("color: #909090; font-size: 15px;") right_col.addWidget(self.qr_hint) right_col.addStretch() split_row.addLayout(right_col, 1) hotspot_layout.addLayout(split_row) self.settings_stack.addWidget(self._wrap_scroll(hotspot_widget)) def _apply_toggle_style(self, checked): if checked: self.hotspot_toggle.setStyleSheet(""" QPushButton { background-color: #4CAF50; border: 2px solid #66BB6A; border-radius: 20px; font-size: 16px; font-weight: bold; color: white; } QPushButton:checked { background-color: #4CAF50; border: 2px solid #66BB6A; } """) self.hotspot_toggle.setText("ON") else: self.hotspot_toggle.setStyleSheet(""" QPushButton { background-color: #555555; border: 2px solid #707070; border-radius: 20px; font-size: 16px; font-weight: bold; color: #aaaaaa; } QPushButton:checked { background-color: #4CAF50; border: 2px solid #66BB6A; } """) self.hotspot_toggle.setText("OFF") def _on_hotspot_toggled(self, checked): # 立即阻塞信号,防止递归触发 self.hotspot_toggle.blockSignals(True) if checked: ssid = self.hotspot_ssid.text().strip() password = self.hotspot_password.text().strip() if not ssid: self.hotspot_toggle.setChecked(False) self.hotspot_toggle.blockSignals(False) self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请输入热点名称") return if len(password) < 8: self.hotspot_toggle.setChecked(False) self.hotspot_toggle.blockSignals(False) self._styled_message(QMessageBox.Icon.Warning, self, "提示", "密码至少需要8位") return # 按钮UI反馈 → 显示"开启中……" self.hotspot_toggle.setEnabled(False) self.hotspot_toggle.setText("开启中……") self.hotspot_toggle.blockSignals(False) self._hotspot_thread = QThread() self._hotspot_worker = WifiHotspotWorker(self.wifi_manager, "open", ssid, password) self._hotspot_worker.moveToThread(self._hotspot_thread) self._hotspot_thread.started.connect(self._hotspot_worker.run) self._hotspot_worker.finished.connect(self._on_hotspot_open_finished) self._hotspot_worker.error.connect(self._on_hotspot_open_error) self._hotspot_worker.finished.connect(self._hotspot_thread.quit) self._hotspot_worker.error.connect(self._hotspot_thread.quit) self._hotspot_worker.finished.connect(self._hotspot_worker.deleteLater) self._hotspot_worker.error.connect(self._hotspot_worker.deleteLater) self._hotspot_thread.finished.connect(self._hotspot_thread.deleteLater) self._hotspot_thread.start() else: # 关闭热点 self.hotspot_toggle.setEnabled(False) self.hotspot_toggle.setText("关闭中……") self.hotspot_toggle.blockSignals(False) self._hotspot_thread = QThread() self._hotspot_worker = WifiHotspotWorker(self.wifi_manager, "close") self._hotspot_worker.moveToThread(self._hotspot_thread) self._hotspot_thread.started.connect(self._hotspot_worker.run) self._hotspot_worker.finished.connect(self._on_hotspot_close_finished) self._hotspot_worker.error.connect(self._on_hotspot_close_error) self._hotspot_worker.finished.connect(self._hotspot_thread.quit) self._hotspot_worker.error.connect(self._hotspot_thread.quit) self._hotspot_worker.finished.connect(self._hotspot_worker.deleteLater) self._hotspot_worker.error.connect(self._hotspot_worker.deleteLater) self._hotspot_thread.finished.connect(self._hotspot_thread.deleteLater) self._hotspot_thread.start() def _on_hotspot_open_finished(self, ok, ssid): if ok: self._apply_toggle_style(True) self.hotspot_status.setText(f"热点状态:已开启 ({ssid})") self.hotspot_ssid.setEnabled(False) self.hotspot_password.setEnabled(False) self._generate_qr_code(ssid, self.hotspot_password.text().strip()) else: self._styled_message(QMessageBox.Icon.Critical, self, "错误", "开启热点失败: wpa_cli 返回失败") self.hotspot_toggle.blockSignals(True) self.hotspot_toggle.setChecked(False) self.hotspot_toggle.blockSignals(False) self._apply_toggle_style(False) self.hotspot_toggle.setEnabled(True) def _on_hotspot_open_error(self, err_msg): self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"开启热点失败: {err_msg}") self.hotspot_toggle.blockSignals(True) self.hotspot_toggle.setChecked(False) self.hotspot_toggle.blockSignals(False) self._apply_toggle_style(False) self.hotspot_toggle.setEnabled(True) def _on_hotspot_close_finished(self, ok, _msg): self._apply_toggle_style(False) self.hotspot_status.setText("热点状态:关闭") self.hotspot_ssid.setEnabled(True) self.hotspot_password.setEnabled(True) self.qr_label.clear() self.qr_hint.setText("开启热点后自动生成二维码") self.hotspot_toggle.setEnabled(True) def _on_hotspot_close_error(self, err_msg): # 关闭失败仍尝试恢复UI self._apply_toggle_style(False) self.hotspot_status.setText("热点状态:关闭") self.hotspot_ssid.setEnabled(True) self.hotspot_password.setEnabled(True) self.qr_label.clear() self.qr_hint.setText("开启热点后自动生成二维码") self.hotspot_toggle.setEnabled(True) self._styled_message(QMessageBox.Icon.Warning, self, "提示", f"关闭热点时出现异常: {err_msg}") def _generate_qr_code(self, ssid, password): """生成 WiFi 二维码并显示""" wifi_str = f"WIFI:S:{ssid};T:WPA;P:{password};;" try: qr = qrcode.QRCode(box_size=6, border=2) qr.add_data(wifi_str) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") # 转换为 QPixmap buffer = io.BytesIO() img.save(buffer, format="PNG") buffer.seek(0) pixmap = QPixmap() pixmap.loadFromData(buffer.getvalue()) scaled = pixmap.scaled(220, 220, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.qr_label.setPixmap(scaled) self.qr_hint.setText(f"扫码连接 WiFi: {ssid}") except Exception as e: self.qr_hint.setText(f"二维码生成失败: {str(e)}") def init_power_settings(self): power_widget = QWidget() power_layout = QVBoxLayout(power_widget) power_layout.setContentsMargins(20, 20, 20, 20) power_layout.setSpacing(16) title = QLabel("电源设置") title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;") power_layout.addWidget(title) desc = QLabel("重启或关闭设备") desc.setStyleSheet("color: #aaaaaa; font-size: 16px;") power_layout.addWidget(desc) power_layout.addStretch() # 重启按钮 reboot_btn = QPushButton("↻ 重启系统") reboot_btn.setStyleSheet(""" QPushButton { min-height: 70px; font-size: 26px; font-weight: 700; color: #ffffff; background-color: #2f6f91; border: 2px solid #4a9fc8; border-radius: 14px; padding: 10px 24px; } QPushButton:hover { background-color: #3a85b3; border-color: #6fb8dd; } QPushButton:pressed { background-color: #1e4d66; border-color: #2f6f91; } """) reboot_btn.clicked.connect(self._confirm_reboot) power_layout.addWidget(reboot_btn) power_layout.addSpacing(20) # 关机按钮 shutdown_btn = QPushButton("⏻ 关机") shutdown_btn.setStyleSheet(""" QPushButton { min-height: 70px; font-size: 26px; font-weight: 700; color: #ffffff; background-color: #c0392b; border: 2px solid #e74c3c; border-radius: 14px; padding: 10px 24px; } QPushButton:hover { background-color: #e74c3c; border-color: #ff6b6b; } QPushButton:pressed { background-color: #922b21; border-color: #c0392b; } """) shutdown_btn.clicked.connect(self._confirm_shutdown) power_layout.addWidget(shutdown_btn) power_layout.addStretch() self.settings_stack.addWidget(self._wrap_scroll(power_widget)) def _confirm_reboot(self): reply = QMessageBox.question( self, "确认重启", "确定要重启系统吗?\n所有未保存的数据将丢失。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self._styled_message( QMessageBox.Icon.Information, self, "重启", "系统正在重启..." ) QTimer.singleShot(500, lambda: os.system("sudo reboot")) def _confirm_shutdown(self): reply = QMessageBox.question( self, "确认关机", "确定要关闭系统吗?\n关闭后需要手动重新开机。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self._styled_message( QMessageBox.Icon.Information, self, "关机", "系统正在关机..." ) QTimer.singleShot(500, lambda: os.system("sudo poweroff")) def init_todo_settings(self): todo_widget = QWidget() todo_widget.setStyleSheet( """ QLabel { color: #e4e4e4; } QPushButton { min-height: 50px; font-size: 20px; font-weight: 600; color: #f8f8f8; background-color: #555555; border: 2px solid #888888; border-radius: 10px; padding: 10px 24px; } QPushButton:hover { background-color: #636363; border-color: #aaaaaa; } """ ) todo_layout = QVBoxLayout(todo_widget) todo_layout.setContentsMargins(20, 20, 20, 20) label = QLabel("其它配置项待定") label.setAlignment(Qt.AlignmentFlag.AlignCenter) label.setStyleSheet("color: #cecece; font-size: 26px; font-weight: 600;") todo_layout.addWidget(label) hint = QLabel("更多设置项将在后续版本中添加") hint.setAlignment(Qt.AlignmentFlag.AlignCenter) hint.setStyleSheet("color: #909090; font-size: 18px; margin-top: 12px;") todo_layout.addWidget(hint) todo_layout.addStretch() self.settings_stack.addWidget(self._wrap_scroll(todo_widget)) def refresh_saved_wifi(self): try: saved_networks = self.wifi_manager.list_saved_networks() except Exception as e: self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"无法加载保存的WiFi: {str(e)}") return # 比较与上次缓存的网络列表是否有变化 cache_key = [(n.get("network_id"), n.get("ssid"), n.get("flags")) for n in saved_networks] if cache_key == self._saved_networks_cache: return # 无变化,不清空列表 self._saved_networks_cache = cache_key # 列表有变化时才真正刷新 current_item = self.saved_wifi_list.currentItem() current_net_id = current_item.data(Qt.ItemDataRole.UserRole).get("network_id") if current_item else None self.saved_wifi_list.clear() for network in saved_networks: item_text = f"{network.get('ssid', '')}" item = QListWidgetItem(item_text) item.setData(Qt.ItemDataRole.UserRole, network) self.saved_wifi_list.addItem(item) # 恢复选中 if current_net_id is not None and network.get("network_id") == current_net_id: self.saved_wifi_list.setCurrentItem(item) def scan_nearby_wifi(self): """在后台线程中扫描WiFi,防止界面卡死""" self.nearby_wifi_list.clear() # 把焦点交给页面自身,防止跳到输入框导致画面跳动 self.setFocus() self.scan_button.setEnabled(False) self.scan_button.setText("扫描中……") self._scan_thread = QThread() self._scan_worker = WifiScanWorker(self.wifi_manager) self._scan_worker.moveToThread(self._scan_thread) self._scan_thread.started.connect(self._scan_worker.run) self._scan_worker.scan_finished.connect(self._on_scan_finished) self._scan_worker.scan_error.connect(self._on_scan_error) # 清理线程资源 self._scan_worker.scan_finished.connect(self._scan_thread.quit) self._scan_worker.scan_error.connect(self._scan_thread.quit) self._scan_worker.scan_finished.connect(self._scan_worker.deleteLater) self._scan_worker.scan_error.connect(self._scan_worker.deleteLater) self._scan_thread.finished.connect(self._scan_thread.deleteLater) self._scan_thread.start() def _on_scan_finished(self, networks): """扫描完成后的UI更新(主线程中执行)""" self.scan_button.setEnabled(True) self.scan_button.setText("扫描网络") processed = self._deduplicate_networks(networks) if not processed: self._styled_message(QMessageBox.Icon.Information, self, "提示", "未扫描到可用网络") return for network in processed: ssid = network.get("ssid", "") if not ssid: continue decoded_ssid = self._decode_ssid(ssid) signal = network.get("signal_level", "") # 自定义列表项:SSID靠左,信号强度靠右 item_widget = QWidget() item_widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # 为了比较好看的列表项目,下面的不要删 item_widget.setStyleSheet(""" QWidget { min-height: 40px; padding: 4px 10px; border-radius: 4px; } QWidget:selected { background-color: #2f6f91; color: #ffffff; } QWidget:selected QLabel { color: #ffffff; } QWidget:hover { background-color: #505050; } """) item_layout = QHBoxLayout(item_widget) item_layout.setContentsMargins(8, 2, 12, 2) ssid_label = QLabel(decoded_ssid) ssid_label.setStyleSheet("background: transparent; color: #f2f2f2; font-size: 18px;") try: signal = int(signal) except (ValueError, TypeError): signal = 0 signal_label = QLabel(f"{signal} dBm" if signal < 0 else f"{signal}%") signal_label.setStyleSheet("background: transparent; color: #aaaaaa; font-size: 16px;") item_layout.addWidget(ssid_label) item_layout.addStretch() item_layout.addWidget(signal_label) item = QListWidgetItem() item.setData(Qt.ItemDataRole.UserRole, network) item.setSizeHint(item_widget.sizeHint()) self.nearby_wifi_list.addItem(item) self.nearby_wifi_list.setItemWidget(item, item_widget) def _on_scan_error(self, error_msg): """扫描出错后的UI恢复(主线程中执行)""" self.scan_button.setEnabled(True) self.scan_button.setText("扫描网络") self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"扫描网络失败: {error_msg}") def _on_ssid_text_changed(self, text): """SSID输入框文本变化时,如果清空则重置为开放网络""" if not text.strip(): self.auth_combo.blockSignals(True) self.auth_combo.setCurrentIndex(0) self.auth_combo.blockSignals(False) self.update_auth_fields() @staticmethod def _decode_ssid(ssid): """尝试解码非英文WiFi名称(处理 \\xe9\\x83\\xbd 等转义序列)""" if not ssid: return ssid # 尝试 unicode_escape 解码(处理 \x 转义序列,返回 bytes) try: decoded = ssid.encode('latin-1').decode('unicode-escape').encode('latin-1').decode() if decoded != ssid: return decoded except Exception: pass # 尝试 latin-1 → UTF-8 双重转换 try: decoded = bytes(ssid, encoding='utf-8').decode('utf-8') if decoded != ssid: return decoded except Exception: pass return ssid @staticmethod def _deduplicate_networks(networks): """去重同名网络,每个SSID只保留信号最强的一个""" best = {} for net in networks: ssid = net.get("ssid", "") if not ssid: continue raw = net.get("signal_level", -100) try: signal = int(raw) except (ValueError, TypeError): signal = -100 if ssid not in best or signal > best[ssid].get("_signal_int", -100): net["_signal_int"] = signal best[ssid] = net return list(best.values()) @staticmethod def _detect_auth_mode(network): """根据 wpa_supplicant 返回的标准 flags 判断认证方式""" flags = network.get("flags", "").strip() # 无加密标记 → 开放网络 if not flags or flags in ("", "[ESS]", "[NONE]", "NONE"): return "open" # 检查是否含企业级认证标记 eap_keywords = ( "WPA2-EAP", "WPA-EAP", "WPA3-EAP", "EAP", "SUITE-B", "802.1X", "IEEE8021X", "ENTERPRISE", "FT/EAP", ) if any(kw in flags.upper() for kw in eap_keywords): return "eap" # 检查是否含 PSK 类标记(个人级加密) psk_keywords = ( "PSK", "SAE", "WPA2", "WPA3", "WPA", "CCMP", "TKIP", ) if any(kw in flags.upper() for kw in psk_keywords): return "psk" # 兜底:有标记但无法识别,默认 psk return "psk" def fill_ssid_from_scan(self, item): network = item.data(Qt.ItemDataRole.UserRole) or {} ssid = network.get("ssid", "") if ssid: decoded = self._decode_ssid(ssid) self.ssid_input.setText(decoded) # 根据网络标志自动选择认证方式 auth_mode = self._detect_auth_mode(network) index = self.auth_combo.findData(auth_mode) if index >= 0: self.auth_combo.blockSignals(True) self.auth_combo.setCurrentIndex(index) self.auth_combo.blockSignals(False) self.update_auth_fields() def update_auth_fields(self): auth_mode = self.auth_combo.currentData() if auth_mode == "open": self.identity_label.setVisible(False) self.identity_input.setVisible(False) self.password_label.setVisible(False) self.password_input.setVisible(False) elif auth_mode == "psk": self.identity_label.setVisible(False) self.identity_input.setVisible(False) self.password_label.setVisible(True) self.password_input.setVisible(True) else: self.identity_label.setVisible(True) self.identity_input.setVisible(True) self.password_label.setVisible(True) self.password_input.setVisible(True) def refresh_current_status(self): try: status = self.wifi_manager.get_current_status() ssid = status.get("ssid", "未连接") ip_addr = status.get("ip_address", "-") state = status.get("wpa_state", "UNKNOWN") self.current_status_label.setText(f"当前连接:{ssid} | IP: {ip_addr} | 状态: {state}") except Exception: self.current_status_label.setText("当前连接:未知") def _start_status_timer(self): """启动定时器,每秒刷新已保存网络列表和当前连接状态""" self._status_timer = QTimer(self) self._status_timer.timeout.connect(self._on_status_timer_tick) self._status_timer.start(1000) def _on_status_timer_tick(self): self.refresh_saved_wifi() self.refresh_current_status() def connect_to_saved_wifi(self): """连接已保存列表中选中的网络(后台线程)""" item = self.saved_wifi_list.currentItem() if item is None: self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络") return network = item.data(Qt.ItemDataRole.UserRole) or {} network_id = network.get("network_id") ssid = network.get("ssid", "") if network_id is None: self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效") return # 按钮UI反馈 self.connect_saved_button.setEnabled(False) self.connect_saved_button.setText("连接中……") self._saved_connect_thread = QThread() self._saved_connect_worker = WifiConnectSavedWorker(self.wifi_manager, network_id, ssid) self._saved_connect_worker.moveToThread(self._saved_connect_thread) self._saved_connect_thread.started.connect(self._saved_connect_worker.run) self._saved_connect_worker.finished.connect(self._on_saved_connect_finished) self._saved_connect_worker.error.connect(self._on_saved_connect_error) self._saved_connect_worker.finished.connect(self._saved_connect_thread.quit) self._saved_connect_worker.error.connect(self._saved_connect_thread.quit) self._saved_connect_worker.finished.connect(self._saved_connect_worker.deleteLater) self._saved_connect_worker.error.connect(self._saved_connect_worker.deleteLater) self._saved_connect_thread.finished.connect(self._saved_connect_thread.deleteLater) self._saved_connect_thread.start() def _on_saved_connect_finished(self, ok, ssid): self.connect_saved_button.setEnabled(True) self.connect_saved_button.setText("连接到此网络") if ok: self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}") else: self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败") self.refresh_saved_wifi() self.refresh_current_status() def _on_saved_connect_error(self, err_msg): self.connect_saved_button.setEnabled(True) self.connect_saved_button.setText("连接到此网络") self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {err_msg}") def remove_selected_saved_wifi(self): item = self.saved_wifi_list.currentItem() if item is None: self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络") return network = item.data(Qt.ItemDataRole.UserRole) or {} network_id = network.get("network_id") ssid = network.get("ssid", "") if network_id is None: self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效,无法删除") return # 按钮UI反馈 self.remove_saved_button.setEnabled(False) self.remove_saved_button.setText("删除中……") self._remove_thread = QThread() self._remove_worker = WifiRemoveWorker(self.wifi_manager, network_id, ssid) self._remove_worker.moveToThread(self._remove_thread) self._remove_thread.started.connect(self._remove_worker.run) self._remove_worker.finished.connect(self._on_remove_finished) self._remove_worker.error.connect(self._on_remove_error) self._remove_worker.finished.connect(self._remove_thread.quit) self._remove_worker.error.connect(self._remove_thread.quit) self._remove_worker.finished.connect(self._remove_worker.deleteLater) self._remove_worker.error.connect(self._remove_worker.deleteLater) self._remove_thread.finished.connect(self._remove_thread.deleteLater) self._remove_thread.start() def _on_remove_finished(self, ok, ssid): self.remove_saved_button.setEnabled(True) self.remove_saved_button.setText("删除选中") if ok: self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已删除网络: {ssid}") self.refresh_saved_wifi() self.refresh_current_status() def _on_remove_error(self, err_msg): self.remove_saved_button.setEnabled(True) self.remove_saved_button.setText("删除选中") self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {err_msg}") def connect_to_wifi(self): ssid = self.ssid_input.text().strip() password = self.password_input.text() identity = self.identity_input.text().strip() auth_mode = self.auth_combo.currentData() if not ssid: self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WiFi名称不能为空!") return if auth_mode == "psk" and not password: self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA/WPA2 认证需要密码") return if auth_mode == "eap" and (not identity or not password): self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA-EAP 认证需要身份和密码") return # 按钮UI反馈 self.connect_button.setEnabled(False) self.connect_button.setText("连接中……") self._connect_thread = QThread() self._connect_worker = WifiConnectWorker(self.wifi_manager, auth_mode, ssid, password, identity) self._connect_worker.moveToThread(self._connect_thread) self._connect_thread.started.connect(self._connect_worker.run) self._connect_worker.finished.connect(self._on_connect_finished) self._connect_worker.error.connect(self._on_connect_error) self._connect_worker.finished.connect(self._connect_thread.quit) self._connect_worker.error.connect(self._connect_thread.quit) self._connect_worker.finished.connect(self._connect_worker.deleteLater) self._connect_worker.error.connect(self._connect_worker.deleteLater) self._connect_thread.finished.connect(self._connect_thread.deleteLater) self._connect_thread.start() def _on_connect_finished(self, ok, ssid): self.connect_button.setEnabled(True) self.connect_button.setText("连接") if ok: self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}") else: self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败") self.refresh_saved_wifi() self.refresh_current_status() def _on_connect_error(self, err_msg): self.connect_button.setEnabled(True) self.connect_button.setText("连接") self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {err_msg}") def display_setting(self, index): if index < 0: return self.settings_stack.setCurrentIndex(index) @staticmethod def _wrap_scroll(widget): """将页面 widget 放入独立的 DragScrollArea 中""" scroll = DragScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setWidget(widget) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) return scroll