diff --git a/main.py b/main.py new file mode 100644 index 0000000..ed707c0 --- /dev/null +++ b/main.py @@ -0,0 +1,39 @@ +'''项目入口启动程序''' +import sys +from PyQt6.QtWidgets import QApplication +from login.login_dialog import LoginDialog +from main_window import ConfigEditor + +def main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') + + # 创建登录对话框 + login_dialog = LoginDialog() + + def on_login_success(username, user_config_path): + """登录成功回调""" + # 注意:这里传递的是 user_config_path,这是账密文件路径 + # 而 ConfigEditor 需要的是要编辑的配置文件路径 + print(f"[调试] 登录成功: 用户名={username}, 账密文件={user_config_path}") + + # 创建主窗口,传递账密文件路径和用户名 + # 主窗口会自己加载要编辑的配置文件 + editor = ConfigEditor(user_config_path, username) + editor.show() + + # 连接信号 + login_dialog.login_success.connect(on_login_success) + + # 显示登录对话框 + result = login_dialog.exec() + + # 如果登录对话框被取消(用户点击退出),则退出程序 + if result == 0: + sys.exit(0) + + # 登录成功后,启动应用程序的主事件循环 + sys.exit(app.exec()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/main_window.py b/main_window.py new file mode 100644 index 0000000..0267c3f --- /dev/null +++ b/main_window.py @@ -0,0 +1,1555 @@ +'''主窗体实现程序,项目核心模块''' +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) \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..9d15616 --- /dev/null +++ b/models.py @@ -0,0 +1,143 @@ +'''数据模型定义''' +import json +import datetime +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +@dataclass +class ConfigField: + """配置项元数据类""" + name: str + value: Any + category: str = "未分类" + display_name: str = "" + field_type: str = "auto" + decimals: Optional[int] = None + tooltip: str = "" + hidden: bool = False + encrypted: bool = False # 是否加密 + line_number: Optional[int] = None + original_lines: List[str] = field(default_factory=list) + validation: Dict = field(default_factory=dict) + last_saved_value: Any = None + + def __post_init__(self): + if not self.display_name: + self.display_name = self.name + + def get_actual_field_type(self) -> str: + """获取实际的字段类型""" + if self.field_type != "auto": + return self.field_type + + if isinstance(self.value, bool): + return "bool" + elif isinstance(self.value, int): + return "int" + elif isinstance(self.value, float): + return "float" + elif isinstance(self.value, (list, dict)): + return "json" + else: + return "str" + +@dataclass +class EditorRules: + """编辑器规则数据类""" + categories: Dict[str, List[str]] = field(default_factory=lambda: {"未分类": []}) + display_names: Dict[str, str] = field(default_factory=dict) + tooltips: Dict[str, str] = field(default_factory=dict) + field_types: Dict[str, str] = field(default_factory=dict) + field_decimals: Dict[str, int] = field(default_factory=dict) + hidden: List[str] = field(default_factory=list) + encrypted_fields: List[str] = field(default_factory=list) # 加密字段列表 + validations: Dict[str, Dict] = field(default_factory=dict) + field_order: Dict[str, List[str]] = field(default_factory=dict) + last_saved_values: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict: + """转换为字典""" + return { + "categories": self.categories, + "display_names": self.display_names, + "tooltips": self.tooltips, + "field_types": self.field_types, + "field_decimals": self.field_decimals, + "hidden": self.hidden, + "encrypted_fields": self.encrypted_fields, + "validations": self.validations, + "field_order": self.field_order, + "last_saved_values": self.last_saved_values + } + + @classmethod + def from_dict(cls, data: Dict) -> 'EditorRules': + """从字典创建实例""" + return cls( + categories=data.get("categories", {"未分类": []}), + display_names=data.get("display_names", {}), + tooltips=data.get("tooltips", {}), + field_types=data.get("field_types", {}), + field_decimals=data.get("field_decimals", {}), + hidden=data.get("hidden", []), + encrypted_fields=data.get("encrypted_fields", []), + validations=data.get("validations", {}), + field_order=data.get("field_order", {}), + last_saved_values=data.get("last_saved_values", {}) + ) + +@dataclass +class UserSettings: + """用户设置数据类""" + config_file_path: str = "" + last_used: str = "" + use_relative_path: bool = True + + def to_dict(self) -> Dict: + return { + "config_file_path": self.config_file_path, + "last_used": self.last_used + } + + @classmethod + def from_dict(cls, data: Dict) -> 'UserSettings': + return cls( + config_file_path=data.get("config_file_path", ""), + last_used=data.get("last_used", "") + ) + +@dataclass +class ExportInfo: + """导出信息数据类""" + export_time: str = "" + export_version: str = "1.0" + note: str = "配置文件编辑器规则文件" + + def to_dict(self) -> Dict: + return { + "export_time": self.export_time or datetime.datetime.now().strftime("%Y-%m-d %H:%M:%S"), + "export_version": self.export_version, + "note": self.note + } + +@dataclass +class Statistics: + """统计信息数据类""" + total_fields: int = 0 + display_names_count: int = 0 + categories_count: int = 0 + hidden_fields_count: int = 0 + encrypted_fields_count: int = 0 # 加密字段计数 + field_types_count: int = 0 + validations_count: int = 0 + + def to_dict(self) -> Dict: + return { + "total_fields": self.total_fields, + "display_names_count": self.display_names_count, + "categories_count": self.categories_count, + "hidden_fields_count": self.hidden_fields_count, + "encrypted_fields_count": self.encrypted_fields_count, + "field_types_count": self.field_types_count, + "validations_count": self.validations_count + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72d6a23 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyQt6>=6.5.0 \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..28fb41f --- /dev/null +++ b/utils.py @@ -0,0 +1,263 @@ +'''工具函数集合''' +import os +import json +import shutil +import datetime +import re +import ast +from pathlib import Path +from typing import Dict, Any, List, Tuple, Optional + +def merge_rules(existing_rules: Dict, default_rules: Dict) -> Dict: + """智能合并规则:保留现有规则,只添加默认规则中的新字段""" + merged_rules = existing_rules.copy() + + # 确保所有必需的字段都存在 + for key in default_rules: + if key not in merged_rules: + merged_rules[key] = default_rules[key] + + # 确保"未分类"分组始终存在 + if "categories" not in merged_rules: + merged_rules["categories"] = {} + if "未分类" not in merged_rules["categories"]: + merged_rules["categories"]["未分类"] = [] + + # 确保其他必要字段存在 + for field in ["display_names", "tooltips", "field_types", "field_decimals", + "hidden", "validations", "field_order", "last_saved_values", "encrypted_fields"]: + if field not in merged_rules: + if field == "hidden" or field == "encrypted_fields": + merged_rules[field] = [] + else: + merged_rules[field] = {} + + return merged_rules + +def backup_existing_rules(rules_file: str, program_dir: str) -> str: + """备份现有规则文件""" + if os.path.exists(rules_file): + try: + # 创建备份目录 + backup_dir = os.path.join(program_dir, "backups") + os.makedirs(backup_dir, exist_ok=True) + + # 生成带时间戳的备份文件名 + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = os.path.join(backup_dir, f"config_editor_rules_{timestamp}.json") + + # 备份文件 + shutil.copy2(rules_file, backup_file) + print(f"已备份规则文件到: {backup_file}") + + # 清理旧的备份文件 + cleanup_old_backups(backup_dir) + + return backup_file + except Exception as e: + print(f"备份规则文件失败: {e}") + return "" + +def cleanup_old_backups(backup_dir: str, keep_count: int = 5): + """清理旧的备份文件""" + try: + # 获取所有备份文件 + backup_files = [] + for filename in os.listdir(backup_dir): + if filename.startswith("config_editor_rules_") and filename.endswith(".json"): + filepath = os.path.join(backup_dir, filename) + backup_files.append((filepath, os.path.getmtime(filepath))) + + # 按修改时间排序(从旧到新) + backup_files.sort(key=lambda x: x[1]) + + # 删除旧的备份文件,保留最近几个 + if len(backup_files) > keep_count: + files_to_delete = backup_files[:-keep_count] + for filepath, _ in files_to_delete: + os.remove(filepath) + print(f"已删除旧备份文件: {filepath}") + + except Exception as e: + print(f"清理旧备份文件失败: {e}") + +def get_validation_tooltip(validation: Dict) -> str: + """生成校验规则的提示信息""" + if not validation: + return "" + + rules = [] + if validation.get("required"): + rules.append("• 必填项") + if "min" in validation: + rules.append(f"• 最小值: {validation['min']}") + if "max" in validation: + rules.append(f"• 最大值: {validation['max']}") + if "regex" in validation: + rules.append(f"• 正则表达式: {validation['regex']}") + + return "\n".join(rules) + +def format_dict_value(value: Dict, original_lines: List[str] = None) -> str: + """专门格式化字典值,保持多行格式""" + if not isinstance(value, dict): + return json.dumps(value, ensure_ascii=False) + + # 如果有原始行信息,尝试保持原始格式 + if original_lines and len(original_lines) > 1: + # 检查原始格式是否是漂亮的多行格式 + first_line = original_lines[0].strip() + if first_line.endswith('{') and len(original_lines) > 2: + # 原始是多行格式,我们也使用多行格式 + result = "{\n" + for i, (key, val) in enumerate(value.items()): + # 使用4个空格缩进 + result += f' "{key}": {json.dumps(val, ensure_ascii=False)}' + if i < len(value) - 1: + result += ",\n" + else: + result += "\n" + result += "}" + return result + + # 如果没有原始格式信息或不是多行格式,使用漂亮的JSON格式 + return json.dumps(value, indent=4, ensure_ascii=False) + +def format_value(value: Any, config_field=None) -> str: + """格式化值,保持与Python兼容的格式""" + if value is None: + return "None" + elif isinstance(value, bool): + return "True" if value else "False" + elif isinstance(value, (int, float)): + # 根据小数位数格式化浮点数 + if isinstance(value, float) and config_field and config_field.decimals is not None: + format_str = f"{{:.{config_field.decimals}f}}" + return format_str.format(value) + else: + return str(value) + elif isinstance(value, str): + # 检查字符串是否已经用引号包围 + if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): + return value + # 检查字符串是否包含特殊字符 + elif any(char in value for char in [' ', '\t', '\n', '#', '=']): + return f'"{value}"' + else: + return f'"{value}"' + elif isinstance(value, dict): + # 对于字典,使用专门的格式化函数 + original_lines = config_field.original_lines if config_field else None + return format_dict_value(value, original_lines) + elif isinstance(value, list): + return json.dumps(value, ensure_ascii=False, indent=4) + else: + return repr(value) + +def get_default_rules() -> Dict: + """获取默认规则""" + return { + "categories": {"未分类": []}, + "display_names": {}, + "tooltips": {}, + "field_types": {}, + "field_decimals": {}, + "hidden": [], + "encrypted_fields": [], #加密字段列表 + "validations": {}, + "field_order": {}, + "last_saved_values": {} + } + +def validate_config_data(config_data: Dict[str, Any], all_config_fields: Dict[str, Any]) -> List[str]: + """校验配置数据""" + errors = [] + + for field_name, value in config_data.items(): + config_field = all_config_fields.get(field_name) + if not config_field: + continue + + validation = config_field.validation + if not validation: + continue + + # 必填校验 + if validation.get("required") and (value is None or value == ""): + errors.append(f"配置项 '{config_field.display_name}' 是必填项") + continue + + # 数字范围校验 + if isinstance(value, (int, float)): + if "min" in validation: + try: + min_val = float(validation["min"]) + if value < min_val: + errors.append(f"配置项 '{config_field.display_name}' 的值不能小于 {min_val}") + except ValueError: + pass + + if "max" in validation: + try: + max_val = float(validation["max"]) + if value > max_val: + errors.append(f"配置项 '{config_field.display_name}' 的值不能大于 {max_val}") + except ValueError: + pass + + # 字符串正则校验 + elif isinstance(value, str) and "regex" in validation: + try: + if not re.match(validation["regex"], value): + errors.append(f"配置项 '{config_field.display_name}' 的值不符合格式要求") + except re.error: + errors.append(f"配置项 '{config_field.display_name}' 的正则表达式格式错误") + + return errors + +def get_field_type_from_value(value: Any) -> str: + """根据值推断字段类型""" + if isinstance(value, bool): + return "bool" + elif isinstance(value, int): + return "int" + elif isinstance(value, float): + return "float" + elif isinstance(value, (list, dict)): + return "json" + else: + return "str" + +def ensure_rule_structure(rules: Dict) -> Dict: + """确保规则结构完整""" + # 确保规则中有"未分类"分组 + if "categories" not in rules: + rules["categories"] = {} + if "未分类" not in rules["categories"]: + rules["categories"]["未分类"] = [] + + # 确保规则中有field_decimals字段 + if "field_decimals" not in rules: + rules["field_decimals"] = {} + + # 确保规则中有field_order字段 + if "field_order" not in rules: + rules["field_order"] = {} + + # 确保规则中有last_saved_values字段 + if "last_saved_values" not in rules: + rules["last_saved_values"] = {} + + # 确保规则中有encrypted_fields字段 + if "encrypted_fields" not in rules: + rules["encrypted_fields"] = [] + + # 确保其他必要字段存在 + for field in ["display_names", "tooltips", "field_types", "validations"]: + if field not in rules: + rules[field] = {} + + if "hidden" not in rules: + rules["hidden"] = [] + + return rules \ No newline at end of file