Compare commits

..

17 Commits
1.1 ... main

Author SHA1 Message Date
92cbb8a99e 更新 README.md 2026-01-19 08:13:20 +00:00
2ea550f798 上传文件至 / 2026-01-19 08:10:46 +00:00
2c9087ccf9 上传文件至 / 2026-01-19 08:02:47 +00:00
ab5321a93c 上传文件至 / 2026-01-19 08:02:27 +00:00
fb5f83ff7c 上传文件至 / 2026-01-19 08:01:58 +00:00
93309fb762 上传文件至 / 2026-01-19 08:01:13 +00:00
e51b895bc8 删除 README.md 2026-01-19 08:00:31 +00:00
5e0128fde0 删除 config_editor.py 2026-01-19 08:00:20 +00:00
fd071786f1 删除 config_editor_settings.json 2026-01-19 08:00:12 +00:00
916f08d12f 删除 config_editor_rules.json 2026-01-19 08:00:07 +00:00
271a895419 删除 build.sh 2026-01-19 08:00:00 +00:00
95b27a2d60 更新 config_editor.py 2025-12-31 09:23:12 +00:00
a3278464cd 更新 build.sh 2025-12-31 09:22:08 +00:00
d880ae5cd4 更新 build.sh 2025-12-31 09:21:10 +00:00
8dcd19c97d 更新 README.md 2025-12-23 05:55:26 +00:00
d385796db4 更新 README.md 2025-12-22 10:01:41 +00:00
5b5800d842 更新 README.md 2025-12-22 09:29:18 +00:00
17 changed files with 5362 additions and 2270 deletions

401
README.md
View File

@ -1,208 +1,291 @@
Config Editor - 配置文件编辑工具
一个基于PyQt6开发的Python配置文件可视化编辑工具支持自动解析Python配置文件中的大写变量提供直观的GUI界面进行编辑和管理。
ce编辑器 (Config Editor)
✨ 特性功能
🎯 核心功能
智能解析自动扫描Python文件识别文件中的变量配置项
一个功能强大的Python配置文件和用户管理工具提供直观的GUI界面进行配置管理、用户认证和会话安全控制。
可视化编辑基于PyQt6的现代化GUI界面支持多种数据类型编辑
主要特性
分组管理:可自定义分组,按类别组织配置项
🔐 用户认证系统
格式保持:保存时保持原始文件格式、注释和缩进
首次登录即注册:新用户首次登录自动创建账户
🔧 编辑支持
多种数据类型:
二级密码保护:敏感操作需要二级密码验证
布尔值CheckBox
会话超时锁定空闲超过10分钟自动锁定需要重新登录
整数/浮点数SpinBox
安全的密码存储使用JSON格式加密存储用户凭证
字符串LineEdit/TextEdit
📁 配置文件管理
JSON/字典/列表(格式化编辑)
智能解析自动识别Python配置文件中的配置项
校验规则:
格式保持:编辑时保留原始注释、空格和格式
必填项验证
分组显示:按规则将配置项分组显示在不同标签页
数值范围限制
拖拽排序:支持通过拖拽重新排列配置项顺序
正则表达式匹配
⚙️ 高级功能
自定义错误提示
规则管理:可自定义显示名称、分组、校验规则等
📊 管理功能
显示定制:自定义每个配置项的显示名称和提示信息
加密字段:标记敏感配置项,修改时需要二级密码
字段隐藏:标记隐藏字段,支持显示/隐藏切换
隐藏字段:隐藏不需要显示的配置项
类型推断:自动识别字段类型,支持手动覆盖
导入导出:支持规则文件的导入导出
规则管理:独立的规则管理界面,支持批量配置
校验规则:支持必填、范围、正则表达式等校验
🚀 快速开始
环境要求
Python 3.8+
🎨 用户界面
PyQt6
现代化UI基于PyQt6的响应式界面
构建打包
执行build.sh脚本
直观操作:拖拽排序、分组管理、批量操作
实时反馈:修改高亮、校验提示、状态监控
主题支持标准的Fusion主题视觉舒适
系统要求
操作系统Linux已测试Ubuntu 20.04+
Python版本Python 3.8+
依赖PyQt6 >= 6.5.0
安装方法
使用deb包安装
确保已安装依赖:
安装部署
bash
1.sudo dpkg -i config-editor_1.0_amd64.deb
2.sudo /usr/share/config-editor/setup_venv.sh
在服务器图形化界面搜索Config Editor点击即可使用
卸载程序
sudo apt-get update
sudo apt-get install python3 python3-venv python3-pip
安装deb包
bash
1.sudo dpkg -r config-editor
📁 项目结构
sudo dpkg -i config-editor_1.3_amd64.deb
使用指南
首次运行设置
启动程序:首次运行会显示登录界面
用户注册:
输入用户名和密码(首次登录自动创建账户)
选择账号配置文件保存目录
设置二级密码用于敏感操作验证
配置文件设置:
登录后首次运行需要设置要编辑的配置文件路径
支持浏览选择或手动输入文件路径
可以使用相对路径(相对于程序目录)
主要功能使用
1. 配置文件编辑
打开文件通过菜单或工具栏打开Python配置文件
编辑配置:直接在界面上修改配置项值
保存更改:保存时显示变更列表确认,保留原始格式
重新加载:随时重新加载配置文件
2. 规则管理
进入管理界面:点击"管理规则"按钮
配置项管理:设置显示名称、分组、类型、校验规则等
隐藏/显示:标记配置项为隐藏,或批量显示/隐藏
分组管理:创建、删除和重命名分组
3. 高级操作
拖拽排序:在分组内拖拽配置项标签重新排序
加密字段:标记敏感字段,修改时需要二级密码
批量操作:一键隐藏所有配置项,或全部显示
导入导出:导入/导出规则文件,方便备份和迁移
安全特性
会话管理
程序监控用户活动空闲10分钟后自动锁定
锁定后需要重新登录才能继续操作
取消重新登录会退出程序以保障安全
二级密码验证
以下操作需要验证二级密码:
修改标记为加密的配置项
打开配置文件
配置文件设置
管理规则
显示隐藏项
一键隐藏所有项
项目结构
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/
├── main.py # 程序入口
├── main_window.py # 主窗口实现
├── login/ # 登录认证模块
│ ├── login.py # 用户认证管理器
│ └── login_dialog.py # 登录对话框
├── dialogs.py # 各种对话框
├── config_parser.py # 配置文件解析器
├── file_handler.py # 文件读写管理
├── models.py # 数据模型定义
├── utils.py # 工具函数集合
├── widgets.py # 自定义控件
├── build.sh # 打包脚本
├── requirements.txt # 依赖列表
└── README.md # 本文档
配置文件格式
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配置文件格式
python
# 配置文件示例 config.py
DB_HOST = "localhost" # 数据库地址
DB_PORT = 3306 # 数据库端口
DEBUG_MODE = True # 调试模式
MAX_CONNECTIONS = 100 # 最大连接数
要求:
'''
# 数据库配置
DATABASE_HOST = "localhost"
DATABASE_PORT = 3306
DATABASE_USER = "admin"
DATABASE_PASSWORD = "secret_password" # 可以标记为加密字段
配置变量名必须为大写字母和下划线组成
# 应用配置
APP_NAME = "My Application"
DEBUG_MODE = False
LOG_LEVEL = "INFO"
支持Python基本数据类型字符串、数字、布尔值、列表、字典
# API配置
API_CONFIG = {
"key": "your_api_key",
"timeout": 30,
"retries": 3
}
'''
🔍 技术实现
解析技术
使用Python的ast模块进行语法分析
故障排除
精确识别配置项的位置和注释
常见问题
保持原始格式和缩进
无法启动程序
GUI框架
基于PyQt6构建现代化界面
text
# 检查Python版本
python3 --version
响应式布局,支持调整窗口大小
# 检查PyQt6是否安装
python3 -c "import PyQt6"
虚拟环境问题deb安装
自定义控件适配不同数据类型
bash
# 重新创建虚拟环境
sudo /opt/config-editor/init_venv.sh
数据持久化
规则配置使用JSON格式存储
# 或使用管理工具
sudo python3 /opt/config-editor/manage_venv.py recreate
权限问题
支持相对路径和绝对路径
bash
# 确保用户对以下目录有读写权限
~/.config_editor/
~/.config/config-editor/
/opt/config-editor/config_editor_rules.json
配置文件无法保存
自动保存用户设置
检查配置文件路径是否正确
📝 使用示例
编辑配置文件
启动程序,选择配置文件
确保对配置文件有写权限
在对应的分组中找到要修改的配置项
检查磁盘空间是否充足
编辑值(复选框、数字框、文本框等)
调试模式
如需调试,可以查看以下日志位置:
点击"保存配置"按钮
控制台输出直接运行python3 main.py查看输出
确认变更后,程序会自动更新配置文件
用户设置:~/.config_editor/user_settings.json
自定义规则
点击"管理规则"按钮
账号文件用户选择的目录中的users.json
在左侧列表选择配置项
规则文件程序目录下的config_editor_rules.json
在右侧设置显示名称、分组、类型等
开发指南
环境搭建
克隆项目:
设置校验规则(可选)
bash
git clone <项目仓库>
cd config-editor
创建虚拟环境:
保存规则,程序会自动重新加载界面
bash
python3 -m venv venv
source venv/bin/activate
安装依赖:
bash
pip install -r requirements.txt
运行开发版本:
bash
python main.py
代码结构说明
main_window.py主程序逻辑包含UI生成、事件处理、会话管理等
login/:用户认证模块,处理登录、注册、密码验证
config_parser.py配置文件解析保持格式和注释
dialogs.py各种对话框的实现
models.py数据模型定义使用Python dataclass
utils.py工具函数如规则合并、校验、格式化等
打包说明
使用提供的build.sh脚本打包
bash
# 赋予执行权限
chmod +x build.sh
# 执行打包
./build.sh
打包脚本会:
清理之前的构建
创建deb包目录结构
复制项目文件
创建启动脚本和桌面入口
生成虚拟环境初始化脚本
构建deb包

