diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..db7f014 --- /dev/null +++ b/__init__.py @@ -0,0 +1,10 @@ +""" +配置编辑器模块 +""" + +__version__ = "1.3" +__author__ = "Config Editor" + +from main_window import ConfigEditor + +__all__ = ['ConfigEditor'] \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e5936b7 --- /dev/null +++ b/build.sh @@ -0,0 +1,724 @@ +#!/bin/bash +# 打包脚本:将配置编辑器项目打包成 deb 包 + +set -e # 遇到错误时退出 + +# ==================== 配置区域 ==================== +APP_NAME="config-editor" +APP_VERSION="1.3" +APP_DESCRIPTION="CE编辑器" +MAINTAINER="hjy " +ARCHITECTURE="amd64" +DEPENDENCIES="python3, python3-venv, python3-pip" +PYTHON_VERSION="3.8" +VENV_PATH="/opt/$APP_NAME/venv" +APP_INSTALL_PATH="/opt/$APP_NAME" +# ================================================= + +echo "========================================" +echo "开始构建 $APP_NAME (版本 $APP_VERSION)" +echo "========================================" +echo "将使用虚拟环境: $VENV_PATH" + +# 1. 清理之前的构建 +echo "1. 清理之前的构建..." +rm -rf build/ dist/ deb_dist/ *.egg-info/ 2>/dev/null || true +rm -rf /tmp/$APP_NAME-* 2>/dev/null || true + +# 2. 创建构建目录结构 +echo "2. 创建构建目录结构..." +BUILD_ROOT="/tmp/${APP_NAME}-${APP_VERSION}" +DEBIAN_DIR="$BUILD_ROOT/DEBIAN" +APP_DIR="$BUILD_ROOT$APP_INSTALL_PATH" +BIN_DIR="$BUILD_ROOT/usr/bin" +DESKTOP_DIR="$BUILD_ROOT/usr/share/applications" +ICON_DIR="$BUILD_ROOT/usr/share/icons/hicolor" +MIME_DIR="$BUILD_ROOT/usr/share/mime/packages" +VAR_LIB_DIR="$BUILD_ROOT/var/lib/$APP_NAME" + +# 清理并重新创建目录 +rm -rf "$BUILD_ROOT" +mkdir -p "$DEBIAN_DIR" +mkdir -p "$APP_DIR" +mkdir -p "$BIN_DIR" +mkdir -p "$DESKTOP_DIR" +mkdir -p "$ICON_DIR/48x48/apps" +mkdir -p "$ICON_DIR/64x64/apps" +mkdir -p "$ICON_DIR/128x128/apps" +mkdir -p "$MIME_DIR" +mkdir -p "$VAR_LIB_DIR" + +# 3. 复制项目文件 +echo "3. 复制项目文件..." +cp -r *.py "$APP_DIR/" +cp -r login/ "$APP_DIR/" +cp -r requirements.txt "$APP_DIR/" + +# 清理不必要的文件 +find "$APP_DIR" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true +find "$APP_DIR" -name "*.pyc" -delete 2>/dev/null || true +find "$APP_DIR" -name ".DS_Store" -delete 2>/dev/null || true + +# 4. 创建启动脚本(使用虚拟环境中的 Python) +echo "4. 创建启动脚本..." +cat > "$BIN_DIR/$APP_NAME" << EOF +#!/bin/bash +# 配置编辑器启动脚本(使用虚拟环境) + +# 应用程序安装路径 +APP_DIR="$APP_INSTALL_PATH" +VENV_PATH="$VENV_PATH" + +# 检查虚拟环境是否存在 +if [ ! -f "\$VENV_PATH/bin/activate" ]; then + echo "错误: 虚拟环境不存在。请重新安装应用程序。" + exit 1 +fi + +# 激活虚拟环境并运行程序 +cd "\$APP_DIR" +source "\$VENV_PATH/bin/activate" +exec python main.py "\$@" +EOF + +chmod +x "$BIN_DIR/$APP_NAME" + +# 5. 创建桌面入口文件 +echo "5. 创建桌面入口文件..." +cat > "$DESKTOP_DIR/$APP_NAME.desktop" << EOF +[Desktop Entry] +Name=Config Editor +Name[zh_CN]=配置编辑器 +Comment=$APP_DESCRIPTION +Comment[zh_CN]=用于管理和编辑配置文件 +Exec=$APP_NAME +Icon=$APP_NAME +Terminal=false +Type=Application +Categories=Development;Utility; +StartupNotify=true +Keywords=config;editor;settings;configuration; +MimeType=application/x-python; +EOF + +# 6. 创建图标(如果没有提供图标,则生成一个简单的) +echo "6. 创建图标..." +if [ -f "logo.png" ]; then + # 如果有现有图标,使用它 + cp logo.png "$ICON_DIR/48x48/apps/$APP_NAME.png" + cp logo.png "$ICON_DIR/64x64/apps/$APP_NAME.png" + cp logo.png "$ICON_DIR/128x128/apps/$APP_NAME.png" +else + # 如果没有图标,创建一个简单的占位符 + echo "未找到图标文件,创建简单图标..." + + # 使用 ImageMagick 创建图标(如果可用) + if command -v convert &> /dev/null; then + # 创建 48x48 图标 + convert -size 48x48 xc:#2196F3 \ + -fill white -pointsize 24 -gravity center -annotate 0 "CE" \ + "$ICON_DIR/48x48/apps/$APP_NAME.png" + + # 创建 64x64 图标 + convert -size 64x64 xc:#2196F3 \ + -fill white -pointsize 32 -gravity center -annotate 0 "CE" \ + "$ICON_DIR/64x64/apps/$APP_NAME.png" + + # 创建 128x128 图标 + convert -size 128x128 xc:#2196F3 \ + -fill white -pointsize 48 -gravity center -annotate 0 "CE" \ + "$ICON_DIR/128x128/apps/$APP_NAME.png" + else + echo "警告: 未安装 ImageMagick (convert),跳过图标生成。" + fi +fi + +# 7. 创建 DEBIAN 控制文件 +echo "7. 创建 DEBIAN 控制文件..." +cat > "$DEBIAN_DIR/control" << EOF +Package: $APP_NAME +Version: $APP_VERSION +Section: utils +Priority: optional +Architecture: $ARCHITECTURE +Depends: $DEPENDENCIES +Maintainer: $MAINTAINER +Description: $APP_DESCRIPTION + 配置编辑器是一个功能强大的工具,用于编辑和管理各种配置文件。 + 支持以下功能: + * 智能解析配置文件 + * 分组显示配置项 + * 拖拽重新排序 + * 加密配置项保护 + * 会话超时锁定 + * 用户认证系统 + * 规则管理 + * 在虚拟环境中运行,避免依赖冲突 +Homepage: https://github.com/yourusername/config-editor +EOF + +# 8. 创建安装后脚本(创建虚拟环境) +echo "8. 创建安装后脚本..." +cat > "$DEBIAN_DIR/postinst" << EOF +#!/bin/bash +# 安装后脚本 - 创建虚拟环境并安装依赖 + +set -e + +# 更新桌面数据库 +if [ -d /usr/share/applications ]; then + update-desktop-database /usr/share/applications || true +fi + +# 更新图标缓存 +if command -v gtk-update-icon-cache &> /dev/null; then + gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true +fi + +# 设置文件权限 +chmod 755 $APP_INSTALL_PATH/*.py +chmod 755 $APP_INSTALL_PATH/login/*.py +chmod 755 /usr/bin/$APP_NAME + +# 创建虚拟环境目录 +echo "创建虚拟环境目录..." +mkdir -p "$VENV_PATH" + +# 检查 Python3 是否可用 +if ! command -v python3 &> /dev/null; then + echo "错误: Python3 未安装。请安装 python3。" + exit 1 +fi + +# 创建虚拟环境 +echo "创建 Python 虚拟环境..." +python3 -m venv "$VENV_PATH" --system-site-packages + +# 激活虚拟环境并安装依赖 +echo "在虚拟环境中安装依赖..." +source "$VENV_PATH/bin/activate" + +# 升级 pip +pip install --upgrade pip + +# 安装依赖 +if [ -f "$APP_INSTALL_PATH/requirements.txt" ]; then + echo "从 requirements.txt 安装依赖..." + pip install -r "$APP_INSTALL_PATH/requirements.txt" +else + echo "安装 PyQt6..." + pip install PyQt6 +fi + +# 验证安装 +if python3 -c "import PyQt6; print('PyQt6 安装成功')" 2>/dev/null; then + echo "✅ 虚拟环境设置成功" +else + echo "❌ 虚拟环境设置失败,尝试重新安装 PyQt6..." + pip install --force-reinstall PyQt6 +fi + +# 确保规则文件可写 +touch "$APP_INSTALL_PATH/config_editor_rules.json" 2>/dev/null || true +chmod 666 "$APP_INSTALL_PATH/config_editor_rules.json" 2>/dev/null || true + +# 创建用户数据目录 +mkdir -p "/var/lib/$APP_NAME" +chmod 755 "/var/lib/$APP_NAME" + +echo "安装完成!您可以通过以下方式启动配置编辑器:" +echo "1. 在应用程序菜单中搜索 'Config Editor'" +echo "2. 在终端中运行: $APP_NAME" +echo "3. 首次运行需要设置配置文件路径" +echo "" +echo "虚拟环境位置: $VENV_PATH" +echo "应用程序位置: $APP_INSTALL_PATH" +EOF + +chmod 755 "$DEBIAN_DIR/postinst" + +# 9. 创建卸载前脚本 +echo "9. 创建卸载前脚本..." +cat > "$DEBIAN_DIR/prerm" << EOF +#!/bin/bash +# 卸载前脚本 + +set -e + +# 检查程序是否正在运行 +if pgrep -f "$APP_NAME" > /dev/null; then + echo "正在停止运行中的配置编辑器..." + pkill -f "$APP_NAME" || true + sleep 2 +fi +EOF + +chmod 755 "$DEBIAN_DIR/prerm" + +# 10. 创建卸载后脚本 +echo "10. 创建卸载后脚本..." +cat > "$DEBIAN_DIR/postrm" << EOF +#!/bin/bash +# 卸载后脚本 + +set -e + +# 更新桌面数据库 +if [ -d /usr/share/applications ]; then + update-desktop-database /usr/share/applications || true +fi + +# 更新图标缓存 +if command -v gtk-update-icon-cache &> /dev/null; then + gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true +fi + +# 删除虚拟环境(如果存在) +if [ -d "$VENV_PATH" ]; then + echo "删除虚拟环境..." + rm -rf "$VENV_PATH" +fi + +# 删除应用程序目录(如果存在) +if [ -d "$APP_INSTALL_PATH" ]; then + echo "删除应用程序目录..." + rm -rf "$APP_INSTALL_PATH" +fi + +# 删除系统数据目录 +if [ -d "/var/lib/$APP_NAME" ]; then + echo "删除系统数据目录..." + rm -rf "/var/lib/$APP_NAME" +fi + +# 删除用户数据(可选) +echo "是否要删除用户数据?(n/yes)" +echo "这包括:" +echo " - ~/.config_editor/ 目录" +echo " - ~/.config/config-editor/ 目录" +read -p "输入 'yes' 确认删除,其他跳过: " choice +if [ "\$choice" = "yes" ]; then + rm -rf ~/.config_editor 2>/dev/null || true + rm -rf ~/.config/config-editor 2>/dev/null || true + echo "用户数据已删除。" +else + echo "用户数据保留。" +fi +EOF + +chmod 755 "$DEBIAN_DIR/postrm" + +# 11. 创建版权文件 +echo "11. 创建版权文件..." +cat > "$DEBIAN_DIR/copyright" << EOF +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: $APP_NAME +Source: https://github.com/yourusername/config-editor + +Files: * +Copyright: $(date +%Y) Your Name +License: MIT + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +EOF + +# 12. 设置文件权限 +echo "12. 设置文件权限..." +find "$BUILD_ROOT" -type f -exec chmod 644 {} \; +find "$BUILD_ROOT" -type d -exec chmod 755 {} \; +chmod -R 755 "$DEBIAN_DIR"/* 2>/dev/null || true +chmod 755 "$BIN_DIR/$APP_NAME" +chmod 755 "$APP_DIR"/*.py 2>/dev/null || true +chmod 755 "$APP_DIR"/login/*.py 2>/dev/null || true + +# 13. 创建虚拟环境占位符目录 +echo "13. 创建虚拟环境占位符目录..." +mkdir -p "$BUILD_ROOT$VENV_PATH" +touch "$BUILD_ROOT$VENV_PATH/.placeholder" +echo "这是一个虚拟环境占位符,将在安装时创建实际的虚拟环境。" > "$BUILD_ROOT$VENV_PATH/README.txt" + +# 14. 创建配置文件示例 +echo "14. 创建配置文件示例..." +cat > "$VAR_LIB_DIR/config_example.py" << 'EOF' +"""配置文件示例""" +# 数据库配置 +DATABASE_HOST = "localhost" +DATABASE_PORT = 3306 +DATABASE_USER = "admin" +DATABASE_PASSWORD = "secret_password" +DATABASE_NAME = "myapp_db" + +# 应用配置 +APP_NAME = "My Application" +DEBUG_MODE = False +LOG_LEVEL = "INFO" +MAX_CONNECTIONS = 100 + +# API配置 +API_KEY = "your_api_key_here" +API_TIMEOUT = 30 +API_RETRIES = 3 + +# 服务配置 +ENABLE_CACHE = True +CACHE_SIZE = 1024 +ENABLE_SSL = True + +# 邮件配置 +SMTP_SERVER = "smtp.example.com" +SMTP_PORT = 587 +EMAIL_FROM = "noreply@example.com" + +# 安全配置 +SESSION_TIMEOUT = 3600 +PASSWORD_MIN_LENGTH = 8 +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] + +# 第三方服务配置 +STRIPE_API_KEY = "sk_test_1234567890" +AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE" +AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +EOF + +# 15. 创建虚拟环境初始化脚本 +echo "15. 创建虚拟环境初始化脚本..." +cat > "$APP_DIR/init_venv.sh" << EOF +#!/bin/bash +# 虚拟环境初始化脚本 + +VENV_PATH="$VENV_PATH" +APP_DIR="$APP_INSTALL_PATH" + +echo "初始化虚拟环境..." +echo "虚拟环境路径: \$VENV_PATH" +echo "应用路径: \$APP_DIR" + +# 检查是否已存在虚拟环境 +if [ -f "\$VENV_PATH/bin/activate" ]; then + echo "虚拟环境已存在,重新创建吗?(y/N)" + read -r response + if [[ ! \$response =~ ^[Yy]$ ]]; then + echo "操作取消。" + exit 0 + fi + rm -rf "\$VENV_PATH" +fi + +# 创建虚拟环境 +echo "创建新的虚拟环境..." +python3 -m venv "\$VENV_PATH" --system-site-packages + +# 激活虚拟环境 +source "\$VENV_PATH/bin/activate" + +# 升级pip +pip install --upgrade pip + +# 安装依赖 +if [ -f "\$APP_DIR/requirements.txt" ]; then + echo "安装依赖..." + pip install -r "\$APP_DIR/requirements.txt" +else + echo "安装 PyQt6..." + pip install PyQt6 +fi + +# 验证安装 +if python3 -c "import PyQt6" &> /dev/null; then + echo "✅ 虚拟环境初始化成功!" + echo "" + echo "使用方法:" + echo "1. 激活虚拟环境: source \$VENV_PATH/bin/activate" + echo "2. 运行程序: cd \$APP_DIR && python main.py" + echo "3. 退出虚拟环境: deactivate" +else + echo "❌ 虚拟环境初始化失败!" + exit 1 +fi +EOF + +chmod +x "$APP_DIR/init_venv.sh" + +# 16. 创建虚拟环境管理脚本 +echo "16. 创建虚拟环境管理脚本..." +cat > "$APP_DIR/manage_venv.py" << 'EOF' +#!/usr/bin/env python3 +""" +虚拟环境管理脚本 +用于检查和维护配置编辑器的虚拟环境 +""" +import os +import sys +import subprocess +import venv + +VENV_PATH = "/opt/config-editor/venv" +APP_DIR = "/opt/config-editor" + +def check_venv(): + """检查虚拟环境状态""" + print("检查虚拟环境状态...") + print(f"虚拟环境路径: {VENV_PATH}") + + # 检查虚拟环境是否存在 + if not os.path.exists(os.path.join(VENV_PATH, "bin", "activate")): + print("❌ 虚拟环境不存在") + return False + + # 检查 Python 是否可用 + venv_python = os.path.join(VENV_PATH, "bin", "python3") + if not os.path.exists(venv_python): + print("❌ 虚拟环境中的 Python 不存在") + return False + + # 检查 PyQt6 是否已安装 + try: + result = subprocess.run( + [venv_python, "-c", "import PyQt6; print('✅ PyQt6 已安装')"], + capture_output=True, + text=True + ) + if result.returncode == 0: + print(result.stdout.strip()) + return True + else: + print("❌ PyQt6 未安装或有问题") + print(f"错误信息: {result.stderr}") + return False + except Exception as e: + print(f"❌ 检查时出错: {e}") + return False + +def recreate_venv(): + """重新创建虚拟环境""" + print("重新创建虚拟环境...") + + # 删除现有虚拟环境 + if os.path.exists(VENV_PATH): + print(f"删除现有虚拟环境: {VENV_PATH}") + subprocess.run(["rm", "-rf", VENV_PATH], check=False) + + # 创建新虚拟环境 + print("创建新虚拟环境...") + try: + venv.create(VENV_PATH, with_pip=True, system_site_packages=True) + except Exception as e: + print(f"❌ 创建虚拟环境失败: {e}") + return False + + # 安装依赖 + print("安装依赖...") + venv_pip = os.path.join(VENV_PATH, "bin", "pip3") + requirements_file = os.path.join(APP_DIR, "requirements.txt") + + if os.path.exists(requirements_file): + print(f"从 {requirements_file} 安装依赖...") + result = subprocess.run([venv_pip, "install", "-r", requirements_file], capture_output=True, text=True) + else: + print("安装 PyQt6...") + result = subprocess.run([venv_pip, "install", "PyQt6"], capture_output=True, text=True) + + if result.returncode != 0: + print(f"❌ 安装依赖失败: {result.stderr}") + return False + + print("✅ 虚拟环境重新创建成功!") + return True + +def show_venv_info(): + """显示虚拟环境信息""" + print("虚拟环境信息:") + print(f"路径: {VENV_PATH}") + + if os.path.exists(os.path.join(VENV_PATH, "bin", "python3")): + venv_python = os.path.join(VENV_PATH, "bin", "python3") + result = subprocess.run([venv_python, "--version"], capture_output=True, text=True) + print(f"Python 版本: {result.stdout.strip()}") + + result = subprocess.run([venv_python, "-m", "pip", "list"], capture_output=True, text=True) + print("\n已安装的包:") + print(result.stdout) + else: + print("❌ 虚拟环境不存在或已损坏") + +def main(): + """主函数""" + print("配置编辑器虚拟环境管理工具") + print("=" * 40) + + if len(sys.argv) < 2: + print("使用方法:") + print(" python manage_venv.py check - 检查虚拟环境状态") + print(" python manage_venv.py recreate - 重新创建虚拟环境") + print(" python manage_venv.py info - 显示虚拟环境信息") + return + + command = sys.argv[1] + + if command == "check": + if check_venv(): + print("✅ 虚拟环境状态正常") + else: + print("❌ 虚拟环境有问题") + print("建议运行: python manage_venv.py recreate") + elif command == "recreate": + if recreate_venv(): + print("✅ 虚拟环境重新创建成功") + else: + print("❌ 重新创建虚拟环境失败") + elif command == "info": + show_venv_info() + else: + print(f"未知命令: {command}") + +if __name__ == "__main__": + main() +EOF + +chmod +x "$APP_DIR/manage_venv.py" + +# 17. 创建 README 文件 +echo "17. 创建 README 文件..." +cat > "$APP_DIR/README.md" << EOF +# 配置编辑器 ($APP_NAME) + +版本: $APP_VERSION + +## 概述 +配置编辑器是一个功能强大的工具,用于编辑和管理各种配置文件。 + +## 虚拟环境 +此应用程序在独立的虚拟环境中运行,以避免与其他 Python 应用程序的依赖冲突。 + +虚拟环境位置: $VENV_PATH + +## 使用方法 + +### 从菜单启动 +1. 在应用程序菜单中搜索 "Config Editor" +2. 点击图标启动 + +### 从终端启动 +\`\`\`bash +$APP_NAME +\`\`\` + +### 手动激活虚拟环境 +\`\`\`bash +source $VENV_PATH/bin/activate +cd $APP_INSTALL_PATH +python main.py +\`\`\` + +## 管理虚拟环境 + +### 检查虚拟环境状态 +\`\`\`bash +cd $APP_INSTALL_PATH +python manage_venv.py check +\`\`\` + +### 重新创建虚拟环境 +\`\`\`bash +cd $APP_INSTALL_PATH +python manage_venv.py recreate +\`\`\` + +或使用脚本: +\`\`\`bash +cd $APP_INSTALL_PATH +./init_venv.sh +\`\`\` + +### 查看虚拟环境信息 +\`\`\`bash +cd $APP_INSTALL_PATH +python manage_venv.py info +\`\`\` + +## 配置文件示例 +配置文件示例位于: /var/lib/$APP_NAME/config_example.py + +## 故障排除 + +### 1. 虚拟环境问题 +如果应用程序无法启动,可能是虚拟环境有问题: +\`\`\`bash +cd $APP_INSTALL_PATH +./init_venv.sh +\`\`\` + +### 2. 权限问题 +如果无法保存设置,确保以下目录可写: +- $APP_INSTALL_PATH/config_editor_rules.json +- ~/.config_editor/ +- ~/.config/config-editor/ + +### 3. 依赖问题 +如果 PyQt6 有问题,重新安装: +\`\`\`bash +source $VENV_PATH/bin/activate +pip install --force-reinstall PyQt6 +\`\`\` + +## 支持 +如有问题,请联系: $MAINTAINER +EOF + +# 18. 计算安装大小 +echo "18. 计算安装大小..." +INSTALLED_SIZE=$(du -sk "$BUILD_ROOT" | cut -f1) +sed -i "/^Architecture:/a Installed-Size: $INSTALLED_SIZE" "$DEBIAN_DIR/control" + +# 19. 构建 deb 包 +echo "19. 构建 deb 包..." +PACKAGE_NAME="${APP_NAME}_${APP_VERSION}_${ARCHITECTURE}.deb" +dpkg-deb --build "$BUILD_ROOT" "$PACKAGE_NAME" + +# 20. 验证 deb 包 +echo "20. 验证 deb 包..." +if [ -f "$PACKAGE_NAME" ]; then + echo "✅ 构建成功: $PACKAGE_NAME" + echo "文件大小: $(du -h "$PACKAGE_NAME" | cut -f1)" + + # 显示包信息 + echo -e "\n包内容:" + dpkg -c "$PACKAGE_NAME" | head -30 + + echo -e "\n包信息:" + dpkg -I "$PACKAGE_NAME" + + # 清理构建目录 + rm -rf "$BUILD_ROOT" + + echo -e "\n========================================" + echo "✅ 构建完成!" + echo "生成的包: $PACKAGE_NAME" + echo "安装命令: sudo dpkg -i $PACKAGE_NAME" + echo "如果有依赖问题,运行: sudo apt-get install -f" + echo "" + echo "注意: 此版本使用虚拟环境" + echo "虚拟环境将在安装时自动创建" + echo "========================================" +else + echo "❌ 构建失败!" + exit 1 +fi + diff --git a/config_parser.py b/config_parser.py new file mode 100644 index 0000000..5273c99 --- /dev/null +++ b/config_parser.py @@ -0,0 +1,179 @@ +'''配置文件解析模块''' + +import ast +import json +from typing import Dict, Tuple, List, Any +from models import ConfigField +from utils import get_field_type_from_value + +class ConfigParser: + """配置文件解析器""" + + def __init__(self, rules: Dict): + self.rules = rules + + def parse_config_file(self, config_file_path: str) -> Tuple[Dict, Dict[str, ConfigField], str]: + """解析配置文件,精确匹配每个配置项的位置和注释""" + try: + with open(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 + + # 创建ConfigField实例 + config_field = self._create_config_field( + var_name, var_value, assignment_line, + full_block, comment_lines + ) + all_config_fields[var_name] = config_field + + except: + # 如果无法解析值,使用字符串表示 + try: + value_str = ast.get_source_segment(content, node.value) + config_data[var_name] = value_str + + config_field = self._create_config_field( + var_name, value_str, assignment_line, + full_block, comment_lines, is_string=True + ) + all_config_fields[var_name] = config_field + + except: + config_data[var_name] = "无法解析的值" + + config_field = self._create_config_field( + var_name, "无法解析的值", assignment_line, + full_block, comment_lines, is_string=True + ) + all_config_fields[var_name] = config_field + + return config_data, all_config_fields, content + + except Exception as e: + raise Exception(f"解析配置文件失败:{str(e)}") + + def _create_config_field(self, var_name: str, var_value: Any, assignment_line: int, + full_block: List[str], comment_lines: List[str], + is_string: bool = False) -> ConfigField: + """创建ConfigField实例""" + # 获取字段类型 + field_type = self._get_field_type(var_name, var_value) + + # 获取小数位数 + decimals = self._get_field_decimals(var_name) if field_type == "float" else None + + # 获取上次保存的值 + last_saved_value = self._get_last_saved_value(var_name, var_value) + + return 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), + encrypted=self._is_encrypted(var_name), # 加密状态 + line_number=assignment_line - len(comment_lines), + original_lines=full_block, + validation=self._get_validation(var_name), + last_saved_value=last_saved_value + ) + + def _categorize_field(self, field_name: str) -> str: + """为字段分类""" + for category, fields in self.rules.get("categories", {}).items(): + if field_name in fields: + return category + return "未分类" + + def _get_display_name(self, field_name: str) -> str: + """获取字段的显示名称""" + return self.rules.get("display_names", {}).get(field_name, field_name) + + def _get_tooltip(self, field_name: str) -> str: + """获取字段的提示信息""" + return self.rules.get("tooltips", {}).get(field_name, f"配置项: {field_name}") + + def _get_field_type(self, field_name: str, value: Any) -> str: + """获取字段类型""" + if field_name in self.rules.get("field_types", {}): + return self.rules["field_types"][field_name] + + return get_field_type_from_value(value) + + def _get_field_decimals(self, field_name: str) -> int: + """获取字段的小数位数""" + return self.rules.get("field_decimals", {}).get(field_name, 6) + + def _get_validation(self, field_name: str) -> Dict: + """获取配置项的校验规则""" + return self.rules.get("validations", {}).get(field_name, {}) + + def _is_hidden(self, field_name: str) -> bool: + """检查配置项是否被标记为隐藏""" + return field_name in self.rules.get("hidden", []) + + def _is_encrypted(self, field_name: str) -> bool: + """检查配置项是否被标记为加密""" + return field_name in self.rules.get("encrypted_fields", []) + + def _get_last_saved_value(self, field_name: str, current_value: Any) -> Any: + """获取配置项的上次保存值""" + last_saved_values = self.rules.get("last_saved_values", {}) + if field_name in last_saved_values: + return last_saved_values[field_name] + return None \ No newline at end of file diff --git a/dialogs.py b/dialogs.py new file mode 100644 index 0000000..270430c --- /dev/null +++ b/dialogs.py @@ -0,0 +1,1036 @@ +'''对话交互模块''' +import os +import json +import datetime +from pathlib import Path +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QCheckBox, QPushButton, QTextEdit, + QTableWidget, QTableWidgetItem, QHeaderView, + QDialogButtonBox, QGroupBox, QSplitter, QListWidget, + QListWidgetItem, QComboBox, QInputDialog, QFileDialog, + QMessageBox, QSpinBox, QDoubleSpinBox, QFrame,QWidget) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QBrush, QColor, QFont + +from utils import get_validation_tooltip, ensure_rule_structure + +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_type_combo = QComboBox() + self.search_type_combo.addItems(["变量名", "显示名称"]) + self.search_type_combo.setMaximumWidth(80) + search_layout.addWidget(self.search_type_combo) + + 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) + + # 修改:添加分组显示说明 + list_label = QLabel("配置项列表(上方为未隐藏项,下方为隐藏项):") + list_label.setStyleSheet("color: #333333; font-weight: bold;") + left_layout.addWidget(list_label) + + 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("隐藏此配置项(不在主界面显示)") + self.hidden_checkbox.stateChanged.connect(self.on_hidden_changed) + form_layout.addWidget(self.hidden_checkbox) + + # 加密配置项选项 + self.encrypted_checkbox = QCheckBox("加密配置项(修改时需要验证二级密码)") + self.encrypted_checkbox.stateChanged.connect(self.on_encrypted_changed) + form_layout.addWidget(self.encrypted_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) + + # 批量操作 + batch_group = QGroupBox("批量操作") + batch_layout = QVBoxLayout(batch_group) + batch_layout.setContentsMargins(8, 8, 8, 8) + + batch_btn_layout = QHBoxLayout() + self.show_all_btn = QPushButton("全部显示") + self.show_all_btn.clicked.connect(self.show_all_fields) + self.hide_all_btn = QPushButton("全部隐藏") + self.hide_all_btn.clicked.connect(self.hide_all_fields) + + batch_btn_layout.addWidget(self.show_all_btn) + batch_btn_layout.addWidget(self.hide_all_btn) + batch_btn_layout.addStretch() + + batch_layout.addLayout(batch_btn_layout) + + # 导入导出功能 + import_export_group = QGroupBox("导入导出规则文件") + import_export_layout = QVBoxLayout(import_export_group) + import_export_layout.setContentsMargins(8, 8, 8, 8) + + import_export_btn_layout = QHBoxLayout() + self.import_rules_btn = QPushButton("导入规则文件") + self.import_rules_btn.clicked.connect(self.import_rules_file) + self.export_rules_btn = QPushButton("导出规则文件") + self.export_rules_btn.clicked.connect(self.export_rules_file) + + import_export_btn_layout.addWidget(self.import_rules_btn) + import_export_btn_layout.addWidget(self.export_rules_btn) + import_export_btn_layout.addStretch() + + import_export_layout.addLayout(import_export_btn_layout) + right_layout.addWidget(import_export_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() + + # 获取隐藏字段列表 + hidden_fields = self.rules.get("hidden", []) + + # 获取加密字段列表 + encrypted_fields = self.rules.get("encrypted_fields", []) + + # 分离未隐藏和已隐藏的字段 + visible_fields = [] + hidden_fields_list = [] + + for field_name in sorted(self.config_fields.keys()): + if field_name in hidden_fields: + hidden_fields_list.append(field_name) + else: + visible_fields.append(field_name) + + # 添加分隔项 + if visible_fields and hidden_fields_list: + # 添加分隔符(未隐藏部分) + self.create_separator_item("未隐藏的配置项") + + # 添加未隐藏的字段(高亮显示) + for field_name in visible_fields: + self.create_field_item(field_name, is_hidden=False, is_encrypted=(field_name in encrypted_fields)) + + # 添加分隔项 + if visible_fields and hidden_fields_list: + # 添加分隔符(隐藏部分) + self.create_separator_item("已隐藏的配置项") + + # 添加已隐藏的字段(灰色显示) + for field_name in hidden_fields_list: + self.create_field_item(field_name, is_hidden=True, is_encrypted=(field_name in encrypted_fields)) + + # 如果没有配置项,显示提示 + if not visible_fields and not hidden_fields_list: + item = QListWidgetItem("没有配置项") + self.fields_list.addItem(item) + item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 + item.setForeground(QBrush(QColor("#999999"))) + + self.update_fields_count() + + # 加载分组 + self.category_combo.clear() + categories = list(self.rules.get("categories", {}).keys()) + # 确保"未分类"分组始终存在 + if "未分类" not in categories: + categories.append("未分类") + if categories: + for category in sorted(categories): + self.category_combo.addItem(category) + + def create_separator_item(self, text): + """创建分隔项""" + item = QListWidgetItem(f"--- {text} ---") + self.fields_list.addItem(item) + item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 + item.setForeground(QBrush(QColor("#666666"))) + font = item.font() + font.setItalic(True) + font.setBold(True) + item.setFont(font) + + # 设置背景色 + item.setBackground(QBrush(QColor("#f0f0f0"))) + return item + + def create_field_item(self, field_name, is_hidden=False, is_encrypted=False): + """创建字段项""" + item = QListWidgetItem(field_name) + self.fields_list.addItem(item) + + # 获取显示名称用于搜索 + display_name = self.rules.get("display_names", {}).get(field_name, field_name) + + # 设置自定义数据:变量名和显示名称 + item.setData(Qt.ItemDataRole.UserRole, field_name) # 变量名 + item.setData(Qt.ItemDataRole.UserRole + 1, display_name) # 显示名称 + + # 设置显示文本(包含加密标识) + display_text = field_name + if is_encrypted: + display_text += " 🔒" + + item.setText(display_text) + + if is_hidden: + # 隐藏字段:灰色,斜体 + item.setForeground(QBrush(QColor("#999999"))) + font = item.font() + font.setItalic(True) + item.setFont(font) + item.setToolTip(f"已隐藏: {field_name} ({display_name})" + (" [加密]" if is_encrypted else "")) + else: + # 未隐藏字段:高亮显示 + if is_encrypted: + # 加密字段:橙色高亮 + item.setForeground(QBrush(QColor("#ff5722"))) # 橙色 + font = item.font() + font.setBold(True) + item.setFont(font) + else: + # 普通字段:默认颜色 + item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 + font = item.font() + font.setBold(True) + item.setFont(font) + item.setToolTip(f"未隐藏: {field_name} ({display_name})" + (" [加密]" if is_encrypted else "")) + + return item + + def filter_fields(self, search_text): + """根据搜索文本过滤配置项列表(保持分组),支持变量名和显示名称搜索""" + search_text = search_text.strip().lower() + search_type = self.search_type_combo.currentText() # 获取搜索类型 + + if not search_text: + # 如果搜索文本为空,显示所有项 + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + # 跳过分隔符项 + if not self.is_separator_item(item): + item.setHidden(False) + else: + # 模糊匹配:根据搜索类型进行匹配 + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + # 跳过分隔符项 + if not self.is_separator_item(item): + # 根据搜索类型获取要匹配的文本 + if search_type == "变量名": + # 搜索变量名 + field_name = item.data(Qt.ItemDataRole.UserRole) + match_text = field_name.lower() if field_name else "" + else: # 显示名称 + # 搜索显示名称 + display_name = item.data(Qt.ItemDataRole.UserRole + 1) + match_text = display_name.lower() if display_name else "" + + # 模糊匹配:检查搜索文本是否在匹配文本中 + if search_text in match_text: + item.setHidden(False) + else: + item.setHidden(True) + + self.update_fields_count() + + def is_separator_item(self, item): + """检查是否是分隔符项""" + text = item.text() + return text.startswith("---") and text.endswith("---") + + def clear_search(self): + """清除搜索框内容""" + self.search_edit.clear() + + def update_fields_count(self): + """更新配置项计数显示""" + total_count = 0 + visible_count = 0 + hidden_count = 0 + encrypted_count = 0 + + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + # 跳过分隔符项 + if not self.is_separator_item(item): + total_count += 1 + if not item.isHidden(): + visible_count += 1 + + # 统计隐藏状态 + if "已隐藏" in item.toolTip(): + hidden_count += 1 + + # 统计加密状态 + if "加密" in item.toolTip(): + encrypted_count += 1 + + visible_unhidden = visible_count - hidden_count + visible_hidden = hidden_count + + # 显示搜索类型信息 + search_type = self.search_type_combo.currentText() + self.fields_count_label.setText(f"总计: {total_count} 个配置项 (显示: {visible_unhidden} 未隐藏, {visible_hidden} 隐藏, {encrypted_count} 加密) - 搜索类型: {search_type}") + + 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_hidden_changed(self, state): + """当隐藏状态改变时,更新左侧列表项的外观和位置""" + current_item = self.fields_list.currentItem() + if not current_item or self.is_separator_item(current_item): + return + + field_name = current_item.data(Qt.ItemDataRole.UserRole) + display_name = current_item.data(Qt.ItemDataRole.UserRole + 1) + is_hidden = (state == Qt.CheckState.Checked.value) + + # 更新工具提示 + if is_hidden: + current_item.setToolTip(f"已隐藏: {field_name} ({display_name})") + else: + current_item.setToolTip(f"未隐藏: {field_name} ({display_name})") + + # 更新外观 + if is_hidden: + # 改为隐藏样式 + current_item.setForeground(QBrush(QColor("#999999"))) + font = current_item.font() + font.setItalic(True) + font.setBold(False) + current_item.setFont(font) + current_item.setBackground(QBrush()) # 清除背景色 + else: + # 改为未隐藏样式 + current_item.setForeground(QBrush()) # 恢复默认前景色 + font = current_item.font() + font.setItalic(False) + font.setBold(True) + current_item.setFont(font) + current_item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 + + def on_encrypted_changed(self, state): + """当加密状态改变时,更新左侧列表项的外观""" + current_item = self.fields_list.currentItem() + if not current_item or self.is_separator_item(current_item): + return + + field_name = current_item.data(Qt.ItemDataRole.UserRole) + display_name = current_item.data(Qt.ItemDataRole.UserRole + 1) + is_encrypted = (state == Qt.CheckState.Checked.value) + + # 更新工具提示 + current_tooltip = current_item.toolTip() + if "加密" not in current_tooltip and is_encrypted: + current_item.setToolTip(current_tooltip + " [加密]") + elif "加密" in current_tooltip and not is_encrypted: + current_item.setToolTip(current_tooltip.replace(" [加密]", "")) + + # 更新显示文本 + current_text = current_item.text() + if "🔒" not in current_text and is_encrypted: + current_item.setText(current_text + " 🔒") + elif "🔒" in current_text and not is_encrypted: + current_item.setText(current_text.replace(" 🔒", "")) + + # 更新外观 + if is_encrypted: + # 加密字段:橙色高亮 + current_item.setForeground(QBrush(QColor("#ff5722"))) # 橙色 + font = current_item.font() + font.setBold(True) + current_item.setFont(font) + else: + # 恢复默认外观 + current_item.setForeground(QBrush()) # 恢复默认前景色 + font = current_item.font() + font.setBold(True) + current_item.setFont(font) + + def on_field_selected(self, current, previous): + # 保存前一个字段的更改 + if previous is not None and not self.is_separator_item(previous): + previous_field_name = previous.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(previous_field_name) + + if not current or self.is_separator_item(current): + # 清空右侧面板 + self.name_label.setText("") + self.display_name_edit.clear() + self.category_combo.setCurrentText("") + self.type_combo.setCurrentText("auto") + self.decimal_spinbox.setValue(2) + self.hidden_checkbox.setChecked(False) + self.encrypted_checkbox.setChecked(False) + self.tooltip_edit.clear() + self.min_edit.clear() + self.max_edit.clear() + self.regex_edit.clear() + self.required_checkbox.setChecked(False) + return + + field_name = current.data(Qt.ItemDataRole.UserRole) + display_name = current.data(Qt.ItemDataRole.UserRole + 1) + + self.name_label.setText(field_name) + + # 从规则中获取显示名称,如果规则中没有则使用当前存储的显示名称 + rule_display_name = self.rules.get("display_names", {}).get(field_name, display_name) + self.display_name_edit.setText(rule_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) + + # 加密状态 + encrypted = field_name in self.rules.get("encrypted_fields", []) + self.encrypted_checkbox.setChecked(encrypted) + + 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 == "未分类": + # 从所有分组中移除该字段 + for cat, fields in self.rules.setdefault("categories", {}).items(): + if cat != "未分类" and field_name in fields: + fields.remove(field_name) + # 确保在"未分类"分组中 + if "未分类" not in self.rules["categories"]: + self.rules["categories"]["未分类"] = [] + if field_name not in self.rules["categories"]["未分类"]: + self.rules["categories"]["未分类"].append(field_name) + elif category: # 其他分组 + # 从所有分组中移除该字段 + for cat, fields in self.rules.setdefault("categories", {}).items(): + if field_name in fields: + fields.remove(field_name) + + # 确保"未分类"分组存在 + if "未分类" not in self.rules["categories"]: + self.rules["categories"]["未分类"] = [] + + # 添加到新分组 + 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) + + # 更新加密状态 + encrypted = self.encrypted_checkbox.isChecked() + encrypted_list = self.rules.setdefault("encrypted_fields", []) + if encrypted and field_name not in encrypted_list: + encrypted_list.append(field_name) + elif not encrypted and field_name in encrypted_list: + encrypted_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 and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) + 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}' 吗?\n该分组中的所有配置项将被移到'未分类'分组。") + if reply == QMessageBox.StandardButton.Yes: + if current_group in self.rules.get("categories", {}): + # 将该分组中的配置项移到"未分类"分组 + fields_to_move = self.rules["categories"][current_group] + if "未分类" not in self.rules["categories"]: + self.rules["categories"]["未分类"] = [] + + for field_name in fields_to_move: + if field_name not in self.rules["categories"]["未分类"]: + self.rules["categories"]["未分类"].append(field_name) + + # 删除分组 + del self.rules["categories"][current_group] + + # 从下拉框中移除 + index = self.category_combo.findText(current_group) + if index >= 0: + self.category_combo.removeItem(index) + # 设置当前选中的分组为"未分类" + self.category_combo.setCurrentText("未分类") + + def show_all_fields(self): + """批量显示所有配置项(取消所有隐藏)""" + reply = QMessageBox.question(self, "确认", + "确定要取消所有配置项的隐藏状态吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.Yes: + # 清空隐藏列表 + self.rules["hidden"] = [] + + # 重新加载数据以更新显示 + self.load_data() + + QMessageBox.information(self, "完成", "已取消所有配置项的隐藏状态") + + def hide_all_fields(self): + """批量隐藏所有配置项""" + reply = QMessageBox.question(self, "确认", + "确定要隐藏所有配置项吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.Yes: + # 获取所有配置项的名称 + all_fields = list(self.config_fields.keys()) + + # 将所有配置项添加到隐藏列表 + self.rules["hidden"] = all_fields + + # 重新加载数据以更新显示 + self.load_data() + + QMessageBox.information(self, "完成", "已隐藏所有配置项") + + # 导入规则文件功能 + def import_rules_file(self): + """导入规则文件""" + # 确认保存当前更改 + current_item = self.fields_list.currentItem() + if current_item and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(field_name) + + # 选择要导入的规则文件 + 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.load_data() + + QMessageBox.information( + self, "导入成功", + f"规则文件导入成功!\n\n" + f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" + f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" + f"已导入 {len(self.rules.get('hidden', []))} 个隐藏设置" + ) + + except json.JSONDecodeError: + QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") + except Exception as e: + QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") + + # 导出规则文件功能 + def export_rules_file(self): + """导出规则文件""" + # 确认保存当前更改 + current_item = self.fields_list.currentItem() + if current_item and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(field_name) + + # 选择导出位置和文件名 + 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: + # 准备导出的规则数据 + export_data = self.rules.copy() + + # 添加导出信息 + export_data["_export_info"] = { + "export_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "export_version": "1.0", + "note": "配置文件编辑器规则文件" + } + + # 添加统计信息 + export_data["_statistics"] = { + "total_fields": len(self.all_fields), + "display_names_count": len(self.rules.get("display_names", {})), + "categories_count": len(self.rules.get("categories", {})), + "hidden_fields_count": len(self.rules.get("hidden", [])), + "encrypted_fields_count": len(self.rules.get("encrypted_fields", [])), + "field_types_count": len(self.rules.get("field_types", {})), + "validations_count": len(self.rules.get("validations", {})) + } + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False) + + QMessageBox.information( + self, "导出成功", + f"规则文件已成功导出到:\n{file_path}\n\n" + f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + f"规则数量:{len(export_data) - 2}" # 减去_export_info和_statistics + ) + + except Exception as e: + QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(e)}") + + def get_updated_rules(self): + """获取更新后的规则""" + return self.rules + +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) \ No newline at end of file diff --git a/file_handler.py b/file_handler.py new file mode 100644 index 0000000..50f0564 --- /dev/null +++ b/file_handler.py @@ -0,0 +1,117 @@ +'''文件读写管理模块''' +import os +import json +import shutil +from pathlib import Path +from typing import Dict, Any +from utils import (backup_existing_rules, merge_rules, + get_default_rules, ensure_rule_structure) + +class FileHandler: + """文件处理器""" + + def __init__(self, program_dir: str): + self.program_dir = program_dir + self.rules_file = str(Path(program_dir) / "config_editor_rules.json") + self.settings_file = str(Path(program_dir) / "config_editor_settings.json") + + def load_user_settings(self) -> Dict: + """加载用户设置""" + 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, settings: Dict) -> bool: + """保存用户设置""" + try: + with open(self.settings_file, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + print(f"保存用户设置失败: {e}") + return False + + def load_rules(self) -> Dict: + """加载规则文件""" + try: + # 先备份现有规则文件 + backup_existing_rules(self.rules_file, self.program_dir) + + # 检查是否已有规则文件 + if os.path.exists(self.rules_file): + with open(self.rules_file, 'r', encoding='utf-8') as f: + existing_rules = json.load(f) + + # 检查规则文件是否为空或格式错误 + if not existing_rules or not isinstance(existing_rules, dict): + raise ValueError("规则文件为空或格式错误") + + # 从打包资源加载默认规则(用于合并新字段) + default_rules = get_default_rules() + + # 智能合并:保留现有规则,只添加新字段 + rules = merge_rules(existing_rules, default_rules) + + # 确保规则结构完整 + rules = ensure_rule_structure(rules) + + # 保存合并后的规则(包含新字段) + self.save_rules(rules) + + return rules + else: + # 如果没有规则文件,使用默认规则 + rules = get_default_rules() + # 确保规则结构完整 + rules = ensure_rule_structure(rules) + # 首次运行,创建规则文件 + try: + self.save_rules(rules) + except Exception as e: + print(f"创建规则文件失败: {e}") + return rules + + except (json.JSONDecodeError, ValueError, KeyError) as e: + # 如果规则文件损坏,备份后使用默认规则 + print(f"加载规则文件失败: {e},将使用默认规则") + self._backup_corrupted_rules() + rules = get_default_rules() + rules = ensure_rule_structure(rules) + self.save_rules(rules) + return rules + + except Exception as e: + print(f"加载规则文件失败: {e}") + rules = get_default_rules() + rules = ensure_rule_structure(rules) + return rules + + def save_rules(self, rules: Dict) -> bool: + """保存规则文件""" + try: + # 确保规则结构完整 + rules = ensure_rule_structure(rules) + + with open(self.rules_file, 'w', encoding='utf-8') as f: + json.dump(rules, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + print(f"保存规则文件失败: {e}") + return False + + def _backup_corrupted_rules(self): + """备份损坏的规则文件""" + if os.path.exists(self.rules_file): + import datetime + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = f"{self.rules_file}.corrupted_{timestamp}.bak" + try: + shutil.copy2(self.rules_file, backup_file) + print(f"已备份损坏的规则文件到: {backup_file}") + except Exception as e: + print(f"备份损坏规则文件失败: {e}") \ No newline at end of file