From 7391a4b29ba19210968b25da0380150a7a12a7de Mon Sep 17 00:00:00 2001 From: hujinyang Date: Wed, 3 Dec 2025 07:42:43 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第一次提交 Signed-off-by: hujinyang --- README.md | 208 +++++ build.sh | 320 +++++++ config_editor.py | 1686 +++++++++++++++++++++++++++++++++++ config_editor_rules.json | 131 +++ config_editor_settings.json | 4 + 5 files changed, 2349 insertions(+) create mode 100644 README.md create mode 100644 build.sh create mode 100644 config_editor.py create mode 100644 config_editor_rules.json create mode 100644 config_editor_settings.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5f432e --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +Config Editor - 配置文件编辑工具 +一个基于PyQt6开发的Python配置文件可视化编辑工具,支持自动解析Python配置文件中的大写变量,提供直观的GUI界面进行编辑和管理。 + +✨ 特性功能 +🎯 核心功能 +智能解析:自动扫描Python文件,识别文件中的变量配置项 + +可视化编辑:基于PyQt6的现代化GUI界面,支持多种数据类型编辑 + +分组管理:可自定义分组,按类别组织配置项 + +格式保持:保存时保持原始文件格式、注释和缩进 + +🔧 编辑支持 +多种数据类型: + +布尔值(CheckBox) + +整数/浮点数(SpinBox) + +字符串(LineEdit/TextEdit) + +JSON/字典/列表(格式化编辑) + +校验规则: + +必填项验证 + +数值范围限制 + +正则表达式匹配 + +自定义错误提示 + +📊 管理功能 +显示定制:自定义每个配置项的显示名称和提示信息 + +字段隐藏:标记隐藏字段,支持显示/隐藏切换 + +类型推断:自动识别字段类型,支持手动覆盖 + +规则管理:独立的规则管理界面,支持批量配置 + +🚀 快速开始 +环境要求 +Python 3.8+ + +PyQt6 + +构建打包 +执行build.sh脚本 + +安装部署 +bash +1.sudo dpkg -i config-editor_1.0_amd64.deb +2.sudo /usr/share/config-editor/setup_venv.sh +在服务器图形化界面,搜索Config Editor点击即可使用 +卸载程序 +bash +1.sudo dpkg -r config-editor +📁 项目结构 +text +config_editor/ +├── config_editor.py # 主程序 +├── config_editor_rules.json # 规则配置文件 +├── config_editor_settings.json # 用户设置文件 +├── build.sh # 一键打包脚本 +├── CHANGELOG.md # 变更日志 +└── README.md # 说明文档 +配置文件说明 +config_editor_rules.json:存储所有配置项的元数据(分组、显示名、校验规则等) + +config_editor_settings.json:存储用户设置(最近使用的配置文件路径等) + +🖥️ 使用指南 +1. 首次运行 +首次启动程序时,会自动弹出配置文件设置对话框: + +选择要编辑的Python配置文件 + +程序会自动解析配置文件中的大写变量 + +2. 界面布局 +顶部信息栏:显示当前配置文件的路径 + +标签页:按分组显示配置项 + +编辑区域:每个配置项包含: + +显示名称(可自定义) + +编辑控件(根据类型自动适配) + +变量名(原始名称) + +工具栏:常用操作按钮 + +3. 基本操作 +打开文件:使用菜单或按钮打开其他配置文件 + +编辑配置:直接在对应的控件中修改值 + +保存配置:保存修改到原配置文件 + +重新加载:放弃修改,重新读取配置文件 + +4. 高级功能 +规则管理 +点击"管理规则"按钮进入规则管理界面: + +字段属性:设置显示名称、分组、类型、提示信息等 + +校验规则:设置最小值、最大值、正则表达式、必填项 + +分组管理:添加、删除分组,调整字段分组 + +字段筛选 +搜索功能:快速查找配置项 + +隐藏字段:支持显示/隐藏被标记为隐藏的配置项 + +⚙️ 配置规则详解 +分组配置 +json +"categories": { + "数据库配置": ["DB_HOST", "DB_PORT"], + "应用配置": ["DEBUG_MODE", "LOG_LEVEL"] +} +字段属性 +json +"display_names": { + "DB_HOST": "数据库主机地址" +}, +"field_types": { + "DB_PORT": "int" +}, +"tooltips": { + "DB_HOST": "请输入数据库服务器的IP地址或域名" +} +校验规则 +json +"validations": { + "DB_PORT": { + "required": true, + "min": "1024", + "max": "65535", + "regex": "^[0-9]+$" + } +} +🎨 支持的Python配置文件格式 +程序可以解析以下格式的配置: + +python +# 配置文件示例 config.py +DB_HOST = "localhost" # 数据库地址 +DB_PORT = 3306 # 数据库端口 +DEBUG_MODE = True # 调试模式 +MAX_CONNECTIONS = 100 # 最大连接数 +要求: + +配置变量名必须为大写字母和下划线组成 + +支持Python基本数据类型(字符串、数字、布尔值、列表、字典) + +🔍 技术实现 +解析技术 +使用Python的ast模块进行语法分析 + +精确识别配置项的位置和注释 + +保持原始格式和缩进 + +GUI框架 +基于PyQt6构建现代化界面 + +响应式布局,支持调整窗口大小 + +自定义控件适配不同数据类型 + +数据持久化 +规则配置使用JSON格式存储 + +支持相对路径和绝对路径 + +自动保存用户设置 + +📝 使用示例 +编辑配置文件 +启动程序,选择配置文件 + +在对应的分组中找到要修改的配置项 + +编辑值(复选框、数字框、文本框等) + +点击"保存配置"按钮 + +确认变更后,程序会自动更新配置文件 + +自定义规则 +点击"管理规则"按钮 + +在左侧列表选择配置项 + +在右侧设置显示名称、分组、类型等 + +设置校验规则(可选) + +保存规则,程序会自动重新加载界面 \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b5f725c --- /dev/null +++ b/build.sh @@ -0,0 +1,320 @@ +# config Editor 一键构建脚本 +set -e + +# 配置变量 +PACKAGE_NAME="config-editor" +VERSION="1.0" +ARCHITECTURE="amd64" +MAINTAINER="hjy " + +echo "==========================================" +echo "Config Editor 构建脚本 (虚拟环境版)" +echo "版本: $VERSION" +echo "架构: $ARCHITECTURE" +echo "==========================================" + +# 检查依赖 +echo "检查构建依赖..." +if ! command -v dpkg-deb &> /dev/null; then + echo "错误: 需要 dpkg-deb,请安装: sudo apt install dpkg-dev" + exit 1 +fi + +# 检查源代码是否存在 +echo "检查源代码..." +if [ ! -f "config_editor.py" ]; then + echo "错误: 未找到主程序文件 config_editor.py" + echo "请确保在项目根目录运行此脚本,且包含 config_editor.py 文件" + exit 1 +fi + +# 创建构建目录 +BUILD_DIR="/tmp/${PACKAGE_NAME}-build" +echo "创建构建目录: $BUILD_DIR" +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR/DEBIAN" +mkdir -p "$BUILD_DIR/usr/share/applications" +mkdir -p "$BUILD_DIR/usr/share/config-editor" +mkdir -p "$BUILD_DIR/usr/bin" + +# 复制应用文件 +echo "复制应用文件..." +echo "复制主程序 config_editor.py" +cp config_editor.py "$BUILD_DIR/usr/share/config-editor/" + +# 复制配置文件 +if [ -f "config_editor_rules.json" ]; then + echo "复制配置文件 config_editor_rules.json" + cp config_editor_rules.json "$BUILD_DIR/usr/share/config-editor/" +else + echo "警告: 未找到 config_editor_rules.json 文件" +fi + +if [ -f "config_editor_settings.json" ]; then + echo "复制配置文件 config_editor_settings.json" + cp config_editor_settings.json "$BUILD_DIR/usr/share/config-editor/" +else + echo "警告: 未找到 config_editor_settings.json 文件" +fi + +# 复制所有Python文件 +echo "复制其他Python文件..." +find . -maxdepth 1 -name "*.py" ! -name "build.sh" -exec cp {} "$BUILD_DIR/usr/share/config-editor/" \; 2>/dev/null || true + +# 显示复制的文件 +echo "已复制的文件:" +ls -la "$BUILD_DIR/usr/share/config-editor/" + +# 检查是否复制了真实的应用文件 +if [ ! -f "$BUILD_DIR/usr/share/config-editor/config_editor.py" ]; then + echo "错误: 未成功复制 config_editor.py 文件" + exit 1 +fi + +# 创建桌面文件 +echo "创建桌面文件..." +cat > "$BUILD_DIR/usr/share/applications/config-editor.desktop" << EOF +[Desktop Entry] +Version=1.1 +Type=Application +Name=Config Editor +Comment=Configuration Management System +Exec=config-editor +Icon=config-editor +Categories=Development;Settings; +Terminal=false +StartupWMClass=ConfigEditor +EOF + +# 创建虚拟环境安装脚本(已修复权限问题) +echo "创建虚拟环境安装脚本..." +cat > "$BUILD_DIR/usr/share/config-editor/setup_venv.sh" << 'EOF' +#!/bin/bash +# 虚拟环境安装脚本 + +APP_DIR="/usr/share/config-editor" +VENV_DIR="$APP_DIR/venv" + +echo "设置Config Editor虚拟环境..." + +# 检查是否使用sudo +if [ "$EUID" -ne 0 ]; then + echo "错误: 请使用sudo运行此脚本" + echo "命令: sudo $0" + exit 1 +fi + +# 检查虚拟环境是否存在 +if [ ! -d "$VENV_DIR" ]; then + echo "创建虚拟环境..." + python3 -m venv --system-site-packages "$VENV_DIR" + + if [ $? -ne 0 ]; then + echo "错误: 无法创建虚拟环境" + echo "请确保已安装 python3-venv: sudo apt install python3-venv" + exit 1 + fi +fi + +# 激活虚拟环境并安装依赖 +echo "激活虚拟环境并安装依赖..." +source "$VENV_DIR/bin/activate" + +# 修复权限问题 +echo "修复虚拟环境权限..." +chmod -R 755 "$VENV_DIR" +chmod +x "$VENV_DIR/bin"/* 2>/dev/null || true +chmod +x "$VENV_DIR/bin/python" 2>/dev/null || true +chmod +x "$VENV_DIR/bin/pip" 2>/dev/null || true + +# 升级pip +echo "升级pip..." +python -m pip install --upgrade pip + +# 安装PyQt6 +echo "安装PyQt6..." +pip install PyQt6 -i https://pypi.tuna.tsinghua.edu.cn/simple/ + +# 如果有requirements.txt,安装其他依赖 +if [ -f "$APP_DIR/requirements.txt" ]; then + echo "安装其他依赖..." + pip install -r "$APP_DIR/requirements.txt" -i https://pypi.tuna.tsinghua.edu.cn/simple/ +fi + +# 再次确保权限正确 +echo "设置最终权限..." +chmod -R 755 "$VENV_DIR" +chmod +x "$VENV_DIR/bin"/* 2>/dev/null || true + +echo "虚拟环境设置完成" +echo "安装在虚拟环境中的包:" +pip list +EOF + +chmod +x "$BUILD_DIR/usr/share/config-editor/setup_venv.sh" + +# 创建启动脚本 - 使用虚拟环境(已修复权限检查) +echo "创建启动脚本..." +cat > "$BUILD_DIR/usr/bin/config-editor" << 'EOF' +#!/bin/bash +# Config Editor 启动脚本 - 虚拟环境版本 + +# 应用目录 +APP_DIR="/usr/share/config-editor" +VENV_DIR="$APP_DIR/venv" +LOG_FILE="${HOME}/.config/config-editor/log.txt" + +# 创建日志目录 +mkdir -p "$(dirname "$LOG_FILE")" + +# 记录启动日志 +echo "$(date): 启动Config Editor" >> "$LOG_FILE" + +# 设置环境变量 +export DISPLAY=":${XDG_SESSION_DISPLAY:-1}" +export XAUTHORITY="${HOME}/.Xauthority" + +# 尝试多种方法设置DBUS +if [ -f "/run/user/$(id -u)/bus" ]; then + export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus" +elif [ -n "$DBUS_SESSION_BUS_ADDRESS" ]; then + : # 保持已有设置 +else + # 尝试从进程获取 + DBUS_ADDR=$(ps -u $(id -u) -o command | grep -o 'dbus-daemon.*address=unix:path=[^ ]*' | head -1 | sed 's/.*address=//') + if [ -n "$DBUS_ADDR" ]; then + export DBUS_SESSION_BUS_ADDRESS="$DBUS_ADDR" + fi +fi + +# 修复X11权限 +if [ -f "${HOME}/.Xauthority" ]; then + export XAUTHORITY="${HOME}/.Xauthority" +fi + +# 检查虚拟环境 +if [ ! -d "$VENV_DIR" ]; then + echo "错误: 虚拟环境未找到。请先运行:" + echo " sudo /usr/share/config-editor/setup_venv.sh" + exit 1 +fi + +# 检查并修复权限(如果需要) +if [ ! -x "$VENV_DIR/bin/python" ] || [ ! -x "$VENV_DIR/bin/pip" ]; then + echo "警告: 虚拟环境权限问题,请重新运行安装脚本:" + echo " sudo /usr/share/config-editor/setup_venv.sh" + exit 1 +fi + +# 检查PyQt6是否安装 +if ! "$VENV_DIR/bin/python" -c "import PyQt6" 2>/dev/null; then + echo "错误: PyQt6未在虚拟环境中安装。请运行:" + echo " sudo /usr/share/config-editor/setup_venv.sh" + exit 1 +fi + +# 切换到应用目录 +cd "$APP_DIR" + +# 记录环境信息 +echo "DISPLAY: $DISPLAY" >> "$LOG_FILE" +echo "XAUTHORITY: $XAUTHORITY" >> "$LOG_FILE" + +# 使用虚拟环境运行Python应用 +exec "$VENV_DIR/bin/python" config_editor.py "$@" >> "$LOG_FILE" 2>&1 +EOF + +chmod +x "$BUILD_DIR/usr/bin/config-editor" + +# 创建控制文件 +echo "创建DEBIAN控制文件..." +cat > "$BUILD_DIR/DEBIAN/control" << EOF +Package: $PACKAGE_NAME +Version: $VERSION +Architecture: $ARCHITECTURE +Depends: python3, python3-venv +Recommends: libnotify-bin, x11-xserver-utils +Maintainer: $MAINTAINER +Description: Configuration Management System (VirtualEnv) + A PyQt6-based configuration management editor using virtual environment. + Provides grouping management, validation rules, and auto-grouping features. + This version uses virtual environment for PyQt6. +EOF + +# 创建安装后脚本 +echo "创建安装后脚本..." +cat > "$BUILD_DIR/DEBIAN/postinst" << 'EOF' +#!/bin/bash +# 安装后脚本 +set -e + +echo "Config Editor 安装完成!" +echo "" +echo "更新桌面数据库..." +update-desktop-database /usr/share/applications 2>/dev/null || true + +echo "" +echo "重要: 需要设置虚拟环境才能运行应用" +echo "请执行以下命令完成安装:" +echo " sudo /usr/share/config-editor/setup_venv.sh" +echo "" +echo "使用说明:" +echo "1. 设置虚拟环境: sudo /usr/share/config-editor/setup_venv.sh" +echo "2. 在终端运行: config-editor" + +# 设置文件权限 +chmod 755 /usr/bin/config-editor +chmod 755 /usr/share/config-editor/setup_venv.sh + +# 设置应用文件权限 +chmod -R 644 /usr/share/config-editor/* 2>/dev/null || true +chmod 755 /usr/share/config-editor/*.py 2>/dev/null || true +chmod 755 /usr/share/config-editor/*.sh 2>/dev/null || true + +# 为虚拟环境目录预留权限 +mkdir -p /usr/share/config-editor/venv 2>/dev/null || true +chmod 755 /usr/share/config-editor/venv 2>/dev/null || true + +exit 0 +EOF + +chmod +x "$BUILD_DIR/DEBIAN/postinst" + +# 构建deb包 +echo "构建deb包..." +dpkg-deb --build "$BUILD_DIR" "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" + +# 验证包 +echo "验证deb包..." +if dpkg -I "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" > /dev/null; then + echo "✓ 包验证成功" +else + echo "✗ 包验证失败" + exit 1 +fi + +# 清理构建目录 +echo "清理构建目录..." +rm -rf "$BUILD_DIR" + +echo "" +echo "==========================================" +echo "构建成功完成!" +echo "生成的deb包: ${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" +echo "" +echo "安装步骤:" +echo "1. 安装deb包: sudo dpkg -i ${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" +echo "2. 安装依赖(如有需要): sudo apt install -f" +echo "3. 设置虚拟环境: sudo /usr/share/config-editor/setup_venv.sh" +echo "" +echo "此版本特点:" +echo "✓ 修复了虚拟环境权限问题" +echo "✓ 使用 --system-site-packages 参数" +echo "✓ 自动使用清华PyPI镜像" +echo "✓ 简化了依赖和脚本" +echo "==========================================" + +# 显示包信息 +echo "" +echo "包信息:" +dpkg -I "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" diff --git a/config_editor.py b/config_editor.py new file mode 100644 index 0000000..5f6f953 --- /dev/null +++ b/config_editor.py @@ -0,0 +1,1686 @@ +import sys +import os +import ast +import json +import re +import traceback +from pathlib import Path +from PyQt6.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, + QTextEdit, QScrollArea, QMessageBox, QFrame, QSpinBox, + QDoubleSpinBox, QTableWidget, QTableWidgetItem, QHeaderView, + QToolBar, QStatusBar, QDialog, QDialogButtonBox, + QGroupBox, QSplitter, QListWidget, QComboBox, QInputDialog, + QFileDialog, QMenuBar, QMenu) +from PyQt6.QtCore import Qt, QSize, QTimer +from PyQt6.QtGui import QAction + +class ConfigSettingsDialog(QDialog): + """配置文件设置对话框""" + def __init__(self, current_path="", parent=None): + super().__init__(parent) + self.setWindowTitle("配置文件设置") + self.setModal(True) + self.setMinimumSize(500, 200) + self.current_path = current_path + + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(12) + + # 文件路径设置 + file_group = QGroupBox("配置文件路径") + file_layout = QVBoxLayout(file_group) + + path_layout = QHBoxLayout() + path_layout.addWidget(QLabel("配置文件:")) + + self.path_edit = QLineEdit() + self.path_edit.setText(self.current_path) + path_layout.addWidget(self.path_edit) + + self.browse_btn = QPushButton("浏览...") + self.browse_btn.clicked.connect(self.browse_file) + path_layout.addWidget(self.browse_btn) + + file_layout.addLayout(path_layout) + + # 提示信息 + tip_label = QLabel("提示:请选择或输入要编辑的配置文件路径") + tip_label.setStyleSheet("color: #666666; font-size: 11px;") + file_layout.addWidget(tip_label) + + layout.addWidget(file_group) + + # 使用相对路径选项 + self.use_relative_checkbox = QCheckBox("使用相对路径(相对于程序所在目录)") + self.use_relative_checkbox.setChecked(True) + layout.addWidget(self.use_relative_checkbox) + + layout.addStretch() + + # 按钮 + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + def browse_file(self): + """浏览文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择配置文件", + self.path_edit.text() or str(Path.home()), + "Python Files (*.py);;All Files (*)" + ) + if file_path: + self.path_edit.setText(file_path) + + def get_settings(self): + """获取设置""" + config_path = self.path_edit.text().strip() + + # 如果使用相对路径,则转换为相对于程序目录的路径 + if self.use_relative_checkbox.isChecked() and config_path: + if not os.path.isabs(config_path): + # 如果已经是相对路径,相对于程序所在目录 + program_dir = Path(__file__).parent.absolute() + config_path = str(program_dir / config_path) + + return { + "config_file_path": config_path, + "use_relative_path": self.use_relative_checkbox.isChecked() + } + +class ConfigManagementWindow(QDialog): + def __init__(self, rules, config_fields, parent=None): + super().__init__(parent) + self.rules = rules + self.config_fields = config_fields + self.all_fields = list(config_fields.keys()) # 保存所有字段名用于搜索 + self.setWindowTitle("规则管理") + self.setModal(True) + self.setMinimumSize(700, 600) + self.resize(700, 600) + + self.init_ui() + self.load_data() + + def init_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(8) + + # 分割窗口 + splitter = QSplitter(Qt.Orientation.Horizontal) + layout.addWidget(splitter) + + # 左侧:配置项列表 + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + left_layout.setContentsMargins(4, 4, 4, 4) + + # 搜索框 + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("搜索:")) + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("输入配置项名称进行搜索...") + self.search_edit.textChanged.connect(self.filter_fields) + search_layout.addWidget(self.search_edit) + + # 清除搜索按钮 + self.clear_search_btn = QPushButton("清除") + self.clear_search_btn.clicked.connect(self.clear_search) + self.clear_search_btn.setMaximumWidth(60) + search_layout.addWidget(self.clear_search_btn) + + left_layout.addLayout(search_layout) + + left_layout.addWidget(QLabel("配置项列表:")) + self.fields_list = QListWidget() + self.fields_list.currentItemChanged.connect(self.on_field_selected) + left_layout.addWidget(self.fields_list) + + # 添加统计信息 + self.fields_count_label = QLabel("") + left_layout.addWidget(self.fields_count_label) + + splitter.addWidget(left_widget) + + # 右侧:编辑区域 + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + right_layout.setContentsMargins(4, 4, 4, 4) + + # 基本信息 + info_group = QGroupBox("字段属性") + form_layout = QVBoxLayout(info_group) + form_layout.setContentsMargins(8, 8, 8, 8) + + # 变量名 + self.name_label = QLabel() + self.create_form_row(form_layout, "变量名:", self.name_label) + + # 显示名称 + self.display_name_edit = QLineEdit() + self.create_form_row(form_layout, "显示名称:", self.display_name_edit) + + # 分组和类型 + category_layout = QHBoxLayout() + category_layout.addWidget(QLabel("分组:")) + self.category_combo = QComboBox() + self.category_combo.setEditable(True) + category_layout.addWidget(self.category_combo) + category_layout.addStretch() + + category_layout.addWidget(QLabel("类型:")) + self.type_combo = QComboBox() + self.type_combo.addItems(["auto", "str", "int", "float", "bool", "json"]) + self.type_combo.currentTextChanged.connect(self.on_type_changed) + category_layout.addWidget(self.type_combo) + + form_layout.addLayout(category_layout) + + # 小数位数设置(仅当类型为float或int时显示) + self.decimal_row = QHBoxLayout() + self.decimal_label = QLabel("小数位数:") + self.decimal_spinbox = QSpinBox() + self.decimal_spinbox.setRange(0, 10) + self.decimal_spinbox.setValue(2) # 默认值 + self.decimal_spinbox.setEnabled(False) # 默认禁用,只有float类型才启用 + self.decimal_row.addWidget(self.decimal_label) + self.decimal_row.addWidget(self.decimal_spinbox) + self.decimal_row.addStretch() + form_layout.addLayout(self.decimal_row) + + # 隐藏状态 + self.hidden_checkbox = QCheckBox("隐藏此配置项(不在主界面显示)") + form_layout.addWidget(self.hidden_checkbox) + + # 提示信息 + form_layout.addWidget(QLabel("提示信息:")) + self.tooltip_edit = QTextEdit() + self.tooltip_edit.setMaximumHeight(80) + form_layout.addWidget(self.tooltip_edit) + + right_layout.addWidget(info_group) + + # 校验规则 + validation_group = QGroupBox("校验规则") + validation_layout = QVBoxLayout(validation_group) + validation_layout.setContentsMargins(8, 8, 8, 8) + + # 最小值 + min_layout = QHBoxLayout() + min_layout.addWidget(QLabel("最小值:")) + self.min_edit = QLineEdit() + self.min_edit.setPlaceholderText("对于数字类型有效") + min_layout.addWidget(self.min_edit) + min_layout.addStretch() + validation_layout.addLayout(min_layout) + + # 最大值 + max_layout = QHBoxLayout() + max_layout.addWidget(QLabel("最大值:")) + self.max_edit = QLineEdit() + self.max_edit.setPlaceholderText("对于数字类型有效") + max_layout.addWidget(self.max_edit) + max_layout.addStretch() + validation_layout.addLayout(max_layout) + + # 正则表达式 + validation_layout.addWidget(QLabel("正则表达式:")) + self.regex_edit = QLineEdit() + self.regex_edit.setPlaceholderText("对于字符串类型有效,如: ^[A-Za-z0-9_]+$") + validation_layout.addWidget(self.regex_edit) + + # 必填项 + self.required_checkbox = QCheckBox("必填项") + validation_layout.addWidget(self.required_checkbox) + + right_layout.addWidget(validation_group) + + # 分组管理 + group_group = QGroupBox("分组管理") + group_layout = QVBoxLayout(group_group) + + group_btn_layout = QHBoxLayout() + self.add_group_btn = QPushButton("添加分组") + self.remove_group_btn = QPushButton("删除分组") + + self.add_group_btn.clicked.connect(self.add_group) + self.remove_group_btn.clicked.connect(self.remove_group) + + group_btn_layout.addWidget(self.add_group_btn) + group_btn_layout.addWidget(self.remove_group_btn) + group_btn_layout.addStretch() + + group_layout.addLayout(group_btn_layout) + right_layout.addWidget(group_group) + + right_layout.addStretch() + splitter.addWidget(right_widget) + + # 设置分割比例 + splitter.setSizes([250, 420]) + + # 按钮 + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + def create_form_row(self, layout, label_text, widget): + """创建舒适的表单行""" + row_layout = QHBoxLayout() + label = QLabel(label_text) + label.setMinimumWidth(80) + row_layout.addWidget(label) + row_layout.addWidget(widget) + layout.addLayout(row_layout) + + def load_data(self): + """加载数据到界面""" + self.fields_list.clear() + for field_name in sorted(self.config_fields.keys()): + self.fields_list.addItem(field_name) + + self.update_fields_count() + + self.category_combo.clear() + categories = list(self.rules.get("categories", {}).keys()) + for category in sorted(categories): + self.category_combo.addItem(category) + self.category_combo.addItem("未分类") + + def filter_fields(self, search_text): + """根据搜索文本过滤配置项列表""" + search_text = search_text.strip().lower() + + if not search_text: + # 如果搜索文本为空,显示所有项 + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + item.setHidden(False) + else: + # 模糊匹配:显示包含搜索文本的项 + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + field_name = item.text().lower() + if search_text in field_name: + item.setHidden(False) + else: + item.setHidden(True) + + self.update_fields_count() + + def clear_search(self): + """清除搜索框内容""" + self.search_edit.clear() + + def update_fields_count(self): + """更新配置项计数显示""" + total_count = self.fields_list.count() + visible_count = 0 + + for i in range(total_count): + if not self.fields_list.item(i).isHidden(): + visible_count += 1 + + if visible_count == total_count: + self.fields_count_label.setText(f"总计: {total_count} 个配置项") + else: + self.fields_count_label.setText(f"显示: {visible_count}/{total_count} 个配置项") + + def on_type_changed(self, field_type): + """当类型改变时,控制小数位数设置的显示""" + # 只有float类型才显示小数位数设置 + if field_type == "float": + self.decimal_label.setVisible(True) + self.decimal_spinbox.setVisible(True) + self.decimal_spinbox.setEnabled(True) + else: + self.decimal_label.setVisible(False) + self.decimal_spinbox.setVisible(False) + self.decimal_spinbox.setEnabled(False) + + def on_field_selected(self, current, previous): + # 保存前一个字段的更改 + if previous is not None: + previous_field_name = previous.text() + self.save_current_field_changes(previous_field_name) + + if not current: + return + + field_name = current.text() + self.name_label.setText(field_name) + + display_name = self.rules.get("display_names", {}).get(field_name, field_name) + self.display_name_edit.setText(display_name) + + category = "未分类" + for cat, fields in self.rules.get("categories", {}).items(): + if field_name in fields: + category = cat + break + + index = self.category_combo.findText(category) + if index >= 0: + self.category_combo.setCurrentIndex(index) + else: + self.category_combo.setCurrentText(category) + + field_type = self.rules.get("field_types", {}).get(field_name, "auto") + index = self.type_combo.findText(field_type) + if index >= 0: + self.type_combo.setCurrentIndex(index) + + # 设置小数位数 + decimals = self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位小数 + self.decimal_spinbox.setValue(decimals) + + # 根据类型显示/隐藏小数位数设置 + self.on_type_changed(field_type) + + # 隐藏状态 + hidden = field_name in self.rules.get("hidden", []) + self.hidden_checkbox.setChecked(hidden) + + tooltip = self.rules.get("tooltips", {}).get(field_name, "") + self.tooltip_edit.setPlainText(tooltip) + + # 校验规则 + validation = self.rules.get("validations", {}).get(field_name, {}) + self.min_edit.setText(validation.get("min", "")) + self.max_edit.setText(validation.get("max", "")) + self.regex_edit.setText(validation.get("regex", "")) + self.required_checkbox.setChecked(validation.get("required", False)) + + def save_current_field_changes(self, field_name): + """保存当前字段的更改到规则字典""" + if not field_name: + return + + # 更新显示名称 + display_name = self.display_name_edit.text().strip() + if display_name: + self.rules.setdefault("display_names", {})[field_name] = display_name + elif field_name in self.rules.get("display_names", {}): + del self.rules["display_names"][field_name] + + # 更新分组 + category = self.category_combo.currentText().strip() + if category and category != "未分类": + # 从旧分组移除 + for cat, fields in self.rules.get("categories", {}).items(): + if field_name in fields: + fields.remove(field_name) + break + + # 添加到新分组 + if category not in self.rules["categories"]: + self.rules["categories"][category] = [] + if field_name not in self.rules["categories"][category]: + self.rules["categories"][category].append(field_name) + + # 更新字段类型 + field_type = self.type_combo.currentText() + if field_type != "auto": + self.rules.setdefault("field_types", {})[field_name] = field_type + + # 更新小数位数(仅当类型为float时保存) + if field_type == "float": + decimals = self.decimal_spinbox.value() + self.rules.setdefault("field_decimals", {})[field_name] = decimals + elif field_name in self.rules.get("field_decimals", {}): + # 如果类型不是float但存在小数位数设置,删除它 + del self.rules["field_decimals"][field_name] + elif field_name in self.rules.get("field_types", {}): + del self.rules["field_types"][field_name] + # 如果字段类型被删除,也删除小数位数设置 + if field_name in self.rules.get("field_decimals", {}): + del self.rules["field_decimals"][field_name] + + # 更新提示信息 + tooltip = self.tooltip_edit.toPlainText().strip() + if tooltip: + self.rules.setdefault("tooltips", {})[field_name] = tooltip + elif field_name in self.rules.get("tooltips", {}): + del self.rules["tooltips"][field_name] + + # 更新隐藏状态 + hidden = self.hidden_checkbox.isChecked() + hidden_list = self.rules.setdefault("hidden", []) + if hidden and field_name not in hidden_list: + hidden_list.append(field_name) + elif not hidden and field_name in hidden_list: + hidden_list.remove(field_name) + + # 更新校验规则 + validation = {} + min_val = self.min_edit.text().strip() + if min_val: + validation["min"] = min_val + + max_val = self.max_edit.text().strip() + if max_val: + validation["max"] = max_val + + regex_val = self.regex_edit.text().strip() + if regex_val: + validation["regex"] = regex_val + + validation["required"] = self.required_checkbox.isChecked() + + if validation: + self.rules.setdefault("validations", {})[field_name] = validation + elif field_name in self.rules.get("validations", {}): + del self.rules["validations"][field_name] + + def accept(self): + """重写accept方法,确保保存当前字段的更改""" + current_item = self.fields_list.currentItem() + if current_item: + field_name = current_item.text() + self.save_current_field_changes(field_name) + + super().accept() + + def add_group(self): + new_group, ok = QInputDialog.getText(self, "添加分组", "请输入新分组名称:") + if ok and new_group.strip(): + if new_group not in self.rules.setdefault("categories", {}): + self.rules["categories"][new_group] = [] + self.category_combo.addItem(new_group) + + def remove_group(self): + current_group = self.category_combo.currentText() + if current_group and current_group != "未分类": + reply = QMessageBox.question(self, "确认删除", + f"确定要删除分组 '{current_group}' 吗?") + if reply == QMessageBox.StandardButton.Yes: + if current_group in self.rules.get("categories", {}): + del self.rules["categories"][current_group] + self.category_combo.removeItem(self.category_combo.findText(current_group)) + + def get_updated_rules(self): + """获取更新后的规则""" + return self.rules + +class NoWheelSpinBox(QSpinBox): + """禁用鼠标滚轮的SpinBox""" + def wheelEvent(self, event): + event.ignore() + +class NoWheelDoubleSpinBox(QDoubleSpinBox): + """禁用鼠标滚轮的DoubleSpinBox""" + def wheelEvent(self, event): + event.ignore() + + def setDecimalsFromRules(self, decimals): + """根据规则设置小数位数""" + self.setDecimals(decimals) + +class ConfigField: + """配置项元数据类""" + def __init__(self, name, value, category="未分类", display_name=None, + field_type="auto", decimals=None, tooltip="", hidden=False, + line_number=None, original_lines=None, validation=None): + self.name = name + self.value = value + self.category = category + self.display_name = display_name or name + self.field_type = field_type + self.decimals = decimals # 小数位数(仅对float类型有效) + self.tooltip = tooltip + self.hidden = hidden + self.line_number = line_number # 记录配置项在文件中的行号 + self.original_lines = original_lines or [] # 保存原始行内容(包括注释) + self.validation = validation or {} # 校验规则 + + def get_actual_field_type(self): + """获取实际的字段类型""" + 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" + +class ChangeConfirmDialog(QDialog): + """变更确认对话框""" + def __init__(self, changes, parent=None): + super().__init__(parent) + self.setWindowTitle("确认配置变更") + self.setModal(True) + self.setMinimumSize(600, 400) + self.resize(600, 400) + + # 设置窗口标志,避免重影问题 + self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint | + Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowStaysOnTopHint) + + self.init_ui(changes) + + def init_ui(self, changes): + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel(f"以下 {len(changes)} 个配置项将被修改:") + title_label.setStyleSheet("font-weight: bold; font-size: 14px;") + layout.addWidget(title_label) + + # 变更列表 + table = QTableWidget() + table.setColumnCount(3) + table.setHorizontalHeaderLabels(["配置项", "原值", "新值"]) + table.setRowCount(len(changes)) + + for row, (field_name, (old_value, new_value)) in enumerate(changes.items()): + table.setItem(row, 0, QTableWidgetItem(field_name)) + table.setItem(row, 1, QTableWidgetItem(str(old_value))) + table.setItem(row, 2, QTableWidgetItem(str(new_value))) + + table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + layout.addWidget(table) + + # 按钮 + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + +class ConfigEditor(QMainWindow): + def __init__(self): + super().__init__() + + # 获取程序所在目录 + if getattr(sys, 'frozen', False): + # 打包后的可执行文件 + self.program_dir = Path(sys.executable).parent + else: + # 直接运行脚本 + self.program_dir = Path(__file__).parent + + # 规则文件固定位置:与主程序在同一目录 + self.rules_file = str(self.program_dir / "config_editor_rules.json") + + # 用户设置文件 + self.settings_file = str(self.program_dir / "config_editor_settings.json") + + # 加载用户设置 + self.user_settings = self.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.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_user_settings(self): + """加载用户设置""" + try: + if os.path.exists(self.settings_file): + with open(self.settings_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"加载用户设置失败: {e}") + + return {} + + def save_user_settings(self): + """保存用户设置""" + try: + self.user_settings.update({ + "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 "" + }) + + with open(self.settings_file, 'w', encoding='utf-8') as f: + json.dump(self.user_settings, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"保存用户设置失败: {e}") + + def init_ui(self): + self.setWindowTitle("配置管理页") + self.setGeometry(100, 100, 1000, 700) + self.resize(1000, 700) + + # 创建菜单栏 + menubar = self.menuBar() + + # 文件菜单 + file_menu = menubar.addMenu("文件") + + self.open_action = QAction("打开配置文件", self) + self.open_action.triggered.connect(self.open_config_file) + file_menu.addAction(self.open_action) + + self.settings_action = QAction("配置文件设置", self) + self.settings_action.triggered.connect(lambda: self.show_settings_dialog(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) + edit_menu.addAction(self.save_action) + + # 工具菜单 + tools_menu = menubar.addMenu("工具") + + self.rules_action = QAction("管理规则", self) + self.rules_action.triggered.connect(self.manage_rules) + tools_menu.addAction(self.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: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + } + """) + info_layout = QHBoxLayout(info_frame) + info_layout.setContentsMargins(8, 4, 8, 4) + + self.path_label = QLabel() + self.path_label.setStyleSheet("color: #666666; font-size: 11px;") + 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) + button_layout.addWidget(self.open_btn) + + self.settings_btn = QPushButton("配置文件设置") + self.settings_btn.clicked.connect(lambda: self.show_settings_dialog(initial_setup=False)) + button_layout.addWidget(self.settings_btn) + + button_layout.addStretch() + + self.save_btn = QPushButton("保存配置") + self.save_btn.clicked.connect(self.save_config) + 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) + 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) + button_layout.addWidget(self.toggle_hidden_btn) + + main_layout.addLayout(button_layout) + + # 更新路径显示 + self.update_path_display() + + 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 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.save_user_settings() + + # 更新路径显示 + 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.save_user_settings() + self.update_path_display() + self.load_config() + + def init_category_tabs(self): + self.category_widgets.clear() + categories = list(self.rules.get("categories", {}).keys()) + if not categories: + categories = ["未分类"] + + for category in categories: + 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): + try: + if os.path.exists(self.rules_file): + with open(self.rules_file, 'r', encoding='utf-8') as f: + self.rules = json.load(f) + + # 确保规则中有field_decimals字段 + if "field_decimals" not in self.rules: + self.rules["field_decimals"] = {} + else: + self.rules = { + "categories": {}, + "display_names": {}, + "tooltips": {}, + "field_types": {}, + "field_decimals": {}, # 新增:小数位数设置 + "hidden": [], # 新增隐藏列表 + "validations": {} # 新增校验规则 + } + # 首次运行,尝试创建规则文件 + try: + self.save_rules() + except Exception as e: + print(f"创建规则文件失败: {e}") + except Exception as e: + print(f"加载规则文件失败: {e}") + self.rules = { + "categories": {}, + "display_names": {}, + "tooltips": {}, + "field_types": {}, + "field_decimals": {}, + "hidden": [], + "validations": {} + } + + def save_rules(self): + try: + with open(self.rules_file, 'w', encoding='utf-8') as f: + json.dump(self.rules, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"保存规则文件失败: {e}") + + def categorize_field(self, field_name): + for category, fields in self.rules.get("categories", {}).items(): + if field_name in fields: + return category + return "未分类" + + def get_display_name(self, field_name): + return self.rules.get("display_names", {}).get(field_name, field_name) + + def get_tooltip(self, field_name): + return self.rules.get("tooltips", {}).get(field_name, f"配置项: {field_name}") + + def get_field_type(self, field_name, value): + if field_name in self.rules.get("field_types", {}): + return self.rules["field_types"][field_name] + + 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 get_field_decimals(self, field_name): + """获取字段的小数位数""" + return self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位 + + def get_validation(self, field_name): + """获取配置项的校验规则""" + return self.rules.get("validations", {}).get(field_name, {}) + + def is_hidden(self, field_name): + """检查配置项是否被标记为隐藏""" + return field_name in self.rules.get("hidden", []) + + def parse_config_file(self): + """解析配置文件,精确匹配每个配置项的位置和注释""" + try: + with open(self.config_file_path, 'r', encoding='utf-8') as f: + content = f.read() + lines = content.split('\n') + + tree = ast.parse(content) + config_data = {} + all_config_fields = {} + + # 第一遍:识别所有配置项及其位置 + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id.isupper(): + var_name = target.id + + # 获取配置项在文件中的位置 + assignment_line = node.lineno - 1 # ast行号从1开始,我们使用0基索引 + + # 向上查找配置项上面的注释行 + comment_lines = [] + current_line = assignment_line - 1 + while current_line >= 0: + line = lines[current_line].strip() + # 如果是注释行或空行,则包含在注释中 + if line.startswith('#') or line == '': + comment_lines.insert(0, lines[current_line]) + current_line -= 1 + # 如果是多行注释的开始 + elif line.startswith("'''") or line.startswith('"""'): + # 找到多行注释的起始位置 + comment_start = current_line + while comment_start >= 0 and not (lines[comment_start].strip().endswith("'''") or lines[comment_start].strip().endswith('"""')): + comment_start -= 1 + if comment_start >= 0: + for i in range(comment_start, current_line + 1): + comment_lines.insert(0, lines[i]) + current_line = comment_start - 1 + else: + break + else: + break + + # 完整的配置项块(包括注释和赋值行) + full_block = comment_lines + [lines[assignment_line]] + + # 如果赋值行有后续行(如多行字符串),添加它们 + end_line = getattr(node, 'end_lineno', node.lineno) - 1 + if end_line > assignment_line: + for i in range(assignment_line + 1, end_line + 1): + full_block.append(lines[i]) + + try: + # 获取配置项的值 + var_value = eval(compile(ast.Expression(node.value), '', 'eval')) + config_data[var_name] = var_value + + # 获取字段类型和小数位数 + field_type = self.get_field_type(var_name, var_value) + decimals = self.get_field_decimals(var_name) if field_type == "float" else None + + all_config_fields[var_name] = ConfigField( + name=var_name, + value=var_value, + category=self.categorize_field(var_name), + display_name=self.get_display_name(var_name), + field_type=field_type, + decimals=decimals, + tooltip=self.get_tooltip(var_name), + hidden=self.is_hidden(var_name), + line_number=assignment_line - len(comment_lines), + original_lines=full_block, + validation=self.get_validation(var_name) + ) + + except: + # 如果无法解析值,使用字符串表示 + try: + value_str = ast.get_source_segment(content, node.value) + config_data[var_name] = value_str + + all_config_fields[var_name] = ConfigField( + name=var_name, + value=value_str, + category=self.categorize_field(var_name), + display_name=self.get_display_name(var_name), + field_type="str", + decimals=None, + tooltip=self.get_tooltip(var_name), + hidden=self.is_hidden(var_name), + line_number=assignment_line - len(comment_lines), + original_lines=full_block, + validation=self.get_validation(var_name) + ) + + except: + config_data[var_name] = "无法解析的值" + all_config_fields[var_name] = ConfigField( + name=var_name, + value="无法解析的值", + category=self.categorize_field(var_name), + display_name=self.get_display_name(var_name), + field_type="str", + decimals=None, + tooltip=self.get_tooltip(var_name), + hidden=self.is_hidden(var_name), + line_number=assignment_line - len(comment_lines), + original_lines=full_block, + validation=self.get_validation(var_name) + ) + + return config_data, all_config_fields, content + + except Exception as e: + QMessageBox.critical(self, "错误", f"解析配置文件失败:{str(e)}") + return {}, {}, "" + + 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.setDecimals(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 = self.get_validation_tooltip(config_field) + 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 get_validation_tooltip(self, config_field): + """生成校验规则的提示信息""" + validation = config_field.validation + 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 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 { + background-color: #fff9c4; + } + """) + + 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 { + background-color: transparent; + } + """) + + def update_status_bar(self): + """更新状态栏显示修改数量""" + if self.modified_fields: + self.statusBar().showMessage(f"已修改 {len(self.modified_fields)} 个配置项") + else: + self.statusBar().showMessage("就绪") + + def create_field_editor(self, config_field): + """创建字段编辑器,返回容器和控件""" + # 创建容器框架 + container = QFrame() + container.setObjectName("field_container") # 设置对象名用于样式选择 + container.setFrameShape(QFrame.Shape.StyledPanel) + # 设置基础样式,使用对象名选择器确保只影响容器本身 + container.setStyleSheet(""" + QFrame#field_container { + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 4px; + background-color: transparent; + } + """) + + layout = QHBoxLayout(container) + layout.setContentsMargins(8, 4, 8, 4) + layout.setSpacing(8) + + # 标签 + label = QLabel(config_field.display_name) + label.setMinimumWidth(180) + + # 添加校验指示器(如果配置项有校验规则) + 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;") + + label.setText(f"{config_field.display_name}{validation_indicator}") + label.setToolTip(self.get_validation_tooltip(config_field)) + + # 如果配置项是隐藏的,添加视觉提示 + 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) + + 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() # 清空容器引用 + + 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) + + for category, fields in categorized_fields.items(): + if category not in self.category_widgets: + self.create_category_tab(category) + + layout = self.category_widgets[category] + 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 + + self.config_data, self.all_config_fields, self.original_content = self.parse_config_file() + + 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} + + # 根据当前显示状态生成UI + show_hidden = getattr(self, 'show_hidden', False) + self.generate_dynamic_ui(show_hidden) + + hidden_count = len(self.all_config_fields) - len(self.config_fields) + self.statusBar().showMessage(f"成功加载 {len(self.config_fields)} 个配置项 ({hidden_count} 个已隐藏)") + + 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 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.save_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 validate_config_data(self, config_data): + """校验配置数据""" + errors = [] + + for field_name, value in config_data.items(): + config_field = self.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_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 format_dict_value(self, value, original_lines=None): + """专门格式化字典值,保持多行格式""" + 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(self, value, config_field=None): + """格式化值,保持与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): + # 对于字典,使用专门的格式化函数 + return self.format_dict_value(value, config_field.original_lines if config_field else None) + elif isinstance(value, list): + return json.dumps(value, ensure_ascii=False, indent=4) + else: + return repr(value) + + def save_config(self): + """保存配置,保留所有注释、空格和格式""" + try: + # 检查配置文件是否存在 + 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 = self.validate_config_data(new_data) + 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 + + # 读取原始文件内容 + 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. 行尾注释(如果有) + 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 = self.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 = self.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() + + 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 main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') + + # 创建编辑器 + editor = ConfigEditor() + editor.show() + sys.exit(app.exec()) + +if __name__ == '__main__': + main() diff --git a/config_editor_rules.json b/config_editor_rules.json new file mode 100644 index 0000000..f3dfa9f --- /dev/null +++ b/config_editor_rules.json @@ -0,0 +1,131 @@ +{ + "categories": { + "数据库配置": [ + "CONNECTION", + "REDIS_ADDRESS", + "REDIS_PORT", + "SYNC_INTERVAL", + "REDIS_FIELD_MAPPING" + ], + "PLC控制": [ + "PLC_ADDRESS", + "PLC_PORT", + "NH3_LOWER", + "NH3_UPPER", + "IS_AUTO", + "AI_NH3_ADDRESS", + "FLOW_FEEDBACK_ADDRESS", + "NH3_MANUAL" + ], + "训练参数": [ + "RESAMPLE_SIZE", + "MISSING_VALUE_LENGTH", + "MEAN_MASE", + "INTERVAL_SIZE_V2", + "LAGS", + "PREDICT_FH_V2", + "TRAIN_DAYS_V2", + "THREAD_COUNT", + "GPU_RAM_PART", + "CNF_PREDICT_NUMBER", + "CNF_LAGS", + "CNF_LAGS_PC", + "CNF_OUTPUT_CHUNK_LENGTH", + "CNF_THREAD_COUNT", + "CNF_ALARM_LEVEL_1", + "CNF_ALARM_LEVEL_2", + "CNF_ALARM_LEVEL_3" + ], + "计算参数": [ + "NH3_LOWER", + "NH3_UPPER", + "SMOKE_VOLUME", + "SMOKE_VOLUME_UPPER", + "SMOKE_VOLUME_LOWER", + "CONC", + "DENSITY", + "IS_SOLUTION", + "DECLINE_LIMIT", + "LOWER_LIMIT_DURATION", + "RAISE_LIMIT", + "UPPER_LIMIT_DURATION", + "PAS_TARGET", + "ACTUAL_PAS_TARGET_DIFF_CEMS", + "ACTUAL_PAS_TARGET_DIFF_PW", + "ACTUAL_PAS_TARGET_CEMS", + "ACTUAL_PAS_TARGET_PW", + "DYNAMIC_TARGET_UPPER", + "DYNAMIC_TARGET_LOWER", + "DYNAMIC_TARGET_START_MINUTE", + "DYNAMIC_TARGET_END_MINUTE", + "NH3_MANUAL" + ], + "控制修正": [ + "OUT_NH3_TARGET", + "OUT_NH3_TARGET_LOWER", + "OUT_NH3_TARGET_UPPER", + "CORR_PER_LIMIT", + "CORR_SLOPE", + "AVG_PRIMARY_ADJUST_TIME", + "PRIMARY_ADJUST_THRESHOLD", + "PRIMARY_ADJUST_INC_MULT", + "PRIMARY_ADJUST_DEC_MULT", + "CONTROL_METHOD_THRESHOLD", + "AVG_CORR_NOX_THRESHOLD", + "AVG_SMOKE_ADJUST_TIME", + "PRIMARY_ADJUST_SWITCH", + "SECONDARY_ADJUST_SWITCH", + "AVG_PRED_NOX_ADJUST_TIME", + "ADVANCE_TIME", + "BLOWBACK_TIME", + "EFFICIENCY", + "EFFICIENCY_ADJUST_INTERVAL", + "EFFICIENCY_ADJUST_ADVANCE_TIME", + "AVG_EFFICIENCY_ADJUST_TIME", + "EFFICIENCY_ADJUST_THRESHOLD", + "EFFICIENCY_ADJUST_UPPER", + "EFFICIENCY_ADJUST_LOWER", + "CORR_SLOPE_2", + "OUT_NH3_ADJUST_SWITCH", + "OUT_NH3_ADJUST_THRESHOLD", + "OUT_NH3_ADJUST_PCT" + ], + "PID控制": [ + "PID_VALVE_IS_AUTO", + "PID_VALVE_LOWER", + "PID_VALVE_UPPER", + "PID_VALVE_MANUAL", + "PID_VALVE_AUTO_ADDRESS", + "PID_VALVE_READ_ADDRESS", + "PID_VALVE_MANUAL_ADDRESS", + "PID_VALVE_FEEDBACK_ADDRESS" + ], + "MQTT配置": [ + "MQTT_HOST", + "MQTT_PORT", + "MQTT_USERNAME", + "MQTT_PASSWORD", + "MQTT_TOPIC", + "MQTT_CLIENT_ID" + ], + "其他配置": [ + "VALVE_UPPER", + "VALVE_LOWER", + "MODEL_CHANNEL", + "CLEAN_UPPER", + "CLEAN_LOWER", + "ALARM_WEBHOOK", + "ABS_DIFF_THRESHOLD" + ], + "未分类": [ + "EMERGENCY_ADJUST_INC_MULT", + "USED_RAM_LIMIT" + ] + }, + "display_names": { + "ABS_DIFF_THRESHOLD": "两个出口NOx绝对值误差阈值" + }, + "tooltips": {}, + "field_types": {}, + "hidden": [] +} \ No newline at end of file diff --git a/config_editor_settings.json b/config_editor_settings.json new file mode 100644 index 0000000..a17a560 --- /dev/null +++ b/config_editor_settings.json @@ -0,0 +1,4 @@ +{ + "config_file_path": "", + "last_used": "" +}