10
__init__.py Normal file
View File

@ -0,0 +1,10 @@
"""
配置编辑器模块
"""
__version__ = "1.3"
__author__ = "Config Editor"
from main_window import ConfigEditor
__all__ = ['ConfigEditor']

906
build.sh
View File

@ -1,260 +1,724 @@
#!/bin/bash
# 打包脚本:将配置编辑器项目打包成 deb 包
# config editor 一键构建脚本 - 修复虚拟环境安装问题
set -e
set -e # 遇到错误时退出
# 配置变量
PACKAGE_NAME="config-editor"
VERSION="1.1"
# ==================== 配置区域 ====================
APP_NAME="config-editor"
APP_VERSION="1.3"
APP_DESCRIPTION="CE编辑器"
MAINTAINER="hjy <hujinyang@jspwkj.com.cn>"
ARCHITECTURE="amd64"
MAINTAINER="hjy <hjy@pw.com>"
DEPENDENCIES="python3, python3-venv, python3-pip"
PYTHON_VERSION="3.8"
VENV_PATH="/opt/$APP_NAME/venv"
APP_INSTALL_PATH="/opt/$APP_NAME"
# =================================================
echo "=========================================="
echo "Config Editor 构建脚本 - 版本 $VERSION"
echo "架构: $ARCHITECTURE"
echo "构建时间: $(date)"
echo "=========================================="
echo "========================================"
echo "开始构建 $APP_NAME (版本 $APP_VERSION)"
echo "========================================"
echo "将使用虚拟环境: $VENV_PATH"
# 检查依赖
echo "检查构建依赖..."
if ! command -v dpkg-deb &> /dev/null; then
echo "错误: 需要 dpkg-deb请安装: sudo apt install dpkg-dev"
# 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
# 检查源代码是否存在
echo "检查源代码..."
if [ ! -f "config_editor.py" ]; then
echo "错误: 未找到主程序文件 config_editor.py"
exit 1
fi
# 激活虚拟环境并运行程序
cd "\$APP_DIR"
source "\$VENV_PATH/bin/activate"
exec python main.py "\$@"
EOF
# 创建构建目录
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"
chmod +x "$BIN_DIR/$APP_NAME"
# 复制应用文件
echo "复制应用文件..."
cp config_editor.py "$BUILD_DIR/usr/share/config-editor/"
# 复制配置文件
if [ -f "config_editor_rules.json" ]; then
cp config_editor_rules.json "$BUILD_DIR/usr/share/config-editor/"
fi
if [ -f "config_editor_settings.json" ]; then
cp config_editor_settings.json "$BUILD_DIR/usr/share/config-editor/"
fi
# 创建桌面文件
echo "创建桌面文件..."
cat > "$BUILD_DIR/usr/share/applications/config-editor.desktop" << 'EOF'
# 5. 创建桌面入口文件
echo "5. 创建桌面入口文件..."
cat > "$DESKTOP_DIR/$APP_NAME.desktop" << EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=Config Editor
Comment=Configuration Management Tool
Exec=/usr/bin/config-editor
Icon=config-editor
Categories=Development;Utility;Settings;
Name[zh_CN]=配置编辑器
Comment=$APP_DESCRIPTION
Comment[zh_CN]=用于管理和编辑配置文件
Exec=$APP_NAME
Icon=$APP_NAME
Terminal=false
Type=Application
Categories=Development;Utility;
StartupNotify=true
StartupWMClass=ConfigEditor
Keywords=config;editor;settings;configuration;
MimeType=application/x-python;
EOF
# 创建虚拟环境安装脚本 - 修复版本
echo "创建虚拟环境安装脚本..."
cat > "$BUILD_DIR/usr/share/config-editor/setup_venv.sh" << 'EOF'
#!/bin/bash
# 虚拟环境安装脚本 - 修复sudo环境问题
# 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 "未找到图标文件,创建简单图标..."
set -e
# 使用 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"
APP_DIR="/usr/share/config-editor"
VENV_DIR="$APP_DIR/venv"
# 创建 64x64 图标
convert -size 64x64 xc:#2196F3 \
-fill white -pointsize 32 -gravity center -annotate 0 "CE" \
"$ICON_DIR/64x64/apps/$APP_NAME.png"
echo "设置Config Editor虚拟环境..."
# 检查是否使用sudo
if [ "$EUID" -ne 0 ]; then
echo "错误: 请使用sudo运行此脚本"
echo "命令: sudo $0"
exit 1
fi
# 清理旧的虚拟环境(如果存在)
if [ -d "$VENV_DIR" ]; then
echo "清理旧的虚拟环境..."
rm -rf "$VENV_DIR"
fi
# 创建虚拟环境
echo "创建虚拟环境..."
python3 -m venv "$VENV_DIR"
if [ $? -ne 0 ]; then
echo "错误: 无法创建虚拟环境"
echo "请确保已安装 python3-venv: sudo apt install python3-venv"
exit 1
fi
echo "✓ 虚拟环境创建成功"
# 使用虚拟环境中的pip安装PyQt6 - 关键修复:使用绝对路径
echo "安装PyQt6..."
if ! "$VENV_DIR/bin/python" -m pip install PyQt6 2>/dev/null; then
echo "尝试使用国内镜像安装..."
"$VENV_DIR/bin/python" -m pip install PyQt6 -i https://pypi.tuna.tsinghua.edu.cn/simple/
fi
# 验证安装
if ! "$VENV_DIR/bin/python" -c "import PyQt6" 2>/dev/null; then
echo "错误: PyQt6安装失败"
echo "请尝试手动安装:"
echo " sudo $VENV_DIR/bin/pip install PyQt6"
exit 1
fi
echo "✓ PyQt6安装成功"
# 设置权限
chmod -R 755 "$VENV_DIR"
echo ""
echo "虚拟环境设置完成!"
echo "虚拟环境位置: $VENV_DIR"
echo ""
echo "可以测试运行:"
echo " $VENV_DIR/bin/python $APP_DIR/config_editor.py"
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_DIR="${HOME}/.config/config-editor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/startup.log"
# 记录启动信息
{
echo "========================================"
echo "启动时间: $(date)"
echo "用户: $(whoami)"
echo "DISPLAY: $DISPLAY"
} >> "$LOG_FILE"
# 检查虚拟环境
if [ ! -d "$VENV_DIR" ]; then
echo "错误: 虚拟环境未找到。请先运行:"
echo " sudo /usr/share/config-editor/setup_venv.sh"
exit 1
fi
# 检查Python可执行文件
PYTHON_EXEC="$VENV_DIR/bin/python"
if [ ! -x "$PYTHON_EXEC" ]; then
PYTHON_EXEC="$VENV_DIR/bin/python3"
if [ ! -x "$PYTHON_EXEC" ]; then
echo "错误: 虚拟环境中找不到Python可执行文件"
exit 1
# 创建 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
# 检查PyQt6是否安装
if ! "$PYTHON_EXEC" -c "import PyQt6" 2>/dev/null; then
echo "错误: PyQt6未在虚拟环境中安装。请运行:"
echo " sudo /usr/share/config-editor/setup_venv.sh"
echo "如果仍然失败,请手动安装:"
echo " sudo $VENV_DIR/bin/pip install PyQt6"
exit 1
fi
# 切换到应用目录
cd "$APP_DIR"
# 启动应用
exec "$PYTHON_EXEC" config_editor.py "$@"
EOF
chmod +x "$BUILD_DIR/usr/bin/config-editor"
# 创建控制文件
echo "创建DEBIAN控制文件..."
cat > "$BUILD_DIR/DEBIAN/control" << EOF
Package: $PACKAGE_NAME
Version: $VERSION
# 7. 创建 DEBIAN 控制文件
echo "7. 创建 DEBIAN 控制文件..."
cat > "$DEBIAN_DIR/control" << EOF
Package: $APP_NAME
Version: $APP_VERSION
Section: utils
Priority: optional
Architecture: $ARCHITECTURE
Depends: python3, python3-venv
Depends: $DEPENDENCIES
Maintainer: $MAINTAINER
Description: Config Editor with one-click hide feature
A PyQt6-based configuration management editor.
Features include one-click hide all configuration items.
Description: $APP_DESCRIPTION
配置编辑器是一个功能强大的工具,用于编辑和管理各种配置文件。
支持以下功能:
* 智能解析配置文件
* 分组显示配置项
* 拖拽重新排序
* 加密配置项保护
* 会话超时锁定
* 用户认证系统
* 规则管理
* 在虚拟环境中运行,避免依赖冲突
Homepage: https://github.com/yourusername/config-editor
EOF
# 创建安装后脚本
echo "创建安装后脚本..."
cat > "$BUILD_DIR/DEBIAN/postinst" << 'EOF'
# 8. 创建安装后脚本(创建虚拟环境)
echo "8. 创建安装后脚本..."
cat > "$DEBIAN_DIR/postinst" << EOF
#!/bin/bash
# 安装后脚本
# 安装后脚本 - 创建虚拟环境并安装依赖
echo "Config Editor 安装完成!"
echo ""
echo "重要: 需要设置虚拟环境才能运行应用"
echo "请执行以下命令完成安装:"
echo " sudo /usr/share/config-editor/setup_venv.sh"
echo ""
echo "注意: 如果之前安装失败,新版本修复了虚拟环境安装问题"
echo " 请确保运行上述命令安装PyQt6到正确的虚拟环境中"
set -e
# 更新桌面数据库
update-desktop-database /usr/share/applications 2>/dev/null || true
if [ -d /usr/share/applications ]; then
update-desktop-database /usr/share/applications || true
fi
# 设置权限
chmod 755 /usr/bin/config-editor
chmod 755 /usr/share/config-editor/setup_venv.sh
# 更新图标缓存
if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true
fi
exit 0
EOF
# 设置文件权限
chmod 755 $APP_INSTALL_PATH/*.py
chmod 755 $APP_INSTALL_PATH/login/*.py
chmod 755 /usr/bin/$APP_NAME
chmod +x "$BUILD_DIR/DEBIAN/postinst"
# 创建虚拟环境目录
echo "创建虚拟环境目录..."
mkdir -p "$VENV_PATH"
# 构建deb包
echo "构建deb包..."
dpkg-deb --build "$BUILD_DIR" "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
# 验证包
if dpkg -I "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" > /dev/null; then
echo "✓ 包验证成功"
else
echo "✗ 包验证失败"
# 检查 Python3 是否可用
if ! command -v python3 &> /dev/null; then
echo "错误: Python3 未安装。请安装 python3。"
exit 1
fi
# 清理
rm -rf "$BUILD_DIR"
# 创建虚拟环境
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 "=========================================="
echo "构建完成!"
echo "生成的deb包: ${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
echo ""
echo "安装步骤:"
echo "1. 卸载旧版本: sudo dpkg -r config-editor"
echo "2. 安装新版本: sudo dpkg -i ${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
echo "3. 修复虚拟环境: sudo /usr/share/config-editor/setup_venv.sh"
echo ""
echo "重要: 新版本修复了虚拟环境安装问题"
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

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
{
"categories": {
"未分类": []
},
"display_names": {},
"tooltips": {},
"field_types": {},
"field_decimals": {},
"hidden": [],
"validations": {}
}

View File

@ -1,4 +0,0 @@
{
"config_file_path": "",
"last_used": ""
}

179
config_parser.py Normal file
View File

@ -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), '<string>', '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

1036
dialogs.py Normal file

File diff suppressed because it is too large Load Diff

117
file_handler.py Normal file
View File

@ -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}")

240
login.py Normal file
View File

@ -0,0 +1,240 @@
'''用户认证登录模块'''
import json
import os
import datetime
from pathlib import Path
from typing import Tuple, Optional
class SimpleAuth:
"""用户认证管理器"""
def __init__(self, config_path: str = None):
"""
初始化认证管理器
Args:
config_path: 用户配置文件路径
"""
self.config_path = config_path
self.users = {}
# 初始化时加载用户数据
if config_path:
self.load_users()
def load_users(self) -> bool:
"""加载用户数据"""
try:
if not self.config_path:
print("未设置配置文件路径")
return False
# 如果文件不存在,创建空文件
if not os.path.exists(self.config_path):
print(f"配置文件不存在,创建空文件: {self.config_path}")
return self.create_empty_file()
# 读取文件内容
with open(self.config_path, 'r', encoding='utf-8') as f:
content = f.read().strip()
if not content: # 空文件
self.users = {}
return True
# 解析JSON
self.users = json.loads(content)
print(f"已加载 {len(self.users)} 个用户")
return True
except json.JSONDecodeError:
print("配置文件格式错误,重置为空文件")
return self.create_empty_file()
except Exception as e:
print(f"加载用户数据失败: {e}")
return False
def create_empty_file(self) -> bool:
"""创建空的配置文件"""
try:
if not self.config_path:
return False
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
self.users = {}
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump({}, f, indent=2, ensure_ascii=False)
print(f"已创建空白配置文件: {self.config_path}")
return True
except Exception as e:
print(f"创建配置文件失败: {e}")
return False
def save_users(self) -> bool:
"""保存用户数据"""
try:
if not self.config_path:
return False
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(self.users, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存用户数据失败: {e}")
return False
def authenticate(self, username: str, password: str) -> Tuple[bool, str]:
"""
验证用户凭据
重要如果用户不存在则创建用户首次登录即注册
"""
if not username or not password:
return False, "用户名和密码不能为空"
# 加载用户数据
if not self.load_users():
return False, "加载用户数据失败"
# 检查用户名是否存在(作为字典键)
if username not in self.users:
# 首次登录,自动创建用户
print(f"用户 '{username}' 不存在,首次登录将自动创建用户")
return self.create_first_user(username, password)
# 检查密码是否正确
user_info = self.users[username]
if user_info.get("password") != password:
return False, "密码错误"
# 更新最后登录时间
user_info["last_login"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.save_users()
return True, "登录成功"
def create_first_user(self, username: str, password: str) -> Tuple[bool, str]:
"""创建第一个用户(首次登录)"""
try:
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 创建用户信息
self.users[username] = {
"username": username,
"password": password,
"secondary_password": "", # 二级密码初始为空,首次登录后设置
"created_at": current_time,
"last_login": current_time,
"is_first_user": True
}
# 保存用户数据
if self.save_users():
print(f"已创建用户 '{username}' 并登录")
return True, f"首次登录成功,已创建用户 '{username}'"
else:
return False, "创建用户失败"
except Exception as e:
print(f"创建用户失败: {e}")
return False, f"创建用户失败: {str(e)}"
def set_secondary_password(self, username: str, secondary_password: str) -> Tuple[bool, str]:
"""设置二级密码"""
try:
if not self.load_users():
return False, "加载用户数据失败"
if username not in self.users:
return False, "用户不存在"
if not secondary_password:
return False, "二级密码不能为空"
# 更新二级密码
self.users[username]["secondary_password"] = secondary_password
self.users[username]["secondary_password_updated_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if self.save_users():
return True, "二级密码设置成功"
else:
return False, "保存二级密码失败"
except Exception as e:
return False, f"设置二级密码失败: {str(e)}"
def verify_secondary_password(self, username: str, secondary_password: str) -> Tuple[bool, str]:
"""验证二级密码"""
try:
if not self.load_users():
return False, "加载用户数据失败"
if username not in self.users:
return False, "用户不存在"
user_info = self.users[username]
# 检查用户是否设置了二级密码
if not user_info.get("secondary_password"):
return False, "未设置二级密码,请先设置"
# 验证二级密码
if user_info.get("secondary_password") != secondary_password:
return False, "二级密码错误"
return True, "二级密码验证成功"
except Exception as e:
return False, f"验证二级密码失败: {str(e)}"
def get_secondary_password_status(self, username: str) -> Tuple[bool, str]:
"""获取二级密码设置状态"""
try:
if not self.load_users():
return False, "加载用户数据失败"
if username not in self.users:
return False, "用户不存在"
user_info = self.users[username]
if user_info.get("secondary_password"):
return True, f"已设置二级密码(更新于: {user_info.get('secondary_password_updated_at', '未知时间')}"
else:
return False, "未设置二级密码"
except Exception as e:
return False, f"获取二级密码状态失败: {str(e)}"
def get_default_config_path(self) -> str:
"""获取默认配置文件路径"""
return str(Path.home() / ".config_editor" / "users.json")
@staticmethod
def validate_config_path(config_path: str) -> bool:
"""验证配置文件路径是否有效"""
try:
# 检查路径是否包含有效的目录
dir_path = os.path.dirname(config_path)
if not dir_path:
return False
# 检查是否是绝对路径
if not os.path.isabs(config_path):
return False
# 检查文件扩展名(可选)
if not config_path.endswith(('.json', '.txt', '.dat')):
return False
return True
except Exception:
return False

783
login_dialog.py Normal file
View File

@ -0,0 +1,783 @@
'''登录对话模块'''
import os
import json
from pathlib import Path
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QMessageBox,
QFileDialog, QCheckBox, QSpacerItem,
QSizePolicy, QFrame)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from login.login import SimpleAuth
class LoginDialog(QDialog):
"""智能登录对话框 - 首次登录自动创建用户"""
login_success = pyqtSignal(str, str) # 登录成功信号username, config_file_path
def __init__(self, parent=None):
super().__init__(parent)
self.config_file_path = None
self.user_config_path = None
self.user_config_dir = None
self.DEFAULT_CONFIG_FILE = "users.json" # 硬编码的账密文件名
self.setup_ui()
# 检查是否第一次运行
self.check_first_run()
def setup_ui(self):
# 设置窗口标题和大小
self.setWindowTitle("登录")
self.setMinimumSize(600, 400)
self.resize(600, 400)
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setSpacing(20) # 增加间距
main_layout.setContentsMargins(40, 40, 40, 40) # 增加边距
# 用户名输入
username_layout = QVBoxLayout()
username_layout.setSpacing(8)
username_label = QLabel("用户名:")
username_label.setStyleSheet("""
QLabel {
font-weight: bold;
font-size: 14px;
color: #333333;
}
""")
self.username_edit = QLineEdit()
self.username_edit.setPlaceholderText("请输入用户名")
self.username_edit.setMinimumHeight(42)
self.username_edit.setMaximumHeight(42)
self.username_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 14px;
background-color: white;
}
QLineEdit:focus {
border: 2px solid #3498db;
background-color: #f8fdff;
}
""")
username_layout.addWidget(username_label)
username_layout.addWidget(self.username_edit)
main_layout.addLayout(username_layout)
# 添加间距
main_layout.addSpacing(15)
# 密码输入
password_layout = QVBoxLayout()
password_layout.setSpacing(8)
password_label = QLabel("密码:")
password_label.setStyleSheet("""
QLabel {
font-weight: bold;
font-size: 14px;
color: #333333;
}
""")
self.password_edit = QLineEdit()
self.password_edit.setPlaceholderText("请输入密码")
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setMinimumHeight(42)
self.password_edit.setMaximumHeight(42)
self.password_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 14px;
background-color: white;
}
QLineEdit:focus {
border: 2px solid #3498db;
background-color: #f8fdff;
}
""")
password_layout.addWidget(password_label)
password_layout.addWidget(self.password_edit)
main_layout.addLayout(password_layout)
# 添加弹性空间
main_layout.addSpacing(20)
# 配置文件路径设置
self.config_group = QVBoxLayout()
self.config_group.setSpacing(8)
self.config_path_label = QLabel("账号配置文件目录:")
self.config_path_label.setStyleSheet("""
QLabel {
font-weight: bold;
font-size: 14px;
color: #333333;
}
""")
# 路径输入和浏览按钮
path_layout = QHBoxLayout()
path_layout.setSpacing(12)
# 文件路径输入框 - 不设置默认路径
self.config_path_edit = QLineEdit()
self.config_path_edit.setPlaceholderText("请选择账号配置文件保存目录")
self.config_path_edit.setMinimumHeight(42)
self.config_path_edit.setMaximumHeight(42)
self.config_path_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 13px;
background-color: white;
color: #333;
}
QLineEdit:focus {
border: 2px solid #3498db;
background-color: #f8fdff;
}
""")
self.config_path_edit.setToolTip(f"请选择账号配置文件存放目录,程序将在此创建 {self.DEFAULT_CONFIG_FILE} 文件")
# 浏览按钮
self.config_browse_btn = QPushButton("浏览")
self.config_browse_btn.setMinimumHeight(42)
self.config_browse_btn.setMinimumWidth(100)
self.config_browse_btn.setStyleSheet("""
QPushButton {
background-color: #7f8c8d;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
padding: 10px 15px;
}
QPushButton:hover {
background-color: #95a5a6;
}
QPushButton:pressed {
background-color: #6c7b7d;
padding: 11px 15px 9px 15px;
}
""")
self.config_browse_btn.clicked.connect(self.browse_user_config)
# 使用弹性布局输入框占4份按钮占1份
path_layout.addWidget(self.config_path_edit, 4)
path_layout.addWidget(self.config_browse_btn, 1)
self.config_group.addWidget(self.config_path_label)
self.config_group.addLayout(path_layout)
# 文件名提示
self.filename_label = QLabel(f"* 账号配置文件将自动保存为:{self.DEFAULT_CONFIG_FILE}")
main_layout.addLayout(self.config_group)
main_layout.addSpacing(15)
# 按钮区域
button_layout = QHBoxLayout()
button_layout.setSpacing(20)
# 左侧添加弹性空间
button_layout.addStretch()
# 退出按钮
self.exit_button = QPushButton("退出")
self.exit_button.setMinimumSize(120, 45)
self.exit_button.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
padding: 10px 25px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
QPushButton:pressed {
background-color: #6c7b7d;
padding: 11px 25px 9px 25px;
}
""")
self.exit_button.clicked.connect(self.reject)
# 登录按钮
self.login_button = QPushButton("登录")
self.login_button.setMinimumSize(120, 45)
self.login_button.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
padding: 10px 25px;
}
QPushButton:hover {
background-color: #2980b9;
}
QPushButton:pressed {
background-color: #1c6ea4;
padding: 11px 25px 9px 25px;
}
""")
self.login_button.clicked.connect(self.attempt_login)
button_layout.addWidget(self.exit_button)
button_layout.addWidget(self.login_button)
# 右侧添加弹性空间
button_layout.addStretch()
main_layout.addLayout(button_layout)
# 底部提示信息
self.info_label = QLabel()
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.info_label.setStyleSheet("color: #888888; font-size: 12px; margin-top: 10px;")
main_layout.addWidget(self.info_label)
# 添加底部弹性空间
main_layout.addStretch()
# 设置默认焦点
self.username_edit.setFocus()
# 设置回车键事件
self.username_edit.returnPressed.connect(self.password_edit.setFocus)
self.password_edit.returnPressed.connect(self.attempt_login)
self.config_path_edit.returnPressed.connect(self.attempt_login)
def check_first_run(self):
"""检查是否显示目录选择部分"""
# 检查是否有保存的用户配置文件路径
settings_file = Path.home() / ".config_editor" / "user_settings.json"
if settings_file.exists():
try:
with open(settings_file, 'r', encoding='utf-8') as f:
settings = json.load(f)
user_config_dir = settings.get('user_config_dir')
if user_config_dir:
# 构建完整文件路径
user_config_path = os.path.join(user_config_dir, self.DEFAULT_CONFIG_FILE)
# 检查目录和文件是否存在
if os.path.isdir(user_config_dir) and os.path.exists(user_config_path):
# 目录和文件都存在,隐藏目录选择部分
self.user_config_dir = user_config_dir
self.user_config_path = user_config_path
# 更新界面:隐藏目录选择部分
self.config_path_label.setVisible(False)
self.config_path_edit.setVisible(False)
self.config_browse_btn.setVisible(False)
self.filename_label.setVisible(False)
# 更新提示信息
self.info_label.setText(f"账号配置文件位于:{user_config_path}")
return
else:
# 目录存在但文件不存在,显示目录选择部分并填充已保存的目录
self.config_path_edit.setText(user_config_dir)
self.info_label.setText("账号配置文件不存在,请重新选择目录或使用原目录重新创建")
return
except Exception as e:
print(f"读取用户设置失败: {e}")
# 第一次运行或设置无效,需要选择目录
self.info_label.setText("首次运行请选择账号配置文件保存目录(首次登录将自动创建用户)")
def save_user_preferences(self, user_config_dir):
"""保存用户选择的配置文件目录"""
try:
settings_dir = Path.home() / ".config_editor"
settings_dir.mkdir(parents=True, exist_ok=True)
settings_file = settings_dir / "user_settings.json"
settings = {
'user_config_dir': user_config_dir,
'config_filename': self.DEFAULT_CONFIG_FILE,
'last_updated': os.path.getmtime(str(settings_file)) if settings_file.exists() else None
}
with open(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 browse_user_config(self):
"""浏览并选择账号配置文件目录"""
# 选择目录,而不是文件
dir_path = QFileDialog.getExistingDirectory(
self,
"选择账号配置文件保存目录",
str(Path.home()) # 默认从用户主目录开始
)
if dir_path:
# 只保存目录路径
self.config_path_edit.setText(dir_path)
def attempt_login(self):
"""尝试登录(首次登录自动创建用户)"""
username = self.username_edit.text().strip()
password = self.password_edit.text().strip()
# 验证用户名和密码
if not username:
QMessageBox.warning(self, "输入错误", "请输入用户名")
self.username_edit.setFocus()
return
if not password:
QMessageBox.warning(self, "输入错误", "请输入密码")
self.password_edit.setFocus()
return
# 检查是否需要目录(如果已保存的目录和文件都存在,则不需要)
if not self.user_config_path:
# 需要获取目录
user_config_dir = self.config_path_edit.text().strip()
# 验证路径是否有效
if not user_config_dir:
QMessageBox.warning(self, "输入错误", "请选择账号配置文件保存目录")
self.config_path_edit.setFocus()
return
# 检查目录是否存在
if not os.path.isdir(user_config_dir):
reply = QMessageBox.question(
self, "目录不存在",
f"选择的目录不存在:{user_config_dir}\n是否创建此目录?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
os.makedirs(user_config_dir, exist_ok=True)
except Exception as e:
QMessageBox.warning(self, "创建目录失败", f"无法创建目录:{str(e)}")
return
else:
return
# 构建完整文件路径
user_config_path = os.path.join(user_config_dir, self.DEFAULT_CONFIG_FILE)
# 保存用户选择的账密文件目录
self.user_config_dir = user_config_dir
self.user_config_path = user_config_path
# 保存用户偏好设置(只保存目录)
self.save_user_preferences(user_config_dir)
# 隐藏目录选择部分
self.config_path_label.setVisible(False)
self.config_path_edit.setVisible(False)
self.config_browse_btn.setVisible(False)
self.filename_label.setVisible(False)
# 更新提示信息
self.info_label.setText(f"账号配置文件位于:{user_config_path}")
# 创建认证管理器,使用完整的文件路径
auth = SimpleAuth(self.user_config_path)
# 验证凭据(首次登录会自动创建用户)
success, message = auth.authenticate(username, password)
if success:
# 如果是首次登录或二级密码未设置,提示设置二级密码
user_info = auth.users.get(username, {})
if not user_info.get("secondary_password"):
self.prompt_set_secondary_password(auth, username)
else:
# 发送登录成功信号,传递用户名和配置文件路径
self.login_success.emit(username, self.user_config_path)
# 关闭对话框
self.accept()
else:
QMessageBox.warning(self, "登录失败", message)
self.password_edit.clear()
self.password_edit.setFocus()
def prompt_set_secondary_password(self, auth, username):
"""提示用户设置二级密码"""
from PyQt6.QtWidgets import QInputDialog
# 弹出对话框要求设置二级密码
secondary_password, ok = QInputDialog.getText(
self, "设置二级密码",
"首次登录需要设置二级密码(用于敏感操作验证):",
QLineEdit.EchoMode.Password
)
if ok and secondary_password:
# 确认二级密码
confirm_password, ok2 = QInputDialog.getText(
self, "确认二级密码",
"请再次输入二级密码确认:",
QLineEdit.EchoMode.Password
)
if ok2 and secondary_password == confirm_password:
# 设置二级密码
success, message = auth.set_secondary_password(username, secondary_password)
if success:
QMessageBox.information(self, "设置成功", "二级密码设置成功!")
# 发送登录成功信号
self.login_success.emit(username, self.user_config_path)
self.accept()
else:
QMessageBox.warning(self, "设置失败", f"设置二级密码失败:{message}")
self.password_edit.clear()
self.password_edit.setFocus()
elif ok2:
QMessageBox.warning(self, "密码不一致", "两次输入的密码不一致,请重新设置")
self.prompt_set_secondary_password(auth, username)
elif ok:
QMessageBox.warning(self, "密码为空", "二级密码不能为空,请重新设置")
self.prompt_set_secondary_password(auth, username)
else:
QMessageBox.warning(self, "操作取消", "必须设置二级密码才能继续使用")
self.password_edit.clear()
self.password_edit.setFocus()
def get_config_file_path(self) -> str:
"""获取配置文件路径(主配置文件)"""
return self.config_file_path
def get_user_config_path(self) -> str:
"""获取账号配置文件路径(完整路径)"""
return self.user_config_path
class SecondaryPasswordDialog(QDialog):
"""二级密码验证对话框"""
verified = pyqtSignal() # 验证成功信号
def __init__(self, auth_config_path, username, parent=None):
super().__init__(parent)
self.auth_config_path = auth_config_path
self.username = username
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("二级密码验证")
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint)
self.setMinimumSize(450, 250)
self.resize(450, 250)
layout = QVBoxLayout(self)
layout.setSpacing(25)
layout.setContentsMargins(40, 40, 40, 40)
# 标题
title_label = QLabel("需要验证二级密码")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("color: #333333; font-weight: bold; font-size: 16px;")
layout.addWidget(title_label)
layout.addSpacing(15)
# 密码输入
password_layout = QVBoxLayout()
password_layout.setSpacing(10)
# password_label = QLabel("二级密码:")
# password_label.setStyleSheet("font-weight: bold; color: #333333; font-size: 12px;")
self.password_edit = QLineEdit()
self.password_edit.setPlaceholderText("请输入二级密码")
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setMinimumHeight(48)
self.password_edit.setMaximumHeight(48)
self.password_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 14px;
}
QLineEdit:focus {
border: 2px solid #3498db;
}
""")
# password_layout.addWidget(password_label)
password_layout.addWidget(self.password_edit)
layout.addLayout(password_layout)
layout.addSpacing(25)
# 按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(20)
button_layout.addStretch()
verify_btn = QPushButton("验证")
verify_btn.setMinimumSize(120, 45)
verify_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
verify_btn.clicked.connect(self.attempt_verify)
cancel_btn = QPushButton("取消")
cancel_btn.setMinimumSize(120, 45)
cancel_btn.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
padding: 10px 20px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(verify_btn)
button_layout.addWidget(cancel_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# 设置焦点和回车键事件
self.password_edit.setFocus()
self.password_edit.returnPressed.connect(self.attempt_verify)
def attempt_verify(self):
"""尝试验证二级密码"""
password = self.password_edit.text().strip()
if not password:
QMessageBox.warning(self, "输入错误", "请输入二级密码")
return
# 创建认证管理器
auth = SimpleAuth(self.auth_config_path)
# 验证二级密码
success, message = auth.verify_secondary_password(self.username, password)
if success:
# 发送验证成功信号
self.verified.emit()
self.accept()
else:
QMessageBox.warning(self, "验证失败", message)
self.password_edit.clear()
self.password_edit.setFocus()
class ReLoginDialog(QDialog):
"""重新登录对话框(会话超时后显示)"""
login_success = pyqtSignal(str, str) # 重新登录成功信号username, config_file_path
def __init__(self, config_file_path, parent=None):
super().__init__(parent)
self.config_file_path = config_file_path
self.user_config_path = config_file_path
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("会话超时 - 重新登录")
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint)
self.setMinimumSize(500, 320)
self.resize(500, 320)
layout = QVBoxLayout(self)
layout.setSpacing(25)
layout.setContentsMargins(50, 40, 50, 40)
# 提示信息
info_label = QLabel("会话已超时,请重新登录")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setStyleSheet("color: #e74c3c; font-weight: bold; font-size: 16px;")
layout.addWidget(info_label)
layout.addSpacing(10)
# 用户名输入
username_layout = QVBoxLayout()
username_layout.setSpacing(10)
# username_label = QLabel("用户名:")
# username_label.setStyleSheet("font-weight: bold; color: #333333; font-size: 14px;")
self.username_edit = QLineEdit()
self.username_edit.setPlaceholderText("请输入用户名")
self.username_edit.setMinimumHeight(48)
self.username_edit.setMaximumHeight(48)
self.username_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 14px;
}
QLineEdit:focus {
border: 2px solid #3498db;
}
""")
# username_layout.addWidget(username_label)
username_layout.addWidget(self.username_edit)
layout.addLayout(username_layout)
layout.addSpacing(15)
# 密码输入
password_layout = QVBoxLayout()
password_layout.setSpacing(8)
# password_label = QLabel("密码:")
# password_label.setStyleSheet("font-weight: bold; color: #333333; font-size: 14px;")
self.password_edit = QLineEdit()
self.password_edit.setPlaceholderText("请输入密码")
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setMinimumHeight(42)
self.password_edit.setMaximumHeight(42)
self.password_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px 15px;
font-size: 14px;
}
QLineEdit:focus {
border: 2px solid #3498db;
}
""")
# password_layout.addWidget(password_label)
password_layout.addWidget(self.password_edit)
layout.addLayout(password_layout)
layout.addSpacing(30)
# 按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(30)
button_layout.addStretch()
login_btn = QPushButton("重新登录")
login_btn.setMinimumSize(150, 50)
login_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
padding: 10px 25px;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
login_btn.clicked.connect(self.attempt_login)
cancel_btn = QPushButton("取消")
cancel_btn.setMinimumSize(140, 45)
cancel_btn.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
padding: 10px 25px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(login_btn)
button_layout.addWidget(cancel_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# 设置焦点和回车键事件
self.username_edit.setFocus()
self.username_edit.returnPressed.connect(self.password_edit.setFocus)
self.password_edit.returnPressed.connect(self.attempt_login)
def attempt_login(self):
"""尝试重新登录"""
username = self.username_edit.text().strip()
password = self.password_edit.text().strip()
if not username or not password:
QMessageBox.warning(self, "输入错误", "请输入用户名和密码")
return
# 创建认证管理器,使用原来的账号文件路径
auth = SimpleAuth(self.user_config_path)
# 验证凭据
success, message = auth.authenticate(username, password)
if success:
# 发送重新登录成功信号
self.login_success.emit(username, self.user_config_path)
self.accept()
else:
QMessageBox.warning(self, "登录失败", message)
self.password_edit.clear()
self.password_edit.setFocus()

39
main.py Normal file
View File

@ -0,0 +1,39 @@
'''项目入口启动程序'''
import sys
from PyQt6.QtWidgets import QApplication
from login.login_dialog import LoginDialog
from main_window import ConfigEditor
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
# 创建登录对话框
login_dialog = LoginDialog()
def on_login_success(username, user_config_path):
"""登录成功回调"""
# 注意:这里传递的是 user_config_path这是账密文件路径
# 而 ConfigEditor 需要的是要编辑的配置文件路径
print(f"[调试] 登录成功: 用户名={username}, 账密文件={user_config_path}")
# 创建主窗口,传递账密文件路径和用户名
# 主窗口会自己加载要编辑的配置文件
editor = ConfigEditor(user_config_path, username)
editor.show()
# 连接信号
login_dialog.login_success.connect(on_login_success)
# 显示登录对话框
result = login_dialog.exec()
# 如果登录对话框被取消(用户点击退出),则退出程序
if result == 0:
sys.exit(0)
# 登录成功后,启动应用程序的主事件循环
sys.exit(app.exec())
if __name__ == '__main__':
main()

1555
main_window.py Normal file

File diff suppressed because it is too large Load Diff

143
models.py Normal file
View File

@ -0,0 +1,143 @@
'''数据模型定义'''
import json
import datetime
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
@dataclass
class ConfigField:
"""配置项元数据类"""
name: str
value: Any
category: str = "未分类"
display_name: str = ""
field_type: str = "auto"
decimals: Optional[int] = None
tooltip: str = ""
hidden: bool = False
encrypted: bool = False # 是否加密
line_number: Optional[int] = None
original_lines: List[str] = field(default_factory=list)
validation: Dict = field(default_factory=dict)
last_saved_value: Any = None
def __post_init__(self):
if not self.display_name:
self.display_name = self.name
def get_actual_field_type(self) -> str:
"""获取实际的字段类型"""
if self.field_type != "auto":
return self.field_type
if isinstance(self.value, bool):
return "bool"
elif isinstance(self.value, int):
return "int"
elif isinstance(self.value, float):
return "float"
elif isinstance(self.value, (list, dict)):
return "json"
else:
return "str"
@dataclass
class EditorRules:
"""编辑器规则数据类"""
categories: Dict[str, List[str]] = field(default_factory=lambda: {"未分类": []})
display_names: Dict[str, str] = field(default_factory=dict)
tooltips: Dict[str, str] = field(default_factory=dict)
field_types: Dict[str, str] = field(default_factory=dict)
field_decimals: Dict[str, int] = field(default_factory=dict)
hidden: List[str] = field(default_factory=list)
encrypted_fields: List[str] = field(default_factory=list) # 加密字段列表
validations: Dict[str, Dict] = field(default_factory=dict)
field_order: Dict[str, List[str]] = field(default_factory=dict)
last_saved_values: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"categories": self.categories,
"display_names": self.display_names,
"tooltips": self.tooltips,
"field_types": self.field_types,
"field_decimals": self.field_decimals,
"hidden": self.hidden,
"encrypted_fields": self.encrypted_fields,
"validations": self.validations,
"field_order": self.field_order,
"last_saved_values": self.last_saved_values
}
@classmethod
def from_dict(cls, data: Dict) -> 'EditorRules':
"""从字典创建实例"""
return cls(
categories=data.get("categories", {"未分类": []}),
display_names=data.get("display_names", {}),
tooltips=data.get("tooltips", {}),
field_types=data.get("field_types", {}),
field_decimals=data.get("field_decimals", {}),
hidden=data.get("hidden", []),
encrypted_fields=data.get("encrypted_fields", []),
validations=data.get("validations", {}),
field_order=data.get("field_order", {}),
last_saved_values=data.get("last_saved_values", {})
)
@dataclass
class UserSettings:
"""用户设置数据类"""
config_file_path: str = ""
last_used: str = ""
use_relative_path: bool = True
def to_dict(self) -> Dict:
return {
"config_file_path": self.config_file_path,
"last_used": self.last_used
}
@classmethod
def from_dict(cls, data: Dict) -> 'UserSettings':
return cls(
config_file_path=data.get("config_file_path", ""),
last_used=data.get("last_used", "")
)
@dataclass
class ExportInfo:
"""导出信息数据类"""
export_time: str = ""
export_version: str = "1.0"
note: str = "配置文件编辑器规则文件"
def to_dict(self) -> Dict:
return {
"export_time": self.export_time or datetime.datetime.now().strftime("%Y-%m-d %H:%M:%S"),
"export_version": self.export_version,
"note": self.note
}
@dataclass
class Statistics:
"""统计信息数据类"""
total_fields: int = 0
display_names_count: int = 0
categories_count: int = 0
hidden_fields_count: int = 0
encrypted_fields_count: int = 0 # 加密字段计数
field_types_count: int = 0
validations_count: int = 0
def to_dict(self) -> Dict:
return {
"total_fields": self.total_fields,
"display_names_count": self.display_names_count,
"categories_count": self.categories_count,
"hidden_fields_count": self.hidden_fields_count,
"encrypted_fields_count": self.encrypted_fields_count,
"field_types_count": self.field_types_count,
"validations_count": self.validations_count
}

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
PyQt6>=6.5.0

263
utils.py Normal file
View File

@ -0,0 +1,263 @@
'''工具函数集合'''
import os
import json
import shutil
import datetime
import re
import ast
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
def merge_rules(existing_rules: Dict, default_rules: Dict) -> Dict:
"""智能合并规则:保留现有规则,只添加默认规则中的新字段"""
merged_rules = existing_rules.copy()
# 确保所有必需的字段都存在
for key in default_rules:
if key not in merged_rules:
merged_rules[key] = default_rules[key]
# 确保"未分类"分组始终存在
if "categories" not in merged_rules:
merged_rules["categories"] = {}
if "未分类" not in merged_rules["categories"]:
merged_rules["categories"]["未分类"] = []
# 确保其他必要字段存在
for field in ["display_names", "tooltips", "field_types", "field_decimals",
"hidden", "validations", "field_order", "last_saved_values", "encrypted_fields"]:
if field not in merged_rules:
if field == "hidden" or field == "encrypted_fields":
merged_rules[field] = []
else:
merged_rules[field] = {}
return merged_rules
def backup_existing_rules(rules_file: str, program_dir: str) -> str:
"""备份现有规则文件"""
if os.path.exists(rules_file):
try:
# 创建备份目录
backup_dir = os.path.join(program_dir, "backups")
os.makedirs(backup_dir, exist_ok=True)
# 生成带时间戳的备份文件名
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = os.path.join(backup_dir, f"config_editor_rules_{timestamp}.json")
# 备份文件
shutil.copy2(rules_file, backup_file)
print(f"已备份规则文件到: {backup_file}")
# 清理旧的备份文件
cleanup_old_backups(backup_dir)
return backup_file
except Exception as e:
print(f"备份规则文件失败: {e}")
return ""
def cleanup_old_backups(backup_dir: str, keep_count: int = 5):
"""清理旧的备份文件"""
try:
# 获取所有备份文件
backup_files = []
for filename in os.listdir(backup_dir):
if filename.startswith("config_editor_rules_") and filename.endswith(".json"):
filepath = os.path.join(backup_dir, filename)
backup_files.append((filepath, os.path.getmtime(filepath)))
# 按修改时间排序(从旧到新)
backup_files.sort(key=lambda x: x[1])
# 删除旧的备份文件,保留最近几个
if len(backup_files) > keep_count:
files_to_delete = backup_files[:-keep_count]
for filepath, _ in files_to_delete:
os.remove(filepath)
print(f"已删除旧备份文件: {filepath}")
except Exception as e:
print(f"清理旧备份文件失败: {e}")
def get_validation_tooltip(validation: Dict) -> str:
"""生成校验规则的提示信息"""
if not validation:
return ""
rules = []
if validation.get("required"):
rules.append("• 必填项")
if "min" in validation:
rules.append(f"• 最小值: {validation['min']}")
if "max" in validation:
rules.append(f"• 最大值: {validation['max']}")
if "regex" in validation:
rules.append(f"• 正则表达式: {validation['regex']}")
return "\n".join(rules)
def format_dict_value(value: Dict, original_lines: List[str] = None) -> str:
"""专门格式化字典值,保持多行格式"""
if not isinstance(value, dict):
return json.dumps(value, ensure_ascii=False)
# 如果有原始行信息,尝试保持原始格式
if original_lines and len(original_lines) > 1:
# 检查原始格式是否是漂亮的多行格式
first_line = original_lines[0].strip()
if first_line.endswith('{') and len(original_lines) > 2:
# 原始是多行格式,我们也使用多行格式
result = "{\n"
for i, (key, val) in enumerate(value.items()):
# 使用4个空格缩进
result += f' "{key}": {json.dumps(val, ensure_ascii=False)}'
if i < len(value) - 1:
result += ",\n"
else:
result += "\n"
result += "}"
return result
# 如果没有原始格式信息或不是多行格式使用漂亮的JSON格式
return json.dumps(value, indent=4, ensure_ascii=False)
def format_value(value: Any, config_field=None) -> str:
"""格式化值保持与Python兼容的格式"""
if value is None:
return "None"
elif isinstance(value, bool):
return "True" if value else "False"
elif isinstance(value, (int, float)):
# 根据小数位数格式化浮点数
if isinstance(value, float) and config_field and config_field.decimals is not None:
format_str = f"{{:.{config_field.decimals}f}}"
return format_str.format(value)
else:
return str(value)
elif isinstance(value, str):
# 检查字符串是否已经用引号包围
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
return value
# 检查字符串是否包含特殊字符
elif any(char in value for char in [' ', '\t', '\n', '#', '=']):
return f'"{value}"'
else:
return f'"{value}"'
elif isinstance(value, dict):
# 对于字典,使用专门的格式化函数
original_lines = config_field.original_lines if config_field else None
return format_dict_value(value, original_lines)
elif isinstance(value, list):
return json.dumps(value, ensure_ascii=False, indent=4)
else:
return repr(value)
def get_default_rules() -> Dict:
"""获取默认规则"""
return {
"categories": {"未分类": []},
"display_names": {},
"tooltips": {},
"field_types": {},
"field_decimals": {},
"hidden": [],
"encrypted_fields": [], #加密字段列表
"validations": {},
"field_order": {},
"last_saved_values": {}
}
def validate_config_data(config_data: Dict[str, Any], all_config_fields: Dict[str, Any]) -> List[str]:
"""校验配置数据"""
errors = []
for field_name, value in config_data.items():
config_field = all_config_fields.get(field_name)
if not config_field:
continue
validation = config_field.validation
if not validation:
continue
# 必填校验
if validation.get("required") and (value is None or value == ""):
errors.append(f"配置项 '{config_field.display_name}' 是必填项")
continue
# 数字范围校验
if isinstance(value, (int, float)):
if "min" in validation:
try:
min_val = float(validation["min"])
if value < min_val:
errors.append(f"配置项 '{config_field.display_name}' 的值不能小于 {min_val}")
except ValueError:
pass
if "max" in validation:
try:
max_val = float(validation["max"])
if value > max_val:
errors.append(f"配置项 '{config_field.display_name}' 的值不能大于 {max_val}")
except ValueError:
pass
# 字符串正则校验
elif isinstance(value, str) and "regex" in validation:
try:
if not re.match(validation["regex"], value):
errors.append(f"配置项 '{config_field.display_name}' 的值不符合格式要求")
except re.error:
errors.append(f"配置项 '{config_field.display_name}' 的正则表达式格式错误")
return errors
def get_field_type_from_value(value: Any) -> str:
"""根据值推断字段类型"""
if isinstance(value, bool):
return "bool"
elif isinstance(value, int):
return "int"
elif isinstance(value, float):
return "float"
elif isinstance(value, (list, dict)):
return "json"
else:
return "str"
def ensure_rule_structure(rules: Dict) -> Dict:
"""确保规则结构完整"""
# 确保规则中有"未分类"分组
if "categories" not in rules:
rules["categories"] = {}
if "未分类" not in rules["categories"]:
rules["categories"]["未分类"] = []
# 确保规则中有field_decimals字段
if "field_decimals" not in rules:
rules["field_decimals"] = {}
# 确保规则中有field_order字段
if "field_order" not in rules:
rules["field_order"] = {}
# 确保规则中有last_saved_values字段
if "last_saved_values" not in rules:
rules["last_saved_values"] = {}
# 确保规则中有encrypted_fields字段
if "encrypted_fields" not in rules:
rules["encrypted_fields"] = []
# 确保其他必要字段存在
for field in ["display_names", "tooltips", "field_types", "validations"]:
if field not in rules:
rules[field] = {}
if "hidden" not in rules:
rules["hidden"] = []
return rules

17
widgets.py Normal file
View File

@ -0,0 +1,17 @@
'''自定义控件实现'''
from PyQt6.QtWidgets import QSpinBox, QDoubleSpinBox
from PyQt6.QtCore import Qt
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)