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)
|
Config Editor - 配置文件编辑工具
|
||||||
|
一个基于PyQt6开发的Python配置文件可视化编辑工具,支持自动解析Python配置文件中的大写变量,提供直观的GUI界面进行编辑和管理。
|
||||||
一个功能强大的Python配置文件和用户管理工具,提供直观的GUI界面进行配置管理、用户认证和会话安全控制。
|
|
||||||
|
✨ 特性功能
|
||||||
主要特性
|
🎯 核心功能
|
||||||
|
智能解析:自动扫描Python文件,识别文件中的变量配置项
|
||||||
🔐 用户认证系统
|
|
||||||
|
可视化编辑:基于PyQt6的现代化GUI界面,支持多种数据类型编辑
|
||||||
首次登录即注册:新用户首次登录自动创建账户
|
|
||||||
|
分组管理:可自定义分组,按类别组织配置项
|
||||||
二级密码保护:敏感操作需要二级密码验证
|
|
||||||
|
格式保持:保存时保持原始文件格式、注释和缩进
|
||||||
会话超时锁定:空闲超过10分钟自动锁定,需要重新登录
|
|
||||||
|
🔧 编辑支持
|
||||||
安全的密码存储:使用JSON格式加密存储用户凭证
|
多种数据类型:
|
||||||
|
|
||||||
📁 配置文件管理
|
布尔值(CheckBox)
|
||||||
|
|
||||||
智能解析:自动识别Python配置文件中的配置项
|
整数/浮点数(SpinBox)
|
||||||
|
|
||||||
格式保持:编辑时保留原始注释、空格和格式
|
字符串(LineEdit/TextEdit)
|
||||||
|
|
||||||
分组显示:按规则将配置项分组显示在不同标签页
|
JSON/字典/列表(格式化编辑)
|
||||||
|
|
||||||
拖拽排序:支持通过拖拽重新排列配置项顺序
|
校验规则:
|
||||||
|
|
||||||
⚙️ 高级功能
|
必填项验证
|
||||||
|
|
||||||
规则管理:可自定义显示名称、分组、校验规则等
|
数值范围限制
|
||||||
|
|
||||||
加密字段:标记敏感配置项,修改时需要二级密码
|
正则表达式匹配
|
||||||
|
|
||||||
隐藏字段:隐藏不需要显示的配置项
|
自定义错误提示
|
||||||
|
|
||||||
导入导出:支持规则文件的导入导出
|
📊 管理功能
|
||||||
|
显示定制:自定义每个配置项的显示名称和提示信息
|
||||||
校验规则:支持必填、范围、正则表达式等校验
|
|
||||||
|
字段隐藏:标记隐藏字段,支持显示/隐藏切换
|
||||||
🎨 用户界面
|
|
||||||
|
类型推断:自动识别字段类型,支持手动覆盖
|
||||||
现代化UI:基于PyQt6的响应式界面
|
|
||||||
|
规则管理:独立的规则管理界面,支持批量配置
|
||||||
直观操作:拖拽排序、分组管理、批量操作
|
|
||||||
|
🚀 快速开始
|
||||||
实时反馈:修改高亮、校验提示、状态监控
|
环境要求
|
||||||
|
Python 3.8+
|
||||||
主题支持:标准的Fusion主题,视觉舒适
|
|
||||||
|
PyQt6
|
||||||
系统要求
|
|
||||||
操作系统:Linux(已测试Ubuntu 20.04+)
|
构建打包
|
||||||
|
执行build.sh脚本
|
||||||
Python版本:Python 3.8+
|
|
||||||
|
安装部署
|
||||||
依赖:PyQt6 >= 6.5.0
|
bash
|
||||||
|
1.sudo dpkg -i config-editor_1.0_amd64.deb
|
||||||
安装方法
|
2.sudo /usr/share/config-editor/setup_venv.sh
|
||||||
|
在服务器图形化界面,搜索Config Editor点击即可使用
|
||||||
使用deb包安装
|
卸载程序
|
||||||
确保已安装依赖:
|
bash
|
||||||
|
1.sudo dpkg -r config-editor
|
||||||
bash
|
📁 项目结构
|
||||||
sudo apt-get update
|
text
|
||||||
sudo apt-get install python3 python3-venv python3-pip
|
config_editor/
|
||||||
安装deb包:
|
├── config_editor.py # 主程序
|
||||||
|
├── config_editor_rules.json # 规则配置文件
|
||||||
bash
|
├── config_editor_settings.json # 用户设置文件
|
||||||
sudo dpkg -i config-editor_1.3_amd64.deb
|
├── build.sh # 一键打包脚本
|
||||||
|
├── CHANGELOG.md # 变更日志
|
||||||
|
└── README.md # 说明文档
|
||||||
使用指南
|
配置文件说明
|
||||||
|
config_editor_rules.json:存储所有配置项的元数据(分组、显示名、校验规则等)
|
||||||
首次运行设置
|
|
||||||
启动程序:首次运行会显示登录界面
|
config_editor_settings.json:存储用户设置(最近使用的配置文件路径等)
|
||||||
|
|
||||||
用户注册:
|
🖥️ 使用指南
|
||||||
|
1. 首次运行
|
||||||
输入用户名和密码(首次登录自动创建账户)
|
首次启动程序时,会自动弹出配置文件设置对话框:
|
||||||
|
|
||||||
选择账号配置文件保存目录
|
选择要编辑的Python配置文件
|
||||||
|
|
||||||
设置二级密码用于敏感操作验证
|
程序会自动解析配置文件中的大写变量
|
||||||
|
|
||||||
配置文件设置:
|
2. 界面布局
|
||||||
|
顶部信息栏:显示当前配置文件的路径
|
||||||
登录后首次运行需要设置要编辑的配置文件路径
|
|
||||||
|
标签页:按分组显示配置项
|
||||||
支持浏览选择或手动输入文件路径
|
|
||||||
|
编辑区域:每个配置项包含:
|
||||||
可以使用相对路径(相对于程序目录)
|
|
||||||
|
显示名称(可自定义)
|
||||||
主要功能使用
|
|
||||||
|
编辑控件(根据类型自动适配)
|
||||||
1. 配置文件编辑
|
|
||||||
|
变量名(原始名称)
|
||||||
打开文件:通过菜单或工具栏打开Python配置文件
|
|
||||||
|
工具栏:常用操作按钮
|
||||||
编辑配置:直接在界面上修改配置项值
|
|
||||||
|
3. 基本操作
|
||||||
保存更改:保存时显示变更列表确认,保留原始格式
|
打开文件:使用菜单或按钮打开其他配置文件
|
||||||
|
|
||||||
重新加载:随时重新加载配置文件
|
编辑配置:直接在对应的控件中修改值
|
||||||
|
|
||||||
2. 规则管理
|
保存配置:保存修改到原配置文件
|
||||||
|
|
||||||
进入管理界面:点击"管理规则"按钮
|
重新加载:放弃修改,重新读取配置文件
|
||||||
|
|
||||||
配置项管理:设置显示名称、分组、类型、校验规则等
|
4. 高级功能
|
||||||
|
规则管理
|
||||||
隐藏/显示:标记配置项为隐藏,或批量显示/隐藏
|
点击"管理规则"按钮进入规则管理界面:
|
||||||
|
|
||||||
分组管理:创建、删除和重命名分组
|
字段属性:设置显示名称、分组、类型、提示信息等
|
||||||
|
|
||||||
3. 高级操作
|
校验规则:设置最小值、最大值、正则表达式、必填项
|
||||||
|
|
||||||
拖拽排序:在分组内拖拽配置项标签重新排序
|
分组管理:添加、删除分组,调整字段分组
|
||||||
|
|
||||||
加密字段:标记敏感字段,修改时需要二级密码
|
字段筛选
|
||||||
|
搜索功能:快速查找配置项
|
||||||
批量操作:一键隐藏所有配置项,或全部显示
|
|
||||||
|
隐藏字段:支持显示/隐藏被标记为隐藏的配置项
|
||||||
导入导出:导入/导出规则文件,方便备份和迁移
|
|
||||||
|
⚙️ 配置规则详解
|
||||||
安全特性
|
分组配置
|
||||||
|
json
|
||||||
会话管理
|
"categories": {
|
||||||
|
"数据库配置": ["DB_HOST", "DB_PORT"],
|
||||||
程序监控用户活动,空闲10分钟后自动锁定
|
"应用配置": ["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]+$"
|
||||||
|
}
|
||||||
项目结构
|
}
|
||||||
text
|
🎨 支持的Python配置文件格式
|
||||||
config-editor/
|
程序可以解析以下格式的配置:
|
||||||
├── main.py # 程序入口
|
|
||||||
├── main_window.py # 主窗口实现
|
python
|
||||||
├── login/ # 登录认证模块
|
# 配置文件示例 config.py
|
||||||
│ ├── login.py # 用户认证管理器
|
DB_HOST = "localhost" # 数据库地址
|
||||||
│ └── login_dialog.py # 登录对话框
|
DB_PORT = 3306 # 数据库端口
|
||||||
├── dialogs.py # 各种对话框
|
DEBUG_MODE = True # 调试模式
|
||||||
├── config_parser.py # 配置文件解析器
|
MAX_CONNECTIONS = 100 # 最大连接数
|
||||||
├── file_handler.py # 文件读写管理
|
要求:
|
||||||
├── models.py # 数据模型定义
|
|
||||||
├── utils.py # 工具函数集合
|
配置变量名必须为大写字母和下划线组成
|
||||||
├── widgets.py # 自定义控件
|
|
||||||
├── build.sh # 打包脚本
|
支持Python基本数据类型(字符串、数字、布尔值、列表、字典)
|
||||||
├── requirements.txt # 依赖列表
|
|
||||||
└── README.md # 本文档
|
🔍 技术实现
|
||||||
配置文件格式
|
解析技术
|
||||||
|
使用Python的ast模块进行语法分析
|
||||||
程序支持标准的Python配置文件格式:
|
|
||||||
|
精确识别配置项的位置和注释
|
||||||
python
|
|
||||||
'''
|
保持原始格式和缩进
|
||||||
# 数据库配置
|
|
||||||
DATABASE_HOST = "localhost"
|
GUI框架
|
||||||
DATABASE_PORT = 3306
|
基于PyQt6构建现代化界面
|
||||||
DATABASE_USER = "admin"
|
|
||||||
DATABASE_PASSWORD = "secret_password" # 可以标记为加密字段
|
响应式布局,支持调整窗口大小
|
||||||
|
|
||||||
# 应用配置
|
自定义控件适配不同数据类型
|
||||||
APP_NAME = "My Application"
|
|
||||||
DEBUG_MODE = False
|
数据持久化
|
||||||
LOG_LEVEL = "INFO"
|
规则配置使用JSON格式存储
|
||||||
|
|
||||||
# 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包
|
|
||||||
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"
|
PACKAGE_NAME="config-editor"
|
||||||
APP_VERSION="1.3"
|
VERSION="1.1"
|
||||||
APP_DESCRIPTION="CE编辑器"
|
|
||||||
MAINTAINER="hjy <hujinyang@jspwkj.com.cn>"
|
|
||||||
ARCHITECTURE="amd64"
|
ARCHITECTURE="amd64"
|
||||||
DEPENDENCIES="python3, python3-venv, python3-pip"
|
MAINTAINER="hjy <hjy@pw.com>"
|
||||||
PYTHON_VERSION="3.8"
|
|
||||||
VENV_PATH="/opt/$APP_NAME/venv"
|
|
||||||
APP_INSTALL_PATH="/opt/$APP_NAME"
|
|
||||||
# =================================================
|
|
||||||
|
|
||||||
echo "========================================"
|
echo "=========================================="
|
||||||
echo "开始构建 $APP_NAME (版本 $APP_VERSION)"
|
echo "Config Editor 构建脚本 - 版本 $VERSION"
|
||||||
echo "========================================"
|
echo "架构: $ARCHITECTURE"
|
||||||
echo "将使用虚拟环境: $VENV_PATH"
|
echo "构建时间: $(date)"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
# 1. 清理之前的构建
|
# 检查依赖
|
||||||
echo "1. 清理之前的构建..."
|
echo "检查构建依赖..."
|
||||||
rm -rf build/ dist/ deb_dist/ *.egg-info/ 2>/dev/null || true
|
if ! command -v dpkg-deb &> /dev/null; then
|
||||||
rm -rf /tmp/$APP_NAME-* 2>/dev/null || true
|
echo "错误: 需要 dpkg-deb,请安装: sudo apt install dpkg-dev"
|
||||||
|
|
||||||
# 2. 创建构建目录结构
|
|
||||||
echo "2. 创建构建目录结构..."
|
|
||||||
BUILD_ROOT="/tmp/${APP_NAME}-${APP_VERSION}"
|
|
||||||
DEBIAN_DIR="$BUILD_ROOT/DEBIAN"
|
|
||||||
APP_DIR="$BUILD_ROOT$APP_INSTALL_PATH"
|
|
||||||
BIN_DIR="$BUILD_ROOT/usr/bin"
|
|
||||||
DESKTOP_DIR="$BUILD_ROOT/usr/share/applications"
|
|
||||||
ICON_DIR="$BUILD_ROOT/usr/share/icons/hicolor"
|
|
||||||
MIME_DIR="$BUILD_ROOT/usr/share/mime/packages"
|
|
||||||
VAR_LIB_DIR="$BUILD_ROOT/var/lib/$APP_NAME"
|
|
||||||
|
|
||||||
# 清理并重新创建目录
|
|
||||||
rm -rf "$BUILD_ROOT"
|
|
||||||
mkdir -p "$DEBIAN_DIR"
|
|
||||||
mkdir -p "$APP_DIR"
|
|
||||||
mkdir -p "$BIN_DIR"
|
|
||||||
mkdir -p "$DESKTOP_DIR"
|
|
||||||
mkdir -p "$ICON_DIR/48x48/apps"
|
|
||||||
mkdir -p "$ICON_DIR/64x64/apps"
|
|
||||||
mkdir -p "$ICON_DIR/128x128/apps"
|
|
||||||
mkdir -p "$MIME_DIR"
|
|
||||||
mkdir -p "$VAR_LIB_DIR"
|
|
||||||
|
|
||||||
# 3. 复制项目文件
|
|
||||||
echo "3. 复制项目文件..."
|
|
||||||
cp -r *.py "$APP_DIR/"
|
|
||||||
cp -r login/ "$APP_DIR/"
|
|
||||||
cp -r requirements.txt "$APP_DIR/"
|
|
||||||
|
|
||||||
# 清理不必要的文件
|
|
||||||
find "$APP_DIR" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
||||||
find "$APP_DIR" -name "*.pyc" -delete 2>/dev/null || true
|
|
||||||
find "$APP_DIR" -name ".DS_Store" -delete 2>/dev/null || true
|
|
||||||
|
|
||||||
# 4. 创建启动脚本(使用虚拟环境中的 Python)
|
|
||||||
echo "4. 创建启动脚本..."
|
|
||||||
cat > "$BIN_DIR/$APP_NAME" << EOF
|
|
||||||
#!/bin/bash
|
|
||||||
# 配置编辑器启动脚本(使用虚拟环境)
|
|
||||||
|
|
||||||
# 应用程序安装路径
|
|
||||||
APP_DIR="$APP_INSTALL_PATH"
|
|
||||||
VENV_PATH="$VENV_PATH"
|
|
||||||
|
|
||||||
# 检查虚拟环境是否存在
|
|
||||||
if [ ! -f "\$VENV_PATH/bin/activate" ]; then
|
|
||||||
echo "错误: 虚拟环境不存在。请重新安装应用程序。"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 激活虚拟环境并运行程序
|
# 检查源代码是否存在
|
||||||
cd "\$APP_DIR"
|
echo "检查源代码..."
|
||||||
source "\$VENV_PATH/bin/activate"
|
if [ ! -f "config_editor.py" ]; then
|
||||||
exec python main.py "\$@"
|
echo "错误: 未找到主程序文件 config_editor.py"
|
||||||
EOF
|
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. 创建桌面入口文件..."
|
echo "复制应用文件..."
|
||||||
cat > "$DESKTOP_DIR/$APP_NAME.desktop" << EOF
|
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]
|
[Desktop Entry]
|
||||||
Name=Config Editor
|
Version=1.0
|
||||||
Name[zh_CN]=配置编辑器
|
|
||||||
Comment=$APP_DESCRIPTION
|
|
||||||
Comment[zh_CN]=用于管理和编辑配置文件
|
|
||||||
Exec=$APP_NAME
|
|
||||||
Icon=$APP_NAME
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
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
|
StartupNotify=true
|
||||||
Keywords=config;editor;settings;configuration;
|
StartupWMClass=ConfigEditor
|
||||||
MimeType=application/x-python;
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# 6. 创建图标(如果没有提供图标,则生成一个简单的)
|
# 创建虚拟环境安装脚本 - 修复版本
|
||||||
echo "6. 创建图标..."
|
echo "创建虚拟环境安装脚本..."
|
||||||
if [ -f "logo.png" ]; then
|
cat > "$BUILD_DIR/usr/share/config-editor/setup_venv.sh" << 'EOF'
|
||||||
# 如果有现有图标,使用它
|
|
||||||
cp logo.png "$ICON_DIR/48x48/apps/$APP_NAME.png"
|
|
||||||
cp logo.png "$ICON_DIR/64x64/apps/$APP_NAME.png"
|
|
||||||
cp logo.png "$ICON_DIR/128x128/apps/$APP_NAME.png"
|
|
||||||
else
|
|
||||||
# 如果没有图标,创建一个简单的占位符
|
|
||||||
echo "未找到图标文件,创建简单图标..."
|
|
||||||
|
|
||||||
# 使用 ImageMagick 创建图标(如果可用)
|
|
||||||
if command -v convert &> /dev/null; then
|
|
||||||
# 创建 48x48 图标
|
|
||||||
convert -size 48x48 xc:#2196F3 \
|
|
||||||
-fill white -pointsize 24 -gravity center -annotate 0 "CE" \
|
|
||||||
"$ICON_DIR/48x48/apps/$APP_NAME.png"
|
|
||||||
|
|
||||||
# 创建 64x64 图标
|
|
||||||
convert -size 64x64 xc:#2196F3 \
|
|
||||||
-fill white -pointsize 32 -gravity center -annotate 0 "CE" \
|
|
||||||
"$ICON_DIR/64x64/apps/$APP_NAME.png"
|
|
||||||
|
|
||||||
# 创建 128x128 图标
|
|
||||||
convert -size 128x128 xc:#2196F3 \
|
|
||||||
-fill white -pointsize 48 -gravity center -annotate 0 "CE" \
|
|
||||||
"$ICON_DIR/128x128/apps/$APP_NAME.png"
|
|
||||||
else
|
|
||||||
echo "警告: 未安装 ImageMagick (convert),跳过图标生成。"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 7. 创建 DEBIAN 控制文件
|
|
||||||
echo "7. 创建 DEBIAN 控制文件..."
|
|
||||||
cat > "$DEBIAN_DIR/control" << EOF
|
|
||||||
Package: $APP_NAME
|
|
||||||
Version: $APP_VERSION
|
|
||||||
Section: utils
|
|
||||||
Priority: optional
|
|
||||||
Architecture: $ARCHITECTURE
|
|
||||||
Depends: $DEPENDENCIES
|
|
||||||
Maintainer: $MAINTAINER
|
|
||||||
Description: $APP_DESCRIPTION
|
|
||||||
配置编辑器是一个功能强大的工具,用于编辑和管理各种配置文件。
|
|
||||||
支持以下功能:
|
|
||||||
* 智能解析配置文件
|
|
||||||
* 分组显示配置项
|
|
||||||
* 拖拽重新排序
|
|
||||||
* 加密配置项保护
|
|
||||||
* 会话超时锁定
|
|
||||||
* 用户认证系统
|
|
||||||
* 规则管理
|
|
||||||
* 在虚拟环境中运行,避免依赖冲突
|
|
||||||
Homepage: https://github.com/yourusername/config-editor
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 8. 创建安装后脚本(创建虚拟环境)
|
|
||||||
echo "8. 创建安装后脚本..."
|
|
||||||
cat > "$DEBIAN_DIR/postinst" << EOF
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 安装后脚本 - 创建虚拟环境并安装依赖
|
# 虚拟环境安装脚本 - 修复sudo环境问题
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# 更新桌面数据库
|
APP_DIR="/usr/share/config-editor"
|
||||||
if [ -d /usr/share/applications ]; then
|
VENV_DIR="$APP_DIR/venv"
|
||||||
update-desktop-database /usr/share/applications || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 更新图标缓存
|
echo "设置Config Editor虚拟环境..."
|
||||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
|
||||||
gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 设置文件权限
|
# 检查是否使用sudo
|
||||||
chmod 755 $APP_INSTALL_PATH/*.py
|
if [ "$EUID" -ne 0 ]; then
|
||||||
chmod 755 $APP_INSTALL_PATH/login/*.py
|
echo "错误: 请使用sudo运行此脚本"
|
||||||
chmod 755 /usr/bin/$APP_NAME
|
echo "命令: sudo $0"
|
||||||
|
|
||||||
# 创建虚拟环境目录
|
|
||||||
echo "创建虚拟环境目录..."
|
|
||||||
mkdir -p "$VENV_PATH"
|
|
||||||
|
|
||||||
# 检查 Python3 是否可用
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "错误: Python3 未安装。请安装 python3。"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 清理旧的虚拟环境(如果存在)
|
||||||
|
if [ -d "$VENV_DIR" ]; then
|
||||||
|
echo "清理旧的虚拟环境..."
|
||||||
|
rm -rf "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# 创建虚拟环境
|
# 创建虚拟环境
|
||||||
echo "创建 Python 虚拟环境..."
|
echo "创建虚拟环境..."
|
||||||
python3 -m venv "$VENV_PATH" --system-site-packages
|
python3 -m venv "$VENV_DIR"
|
||||||
|
|
||||||
# 激活虚拟环境并安装依赖
|
if [ $? -ne 0 ]; then
|
||||||
echo "在虚拟环境中安装依赖..."
|
echo "错误: 无法创建虚拟环境"
|
||||||
source "$VENV_PATH/bin/activate"
|
echo "请确保已安装 python3-venv: sudo apt install python3-venv"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 升级 pip
|
echo "✓ 虚拟环境创建成功"
|
||||||
pip install --upgrade pip
|
|
||||||
|
|
||||||
# 安装依赖
|
# 使用虚拟环境中的pip安装PyQt6 - 关键修复:使用绝对路径
|
||||||
if [ -f "$APP_INSTALL_PATH/requirements.txt" ]; then
|
echo "安装PyQt6..."
|
||||||
echo "从 requirements.txt 安装依赖..."
|
if ! "$VENV_DIR/bin/python" -m pip install PyQt6 2>/dev/null; then
|
||||||
pip install -r "$APP_INSTALL_PATH/requirements.txt"
|
echo "尝试使用国内镜像安装..."
|
||||||
else
|
"$VENV_DIR/bin/python" -m pip install PyQt6 -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||||
echo "安装 PyQt6..."
|
|
||||||
pip install PyQt6
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 验证安装
|
# 验证安装
|
||||||
if python3 -c "import PyQt6; print('PyQt6 安装成功')" 2>/dev/null; then
|
if ! "$VENV_DIR/bin/python" -c "import PyQt6" 2>/dev/null; then
|
||||||
echo "✅ 虚拟环境设置成功"
|
echo "错误: PyQt6安装失败"
|
||||||
else
|
echo "请尝试手动安装:"
|
||||||
echo "❌ 虚拟环境设置失败,尝试重新安装 PyQt6..."
|
echo " sudo $VENV_DIR/bin/pip install PyQt6"
|
||||||
pip install --force-reinstall PyQt6
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 确保规则文件可写
|
echo "✓ PyQt6安装成功"
|
||||||
touch "$APP_INSTALL_PATH/config_editor_rules.json" 2>/dev/null || true
|
|
||||||
chmod 666 "$APP_INSTALL_PATH/config_editor_rules.json" 2>/dev/null || true
|
|
||||||
|
|
||||||
# 创建用户数据目录
|
# 设置权限
|
||||||
mkdir -p "/var/lib/$APP_NAME"
|
chmod -R 755 "$VENV_DIR"
|
||||||
chmod 755 "/var/lib/$APP_NAME"
|
|
||||||
|
|
||||||
echo "安装完成!您可以通过以下方式启动配置编辑器:"
|
|
||||||
echo "1. 在应用程序菜单中搜索 'Config Editor'"
|
|
||||||
echo "2. 在终端中运行: $APP_NAME"
|
|
||||||
echo "3. 首次运行需要设置配置文件路径"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "虚拟环境位置: $VENV_PATH"
|
echo "虚拟环境设置完成!"
|
||||||
echo "应用程序位置: $APP_INSTALL_PATH"
|
echo "虚拟环境位置: $VENV_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "可以测试运行:"
|
||||||
|
echo " $VENV_DIR/bin/python $APP_DIR/config_editor.py"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 755 "$DEBIAN_DIR/postinst"
|
chmod +x "$BUILD_DIR/usr/share/config-editor/setup_venv.sh"
|
||||||
|
|
||||||
# 9. 创建卸载前脚本
|
# 创建启动脚本
|
||||||
echo "9. 创建卸载前脚本..."
|
echo "创建启动脚本..."
|
||||||
cat > "$DEBIAN_DIR/prerm" << EOF
|
cat > "$BUILD_DIR/usr/bin/config-editor" << 'EOF'
|
||||||
#!/bin/bash
|
#!/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
|
LOG_DIR="${HOME}/.config/config-editor"
|
||||||
echo "正在停止运行中的配置编辑器..."
|
mkdir -p "$LOG_DIR"
|
||||||
pkill -f "$APP_NAME" || true
|
LOG_FILE="$LOG_DIR/startup.log"
|
||||||
sleep 2
|
|
||||||
|
# 记录启动信息
|
||||||
|
{
|
||||||
|
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
|
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
|
EOF
|
||||||
|
|
||||||
chmod 755 "$DEBIAN_DIR/prerm"
|
chmod +x "$BUILD_DIR/usr/bin/config-editor"
|
||||||
|
|
||||||
# 10. 创建卸载后脚本
|
# 创建控制文件
|
||||||
echo "10. 创建卸载后脚本..."
|
echo "创建DEBIAN控制文件..."
|
||||||
cat > "$DEBIAN_DIR/postrm" << EOF
|
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
|
#!/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 2>/dev/null || true
|
||||||
update-desktop-database /usr/share/applications || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 更新图标缓存
|
# 设置权限
|
||||||
if command -v gtk-update-icon-cache &> /dev/null; then
|
chmod 755 /usr/bin/config-editor
|
||||||
gtk-update-icon-cache -f -t /usr/share/icons/hicolor || true
|
chmod 755 /usr/share/config-editor/setup_venv.sh
|
||||||
fi
|
|
||||||
|
|
||||||
# 删除虚拟环境(如果存在)
|
exit 0
|
||||||
if [ -d "$VENV_PATH" ]; then
|
EOF
|
||||||
echo "删除虚拟环境..."
|
|
||||||
rm -rf "$VENV_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 删除应用程序目录(如果存在)
|
chmod +x "$BUILD_DIR/DEBIAN/postinst"
|
||||||
if [ -d "$APP_INSTALL_PATH" ]; then
|
|
||||||
echo "删除应用程序目录..."
|
|
||||||
rm -rf "$APP_INSTALL_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 删除系统数据目录
|
# 构建deb包
|
||||||
if [ -d "/var/lib/$APP_NAME" ]; then
|
echo "构建deb包..."
|
||||||
echo "删除系统数据目录..."
|
dpkg-deb --build "$BUILD_DIR" "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
|
||||||
rm -rf "/var/lib/$APP_NAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 删除用户数据(可选)
|
# 验证包
|
||||||
echo "是否要删除用户数据?(n/yes)"
|
if dpkg -I "${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb" > /dev/null; then
|
||||||
echo "这包括:"
|
echo "✓ 包验证成功"
|
||||||
echo " - ~/.config_editor/ 目录"
|
|
||||||
echo " - ~/.config/config-editor/ 目录"
|
|
||||||
read -p "输入 'yes' 确认删除,其他跳过: " choice
|
|
||||||
if [ "\$choice" = "yes" ]; then
|
|
||||||
rm -rf ~/.config_editor 2>/dev/null || true
|
|
||||||
rm -rf ~/.config/config-editor 2>/dev/null || true
|
|
||||||
echo "用户数据已删除。"
|
|
||||||
else
|
else
|
||||||
echo "用户数据保留。"
|
echo "✗ 包验证失败"
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 755 "$DEBIAN_DIR/postrm"
|
|
||||||
|
|
||||||
# 11. 创建版权文件
|
|
||||||
echo "11. 创建版权文件..."
|
|
||||||
cat > "$DEBIAN_DIR/copyright" << EOF
|
|
||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: $APP_NAME
|
|
||||||
Source: https://github.com/yourusername/config-editor
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: $(date +%Y) Your Name
|
|
||||||
License: MIT
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
.
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
.
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 12. 设置文件权限
|
|
||||||
echo "12. 设置文件权限..."
|
|
||||||
find "$BUILD_ROOT" -type f -exec chmod 644 {} \;
|
|
||||||
find "$BUILD_ROOT" -type d -exec chmod 755 {} \;
|
|
||||||
chmod -R 755 "$DEBIAN_DIR"/* 2>/dev/null || true
|
|
||||||
chmod 755 "$BIN_DIR/$APP_NAME"
|
|
||||||
chmod 755 "$APP_DIR"/*.py 2>/dev/null || true
|
|
||||||
chmod 755 "$APP_DIR"/login/*.py 2>/dev/null || true
|
|
||||||
|
|
||||||
# 13. 创建虚拟环境占位符目录
|
|
||||||
echo "13. 创建虚拟环境占位符目录..."
|
|
||||||
mkdir -p "$BUILD_ROOT$VENV_PATH"
|
|
||||||
touch "$BUILD_ROOT$VENV_PATH/.placeholder"
|
|
||||||
echo "这是一个虚拟环境占位符,将在安装时创建实际的虚拟环境。" > "$BUILD_ROOT$VENV_PATH/README.txt"
|
|
||||||
|
|
||||||
# 14. 创建配置文件示例
|
|
||||||
echo "14. 创建配置文件示例..."
|
|
||||||
cat > "$VAR_LIB_DIR/config_example.py" << 'EOF'
|
|
||||||
"""配置文件示例"""
|
|
||||||
# 数据库配置
|
|
||||||
DATABASE_HOST = "localhost"
|
|
||||||
DATABASE_PORT = 3306
|
|
||||||
DATABASE_USER = "admin"
|
|
||||||
DATABASE_PASSWORD = "secret_password"
|
|
||||||
DATABASE_NAME = "myapp_db"
|
|
||||||
|
|
||||||
# 应用配置
|
|
||||||
APP_NAME = "My Application"
|
|
||||||
DEBUG_MODE = False
|
|
||||||
LOG_LEVEL = "INFO"
|
|
||||||
MAX_CONNECTIONS = 100
|
|
||||||
|
|
||||||
# API配置
|
|
||||||
API_KEY = "your_api_key_here"
|
|
||||||
API_TIMEOUT = 30
|
|
||||||
API_RETRIES = 3
|
|
||||||
|
|
||||||
# 服务配置
|
|
||||||
ENABLE_CACHE = True
|
|
||||||
CACHE_SIZE = 1024
|
|
||||||
ENABLE_SSL = True
|
|
||||||
|
|
||||||
# 邮件配置
|
|
||||||
SMTP_SERVER = "smtp.example.com"
|
|
||||||
SMTP_PORT = 587
|
|
||||||
EMAIL_FROM = "noreply@example.com"
|
|
||||||
|
|
||||||
# 安全配置
|
|
||||||
SESSION_TIMEOUT = 3600
|
|
||||||
PASSWORD_MIN_LENGTH = 8
|
|
||||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
|
||||||
|
|
||||||
# 第三方服务配置
|
|
||||||
STRIPE_API_KEY = "sk_test_1234567890"
|
|
||||||
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
|
|
||||||
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 15. 创建虚拟环境初始化脚本
|
|
||||||
echo "15. 创建虚拟环境初始化脚本..."
|
|
||||||
cat > "$APP_DIR/init_venv.sh" << EOF
|
|
||||||
#!/bin/bash
|
|
||||||
# 虚拟环境初始化脚本
|
|
||||||
|
|
||||||
VENV_PATH="$VENV_PATH"
|
|
||||||
APP_DIR="$APP_INSTALL_PATH"
|
|
||||||
|
|
||||||
echo "初始化虚拟环境..."
|
|
||||||
echo "虚拟环境路径: \$VENV_PATH"
|
|
||||||
echo "应用路径: \$APP_DIR"
|
|
||||||
|
|
||||||
# 检查是否已存在虚拟环境
|
|
||||||
if [ -f "\$VENV_PATH/bin/activate" ]; then
|
|
||||||
echo "虚拟环境已存在,重新创建吗?(y/N)"
|
|
||||||
read -r response
|
|
||||||
if [[ ! \$response =~ ^[Yy]$ ]]; then
|
|
||||||
echo "操作取消。"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
rm -rf "\$VENV_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建虚拟环境
|
|
||||||
echo "创建新的虚拟环境..."
|
|
||||||
python3 -m venv "\$VENV_PATH" --system-site-packages
|
|
||||||
|
|
||||||
# 激活虚拟环境
|
|
||||||
source "\$VENV_PATH/bin/activate"
|
|
||||||
|
|
||||||
# 升级pip
|
|
||||||
pip install --upgrade pip
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
if [ -f "\$APP_DIR/requirements.txt" ]; then
|
|
||||||
echo "安装依赖..."
|
|
||||||
pip install -r "\$APP_DIR/requirements.txt"
|
|
||||||
else
|
|
||||||
echo "安装 PyQt6..."
|
|
||||||
pip install PyQt6
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 验证安装
|
|
||||||
if python3 -c "import PyQt6" &> /dev/null; then
|
|
||||||
echo "✅ 虚拟环境初始化成功!"
|
|
||||||
echo ""
|
|
||||||
echo "使用方法:"
|
|
||||||
echo "1. 激活虚拟环境: source \$VENV_PATH/bin/activate"
|
|
||||||
echo "2. 运行程序: cd \$APP_DIR && python main.py"
|
|
||||||
echo "3. 退出虚拟环境: deactivate"
|
|
||||||
else
|
|
||||||
echo "❌ 虚拟环境初始化失败!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x "$APP_DIR/init_venv.sh"
|
|
||||||
|
|
||||||
# 16. 创建虚拟环境管理脚本
|
|
||||||
echo "16. 创建虚拟环境管理脚本..."
|
|
||||||
cat > "$APP_DIR/manage_venv.py" << 'EOF'
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
虚拟环境管理脚本
|
|
||||||
用于检查和维护配置编辑器的虚拟环境
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import venv
|
|
||||||
|
|
||||||
VENV_PATH = "/opt/config-editor/venv"
|
|
||||||
APP_DIR = "/opt/config-editor"
|
|
||||||
|
|
||||||
def check_venv():
|
|
||||||
"""检查虚拟环境状态"""
|
|
||||||
print("检查虚拟环境状态...")
|
|
||||||
print(f"虚拟环境路径: {VENV_PATH}")
|
|
||||||
|
|
||||||
# 检查虚拟环境是否存在
|
|
||||||
if not os.path.exists(os.path.join(VENV_PATH, "bin", "activate")):
|
|
||||||
print("❌ 虚拟环境不存在")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查 Python 是否可用
|
|
||||||
venv_python = os.path.join(VENV_PATH, "bin", "python3")
|
|
||||||
if not os.path.exists(venv_python):
|
|
||||||
print("❌ 虚拟环境中的 Python 不存在")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查 PyQt6 是否已安装
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[venv_python, "-c", "import PyQt6; print('✅ PyQt6 已安装')"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(result.stdout.strip())
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("❌ PyQt6 未安装或有问题")
|
|
||||||
print(f"错误信息: {result.stderr}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 检查时出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def recreate_venv():
|
|
||||||
"""重新创建虚拟环境"""
|
|
||||||
print("重新创建虚拟环境...")
|
|
||||||
|
|
||||||
# 删除现有虚拟环境
|
|
||||||
if os.path.exists(VENV_PATH):
|
|
||||||
print(f"删除现有虚拟环境: {VENV_PATH}")
|
|
||||||
subprocess.run(["rm", "-rf", VENV_PATH], check=False)
|
|
||||||
|
|
||||||
# 创建新虚拟环境
|
|
||||||
print("创建新虚拟环境...")
|
|
||||||
try:
|
|
||||||
venv.create(VENV_PATH, with_pip=True, system_site_packages=True)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 创建虚拟环境失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
print("安装依赖...")
|
|
||||||
venv_pip = os.path.join(VENV_PATH, "bin", "pip3")
|
|
||||||
requirements_file = os.path.join(APP_DIR, "requirements.txt")
|
|
||||||
|
|
||||||
if os.path.exists(requirements_file):
|
|
||||||
print(f"从 {requirements_file} 安装依赖...")
|
|
||||||
result = subprocess.run([venv_pip, "install", "-r", requirements_file], capture_output=True, text=True)
|
|
||||||
else:
|
|
||||||
print("安装 PyQt6...")
|
|
||||||
result = subprocess.run([venv_pip, "install", "PyQt6"], capture_output=True, text=True)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 安装依赖失败: {result.stderr}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✅ 虚拟环境重新创建成功!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def show_venv_info():
|
|
||||||
"""显示虚拟环境信息"""
|
|
||||||
print("虚拟环境信息:")
|
|
||||||
print(f"路径: {VENV_PATH}")
|
|
||||||
|
|
||||||
if os.path.exists(os.path.join(VENV_PATH, "bin", "python3")):
|
|
||||||
venv_python = os.path.join(VENV_PATH, "bin", "python3")
|
|
||||||
result = subprocess.run([venv_python, "--version"], capture_output=True, text=True)
|
|
||||||
print(f"Python 版本: {result.stdout.strip()}")
|
|
||||||
|
|
||||||
result = subprocess.run([venv_python, "-m", "pip", "list"], capture_output=True, text=True)
|
|
||||||
print("\n已安装的包:")
|
|
||||||
print(result.stdout)
|
|
||||||
else:
|
|
||||||
print("❌ 虚拟环境不存在或已损坏")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主函数"""
|
|
||||||
print("配置编辑器虚拟环境管理工具")
|
|
||||||
print("=" * 40)
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("使用方法:")
|
|
||||||
print(" python manage_venv.py check - 检查虚拟环境状态")
|
|
||||||
print(" python manage_venv.py recreate - 重新创建虚拟环境")
|
|
||||||
print(" python manage_venv.py info - 显示虚拟环境信息")
|
|
||||||
return
|
|
||||||
|
|
||||||
command = sys.argv[1]
|
|
||||||
|
|
||||||
if command == "check":
|
|
||||||
if check_venv():
|
|
||||||
print("✅ 虚拟环境状态正常")
|
|
||||||
else:
|
|
||||||
print("❌ 虚拟环境有问题")
|
|
||||||
print("建议运行: python manage_venv.py recreate")
|
|
||||||
elif command == "recreate":
|
|
||||||
if recreate_venv():
|
|
||||||
print("✅ 虚拟环境重新创建成功")
|
|
||||||
else:
|
|
||||||
print("❌ 重新创建虚拟环境失败")
|
|
||||||
elif command == "info":
|
|
||||||
show_venv_info()
|
|
||||||
else:
|
|
||||||
print(f"未知命令: {command}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x "$APP_DIR/manage_venv.py"
|
|
||||||
|
|
||||||
# 17. 创建 README 文件
|
|
||||||
echo "17. 创建 README 文件..."
|
|
||||||
cat > "$APP_DIR/README.md" << EOF
|
|
||||||
# 配置编辑器 ($APP_NAME)
|
|
||||||
|
|
||||||
版本: $APP_VERSION
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
配置编辑器是一个功能强大的工具,用于编辑和管理各种配置文件。
|
|
||||||
|
|
||||||
## 虚拟环境
|
|
||||||
此应用程序在独立的虚拟环境中运行,以避免与其他 Python 应用程序的依赖冲突。
|
|
||||||
|
|
||||||
虚拟环境位置: $VENV_PATH
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 从菜单启动
|
|
||||||
1. 在应用程序菜单中搜索 "Config Editor"
|
|
||||||
2. 点击图标启动
|
|
||||||
|
|
||||||
### 从终端启动
|
|
||||||
\`\`\`bash
|
|
||||||
$APP_NAME
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 手动激活虚拟环境
|
|
||||||
\`\`\`bash
|
|
||||||
source $VENV_PATH/bin/activate
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
python main.py
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 管理虚拟环境
|
|
||||||
|
|
||||||
### 检查虚拟环境状态
|
|
||||||
\`\`\`bash
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
python manage_venv.py check
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 重新创建虚拟环境
|
|
||||||
\`\`\`bash
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
python manage_venv.py recreate
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
或使用脚本:
|
|
||||||
\`\`\`bash
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
./init_venv.sh
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 查看虚拟环境信息
|
|
||||||
\`\`\`bash
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
python manage_venv.py info
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 配置文件示例
|
|
||||||
配置文件示例位于: /var/lib/$APP_NAME/config_example.py
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 1. 虚拟环境问题
|
|
||||||
如果应用程序无法启动,可能是虚拟环境有问题:
|
|
||||||
\`\`\`bash
|
|
||||||
cd $APP_INSTALL_PATH
|
|
||||||
./init_venv.sh
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 2. 权限问题
|
|
||||||
如果无法保存设置,确保以下目录可写:
|
|
||||||
- $APP_INSTALL_PATH/config_editor_rules.json
|
|
||||||
- ~/.config_editor/
|
|
||||||
- ~/.config/config-editor/
|
|
||||||
|
|
||||||
### 3. 依赖问题
|
|
||||||
如果 PyQt6 有问题,重新安装:
|
|
||||||
\`\`\`bash
|
|
||||||
source $VENV_PATH/bin/activate
|
|
||||||
pip install --force-reinstall PyQt6
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 支持
|
|
||||||
如有问题,请联系: $MAINTAINER
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 18. 计算安装大小
|
|
||||||
echo "18. 计算安装大小..."
|
|
||||||
INSTALLED_SIZE=$(du -sk "$BUILD_ROOT" | cut -f1)
|
|
||||||
sed -i "/^Architecture:/a Installed-Size: $INSTALLED_SIZE" "$DEBIAN_DIR/control"
|
|
||||||
|
|
||||||
# 19. 构建 deb 包
|
|
||||||
echo "19. 构建 deb 包..."
|
|
||||||
PACKAGE_NAME="${APP_NAME}_${APP_VERSION}_${ARCHITECTURE}.deb"
|
|
||||||
dpkg-deb --build "$BUILD_ROOT" "$PACKAGE_NAME"
|
|
||||||
|
|
||||||
# 20. 验证 deb 包
|
|
||||||
echo "20. 验证 deb 包..."
|
|
||||||
if [ -f "$PACKAGE_NAME" ]; then
|
|
||||||
echo "✅ 构建成功: $PACKAGE_NAME"
|
|
||||||
echo "文件大小: $(du -h "$PACKAGE_NAME" | cut -f1)"
|
|
||||||
|
|
||||||
# 显示包信息
|
|
||||||
echo -e "\n包内容:"
|
|
||||||
dpkg -c "$PACKAGE_NAME" | head -30
|
|
||||||
|
|
||||||
echo -e "\n包信息:"
|
|
||||||
dpkg -I "$PACKAGE_NAME"
|
|
||||||
|
|
||||||
# 清理构建目录
|
|
||||||
rm -rf "$BUILD_ROOT"
|
|
||||||
|
|
||||||
echo -e "\n========================================"
|
|
||||||
echo "✅ 构建完成!"
|
|
||||||
echo "生成的包: $PACKAGE_NAME"
|
|
||||||
echo "安装命令: sudo dpkg -i $PACKAGE_NAME"
|
|
||||||
echo "如果有依赖问题,运行: sudo apt-get install -f"
|
|
||||||
echo ""
|
|
||||||
echo "注意: 此版本使用虚拟环境"
|
|
||||||
echo "虚拟环境将在安装时自动创建"
|
|
||||||
echo "========================================"
|
|
||||||
else
|
|
||||||
echo "❌ 构建失败!"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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