Compare commits
No commits in common. "main" and "1.1" have entirely different histories.
499
README.md
499
README.md
@ -1,291 +1,208 @@
|
||||
ce编辑器 (Config Editor)
|
||||
|
||||
一个功能强大的Python配置文件和用户管理工具,提供直观的GUI界面进行配置管理、用户认证和会话安全控制。
|
||||
|
||||
主要特性
|
||||
|
||||
🔐 用户认证系统
|
||||
|
||||
首次登录即注册:新用户首次登录自动创建账户
|
||||
|
||||
二级密码保护:敏感操作需要二级密码验证
|
||||
|
||||
会话超时锁定:空闲超过10分钟自动锁定,需要重新登录
|
||||
|
||||
安全的密码存储:使用JSON格式加密存储用户凭证
|
||||
|
||||
📁 配置文件管理
|
||||
|
||||
智能解析:自动识别Python配置文件中的配置项
|
||||
|
||||
格式保持:编辑时保留原始注释、空格和格式
|
||||
|
||||
分组显示:按规则将配置项分组显示在不同标签页
|
||||
|
||||
拖拽排序:支持通过拖拽重新排列配置项顺序
|
||||
|
||||
⚙️ 高级功能
|
||||
|
||||
规则管理:可自定义显示名称、分组、校验规则等
|
||||
|
||||
加密字段:标记敏感配置项,修改时需要二级密码
|
||||
|
||||
隐藏字段:隐藏不需要显示的配置项
|
||||
|
||||
导入导出:支持规则文件的导入导出
|
||||
|
||||
校验规则:支持必填、范围、正则表达式等校验
|
||||
|
||||
🎨 用户界面
|
||||
|
||||
现代化UI:基于PyQt6的响应式界面
|
||||
|
||||
直观操作:拖拽排序、分组管理、批量操作
|
||||
|
||||
实时反馈:修改高亮、校验提示、状态监控
|
||||
|
||||
主题支持:标准的Fusion主题,视觉舒适
|
||||
|
||||
系统要求
|
||||
操作系统:Linux(已测试Ubuntu 20.04+)
|
||||
|
||||
Python版本:Python 3.8+
|
||||
|
||||
依赖:PyQt6 >= 6.5.0
|
||||
|
||||
安装方法
|
||||
|
||||
使用deb包安装
|
||||
确保已安装依赖:
|
||||
|
||||
bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3 python3-venv python3-pip
|
||||
安装deb包:
|
||||
|
||||
bash
|
||||
sudo dpkg -i config-editor_1.3_amd64.deb
|
||||
|
||||
|
||||
使用指南
|
||||
|
||||
首次运行设置
|
||||
启动程序:首次运行会显示登录界面
|
||||
|
||||
用户注册:
|
||||
|
||||
输入用户名和密码(首次登录自动创建账户)
|
||||
|
||||
选择账号配置文件保存目录
|
||||
|
||||
设置二级密码用于敏感操作验证
|
||||
|
||||
配置文件设置:
|
||||
|
||||
登录后首次运行需要设置要编辑的配置文件路径
|
||||
|
||||
支持浏览选择或手动输入文件路径
|
||||
|
||||
可以使用相对路径(相对于程序目录)
|
||||
|
||||
主要功能使用
|
||||
|
||||
1. 配置文件编辑
|
||||
|
||||
打开文件:通过菜单或工具栏打开Python配置文件
|
||||
|
||||
编辑配置:直接在界面上修改配置项值
|
||||
|
||||
保存更改:保存时显示变更列表确认,保留原始格式
|
||||
|
||||
重新加载:随时重新加载配置文件
|
||||
|
||||
2. 规则管理
|
||||
|
||||
进入管理界面:点击"管理规则"按钮
|
||||
|
||||
配置项管理:设置显示名称、分组、类型、校验规则等
|
||||
|
||||
隐藏/显示:标记配置项为隐藏,或批量显示/隐藏
|
||||
|
||||
分组管理:创建、删除和重命名分组
|
||||
|
||||
3. 高级操作
|
||||
|
||||
拖拽排序:在分组内拖拽配置项标签重新排序
|
||||
|
||||
加密字段:标记敏感字段,修改时需要二级密码
|
||||
|
||||
批量操作:一键隐藏所有配置项,或全部显示
|
||||
|
||||
导入导出:导入/导出规则文件,方便备份和迁移
|
||||
|
||||
安全特性
|
||||
|
||||
会话管理
|
||||
|
||||
程序监控用户活动,空闲10分钟后自动锁定
|
||||
|
||||
锁定后需要重新登录才能继续操作
|
||||
|
||||
取消重新登录会退出程序以保障安全
|
||||
|
||||
二级密码验证
|
||||
|
||||
以下操作需要验证二级密码:
|
||||
|
||||
修改标记为加密的配置项
|
||||
|
||||
打开配置文件
|
||||
|
||||
配置文件设置
|
||||
|
||||
管理规则
|
||||
|
||||
显示隐藏项
|
||||
|
||||
一键隐藏所有项
|
||||
|
||||
项目结构
|
||||
text
|
||||
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 # 本文档
|
||||
配置文件格式
|
||||
|
||||
程序支持标准的Python配置文件格式:
|
||||
|
||||
python
|
||||
'''
|
||||
# 数据库配置
|
||||
DATABASE_HOST = "localhost"
|
||||
DATABASE_PORT = 3306
|
||||
DATABASE_USER = "admin"
|
||||
DATABASE_PASSWORD = "secret_password" # 可以标记为加密字段
|
||||
|
||||
# 应用配置
|
||||
APP_NAME = "My Application"
|
||||
DEBUG_MODE = False
|
||||
LOG_LEVEL = "INFO"
|
||||
|
||||
# API配置
|
||||
API_CONFIG = {
|
||||
"key": "your_api_key",
|
||||
"timeout": 30,
|
||||
"retries": 3
|
||||
}
|
||||
'''
|
||||
|
||||
故障排除
|
||||
|
||||
常见问题
|
||||
|
||||
无法启动程序
|
||||
|
||||
text
|
||||
# 检查Python版本
|
||||
python3 --version
|
||||
|
||||
# 检查PyQt6是否安装
|
||||
python3 -c "import PyQt6"
|
||||
虚拟环境问题(deb安装)
|
||||
|
||||
bash
|
||||
# 重新创建虚拟环境
|
||||
sudo /opt/config-editor/init_venv.sh
|
||||
|
||||
# 或使用管理工具
|
||||
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包
|
||||
Config Editor - 配置文件编辑工具
|
||||
一个基于PyQt6开发的Python配置文件可视化编辑工具,支持自动解析Python配置文件中的大写变量,提供直观的GUI界面进行编辑和管理。
|
||||
|
||||
✨ 特性功能
|
||||
🎯 核心功能
|
||||
智能解析:自动扫描Python文件,识别文件中的变量配置项
|
||||
|
||||
可视化编辑:基于PyQt6的现代化GUI界面,支持多种数据类型编辑
|
||||
|
||||
分组管理:可自定义分组,按类别组织配置项
|
||||
|
||||
格式保持:保存时保持原始文件格式、注释和缩进
|
||||
|
||||
🔧 编辑支持
|
||||
多种数据类型:
|
||||
|
||||
布尔值(CheckBox)
|
||||
|
||||
整数/浮点数(SpinBox)
|
||||
|
||||
字符串(LineEdit/TextEdit)
|
||||
|
||||
JSON/字典/列表(格式化编辑)
|
||||
|
||||
校验规则:
|
||||
|
||||
必填项验证
|
||||
|
||||
数值范围限制
|
||||
|
||||
正则表达式匹配
|
||||
|
||||
自定义错误提示
|
||||
|
||||
📊 管理功能
|
||||
显示定制:自定义每个配置项的显示名称和提示信息
|
||||
|
||||
字段隐藏:标记隐藏字段,支持显示/隐藏切换
|
||||
|
||||
类型推断:自动识别字段类型,支持手动覆盖
|
||||
|
||||
规则管理:独立的规则管理界面,支持批量配置
|
||||
|
||||
🚀 快速开始
|
||||
环境要求
|
||||
Python 3.8+
|
||||
|
||||
PyQt6
|
||||
|
||||
构建打包
|
||||
执行build.sh脚本
|
||||
|
||||
安装部署
|
||||
bash
|
||||
1.sudo dpkg -i config-editor_1.0_amd64.deb
|
||||
2.sudo /usr/share/config-editor/setup_venv.sh
|
||||
在服务器图形化界面,搜索Config Editor点击即可使用
|
||||
卸载程序
|
||||
bash
|
||||
1.sudo dpkg -r config-editor
|
||||
📁 项目结构
|
||||
text
|
||||
config_editor/
|
||||
├── config_editor.py # 主程序
|
||||
├── config_editor_rules.json # 规则配置文件
|
||||
├── config_editor_settings.json # 用户设置文件
|
||||
├── build.sh # 一键打包脚本
|
||||
├── CHANGELOG.md # 变更日志
|
||||
└── README.md # 说明文档
|
||||
配置文件说明
|
||||
config_editor_rules.json:存储所有配置项的元数据(分组、显示名、校验规则等)
|
||||
|
||||
config_editor_settings.json:存储用户设置(最近使用的配置文件路径等)
|
||||
|
||||
🖥️ 使用指南
|
||||
1. 首次运行
|
||||
首次启动程序时,会自动弹出配置文件设置对话框:
|
||||
|
||||
选择要编辑的Python配置文件
|
||||
|
||||
程序会自动解析配置文件中的大写变量
|
||||
|
||||
2. 界面布局
|
||||
顶部信息栏:显示当前配置文件的路径
|
||||
|
||||
标签页:按分组显示配置项
|
||||
|
||||
编辑区域:每个配置项包含:
|
||||
|
||||
显示名称(可自定义)
|
||||
|
||||
编辑控件(根据类型自动适配)
|
||||
|
||||
变量名(原始名称)
|
||||
|
||||
工具栏:常用操作按钮
|
||||
|
||||
3. 基本操作
|
||||
打开文件:使用菜单或按钮打开其他配置文件
|
||||
|
||||
编辑配置:直接在对应的控件中修改值
|
||||
|
||||
保存配置:保存修改到原配置文件
|
||||
|
||||
重新加载:放弃修改,重新读取配置文件
|
||||
|
||||
4. 高级功能
|
||||
规则管理
|
||||
点击"管理规则"按钮进入规则管理界面:
|
||||
|
||||
字段属性:设置显示名称、分组、类型、提示信息等
|
||||
|
||||
校验规则:设置最小值、最大值、正则表达式、必填项
|
||||
|
||||
分组管理:添加、删除分组,调整字段分组
|
||||
|
||||
字段筛选
|
||||
搜索功能:快速查找配置项
|
||||
|
||||
隐藏字段:支持显示/隐藏被标记为隐藏的配置项
|
||||
|
||||
⚙️ 配置规则详解
|
||||
分组配置
|
||||
json
|
||||
"categories": {
|
||||
"数据库配置": ["DB_HOST", "DB_PORT"],
|
||||
"应用配置": ["DEBUG_MODE", "LOG_LEVEL"]
|
||||
}
|
||||
字段属性
|
||||
json
|
||||
"display_names": {
|
||||
"DB_HOST": "数据库主机地址"
|
||||
},
|
||||
"field_types": {
|
||||
"DB_PORT": "int"
|
||||
},
|
||||
"tooltips": {
|
||||
"DB_HOST": "请输入数据库服务器的IP地址或域名"
|
||||
}
|
||||
校验规则
|
||||
json
|
||||
"validations": {
|
||||
"DB_PORT": {
|
||||
"required": true,
|
||||
"min": "1024",
|
||||
"max": "65535",
|
||||
"regex": "^[0-9]+$"
|
||||
}
|
||||
}
|
||||
🎨 支持的Python配置文件格式
|
||||
程序可以解析以下格式的配置:
|
||||
|
||||
python
|
||||
# 配置文件示例 config.py
|
||||
DB_HOST = "localhost" # 数据库地址
|
||||
DB_PORT = 3306 # 数据库端口
|
||||
DEBUG_MODE = True # 调试模式
|
||||
MAX_CONNECTIONS = 100 # 最大连接数
|
||||
要求:
|
||||
|
||||
配置变量名必须为大写字母和下划线组成
|
||||
|
||||
支持Python基本数据类型(字符串、数字、布尔值、列表、字典)
|
||||
|
||||
🔍 技术实现
|
||||
解析技术
|
||||
使用Python的ast模块进行语法分析
|
||||
|
||||
精确识别配置项的位置和注释
|
||||
|
||||
保持原始格式和缩进
|
||||
|
||||
GUI框架
|
||||
基于PyQt6构建现代化界面
|
||||
|
||||
响应式布局,支持调整窗口大小
|
||||
|
||||
自定义控件适配不同数据类型
|
||||
|
||||
数据持久化
|
||||
规则配置使用JSON格式存储
|
||||
|
||||
支持相对路径和绝对路径
|
||||
|
||||
自动保存用户设置
|
||||
|
||||
📝 使用示例
|
||||
编辑配置文件
|
||||
启动程序,选择配置文件
|
||||
|
||||
在对应的分组中找到要修改的配置项
|
||||
|
||||
编辑值(复选框、数字框、文本框等)
|
||||
|
||||
点击"保存配置"按钮
|
||||
|
||||
确认变更后,程序会自动更新配置文件
|
||||
|
||||
自定义规则
|
||||
点击"管理规则"按钮
|
||||
|
||||
在左侧列表选择配置项
|
||||
|
||||
在右侧设置显示名称、分组、类型等
|
||||
|
||||
设置校验规则(可选)
|
||||
|
||||
保存规则,程序会自动重新加载界面
|
||||
10
__init__.py
10
__init__.py
@ -1,10 +0,0 @@
|
||||
"""
|
||||
配置编辑器模块
|
||||
"""
|
||||
|
||||
__version__ = "1.3"
|
||||
__author__ = "Config Editor"
|
||||
|
||||
from main_window import ConfigEditor
|
||||
|
||||
__all__ = ['ConfigEditor']
|
||||
866
build.sh
866
build.sh
@ -1,724 +1,260 @@
|
||||
#!/bin/bash
|
||||
# 打包脚本:将配置编辑器项目打包成 deb 包
|
||||
|
||||
set -e # 遇到错误时退出
|
||||
# config editor 一键构建脚本 - 修复虚拟环境安装问题
|
||||
set -e
|
||||
|
||||
# ==================== 配置区域 ====================
|
||||
APP_NAME="config-editor"
|
||||
APP_VERSION="1.3"
|
||||
APP_DESCRIPTION="CE编辑器"
|
||||
MAINTAINER="hjy <hujinyang@jspwkj.com.cn>"
|
||||
# 配置变量
|
||||
PACKAGE_NAME="config-editor"
|
||||
VERSION="1.1"
|
||||
ARCHITECTURE="amd64"
|
||||
DEPENDENCIES="python3, python3-venv, python3-pip"
|
||||
PYTHON_VERSION="3.8"
|
||||
VENV_PATH="/opt/$APP_NAME/venv"
|
||||
APP_INSTALL_PATH="/opt/$APP_NAME"
|
||||
# =================================================
|
||||
MAINTAINER="hjy <hjy@pw.com>"
|
||||
|
||||
echo "========================================"
|
||||
echo "开始构建 $APP_NAME (版本 $APP_VERSION)"
|
||||
echo "========================================"
|
||||
echo "将使用虚拟环境: $VENV_PATH"
|
||||
echo "=========================================="
|
||||
echo "Config Editor 构建脚本 - 版本 $VERSION"
|
||||
echo "架构: $ARCHITECTURE"
|
||||
echo "构建时间: $(date)"
|
||||
echo "=========================================="
|
||||
|
||||
# 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 "错误: 虚拟环境不存在。请重新安装应用程序。"
|
||||
# 检查依赖
|
||||
echo "检查构建依赖..."
|
||||
if ! command -v dpkg-deb &> /dev/null; then
|
||||
echo "错误: 需要 dpkg-deb,请安装: sudo apt install dpkg-dev"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境并运行程序
|
||||
cd "\$APP_DIR"
|
||||
source "\$VENV_PATH/bin/activate"
|
||||
exec python main.py "\$@"
|
||||
EOF
|
||||
# 检查源代码是否存在
|
||||
echo "检查源代码..."
|
||||
if [ ! -f "config_editor.py" ]; then
|
||||
echo "错误: 未找到主程序文件 config_editor.py"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$BIN_DIR/$APP_NAME"
|
||||
# 创建构建目录
|
||||
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"
|
||||
|
||||
# 5. 创建桌面入口文件
|
||||
echo "5. 创建桌面入口文件..."
|
||||
cat > "$DESKTOP_DIR/$APP_NAME.desktop" << EOF
|
||||
# 复制应用文件
|
||||
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'
|
||||
[Desktop Entry]
|
||||
Name=Config Editor
|
||||
Name[zh_CN]=配置编辑器
|
||||
Comment=$APP_DESCRIPTION
|
||||
Comment[zh_CN]=用于管理和编辑配置文件
|
||||
Exec=$APP_NAME
|
||||
Icon=$APP_NAME
|
||||
Terminal=false
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Categories=Development;Utility;
|
||||
Name=Config Editor
|
||||
Comment=Configuration Management Tool
|
||||
Exec=/usr/bin/config-editor
|
||||
Icon=config-editor
|
||||
Categories=Development;Utility;Settings;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
Keywords=config;editor;settings;configuration;
|
||||
MimeType=application/x-python;
|
||||
StartupWMClass=ConfigEditor
|
||||
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
|
||||
# 创建虚拟环境安装脚本 - 修复版本
|
||||
echo "创建虚拟环境安装脚本..."
|
||||
cat > "$BUILD_DIR/usr/share/config-editor/setup_venv.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# 安装后脚本 - 创建虚拟环境并安装依赖
|
||||
# 虚拟环境安装脚本 - 修复sudo环境问题
|
||||
|
||||
set -e
|
||||
|
||||
# 更新桌面数据库
|
||||
if [ -d /usr/share/applications ]; then
|
||||
update-desktop-database /usr/share/applications || true
|
||||
fi
|
||||
APP_DIR="/usr/share/config-editor"
|
||||
VENV_DIR="$APP_DIR/venv"
|
||||
|
||||
# 更新图标缓存
|
||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
||||
gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true
|
||||
fi
|
||||
echo "设置Config Editor虚拟环境..."
|
||||
|
||||
# 设置文件权限
|
||||
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。"
|
||||
# 检查是否使用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 "创建 Python 虚拟环境..."
|
||||
python3 -m venv "$VENV_PATH" --system-site-packages
|
||||
echo "创建虚拟环境..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
# 激活虚拟环境并安装依赖
|
||||
echo "在虚拟环境中安装依赖..."
|
||||
source "$VENV_PATH/bin/activate"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: 无法创建虚拟环境"
|
||||
echo "请确保已安装 python3-venv: sudo apt install python3-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 升级 pip
|
||||
pip install --upgrade pip
|
||||
echo "✓ 虚拟环境创建成功"
|
||||
|
||||
# 安装依赖
|
||||
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
|
||||
# 使用虚拟环境中的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 python3 -c "import PyQt6; print('PyQt6 安装成功')" 2>/dev/null; then
|
||||
echo "✅ 虚拟环境设置成功"
|
||||
else
|
||||
echo "❌ 虚拟环境设置失败,尝试重新安装 PyQt6..."
|
||||
pip install --force-reinstall PyQt6
|
||||
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
|
||||
|
||||
# 确保规则文件可写
|
||||
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
|
||||
echo "✓ PyQt6安装成功"
|
||||
|
||||
# 创建用户数据目录
|
||||
mkdir -p "/var/lib/$APP_NAME"
|
||||
chmod 755 "/var/lib/$APP_NAME"
|
||||
# 设置权限
|
||||
chmod -R 755 "$VENV_DIR"
|
||||
|
||||
echo "安装完成!您可以通过以下方式启动配置编辑器:"
|
||||
echo "1. 在应用程序菜单中搜索 'Config Editor'"
|
||||
echo "2. 在终端中运行: $APP_NAME"
|
||||
echo "3. 首次运行需要设置配置文件路径"
|
||||
echo ""
|
||||
echo "虚拟环境位置: $VENV_PATH"
|
||||
echo "应用程序位置: $APP_INSTALL_PATH"
|
||||
echo "虚拟环境设置完成!"
|
||||
echo "虚拟环境位置: $VENV_DIR"
|
||||
echo ""
|
||||
echo "可以测试运行:"
|
||||
echo " $VENV_DIR/bin/python $APP_DIR/config_editor.py"
|
||||
EOF
|
||||
|
||||
chmod 755 "$DEBIAN_DIR/postinst"
|
||||
chmod +x "$BUILD_DIR/usr/share/config-editor/setup_venv.sh"
|
||||
|
||||
# 9. 创建卸载前脚本
|
||||
echo "9. 创建卸载前脚本..."
|
||||
cat > "$DEBIAN_DIR/prerm" << EOF
|
||||
# 创建启动脚本
|
||||
echo "创建启动脚本..."
|
||||
cat > "$BUILD_DIR/usr/bin/config-editor" << 'EOF'
|
||||
#!/bin/bash
|
||||
# 卸载前脚本
|
||||
# Config Editor 启动脚本
|
||||
|
||||
set -e
|
||||
APP_DIR="/usr/share/config-editor"
|
||||
VENV_DIR="$APP_DIR/venv"
|
||||
|
||||
# 检查程序是否正在运行
|
||||
if pgrep -f "$APP_NAME" > /dev/null; then
|
||||
echo "正在停止运行中的配置编辑器..."
|
||||
pkill -f "$APP_NAME" || true
|
||||
sleep 2
|
||||
# 日志文件
|
||||
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
|
||||
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 755 "$DEBIAN_DIR/prerm"
|
||||
chmod +x "$BUILD_DIR/usr/bin/config-editor"
|
||||
|
||||
# 10. 创建卸载后脚本
|
||||
echo "10. 创建卸载后脚本..."
|
||||
cat > "$DEBIAN_DIR/postrm" << EOF
|
||||
# 创建控制文件
|
||||
echo "创建DEBIAN控制文件..."
|
||||
cat > "$BUILD_DIR/DEBIAN/control" << EOF
|
||||
Package: $PACKAGE_NAME
|
||||
Version: $VERSION
|
||||
Architecture: $ARCHITECTURE
|
||||
Depends: python3, python3-venv
|
||||
Maintainer: $MAINTAINER
|
||||
Description: Config Editor with one-click hide feature
|
||||
A PyQt6-based configuration management editor.
|
||||
Features include one-click hide all configuration items.
|
||||
EOF
|
||||
|
||||
# 创建安装后脚本
|
||||
echo "创建安装后脚本..."
|
||||
cat > "$BUILD_DIR/DEBIAN/postinst" << 'EOF'
|
||||
#!/bin/bash
|
||||
# 卸载后脚本
|
||||
# 安装后脚本
|
||||
|
||||
set -e
|
||||
echo "Config Editor 安装完成!"
|
||||
echo ""
|
||||
echo "重要: 需要设置虚拟环境才能运行应用"
|
||||
echo "请执行以下命令完成安装:"
|
||||
echo " sudo /usr/share/config-editor/setup_venv.sh"
|
||||
echo ""
|
||||
echo "注意: 如果之前安装失败,新版本修复了虚拟环境安装问题"
|
||||
echo " 请确保运行上述命令安装PyQt6到正确的虚拟环境中"
|
||||
|
||||
# 更新桌面数据库
|
||||
if [ -d /usr/share/applications ]; then
|
||||
update-desktop-database /usr/share/applications || true
|
||||
fi
|
||||
update-desktop-database /usr/share/applications 2>/dev/null || true
|
||||
|
||||
# 更新图标缓存
|
||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
||||
gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true
|
||||
fi
|
||||
# 设置权限
|
||||
chmod 755 /usr/bin/config-editor
|
||||
chmod 755 /usr/share/config-editor/setup_venv.sh
|
||||
|
||||
# 删除虚拟环境(如果存在)
|
||||
if [ -d "$VENV_PATH" ]; then
|
||||
echo "删除虚拟环境..."
|
||||
rm -rf "$VENV_PATH"
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
# 删除应用程序目录(如果存在)
|
||||
if [ -d "$APP_INSTALL_PATH" ]; then
|
||||
echo "删除应用程序目录..."
|
||||
rm -rf "$APP_INSTALL_PATH"
|
||||
fi
|
||||
chmod +x "$BUILD_DIR/DEBIAN/postinst"
|
||||
|
||||
# 删除系统数据目录
|
||||
if [ -d "/var/lib/$APP_NAME" ]; then
|
||||
echo "删除系统数据目录..."
|
||||
rm -rf "/var/lib/$APP_NAME"
|
||||
fi
|
||||
# 构建deb包
|
||||
echo "构建deb包..."
|
||||
dpkg-deb --build "$BUILD_DIR" "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
|
||||
|
||||
# 删除用户数据(可选)
|
||||
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 "用户数据已删除。"
|
||||
# 验证包
|
||||
if dpkg -I "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" > /dev/null; then
|
||||
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 "❌ 构建失败!"
|
||||
echo "✗ 包验证失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 清理
|
||||
rm -rf "$BUILD_DIR"
|
||||
|
||||
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 "=========================================="
|
||||
|
||||
1823
config_editor.py
Normal file
1823
config_editor.py
Normal file
File diff suppressed because it is too large
Load Diff
11
config_editor_rules.json
Normal file
11
config_editor_rules.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"categories": {
|
||||
"未分类": []
|
||||
},
|
||||
"display_names": {},
|
||||
"tooltips": {},
|
||||
"field_types": {},
|
||||
"field_decimals": {},
|
||||
"hidden": [],
|
||||
"validations": {}
|
||||
}
|
||||
4
config_editor_settings.json
Normal file
4
config_editor_settings.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"config_file_path": "",
|
||||
"last_used": ""
|
||||
}
|
||||
179
config_parser.py
179
config_parser.py
@ -1,179 +0,0 @@
|
||||
'''配置文件解析模块'''
|
||||
|
||||
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
1036
dialogs.py
File diff suppressed because it is too large
Load Diff
117
file_handler.py
117
file_handler.py
@ -1,117 +0,0 @@
|
||||
'''文件读写管理模块'''
|
||||
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
240
login.py
@ -1,240 +0,0 @@
|
||||
'''用户认证登录模块'''
|
||||
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
783
login_dialog.py
@ -1,783 +0,0 @@
|
||||
'''登录对话模块'''
|
||||
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
39
main.py
@ -1,39 +0,0 @@
|
||||
'''项目入口启动程序'''
|
||||
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
1555
main_window.py
File diff suppressed because it is too large
Load Diff
143
models.py
143
models.py
@ -1,143 +0,0 @@
|
||||
'''数据模型定义'''
|
||||
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 +0,0 @@
|
||||
PyQt6>=6.5.0
|
||||
263
utils.py
263
utils.py
@ -1,263 +0,0 @@
|
||||
'''工具函数集合'''
|
||||
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
17
widgets.py
@ -1,17 +0,0 @@
|
||||
'''自定义控件实现'''
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user