Compare commits

...

11 Commits
1.2 ... main

Author SHA1 Message Date
92cbb8a99e 更新 README.md 2026-01-19 08:13:20 +00:00
2ea550f798 上传文件至 / 2026-01-19 08:10:46 +00:00
2c9087ccf9 上传文件至 / 2026-01-19 08:02:47 +00:00
ab5321a93c 上传文件至 / 2026-01-19 08:02:27 +00:00
fb5f83ff7c 上传文件至 / 2026-01-19 08:01:58 +00:00
93309fb762 上传文件至 / 2026-01-19 08:01:13 +00:00
e51b895bc8 删除 README.md 2026-01-19 08:00:31 +00:00
5e0128fde0 删除 config_editor.py 2026-01-19 08:00:20 +00:00
fd071786f1 删除 config_editor_settings.json 2026-01-19 08:00:12 +00:00
916f08d12f 删除 config_editor_rules.json 2026-01-19 08:00:07 +00:00
271a895419 删除 build.sh 2026-01-19 08:00:00 +00:00
17 changed files with 5292 additions and 3348 deletions

400
README.md
View File

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

10
__init__.py Normal file
View File

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

982
build.sh

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

179
config_parser.py Normal file
View File

@ -0,0 +1,179 @@
'''配置文件解析模块'''
import ast
import json
from typing import Dict, Tuple, List, Any
from models import ConfigField
from utils import get_field_type_from_value
class ConfigParser:
"""配置文件解析器"""
def __init__(self, rules: Dict):
self.rules = rules
def parse_config_file(self, config_file_path: str) -> Tuple[Dict, Dict[str, ConfigField], str]:
"""解析配置文件,精确匹配每个配置项的位置和注释"""
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
tree = ast.parse(content)
config_data = {}
all_config_fields = {}
# 第一遍:识别所有配置项及其位置
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id.isupper():
var_name = target.id
# 获取配置项在文件中的位置
assignment_line = node.lineno - 1 # ast行号从1开始我们使用0基索引
# 向上查找配置项上面的注释行
comment_lines = []
current_line = assignment_line - 1
while current_line >= 0:
line = lines[current_line].strip()
# 如果是注释行或空行,则包含在注释中
if line.startswith('#') or line == '':
comment_lines.insert(0, lines[current_line])
current_line -= 1
# 如果是多行注释的开始
elif line.startswith("'''") or line.startswith('"""'):
# 找到多行注释的起始位置
comment_start = current_line
while comment_start >= 0 and not (lines[comment_start].strip().endswith("'''") or lines[comment_start].strip().endswith('"""')):
comment_start -= 1
if comment_start >= 0:
for i in range(comment_start, current_line + 1):
comment_lines.insert(0, lines[i])
current_line = comment_start - 1
else:
break
else:
break
# 完整的配置项块(包括注释和赋值行)
full_block = comment_lines + [lines[assignment_line]]
# 如果赋值行有后续行(如多行字符串),添加它们
end_line = getattr(node, 'end_lineno', node.lineno) - 1
if end_line > assignment_line:
for i in range(assignment_line + 1, end_line + 1):
full_block.append(lines[i])
try:
# 获取配置项的值
var_value = eval(compile(ast.Expression(node.value), '<string>', 'eval'))
config_data[var_name] = var_value
# 创建ConfigField实例
config_field = self._create_config_field(
var_name, var_value, assignment_line,
full_block, comment_lines
)
all_config_fields[var_name] = config_field
except:
# 如果无法解析值,使用字符串表示
try:
value_str = ast.get_source_segment(content, node.value)
config_data[var_name] = value_str
config_field = self._create_config_field(
var_name, value_str, assignment_line,
full_block, comment_lines, is_string=True
)
all_config_fields[var_name] = config_field
except:
config_data[var_name] = "无法解析的值"
config_field = self._create_config_field(
var_name, "无法解析的值", assignment_line,
full_block, comment_lines, is_string=True
)
all_config_fields[var_name] = config_field
return config_data, all_config_fields, content
except Exception as e:
raise Exception(f"解析配置文件失败:{str(e)}")
def _create_config_field(self, var_name: str, var_value: Any, assignment_line: int,
full_block: List[str], comment_lines: List[str],
is_string: bool = False) -> ConfigField:
"""创建ConfigField实例"""
# 获取字段类型
field_type = self._get_field_type(var_name, var_value)
# 获取小数位数
decimals = self._get_field_decimals(var_name) if field_type == "float" else None
# 获取上次保存的值
last_saved_value = self._get_last_saved_value(var_name, var_value)
return ConfigField(
name=var_name,
value=var_value,
category=self._categorize_field(var_name),
display_name=self._get_display_name(var_name),
field_type=field_type,
decimals=decimals,
tooltip=self._get_tooltip(var_name),
hidden=self._is_hidden(var_name),
encrypted=self._is_encrypted(var_name), # 加密状态
line_number=assignment_line - len(comment_lines),
original_lines=full_block,
validation=self._get_validation(var_name),
last_saved_value=last_saved_value
)
def _categorize_field(self, field_name: str) -> str:
"""为字段分类"""
for category, fields in self.rules.get("categories", {}).items():
if field_name in fields:
return category
return "未分类"
def _get_display_name(self, field_name: str) -> str:
"""获取字段的显示名称"""
return self.rules.get("display_names", {}).get(field_name, field_name)
def _get_tooltip(self, field_name: str) -> str:
"""获取字段的提示信息"""
return self.rules.get("tooltips", {}).get(field_name, f"配置项: {field_name}")
def _get_field_type(self, field_name: str, value: Any) -> str:
"""获取字段类型"""
if field_name in self.rules.get("field_types", {}):
return self.rules["field_types"][field_name]
return get_field_type_from_value(value)
def _get_field_decimals(self, field_name: str) -> int:
"""获取字段的小数位数"""
return self.rules.get("field_decimals", {}).get(field_name, 6)
def _get_validation(self, field_name: str) -> Dict:
"""获取配置项的校验规则"""
return self.rules.get("validations", {}).get(field_name, {})
def _is_hidden(self, field_name: str) -> bool:
"""检查配置项是否被标记为隐藏"""
return field_name in self.rules.get("hidden", [])
def _is_encrypted(self, field_name: str) -> bool:
"""检查配置项是否被标记为加密"""
return field_name in self.rules.get("encrypted_fields", [])
def _get_last_saved_value(self, field_name: str, current_value: Any) -> Any:
"""获取配置项的上次保存值"""
last_saved_values = self.rules.get("last_saved_values", {})
if field_name in last_saved_values:
return last_saved_values[field_name]
return None

