feat(lite): 持久化代理模式配置并发布 9999091816

This commit is contained in:
RoyirinRosmontis
2026-09-18 16:23:40 +08:00
parent 8063b6d4e8
commit 71a67d7dca
3 changed files with 438 additions and 48 deletions
+178 -4
View File
@@ -6,7 +6,7 @@ from dataclasses import dataclass
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QPushButton, QVBoxLayout, QWidget,
)
@@ -292,6 +292,180 @@ class WhitelistDialog(QDialog):
self.accept()
@dataclass(frozen=True)
class FixedRouteServices:
"""域名路由编辑框所需的规则与节点服务。"""
load_icon: object
load_rules: object
available_nodes: object
validate_domain: object
save_rules: object
class FixedRouteDialog(QDialog):
"""为域名配置强制直连或指定节点代理。"""
def __init__(self, services: FixedRouteServices, parent=None):
super().__init__(parent)
self._services = services
self.setWindowTitle("域名路由")
self.setWindowIcon(services.load_icon())
self.setFixedSize(620, 530)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit, QComboBox {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 5px 8px; color: {C_TEXT}; font-size: 12px; }}
QListWidget {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 4px; color: {C_TEXT}; font-size: 12px; }}
QListWidget::item {{ padding: 5px 7px; border-radius: 4px; }}
QListWidget::item:selected {{ background-color: #e3e8ee; color: {C_TEXT}; }}
QPushButton#btnOk {{ background-color: {C_GREEN}; color: #fff; border: none;
border-radius: 6px; padding: 5px 12px; font-size: 12px; font-weight: bold; }}
QPushButton#btnOk:hover {{ background-color: #5ed978; }}
QPushButton#btnSmall {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 4px 10px; font-size: 11px; }}
QPushButton#btnSmall:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
layout = QVBoxLayout(self)
layout.setSpacing(9)
layout.setContentsMargins(18, 14, 18, 14)
title = QLabel("域名路由")
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
hint = QLabel("example.com 匹配根域及子域;*.example.com 仅匹配子域")
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
hint.setAlignment(Qt.AlignCenter)
layout.addWidget(hint)
self.lb_err = QLabel("")
self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
self.lb_err.setFixedHeight(16)
self.lb_err.hide()
layout.addWidget(self.lb_err)
self.list_widget = QListWidget()
self.list_widget.setToolTip("勾选启用规则;选中后可删除")
layout.addWidget(self.list_widget, 1)
domain_row = QHBoxLayout()
domain_row.setSpacing(7)
self.ed_domain = QLineEdit()
self.ed_domain.setPlaceholderText("域名,如 example.com 或 *.example.com")
self.ed_domain.returnPressed.connect(self._add_rule)
domain_row.addWidget(self.ed_domain, 1)
self.cb_action = QComboBox()
self.cb_action.addItem("强制不代理", "direct")
self.cb_action.addItem("强制节点代理", "node")
self.cb_action.currentIndexChanged.connect(self._on_action_changed)
domain_row.addWidget(self.cb_action)
self.cb_node = QComboBox()
self.cb_node.setMinimumWidth(190)
domain_row.addWidget(self.cb_node)
self.btn_add = QPushButton("添加")
self.btn_add.setObjectName("btnOk")
self.btn_add.clicked.connect(self._add_rule)
domain_row.addWidget(self.btn_add)
layout.addLayout(domain_row)
middle = QHBoxLayout()
self.btn_delete = QPushButton("删除选中")
self.btn_delete.setObjectName("btnSmall")
self.btn_delete.clicked.connect(self._delete_selected)
middle.addWidget(self.btn_delete)
middle.addStretch()
layout.addLayout(middle)
buttons = QHBoxLayout()
buttons.addStretch()
self.btn_cancel = QPushButton("取消")
self.btn_cancel.setObjectName("btnSmall")
self.btn_cancel.clicked.connect(self.reject)
buttons.addWidget(self.btn_cancel)
self.btn_save = QPushButton("保存")
self.btn_save.setObjectName("btnOk")
self.btn_save.clicked.connect(self._save)
buttons.addWidget(self.btn_save)
layout.addLayout(buttons)
self._reload_nodes()
for rule in services.load_rules():
self._add_item(rule)
self._on_action_changed()
def _reload_nodes(self):
self.cb_node.clear()
for node in self._services.available_nodes():
self.cb_node.addItem(node["label"], node["ref"])
if not self.cb_node.count():
self.cb_node.addItem("暂无可用节点", None)
def _on_action_changed(self, *_):
self.cb_node.setEnabled(self.cb_action.currentData() == "node")
def _add_item(self, rule):
domain = str(rule.get("domain") or rule.get("value") or "").strip()
if not domain:
return
action = str(rule.get("action") or "direct")
node_label = str(rule.get("node_label") or "节点不可用")
target = "强制不代理" if action == "direct" else f"节点代理:{node_label}"
item = QListWidgetItem(f"{domain} → {target}")
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if rule.get("enabled", True) else Qt.Unchecked)
item.setData(Qt.UserRole, {
"domain": domain,
"action": action,
"node_ref": str(rule.get("node_ref") or ""),
})
self.list_widget.addItem(item)
def _add_rule(self):
domain, error = self._services.validate_domain(self.ed_domain.text())
if error:
self.lb_err.setText(error)
self.lb_err.show()
return
action = self.cb_action.currentData()
node_ref = self.cb_node.currentData() if action == "node" else ""
if action == "node" and not node_ref:
self.lb_err.setText("请先配置至少一个可用节点")
self.lb_err.show()
return
for index in range(self.list_widget.count()):
existing = self.list_widget.item(index).data(Qt.UserRole) or {}
if str(existing.get("domain") or "").lower() == domain.lower():
self.lb_err.setText("该域名规则已存在")
self.lb_err.show()
return
self.lb_err.hide()
self._add_item({
"domain": domain,
"action": action,
"node_ref": node_ref,
"node_label": self.cb_node.currentText() if action == "node" else "",
"enabled": True,
})
self.ed_domain.clear()
def _delete_selected(self):
for item in self.list_widget.selectedItems():
self.list_widget.takeItem(self.list_widget.row(item))
def _save(self):
rules = []
for index in range(self.list_widget.count()):
item = self.list_widget.item(index)
rule = dict(item.data(Qt.UserRole) or {})
rule["enabled"] = item.checkState() == Qt.Checked
rules.append(rule)
self._services.save_rules(rules)
self.accept()
@dataclass(frozen=True)
class JumpServices:
"""代理跳板管理所需的节点摘要与配置回调。"""
@@ -682,7 +856,7 @@ class SettingsServices:
class SettingsDialog(QDialog):
"""代理端口、更新方式、白名单与登录状态设置框。"""
"""代理端口、更新方式、域名路由与登录状态设置框。"""
def __init__(self, services: SettingsServices, parent=None, port_locked=False):
super().__init__(parent)
@@ -728,9 +902,9 @@ class SettingsDialog(QDialog):
layout.addStretch()
whitelist_row = QHBoxLayout()
whitelist_label = QLabel("代理白名单")
whitelist_label = QLabel("域名路由")
whitelist_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
whitelist_label.setToolTip("命中列表的域名 / IP 将强制直连(不经过代理),每条规则可单独启用")
whitelist_label.setToolTip("为域名配置强制不代理,或强制经过指定节点;支持 *.example.com")
whitelist_row.addWidget(whitelist_label)
whitelist_row.addStretch()
self.btn_whitelist = QPushButton("配置")