'''主窗体实现程序,项目核心模块''' import shutil import sys import time import os import json import traceback import datetime from pathlib import Path from PyQt6.QtWidgets import (QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, QTextEdit, QScrollArea, QMessageBox, QFrame, QToolBar, QStatusBar, QMenuBar, QMenu, QGroupBox, QSplitter, QListWidget, QComboBox, QFileDialog, QDialog, QApplication) sys.path.append("CONFIG_EDITOR") from PyQt6.QtCore import Qt, QTimer, QEvent, QMimeData from PyQt6.QtGui import QDrag, QColor, QBrush, QFont, QPainter, QPixmap, QIcon, QPen, QAction from dialogs import ConfigSettingsDialog, ConfigManagementWindow, ChangeConfirmDialog from models import ConfigField from widgets import NoWheelSpinBox, NoWheelDoubleSpinBox from utils import (format_value, validate_config_data, get_validation_tooltip, ensure_rule_structure) from config_parser import ConfigParser from file_handler import FileHandler from login.login_dialog import SecondaryPasswordDialog class ConfigEditor(QMainWindow): def __init__(self, auth_config_path=None, username="admin"): super().__init__() print(f"[调试] ConfigEditor初始化: auth_config_path={auth_config_path}, username={username}") # 用户信息和会话管理 self.current_user = username self.auth_config_path = auth_config_path # 账密文件路径 self.last_activity_time = time.time() # 记录最后活动时间 self.session_timeout = 600 # 超时锁定时间 self.is_locked = False # 是否已锁定 # 启动会话监控定时器 self.session_timer = QTimer() self.session_timer.timeout.connect(self.check_session_timeout) self.session_timer.start(10*1000) # 会话监控频率 # 调试定时器 self.debug_timer = QTimer() self.debug_timer.timeout.connect(self.show_session_status) self.debug_timer.start(10*1000) # 每10秒显示一次状态 # 获取程序所在目录 if getattr(sys, 'frozen', False): # 打包后的可执行文件 self.program_dir = Path(sys.executable).parent else: # 直接运行脚本 self.program_dir = Path(__file__).parent # 初始化文件处理器 self.file_handler = FileHandler(str(self.program_dir)) # 加载用户设置 self.user_settings = self.file_handler.load_user_settings() # 获取配置文件路径 self.config_file_path = self.user_settings.get("config_file_path", "") # 初始化数据 self.config_data = {} self.original_config_data = {} self.config_fields = {} self.all_config_fields = {} self.dynamic_widgets = {} self.category_widgets = {} self.original_content = "" self.rules = {} self.config_lines = [] self.modified_fields = set() self.field_containers = {} # 拖拽相关属性 self.dragging_field = None self.drag_start_pos = None self.drop_target_field = None self.field_order = {} self.init_ui() self.load_rules() # 检查是否第一次启动 if not self.config_file_path or not os.path.exists(self.config_file_path): self.show_settings_dialog(initial_setup=True) else: self.load_config() def load_icon(self): """加载程序图标""" from PyQt6.QtGui import QIcon, QPixmap, QPainter, QFont from PyQt6.QtCore import Qt # 方法1: 尝试加载本地图标文件 icon_paths = [ os.path.join(self.program_dir, "logo.ico"), os.path.join(self.program_dir, "logo.png"), os.path.join(self.program_dir, "resources", "logo.ico"), os.path.join(self.program_dir, "resources", "logo.png"), ] for path in icon_paths: if os.path.exists(path): try: icon = QIcon(path) if not icon.isNull(): print(f"加载图标: {path}") return icon except Exception as e: print(f"加载图标失败 {path}: {e}") # 方法2: 创建简单的CE色块图标 try: sizes = [16, 32, 48, 64, 128] icon = QIcon() for size in sizes: pixmap = QPixmap(size, size) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # 绘制蓝色圆角矩形背景 bg_color = QColor(33, 150, 243) painter.setBrush(QBrush(bg_color)) painter.setPen(Qt.PenStyle.NoPen) corner_radius = size // 8 painter.drawRoundedRect(0, 0, size, size, corner_radius, corner_radius) # 绘制白色CE文字 painter.setPen(QPen(Qt.GlobalColor.white, 1)) font_size = max(size // 3, 8) painter.setFont(QFont("Arial", font_size, QFont.Weight.Bold)) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "CE") painter.end() icon.addPixmap(pixmap) return icon except Exception as e: print(f"创建图标失败: {e}") return QIcon() def init_ui(self): self.setWindowTitle("配置管理页") self.setGeometry(100, 100, 1000, 700) self.resize(1000, 700) # 设置窗口图标 self.setWindowIcon(self.load_icon()) # 创建菜单栏 menubar = self.menuBar() # 文件菜单 file_menu = menubar.addMenu("文件") self.open_action = QAction("打开配置文件", self) self.open_action.triggered.connect(self.open_config_file_with_auth) file_menu.addAction(self.open_action) self.settings_action = QAction("配置文件设置", self) self.settings_action.triggered.connect(lambda: self.show_settings_dialog_with_auth(initial_setup=False)) file_menu.addAction(self.settings_action) file_menu.addSeparator() self.exit_action = QAction("退出", self) self.exit_action.triggered.connect(self.close) file_menu.addAction(self.exit_action) # 编辑菜单 edit_menu = menubar.addMenu("编辑") self.reload_action = QAction("重新加载", self) self.reload_action.triggered.connect(self.load_config) edit_menu.addAction(self.reload_action) self.save_action = QAction("保存配置", self) self.save_action.triggered.connect(self.save_config_with_auth) edit_menu.addAction(self.save_action) # 工具菜单 tools_menu = menubar.addMenu("工具") self.rules_action = QAction("管理规则", self) self.rules_action.triggered.connect(self.manage_rules_with_auth) tools_menu.addAction(self.rules_action) tools_menu.addSeparator() self.import_rules_action = QAction("导入规则文件", self) self.import_rules_action.triggered.connect(self.import_rules_file) tools_menu.addAction(self.import_rules_action) self.export_rules_action = QAction("导出规则文件", self) self.export_rules_action.triggered.connect(self.export_rules_file) tools_menu.addAction(self.export_rules_action) # 状态栏 self.statusBar().showMessage("就绪") central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(8, 8, 8, 8) main_layout.setSpacing(8) # 顶部信息栏(路径显示) info_frame = QFrame() info_frame.setFrameShape(QFrame.Shape.StyledPanel) info_frame.setStyleSheet(""" QFrame { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; } """) info_layout = QHBoxLayout(info_frame) info_layout.setContentsMargins(12, 8, 12, 8) self.path_label = QLabel() self.path_label.setStyleSheet(""" QLabel { color: #495057; font-size: 11px; font-family: 'Consolas', 'Monaco', monospace; } """) info_layout.addWidget(self.path_label) info_layout.addStretch() main_layout.addWidget(info_frame) self.tab_widget = QTabWidget() main_layout.addWidget(self.tab_widget) self.init_category_tabs() # 按钮布局 button_layout = QHBoxLayout() button_layout.setSpacing(8) self.open_btn = QPushButton("打开文件") self.open_btn.clicked.connect(self.open_config_file_with_auth) button_layout.addWidget(self.open_btn) self.settings_btn = QPushButton("配置文件设置") self.settings_btn.clicked.connect(lambda: self.show_settings_dialog_with_auth(initial_setup=False)) button_layout.addWidget(self.settings_btn) button_layout.addStretch() self.save_btn = QPushButton("保存配置") self.save_btn.clicked.connect(self.save_config_with_auth) self.save_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }") button_layout.addWidget(self.save_btn) self.reload_btn = QPushButton("重新加载") self.reload_btn.clicked.connect(self.load_config) button_layout.addWidget(self.reload_btn) self.rules_btn = QPushButton("管理规则") self.rules_btn.clicked.connect(self.manage_rules_with_auth) button_layout.addWidget(self.rules_btn) self.toggle_hidden_btn = QPushButton("显示隐藏项") self.toggle_hidden_btn.setCheckable(True) self.toggle_hidden_btn.toggled.connect(self.toggle_hidden_fields_with_auth) button_layout.addWidget(self.toggle_hidden_btn) self.hide_all_btn = QPushButton("一键隐藏所有项") self.hide_all_btn.clicked.connect(self.hide_all_fields_with_auth) self.hide_all_btn.setStyleSheet("QPushButton { background-color: #ff9800; color: white; }") button_layout.addWidget(self.hide_all_btn) main_layout.addLayout(button_layout) # 安装事件过滤器以监控用户活动 self.installEventFilter(self) # 更新路径显示 self.update_path_display() def show_session_status(self): """显示当前会话状态(用于调试)""" if self.is_locked: return # 如果已经锁定,不显示状态 current_time = time.time() elapsed = current_time - self.last_activity_time remaining = max(0, self.session_timeout - elapsed) # 只在剩余时间小于等于3分钟(180秒)时显示倒计时 if remaining <= 180: # 3分钟 = 180秒 minutes = int(remaining // 60) seconds = int(remaining % 60) if remaining < 60: # 少于1分钟 self.statusBar().showMessage(f"会话即将超时: {seconds}秒后锁定", 5000) else: # 1分钟到3分钟之间 self.statusBar().showMessage(f"会话剩余时间: {minutes}分{seconds}秒", 5000) # 调试信息(只在控制台显示) print(f"[调试] 最后活动时间: {datetime.datetime.fromtimestamp(self.last_activity_time).strftime('%H:%M:%S')}") print(f"[调试] 已空闲: {int(elapsed)}秒, 剩余: {minutes}分{seconds}秒") else: # 剩余时间大于3分钟时,显示空白内容 if not self.modified_fields: self.statusBar().showMessage("", 3000) # 调试信息(只在控制台显示) print(f"[调试] 最后活动时间: {datetime.datetime.fromtimestamp(self.last_activity_time).strftime('%H:%M:%S')}") print(f"[调试] 已空闲: {int(elapsed)}秒, 剩余: {int(remaining//60)}分{int(remaining%60)}秒 (超过3分钟,不显示倒计时)") print(f"[调试] 锁定状态: {self.is_locked}") def update_path_display(self): """更新路径显示""" if self.config_file_path and os.path.exists(self.config_file_path): abs_path = os.path.abspath(self.config_file_path) self.path_label.setText(f"配置文件: {abs_path}") self.path_label.setStyleSheet("color: #666666; font-size: 11px;") elif self.config_file_path: self.path_label.setText(f"配置文件不存在: {self.config_file_path} (点击'配置文件设置'调整)") self.path_label.setStyleSheet("color: #ff0000; font-size: 11px;") else: self.path_label.setText("未设置配置文件路径 (点击'配置文件设置'配置)") self.path_label.setStyleSheet("color: #ff9900; font-size: 11px;") def check_config_file(self): """检查配置文件是否存在""" if not self.config_file_path or not os.path.exists(self.config_file_path): return False return True def verify_secondary_password(self, operation_name=""): """验证二级密码的通用方法""" if not self.auth_config_path: QMessageBox.warning(self, "错误", "无法验证二级密码:未找到认证配置文件") return False dialog = SecondaryPasswordDialog(self.auth_config_path, self.current_user, self) dialog.setWindowTitle(f"二级密码验证 - {operation_name}") # 创建事件循环来等待验证结果 from PyQt6.QtCore import QEventLoop loop = QEventLoop() dialog.verified.connect(loop.quit) dialog.rejected.connect(loop.quit) dialog.show() # 进入事件循环等待 loop.exec() return dialog.result() == QDialog.DialogCode.Accepted def open_config_file_with_auth(self): """打开配置文件(带二级密码验证)""" if self.verify_secondary_password("打开文件"): self.open_config_file() def show_settings_dialog_with_auth(self, initial_setup=False): """显示设置对话框(带二级密码验证)""" if initial_setup or self.verify_secondary_password("配置文件设置"): self.show_settings_dialog(initial_setup) def manage_rules_with_auth(self): """管理规则(带二级密码验证)""" if self.verify_secondary_password("管理规则"): self.manage_rules() def toggle_hidden_fields_with_auth(self, checked): """切换显示/隐藏配置项(带二级密码验证)""" if self.verify_secondary_password("显示隐藏项"): self.toggle_hidden_fields(checked) def hide_all_fields_with_auth(self): """一键隐藏所有配置项(带二级密码验证)""" if self.verify_secondary_password("一键隐藏所有项"): self.hide_all_fields() def show_settings_dialog(self, initial_setup=False): """显示设置对话框 initial_setup: 是否是首次启动的设置 """ dialog = ConfigSettingsDialog(self.config_file_path, self) if initial_setup: # 首次启动,设置对话框模态且必须设置 dialog.setWindowTitle("首次启动 - 配置文件设置") dialog.setWindowModality(Qt.WindowModality.ApplicationModal) if dialog.exec() == QDialog.DialogCode.Accepted: settings = dialog.get_settings() # 更新设置 self.config_file_path = settings["config_file_path"] # 保存用户设置 self.file_handler.save_user_settings({ "config_file_path": self.config_file_path, "last_used": str(Path(self.config_file_path).absolute()) if self.config_file_path and os.path.exists(self.config_file_path) else "" }) # 更新路径显示 self.update_path_display() # 重新加载配置 if self.check_config_file(): self.load_config() if not initial_setup: QMessageBox.information(self, "成功", "配置文件路径已更新并重新加载配置") else: QMessageBox.warning(self, "警告", "配置文件不存在,请检查路径") elif initial_setup: # 首次启动用户取消了设置,退出程序 QMessageBox.warning(self, "提示", "首次启动需要设置配置文件路径,程序将退出。") sys.exit(0) def open_config_file(self): """打开配置文件""" if self.modified_fields: reply = QMessageBox.question( self, "确认", "当前有未保存的修改,是否保存?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel ) if reply == QMessageBox.StandardButton.Cancel: return elif reply == QMessageBox.StandardButton.Yes: self.save_config() # 使用文件对话框选择文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择配置文件", self.config_file_path or str(Path.home()), "Python Files (*.py);;All Files (*)" ) if file_path: self.config_file_path = file_path self.file_handler.save_user_settings({ "config_file_path": self.config_file_path, "last_used": str(Path(self.config_file_path).absolute()) }) self.update_path_display() self.load_config() def init_category_tabs(self): """初始化分类标签页""" # 清空现有的标签页 self.tab_widget.clear() self.category_widgets.clear() # 获取所有分组 categories = list(self.rules.get("categories", {}).keys()) if not categories: categories = ["未分类"] # 统计每个分组中未隐藏的配置项数量 category_counts = {} for category in categories: # 获取该分组中所有配置项 category_fields = self.rules.get("categories", {}).get(category, []) # 统计未隐藏的配置项数量 visible_count = 0 for field_name in category_fields: if field_name in self.config_fields: # config_fields只包含未隐藏的配置项 visible_count += 1 category_counts[category] = visible_count # 检查是否需要隐藏"未分类"分组 hide_uncategorized = False if "未分类" in categories: unclassified_count = category_counts.get("未分类", 0) # 如果有其他分组存在 other_categories = [c for c in categories if c != "未分类"] has_other_categories = len(other_categories) > 0 # 如果"未分类"分组没有未隐藏的配置项,且存在其他分组,则隐藏 if unclassified_count == 0 and has_other_categories: hide_uncategorized = True # 创建标签页 for category in categories: # 如果是"未分类"分组且需要隐藏,则跳过 if category == "未分类" and hide_uncategorized: continue self.create_category_tab(category) def create_category_tab(self, category): """创建分类标签页""" tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(4, 4, 4, 4) scroll = QScrollArea() scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) scroll_layout.setContentsMargins(8, 8, 8, 8) scroll_layout.setSpacing(6) self.category_widgets[category] = scroll_layout scroll.setWidget(scroll_widget) scroll.setWidgetResizable(True) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) layout.addWidget(scroll) self.tab_widget.addTab(tab, category) def load_rules(self): """加载规则""" self.rules = self.file_handler.load_rules() def create_field_widget(self, config_field): """创建字段编辑控件""" field_type = config_field.get_actual_field_type() if field_type == "bool": widget = QCheckBox() widget.setChecked(bool(config_field.value)) # 连接状态改变信号 widget.stateChanged.connect(lambda state, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "int": widget = NoWheelSpinBox() # 使用禁用了滚轮的SpinBox widget.setRange(-1000000, 1000000) if config_field.value is not None: widget.setValue(int(config_field.value)) # 连接值改变信号 widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "float": widget = NoWheelDoubleSpinBox() # 使用禁用了滚轮的DoubleSpinBox widget.setRange(-1000000.0, 1000000.0) # 根据规则设置小数位数 decimals = config_field.decimals or 6 # 默认6位 widget.setDecimalsFromRules(decimals) if config_field.value is not None: widget.setValue(float(config_field.value)) # 连接值改变信号 widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn)) elif field_type == "json": widget = QTextEdit() widget.setMaximumHeight(120) if config_field.value is not None: # 对于字典,使用漂亮的格式化显示 formatted_json = json.dumps(config_field.value, indent=4, ensure_ascii=False) widget.setPlainText(formatted_json) # 连接文本改变信号 widget.textChanged.connect(lambda fn=config_field.name: self.on_field_modified(fn)) else: widget = QLineEdit() if config_field.value is not None: widget.setText(str(config_field.value)) # 连接文本改变信号 widget.textChanged.connect(lambda text, fn=config_field.name: self.on_field_modified(fn)) # 添加校验提示到tooltip validation_tooltip = get_validation_tooltip(config_field.validation) full_tooltip = config_field.tooltip if validation_tooltip: full_tooltip += f"\n\n校验规则:\n{validation_tooltip}" widget.setToolTip(full_tooltip) widget.setMinimumWidth(300) return widget def on_field_modified(self, field_name): """当字段值被修改时的处理""" if field_name not in self.modified_fields: self.modified_fields.add(field_name) # 高亮显示修改的字段 self.highlight_modified_field(field_name) # 更新状态栏显示修改数量 self.update_status_bar() def highlight_modified_field(self, field_name): """高亮显示被修改的字段,但不覆盖子控件样式""" if field_name in self.field_containers: container = self.field_containers[field_name] # 只修改背景色,使用更精确的选择器确保不影响子控件 container.setStyleSheet(""" QFrame#field_container_%s { background-color: #fff9c4; } """ % field_name) def clear_field_highlight(self, field_name): """清除字段的高亮显示,恢复原有样式""" if field_name in self.field_containers: container = self.field_containers[field_name] # 恢复原始样式,不影响子控件 container.setStyleSheet(""" QFrame#field_container_%s { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } QFrame#field_container_%s:hover { border: 1px solid #2196F3; background-color: #f8fdff; } """ % (field_name, field_name)) def update_status_bar(self): """更新状态栏显示修改数量""" if self.modified_fields: self.statusBar().showMessage(f"已修改 {len(self.modified_fields)} 个配置项") else: self.statusBar().showMessage("就绪") # 鼠标按下事件处理器 def create_mouse_press_handler(self, field_name, label_widget): """创建鼠标按下事件处理器""" def mouse_press_handler(event): if event.button() == Qt.MouseButton.LeftButton: self.dragging_field = field_name self.drag_start_pos = event.pos() # 更改光标为抓取手型 label_widget.setCursor(Qt.CursorShape.ClosedHandCursor) # 高亮当前拖拽的字段 self.highlight_dragging_field(field_name, True) # 开始拖拽操作 drag = QDrag(label_widget) mime_data = QMimeData() mime_data.setText(field_name) drag.setMimeData(mime_data) # 设置拖拽时的缩略图 pixmap = label_widget.grab() drag.setPixmap(pixmap) drag.setHotSpot(event.pos()) drag.exec(Qt.DropAction.MoveAction) # 拖拽结束后恢复 label_widget.setCursor(Qt.CursorShape.OpenHandCursor) self.highlight_dragging_field(field_name, False) self.dragging_field = None return mouse_press_handler # 事件过滤器,用于处理拖拽和用户活动监控 def eventFilter(self, obj, event): """事件过滤器,同时处理拖拽和用户活动监控""" # 1. 先检查拖拽相关事件 if isinstance(obj, QFrame) and obj.objectName().startswith("field_container_"): if event.type() == QEvent.Type.DragEnter: # 拖拽进入容器 self.handle_drag_enter_event(obj, event) return True elif event.type() == QEvent.Type.DragLeave: # 拖拽离开容器 self.handle_drag_leave_event(obj) return True elif event.type() == QEvent.Type.Drop: # 放置到容器 self.handle_drop_event(obj, event) return True elif event.type() == QEvent.Type.DragMove: # 拖拽在容器上移动 self.handle_drag_move_event(obj, event) return True # 2. 然后监控用户活动事件 event_types = [ QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseMove, QEvent.Type.KeyPress, QEvent.Type.KeyRelease, QEvent.Type.Wheel, QEvent.Type.FocusIn, QEvent.Type.WindowActivate, QEvent.Type.HoverEnter, QEvent.Type.HoverMove, ] # 监控主窗口及其子控件的活动 if obj == self or obj in self.findChildren(QWidget) or obj == QApplication.activeWindow(): if event.type() in event_types: self.update_activity_time() return super().eventFilter(obj, event) # 处理拖拽进入事件 def handle_drag_enter_event(self, container, event): """处理拖拽进入事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 如果不是拖拽自身,则接受拖拽 if field_name != self.dragging_field: event.acceptProposedAction() self.drop_target_field = field_name self.highlight_drop_target(field_name, True) # 处理拖拽离开事件 def handle_drag_leave_event(self, container): """处理拖拽离开事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 清除目标高亮 if field_name == self.drop_target_field: self.highlight_drop_target(field_name, False) self.drop_target_field = None # 处理放置事件 def handle_drop_event(self, container, event): """处理放置事件""" # 获取源字段和目标字段 source_field = self.dragging_field target_field = container.objectName().replace("field_container_", "") if source_field and target_field and source_field != target_field: # 获取字段的分组 source_config = self.all_config_fields.get(source_field) target_config = self.all_config_fields.get(target_field) if source_config and target_config and source_config.category == target_config.category: # 在同一分组内移动字段 self.reorder_fields_in_category(source_field, target_field, source_config.category) # 重新生成UI show_hidden = getattr(self, 'show_hidden', False) self.generate_dynamic_ui(show_hidden) # 保存顺序到规则文件 self.save_field_order_to_rules() # 不显示弹窗,只更新状态栏 self.statusBar().showMessage(f"已将 '{source_config.display_name}' 移动到 '{target_config.display_name}' 的位置", 3000) # 清除目标高亮 self.highlight_drop_target(target_field, False) self.drop_target_field = None event.acceptProposedAction() # 处理拖拽移动事件 def handle_drag_move_event(self, container, event): """处理拖拽移动事件""" # 获取容器对应的字段名 field_name = container.objectName().replace("field_container_", "") # 如果不是拖拽自身,则接受拖拽移动 if field_name != self.dragging_field: event.acceptProposedAction() # 高亮显示拖拽中的字段 def highlight_dragging_field(self, field_name, is_dragging): """高亮显示拖拽中的字段""" if field_name in self.field_containers: container = self.field_containers[field_name] if is_dragging: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 2px dashed #2196F3; background-color: #e3f2fd; opacity: 0.7; }} """) else: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; }} QFrame#field_container_{field_name}:hover {{ border: 1px solid #2196F3; background-color: #f8fdff; }} """) # 高亮显示放置目标 def highlight_drop_target(self, field_name, is_target): """高亮显示放置目标""" if field_name in self.field_containers: container = self.field_containers[field_name] if is_target: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 2px solid #4CAF50; background-color: #e8f5e9; }} """) else: container.setStyleSheet(f""" QFrame#field_container_{field_name} {{ border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; }} QFrame#field_container_{field_name}:hover {{ border: 1px solid #2196F3; background-color: #f8fdff; }} """) # 重新排序分组内的字段 def reorder_fields_in_category(self, source_field, target_field, category): """重新排序分组内的字段""" # 获取当前分组内的字段顺序 if category not in self.field_order: # 如果还没有该分组的顺序记录,从配置字段中获取 fields_in_category = [] for field_name, config_field in self.all_config_fields.items(): if config_field.category == category: fields_in_category.append(field_name) self.field_order[category] = fields_in_category # 重新排序 order_list = self.field_order[category] # 移除源字段 if source_field in order_list: order_list.remove(source_field) # 找到目标字段的位置,在目标位置插入源字段 if target_field in order_list: target_index = order_list.index(target_field) order_list.insert(target_index, source_field) else: # 如果目标字段不在列表中(不应该发生),添加到末尾 order_list.append(source_field) # 保存字段顺序到规则文件 def save_field_order_to_rules(self): """保存字段顺序到规则文件""" # 将字段顺序转换为规则格式 self.rules["field_order"] = self.field_order # 保存规则文件 self.file_handler.save_rules(self.rules) # 初始化字段顺序 def initialize_field_order(self): """初始化字段顺序""" # 从规则中加载字段顺序 if "field_order" in self.rules: self.field_order = self.rules["field_order"] else: self.field_order = {} # 确保所有字段都在顺序列表中 for field_name, config_field in self.all_config_fields.items(): category = config_field.category if category not in self.field_order: self.field_order[category] = [] if field_name not in self.field_order[category]: self.field_order[category].append(field_name) def create_field_editor(self, config_field): """创建字段编辑器,返回容器和控件(新增拖拽支持)""" # 创建容器框架 container = QFrame() container.setObjectName(f"field_container_{config_field.name}") # 设置唯一ID container.setFrameShape(QFrame.Shape.StyledPanel) # === 新增:设置容器可接受拖拽事件 === container.setAcceptDrops(True) container.installEventFilter(self) # 设置基础样式 container.setStyleSheet(""" QFrame#field_container_%s { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } QFrame#field_container_%s:hover { border: 1px solid #2196F3; background-color: #f8fdff; } """ % (config_field.name, config_field.name)) layout = QHBoxLayout(container) layout.setContentsMargins(8, 4, 8, 4) layout.setSpacing(8) # 标签 - 设置为可拖拽的区域 label = QLabel(config_field.display_name) label.setMinimumWidth(180) # 新增:为标签添加拖拽支持 label.setProperty("field_name", config_field.name) # 存储字段名 label.setProperty("category", config_field.category) # 存储分组名 label.mousePressEvent = self.create_mouse_press_handler(config_field.name, label) label.setCursor(Qt.CursorShape.OpenHandCursor) # 设置手型光标 # 添加校验指示器(如果配置项有校验规则) validation_indicator = "" if config_field.validation: if config_field.validation.get("required"): validation_indicator = " *" label.setStyleSheet("color: #d32f2f; font-weight: bold;") else: label.setStyleSheet("color: #1976d2;") # 添加加密指示器(如果配置项有加密规则) encrypted_indicator = "" if config_field.encrypted: encrypted_indicator = " 🔒" label.setStyleSheet("color: #ff5722; font-weight: bold;") # 橙色高亮显示加密字段 label.setText(f"{config_field.display_name}{validation_indicator}{encrypted_indicator}") label.setToolTip(get_validation_tooltip(config_field.validation)) # 如果配置项是隐藏的,添加视觉提示 if config_field.hidden: label.setStyleSheet("color: #999999; font-style: italic;") label.setText(f"{config_field.display_name} [隐藏]") layout.addWidget(label) # 控件 widget = self.create_field_widget(config_field) # 如果配置项是隐藏的,禁用控件 if config_field.hidden: widget.setEnabled(False) widget.setStyleSheet("background-color: #f0f0f0; color: #999999;") layout.addWidget(widget) # 变量名显示 name_label = QLabel(f"({config_field.name})") name_label.setStyleSheet("color: #666666; font-size: 10px; font-style: italic;") name_label.setMinimumWidth(120) name_label.setToolTip(f"配置变量名: {config_field.name}") layout.addWidget(name_label) # 新增:上次保存的值显示 last_saved_label = QLabel() last_saved_label.setMinimumWidth(120) last_saved_label.setToolTip("上次保存的值") # 格式化显示上次保存的值 if config_field.last_saved_value is not None: # 检查是否是复杂类型 if isinstance(config_field.last_saved_value, (dict, list)): # 对于复杂类型,使用JSON格式化 try: formatted = json.dumps(config_field.last_saved_value, ensure_ascii=False) # 如果太长,截断显示 if len(formatted) > 20: formatted = formatted[:17] + "..." last_saved_label.setText(f"上次: {formatted}") except: value_type = type(config_field.last_saved_value).__name__ last_saved_label.setText(f"上次: [{value_type}]") else: # 对于简单类型,显示完整值 value_str = str(config_field.last_saved_value) # 如果太长,截断显示 if len(value_str) > 20: value_str = value_str[:17] + "..." last_saved_label.setText(f"上次: {value_str}") else: last_saved_label.setText("上次: 无") last_saved_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic; background-color: #f8f8f8; padding: 2px; border-radius: 2px;") layout.addWidget(last_saved_label) layout.addStretch() # 存储容器引用 self.field_containers[config_field.name] = container return container, widget def generate_dynamic_ui(self, show_hidden=False): """生成动态UI,可选择是否显示隐藏的配置项,支持自定义顺序""" self.dynamic_widgets.clear() self.modified_fields.clear() self.field_containers.clear() # 重新初始化标签页(考虑是否隐藏"未分类"分组) self.init_category_tabs() for category, layout in self.category_widgets.items(): while layout.count(): child = layout.takeAt(0) if child.widget(): child.widget().deleteLater() # 根据是否显示隐藏项,选择使用哪个配置项集合 config_fields_to_use = self.all_config_fields if show_hidden else self.config_fields categorized_fields = {} for field_name, config_field in config_fields_to_use.items(): category = config_field.category if category not in categorized_fields: categorized_fields[category] = [] categorized_fields[category].append(config_field) # 初始化字段顺序 self.initialize_field_order() for category, fields in categorized_fields.items(): # 跳过不在当前标签页中的分组(如被隐藏的"未分类"分组) if category not in self.category_widgets: continue layout = self.category_widgets[category] # 修改:按照保存的顺序排序 if category in self.field_order and self.field_order[category]: # 创建从字段名到配置项的映射 field_dict = {f.name: f for f in fields} # 按照保存的顺序排序 ordered_fields = [] for field_name in self.field_order[category]: if field_name in field_dict: ordered_fields.append(field_dict[field_name]) del field_dict[field_name] # 添加不在顺序列表中的字段(新字段) remaining_fields = list(field_dict.values()) remaining_fields.sort(key=lambda x: x.name) # 按字母顺序排序剩余字段 ordered_fields.extend(remaining_fields) fields = ordered_fields else: # 如果没有保存的顺序,按字母顺序排序 fields.sort(key=lambda x: x.name) # 使用GroupBox,更清晰的分组 group_box = QGroupBox(f"{category}") group_layout = QVBoxLayout(group_box) group_layout.setSpacing(6) for config_field in fields: container, widget = self.create_field_editor(config_field) group_layout.addWidget(container) self.dynamic_widgets[config_field.name] = (widget, config_field.get_actual_field_type()) group_layout.addStretch() layout.addWidget(group_box) def load_config(self): """加载配置文件""" try: # 检查配置文件是否存在 if not self.check_config_file(): QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}") return # 使用配置解析器 parser = ConfigParser(self.rules) self.config_data, self.all_config_fields, self.original_content = parser.parse_config_file(self.config_file_path) if not self.config_data: QMessageBox.warning(self, "警告", "未找到任何配置项") return self.original_config_data = self.config_data.copy() self.config_fields = {k: v for k, v in self.all_config_fields.items() if not v.hidden} show_hidden = getattr(self, 'show_hidden', False) self.generate_dynamic_ui(show_hidden) visible_tabs = self.tab_widget.count() hidden_count = len(self.all_config_fields) - len(self.config_fields) self.statusBar().showMessage(f"成功加载 {len(self.config_data)} 个配置项 ({len(self.config_fields)} 个显示, {hidden_count} 个隐藏, {visible_tabs} 个分组)") except Exception as e: QMessageBox.critical(self, "错误", f"加载配置文件失败:{str(e)}") self.statusBar().showMessage("加载配置文件失败") def toggle_hidden_fields(self, checked): """切换显示/隐藏配置项""" self.show_hidden = checked self.generate_dynamic_ui(checked) # 更新按钮文本 if checked: self.toggle_hidden_btn.setText("隐藏隐藏项") self.statusBar().showMessage("已显示所有配置项(包括隐藏的)") else: self.toggle_hidden_btn.setText("显示隐藏项") self.statusBar().showMessage("已隐藏标记为隐藏的配置项") def hide_all_fields(self): """一键隐藏所有配置项""" reply = QMessageBox.question( self, "确认一键隐藏", "确定要隐藏所有配置项吗?\n隐藏后,页面上将不显示任何配置项。\n如需显示,需要在规则管理中取消隐藏。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: # 获取所有配置项的名称 all_field_names = list(self.all_config_fields.keys()) # 将所有配置项添加到隐藏列表 self.rules["hidden"] = all_field_names # 保存规则 self.file_handler.save_rules(self.rules) # 重新加载配置 self.load_config() # 更新状态栏 self.statusBar().showMessage("已隐藏所有配置项,页面已清空") QMessageBox.information(self, "完成", "已成功隐藏所有配置项。\n如需显示配置项,请在规则管理中取消隐藏。") def manage_rules(self): """管理规则""" dialog = ConfigManagementWindow(self.rules, self.all_config_fields, self) if dialog.exec() == QDialog.DialogCode.Accepted: updated_rules = dialog.get_updated_rules() self.rules.update(updated_rules) self.file_handler.save_rules(self.rules) self.load_config() QMessageBox.information(self, "成功", "规则已更新并应用!") def collect_ui_data(self): """从UI收集数据""" ui_data = {} for field_name, (widget, field_type) in self.dynamic_widgets.items(): # 跳过隐藏且禁用的配置项 if not widget.isEnabled(): continue if field_type == "bool": ui_data[field_name] = widget.isChecked() elif field_type == "int": ui_data[field_name] = widget.value() elif field_type == "float": ui_data[field_name] = widget.value() elif field_type == "json": try: text = widget.toPlainText().strip() if text: ui_data[field_name] = json.loads(text) else: ui_data[field_name] = {} except json.JSONDecodeError: QMessageBox.warning(self, "JSON格式错误", f"配置项 {field_name} 的JSON格式不正确") return None else: text = widget.text().strip() ui_data[field_name] = text return ui_data def get_changes(self, new_data): """获取配置变更列表""" changes = {} for field_name, new_value in new_data.items(): old_value = self.original_config_data.get(field_name) if old_value != new_value: changes[field_name] = (old_value, new_value) return changes def save_config_with_auth(self): """保存配置(带二级密码验证)""" # 检查是否有加密字段被修改 encrypted_fields_modified = self.check_encrypted_fields_modified() if encrypted_fields_modified: # 如果有加密字段被修改,需要验证二级密码 if self.verify_secondary_password("保存加密配置项"): self.save_config() else: # 没有加密字段被修改,直接保存 self.save_config() def check_encrypted_fields_modified(self): """检查是否有加密字段被修改""" # 获取加密字段列表 encrypted_fields = self.rules.get("encrypted_fields", []) # 检查修改的字段中是否有加密字段 for field_name in self.modified_fields: if field_name in encrypted_fields: return True return False def save_config(self): """保存配置,保留所有注释、空格和格式""" try: # 保存当前选中的标签页索引 current_tab_index = self.tab_widget.currentIndex() current_tab_text = self.tab_widget.tabText(current_tab_index) if current_tab_index >= 0 else "" # 检查配置文件是否存在 if not self.check_config_file(): QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}") return # 收集UI数据 new_data = self.collect_ui_data() if new_data is None: # JSON解析错误 return # 校验配置数据 validation_errors = validate_config_data(new_data, self.all_config_fields) if validation_errors: error_msg = "配置校验失败:\n\n" + "\n".join(validation_errors) QMessageBox.warning(self, "配置校验失败", error_msg) return # 获取变更列表 changes = self.get_changes(new_data) if not changes: QMessageBox.information(self, "提示", "没有检测到任何配置变更") return # 显示变更确认对话框 dialog = ChangeConfirmDialog(changes, self) result = dialog.exec() if result != QDialog.DialogCode.Accepted: self.statusBar().showMessage("用户取消了保存操作") return # 在保存前更新上次保存的值到规则文件 for field_name, (old_value, new_value) in changes.items(): # 将修改前的值(旧值)保存为上次保存的值 self.rules.setdefault("last_saved_values", {})[field_name] = old_value # 立即保存规则文件,确保上次保存的值被持久化 self.file_handler.save_rules(self.rules) # 读取原始文件内容 with open(self.config_file_path, 'r', encoding='utf-8') as f: original_content = f.read() original_lines = original_content.split('\n') # 构建新文件内容 - 只修改值部分,保留所有其他内容 new_lines = original_lines.copy() # 对每个变更的配置项,找到其位置并修改值,保留空格和格式 for field_name, (old_value, new_value) in changes.items(): config_field = self.all_config_fields.get(field_name) if not config_field or config_field.line_number is None: continue # 获取配置项的原始块 original_block = config_field.original_lines # 查找赋值行在原始块中的位置 assignment_line_index = -1 for i, line in enumerate(original_block): stripped = line.strip() if stripped.startswith(field_name) and '=' in stripped: assignment_line_index = i break if assignment_line_index >= 0: # 获取原始赋值行的完整内容(包括注释等) original_assignment_line = original_block[assignment_line_index] # 使用改进的正则表达式匹配赋值语句 # 这个正则表达式会捕获: # 1. 变量名前的所有空格 # 2. 变量名 # 3. 等号前的空格 # 4. 等号 # 5. 等号后的空格 # 6. 原始值(直到注释或行尾) # 7. 行尾注释(如果有) import re pattern = rf'^(\s*)({re.escape(field_name)})(\s*)(=)(\s*)(.*?)(\s*(#.*)?)$' match = re.match(pattern, original_assignment_line) if match: # 获取匹配的各个部分 leading_spaces = match.group(1) # 变量名前的空格 var_name_part = match.group(2) # 变量名 spaces_before_eq = match.group(3) # 等号前的空格 eq_sign = match.group(4) # 等号 spaces_after_eq = match.group(5) # 等号后的空格 old_value_part = match.group(6) # 原始值 trailing_comment = match.group(7) if match.group(7) else '' # 行尾注释 # 格式化新值 formatted_value = format_value(new_value, config_field) # 构建新行,保持原始的空格格式 new_line = (f"{leading_spaces}{var_name_part}{spaces_before_eq}" f"{eq_sign}{spaces_after_eq}{formatted_value}" f"{trailing_comment}") # 找到赋值行在原始文件中的实际位置 # assignment_line_in_file 是赋值行在原始文件中的行号(从0开始) assignment_line_in_file = config_field.line_number + assignment_line_index if assignment_line_in_file < len(new_lines): new_lines[assignment_line_in_file] = new_line else: # 如果正则匹配失败,使用简单的替换方法 # 查找赋值行在原始文件中的位置 for i in range(len(new_lines)): line = new_lines[i] stripped = line.strip() if stripped.startswith(field_name) and '=' in stripped: # 使用更简单的正则表达式 pattern = rf'^(\s*{re.escape(field_name)}\s*=\s*).*$' match = re.match(pattern, line.rstrip()) if match: # 格式化新值 formatted_value = format_value(new_value, config_field) # 检查是否有行尾注释 comment_pos = line.find('#') if comment_pos > 0: # 有行尾注释,保留注释 line_comment = line[comment_pos:] prefix = match.group(1) new_line = prefix + formatted_value + line_comment else: # 没有行尾注释 prefix = match.group(1) new_line = prefix + formatted_value new_lines[i] = new_line break # 写入文件,保持原有的换行符格式 with open(self.config_file_path, 'w', encoding='utf-8', newline='') as f: f.write('\n'.join(new_lines)) # 清除所有高亮显示 for field_name in self.modified_fields.copy(): self.clear_field_highlight(field_name) self.modified_fields.remove(field_name) # 重新加载配置(这会重新读取规则文件中的上次保存值) self.load_config() # 恢复之前选中的标签页 if current_tab_text: # 查找该标签页是否还存在 for i in range(self.tab_widget.count()): if self.tab_widget.tabText(i) == current_tab_text: self.tab_widget.setCurrentIndex(i) break else: # 如果没找到,尝试使用索引 if 0 <= current_tab_index < self.tab_widget.count(): self.tab_widget.setCurrentIndex(current_tab_index) self.statusBar().showMessage(f"配置文件保存成功,修改了 {len(changes)} 个配置项") # 使用定时器延迟显示成功消息 QTimer.singleShot(100, lambda: QMessageBox.information( self, "成功", f"配置文件保存成功!\n修改了 {len(changes)} 个配置项" )) except Exception as e: self.statusBar().showMessage("保存配置文件失败") QMessageBox.critical(self, "错误", f"保存配置文件失败:{str(e)}\n\n错误详情:{traceback.format_exc()}") # 在主界面直接导入导出规则文件 def import_rules_file(self): """在主界面直接导入规则文件""" # 检查当前是否有未保存的规则更改 if hasattr(self, 'rules_modified') and self.rules_modified: reply = QMessageBox.question( self, "确认导入", "当前有未保存的规则更改,导入将覆盖这些更改。\n是否继续?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # 选择要导入的规则文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择规则文件", str(Path.home()), "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return try: # 读取规则文件 with open(file_path, 'r', encoding='utf-8') as f: imported_rules = json.load(f) # 验证规则文件格式 if not isinstance(imported_rules, dict): QMessageBox.warning(self, "导入失败", "规则文件格式不正确") return # 显示导入确认对话框 reply = QMessageBox.question( self, "确认导入", f"确定要导入规则文件吗?\n\n文件: {os.path.basename(file_path)}\n\n" f"这将覆盖当前的规则设置,并重新加载配置文件。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return # 完全替换现有规则 self.rules.clear() self.rules.update(imported_rules) # 确保规则结构完整 ensure_rule_structure(self.rules) # 保存到规则文件 self.file_handler.save_rules(self.rules) # 重新加载配置(这会应用新规则) self.load_config() QMessageBox.information( self, "导入成功", f"规则文件导入成功!\n\n" f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" f"配置文件已重新加载以应用新规则。" ) except json.JSONDecodeError: QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") except Exception as e: QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") def export_rules_file(self): """在主界面直接导出规则文件""" # 首先保存当前规则到文件(确保最新) self.file_handler.save_rules(self.rules) # 选择导出位置和文件名 default_name = f"config_editor_rules_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" file_path, _ = QFileDialog.getSaveFileName( self, "导出规则文件", default_name, "JSON规则文件 (*.json);;所有文件 (*)" ) if not file_path: return # 确保文件扩展名是.json if not file_path.lower().endswith('.json'): file_path += '.json' try: # 复制规则文件到指定位置 shutil.copy2(self.file_handler.rules_file, file_path) QMessageBox.information( self, "导出成功", f"规则文件已成功导出到:\n{file_path}\n\n" f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" f"规则文件:{os.path.basename(self.file_handler.rules_file)}" ) except Exception as e: QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(e)}") def update_activity_time(self): """更新最后活动时间""" self.last_activity_time = time.time() self.is_locked = False def check_session_timeout(self): """检查会话是否超时""" if self.is_locked: return # 如果已经锁定,不再检查 current_time = time.time() elapsed = current_time - self.last_activity_time # 调试信息 remaining = max(0, self.session_timeout - elapsed) minutes = int(remaining // 60) seconds = int(remaining % 60) print(f"[调试] 检查会话: 已空闲 {int(elapsed)}秒, 超时阈值 {self.session_timeout}秒, 剩余 {minutes}分{seconds}秒") if elapsed > self.session_timeout: print(f"[调试] 会话超时,开始锁定") self.lock_session() def lock_session(self): """锁定会话,要求重新登录""" if not self.is_locked: self.is_locked = True # 立即禁用主窗口所有操作 self.setEnabled(False) print(f"[调试] 锁定会话,重新登录路径: {self.auth_config_path}") # 使用模态对话框,确保用户必须处理 from login.login_dialog import ReLoginDialog dialog = ReLoginDialog(self.auth_config_path, self) def on_relogin_success(username, user_config_path): """重新登录成功回调""" print(f"[调试] 重新登录成功: 用户名={username}, 账密文件={user_config_path}") self.current_user = username self.auth_config_path = user_config_path self.update_activity_time() # 重置活动时间 self.is_locked = False self.setEnabled(True) # 重新启用窗口 self.statusBar().showMessage(f"欢迎回来,{username}!") # 连接成功信号 dialog.login_success.connect(on_relogin_success) # 显示模态对话框,阻塞直到用户处理 result = dialog.exec() # 如果用户取消登录或关闭窗口,直接退出程序 if result != QDialog.DialogCode.Accepted: print("[调试] 用户取消重新登录,退出程序") QMessageBox.critical(self, "会话超时", "登录已取消,程序将退出以保障安全。") self.close() sys.exit(0)