1036
dialogs.py Normal file

File diff suppressed because it is too large Load Diff

117
file_handler.py Normal file
View File

@ -0,0 +1,117 @@
'''文件读写管理模块'''
import os
import json
import shutil
from pathlib import Path
from typing import Dict, Any
from utils import (backup_existing_rules, merge_rules,
get_default_rules, ensure_rule_structure)
class FileHandler:
"""文件处理器"""
def __init__(self, program_dir: str):
self.program_dir = program_dir
self.rules_file = str(Path(program_dir) / "config_editor_rules.json")
self.settings_file = str(Path(program_dir) / "config_editor_settings.json")
def load_user_settings(self) -> Dict:
"""加载用户设置"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载用户设置失败: {e}")
return {}
def save_user_settings(self, settings: Dict) -> bool:
"""保存用户设置"""
try:
with open(self.settings_file, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存用户设置失败: {e}")
return False
def load_rules(self) -> Dict:
"""加载规则文件"""
try:
# 先备份现有规则文件
backup_existing_rules(self.rules_file, self.program_dir)
# 检查是否已有规则文件
if os.path.exists(self.rules_file):
with open(self.rules_file, 'r', encoding='utf-8') as f:
existing_rules = json.load(f)
# 检查规则文件是否为空或格式错误
if not existing_rules or not isinstance(existing_rules, dict):
raise ValueError("规则文件为空或格式错误")
# 从打包资源加载默认规则(用于合并新字段)
default_rules = get_default_rules()
# 智能合并:保留现有规则,只添加新字段
rules = merge_rules(existing_rules, default_rules)
# 确保规则结构完整
rules = ensure_rule_structure(rules)
# 保存合并后的规则(包含新字段)
self.save_rules(rules)
return rules
else:
# 如果没有规则文件,使用默认规则
rules = get_default_rules()
# 确保规则结构完整
rules = ensure_rule_structure(rules)
# 首次运行,创建规则文件
try:
self.save_rules(rules)
except Exception as e:
print(f"创建规则文件失败: {e}")
return rules
except (json.JSONDecodeError, ValueError, KeyError) as e:
# 如果规则文件损坏,备份后使用默认规则
print(f"加载规则文件失败: {e},将使用默认规则")
self._backup_corrupted_rules()
rules = get_default_rules()
rules = ensure_rule_structure(rules)
self.save_rules(rules)
return rules
except Exception as e:
print(f"加载规则文件失败: {e}")
rules = get_default_rules()
rules = ensure_rule_structure(rules)
return rules
def save_rules(self, rules: Dict) -> bool:
"""保存规则文件"""
try:
# 确保规则结构完整
rules = ensure_rule_structure(rules)
with open(self.rules_file, 'w', encoding='utf-8') as f:
json.dump(rules, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存规则文件失败: {e}")
return False
def _backup_corrupted_rules(self):
"""备份损坏的规则文件"""
if os.path.exists(self.rules_file):
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = f"{self.rules_file}.corrupted_{timestamp}.bak"
try:
shutil.copy2(self.rules_file, backup_file)
print(f"已备份损坏的规则文件到: {backup_file}")
except Exception as e:
print(f"备份损坏规则文件失败: {e}")

240
login.py Normal file
View File

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

783
login_dialog.py Normal file
View File

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

39
main.py Normal file
View File

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

1555
main_window.py Normal file

File diff suppressed because it is too large Load Diff

143
models.py Normal file
View File

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

1
requirements.txt Normal file
View File

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

263
utils.py Normal file
View File

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

17
widgets.py Normal file
View File

@ -0,0 +1,17 @@
'''自定义控件实现'''
from PyQt6.QtWidgets import QSpinBox, QDoubleSpinBox
from PyQt6.QtCore import Qt
class NoWheelSpinBox(QSpinBox):
"""禁用鼠标滚轮的SpinBox"""
def wheelEvent(self, event):
event.ignore()
class NoWheelDoubleSpinBox(QDoubleSpinBox):
"""禁用鼠标滚轮的DoubleSpinBox"""
def wheelEvent(self, event):
event.ignore()
def setDecimalsFromRules(self, decimals):
"""根据规则设置小数位数"""
self.setDecimals(decimals)