1555 lines
67 KiB
Python
1555 lines
67 KiB
Python
'''主窗体实现程序,项目核心模块'''
|
||
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) |