|
1440| 14
|
[Python 原创] 墨香密码管理器 v2.0 · 优化更新记录 |
五、打包与交付交付路径
关键信息
打包环境说明(技术备忘)
六、使用提示与后续可优化当前已具备功能 项目源码 [Python] 纯文本查看 复制代码 #version 2.0 by xujc 2026.1.22
import sys
import os
import hashlib
import sqlite3
import webbrowser
import secrets
import string
import time
import json
import zipfile
import io
import pandas as pd
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
QCheckBox, QListWidget, QListWidgetItem,
QComboBox, QMessageBox, QDialog, QTextEdit, QGroupBox,
QHeaderView, QMenu, QAction, QFileDialog, QProgressBar,
QRadioButton, QButtonGroup, QGridLayout, QSpinBox, QSystemTrayIcon)
from PyQt5.QtCore import Qt, QEvent, QTimer, pyqtSignal, QThread, pyqtSlot, QSharedMemory
from PyQt5.QtGui import QFont, QColor, QPalette, QIcon
import openpyxl
import base64
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Protocol.KDF import PBKDF2
# 基础目录:打包后指向 exe 所在目录,开发时指向脚本目录
BASE_DIR = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
def _read_import_dataframe(file_path, sheet_name=None):
"""根据扩展名读取 Excel 或 CSV 为 DataFrame(供导入复用)"""
ext = file_path.lower()
if ext.endswith('.csv'):
return pd.read_csv(file_path, encoding="utf-8-sig")
if sheet_name:
return pd.read_excel(file_path, sheet_name=sheet_name)
return pd.read_excel(file_path)
# ===== 配置项(空闲自动锁定时间等,持久化到 exe 同目录)=====
CONFIG_FILE = os.path.join(BASE_DIR, "mx_config.json")
DEFAULT_IDLE_MINUTES = 5
def load_config():
"""读取本地配置,缺失或异常时返回空字典"""
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def save_config(cfg):
"""写入本地配置"""
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
except Exception:
pass
# ===== 记住账号密码(本地 AES-GCM 加密存储,非明文)=====
REMEMBER_FILE = os.path.join(BASE_DIR, "remember.json")
_REMEMBER_PASSPHRASE = "MoxiangLocalRemember_v1"
_REMEMBER_SALT = b"moxiang_remember_salt_2026"
def _remember_key():
key, _ = DatabaseEncryption.derive_key(_REMEMBER_PASSPHRASE, _REMEMBER_SALT)
return key
def save_remembered(username, remember_pwd=False, master_password=None):
"""保存记住的账号;remember_pwd=True 且提供密码时才连带加密保存密码。
只记账号时密码字段留空(has_pwd=False),下次登录密码框为空。"""
try:
data = {"username": username, "has_pwd": False, "nonce": "", "cipher": ""}
if remember_pwd and master_password:
key = _remember_key()
cipher = AES.new(key, AES.MODE_GCM)
token = cipher.encrypt(master_password.encode("utf-8"))
data["has_pwd"] = True
data["nonce"] = base64.b64encode(cipher.nonce).decode()
data["cipher"] = base64.b64encode(token).decode()
with open(REMEMBER_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
except Exception:
pass
def load_remembered():
"""返回 (username, password_or_None)。
- 无记录 -> (None, None)
- 只记账号 -> (username, None)
- 同时记密码 -> (username, password)
"""
try:
if not os.path.exists(REMEMBER_FILE):
return None, None
with open(REMEMBER_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
uname = data.get("username") or None
if not data.get("has_pwd"):
return uname, None
key = _remember_key()
nonce = base64.b64decode(data["nonce"])
token = base64.b64decode(data["cipher"])
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
pwd = cipher.decrypt(token).decode("utf-8")
return uname, pwd
except Exception:
return None, None
def clear_remembered():
"""清除记住的账号密码"""
try:
if os.path.exists(REMEMBER_FILE):
os.remove(REMEMBER_FILE)
except Exception:
pass
class DatabaseEncryption:
"""数据库加密管理器(优化版)"""
@staticmethod
def derive_key(master_password: str, salt: bytes = None) -> tuple:
"""从主密码派生加密密钥"""
if salt is None:
salt = get_random_bytes(16)
# 使用PBKDF2派生密钥,增加迭代次数提高安全性
key = PBKDF2(master_password, salt, 32, count=1000000) # 增加到100万次迭代
return key, salt
@staticmethod
def encrypt_data(data: str, key: bytes) -> str:
"""加密数据"""
# 对数据进行UTF-8编码前的验证
try:
data.encode('utf-8')
except UnicodeEncodeError:
raise ValueError("数据包含无效的UTF-8字符")
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(data.encode('utf-8'))
encrypted_data = base64.b64encode(cipher.nonce + tag + ciphertext).decode('utf-8')
return encrypted_data
@staticmethod
def decrypt_data(encrypted_data: str, key: bytes) -> str:
"""解密数据"""
try:
data = base64.b64decode(encrypted_data.encode('utf-8'))
if len(data) < 32: # nonce(16) + tag(16) 至少需要32字节
raise ValueError("加密数据长度不足")
nonce = data[:16]
tag = data[16:32]
ciphertext = data[32:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext.decode('utf-8')
except Exception as e:
raise ValueError(f"解密失败: {str(e)}")
class ChineseStyle:
"""中国风样式配置"""
@staticmethod
def setup_style(app):
"""设置应用程序的中国风样式"""
# 设置中文字体
font = QFont("Microsoft YaHei", 10)
app.setFont(font)
# 设置中国风颜色方案
app.setStyle("Fusion")
palette = QPalette()
palette.setColor(QPalette.Window, QColor(250, 245, 235)) # 米白色背景
palette.setColor(QPalette.WindowText, QColor(101, 67, 33)) # 深棕色文字
palette.setColor(QPalette.Base, QColor(255, 253, 248)) # 浅米色基础
palette.setColor(QPalette.AlternateBase, QColor(245, 240, 230))
palette.setColor(QPalette.ToolTipBase, QColor(255, 253, 248))
palette.setColor(QPalette.ToolTipText, QColor(101, 67, 33))
palette.setColor(QPalette.Text, QColor(101, 67, 33))
palette.setColor(QPalette.Button, QColor(180, 150, 100)) # 古铜色按钮
palette.setColor(QPalette.ButtonText, QColor(250, 245, 235))
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(166, 77, 55)) # 朱红色链接
palette.setColor(QPalette.Highlight, QColor(166, 77, 55))
palette.setColor(QPalette.HighlightedText, Qt.white)
app.setPalette(palette)
@staticmethod
def create_gradient_button(text, width=120, height=40, color1="#A64D37", color2="#C46C4E"):
"""创建中国风渐变按钮"""
button = QPushButton(text)
button.setFixedSize(width, height)
button.setProperty("chineseStyle", True)
button.setStyleSheet(f"""
QPushButton[chineseStyle="true"] {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {color1}, stop:1 {color2});
color: white;
border-radius: 20px;
font-weight: bold;
font-family: "Microsoft YaHei";
font-size: 14px;
}}
QPushButton[chineseStyle="true"]:hover {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #B45D47, stop:1 #D47C5E);
}}
QPushButton[chineseStyle="true"]:pressed {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #95452F, stop:1 #B45D47);
}}
""")
return button
class SingleApplication(QApplication):
"""单实例应用程序类"""
def __init__(self, argv):
super().__init__(argv)
# 创建一个唯一的共享内存键(包含可执行文件路径避免冲突)
app_key = f"墨香密码管理器_{hashlib.md5(sys.executable.encode()).hexdigest()[:8]}"
self._shared_memory = QSharedMemory(app_key)
# 尝试附加到共享内存,如果成功说明已有实例运行
if self._shared_memory.attach():
self._is_running = True
else:
# 创建共享内存
self._is_running = False
if not self._shared_memory.create(1):
self._is_running = True
def is_running(self):
return self._is_running
class DatabaseManager:
"""数据库管理器"""
def __init__(self, master_password=None):
self.db_file = os.path.join(BASE_DIR, "password_manager.db")
self.master_password = master_password
self.encryption_key = None
self.salt = None
self.init_database()
def get_connection(self):
"""获取数据库连接"""
conn = sqlite3.connect(self.db_file)
return conn
def init_database(self):
"""初始化数据库表结构"""
conn = self.get_connection()
cursor = conn.cursor()
# 创建加密配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS encryption_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
salt TEXT NOT NULL,
created_date TEXT NOT NULL
)
''')
# 创建用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
master_password TEXT NOT NULL,
created_date TEXT NOT NULL,
last_login TEXT
)
''')
# 创建单位表
cursor.execute('''
CREATE TABLE IF NOT EXISTS unit_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
website_name TEXT NOT NULL,
website_url TEXT,
account TEXT NOT NULL,
password TEXT NOT NULL,
notes TEXT,
created_time TEXT NOT NULL,
updated_time TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 创建个人表
cursor.execute('''
CREATE TABLE IF NOT EXISTS personal_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
website_name TEXT NOT NULL,
website_url TEXT,
account TEXT NOT NULL,
password TEXT NOT NULL,
notes TEXT,
created_time TEXT NOT NULL,
updated_time TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 创建备份记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS backup_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
backup_file TEXT NOT NULL,
backup_time TEXT NOT NULL,
backup_type TEXT NOT NULL,
record_count INTEGER,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
# 检查是否已存在加密配置
cursor.execute('SELECT salt FROM encryption_config LIMIT 1')
result = cursor.fetchone()
if not result and self.master_password:
# 初始化加密配置
salt = get_random_bytes(16)
key, _ = DatabaseEncryption.derive_key(self.master_password, salt)
self.encryption_key = key
self.salt = salt
cursor.execute('''
INSERT INTO encryption_config (salt, created_date)
VALUES (?, ?)
''', (base64.b64encode(salt).decode('utf-8'),
datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
elif result and self.master_password:
# 加载现有加密配置
salt = base64.b64decode(result[0])
key, _ = DatabaseEncryption.derive_key(self.master_password, salt)
self.encryption_key = key
self.salt = salt
conn.commit()
conn.close()
def encrypt_password(self, password: str) -> str:
"""加密密码"""
if self.encryption_key:
return DatabaseEncryption.encrypt_data(password, self.encryption_key)
return password
def decrypt_password(self, encrypted_password: str) -> str:
"""解密密码"""
if self.encryption_key:
try:
return DatabaseEncryption.decrypt_data(encrypted_password, self.encryption_key)
except ValueError:
# 如果解密失败,返回原始数据(可能是未加密的旧数据)
return encrypted_password
return encrypted_password
class PasswordManager:
"""密码管理器核心逻辑"""
def __init__(self):
self.db_manager = None
self.backup_dir = os.path.join(BASE_DIR, "backups")
self.current_user = None
self.current_user_id = None
self.current_data = {
"tables": {
"单位表": [],
"个人表": []
},
"next_id": 1
}
# 创建备份目录
if not os.path.exists(self.backup_dir):
os.makedirs(self.backup_dir)
def get_connection(self):
"""获取数据库连接"""
return self.db_manager.get_connection() if self.db_manager else None
def hash_password(self, password):
"""对密码进行哈希处理"""
return hashlib.sha256(password.encode()).hexdigest()
def register_user(self, username, password):
"""注册新用户"""
if len(password) < 6:
return False, "密码长度至少6位"
hashed_password = self.hash_password(password)
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
# 初始化数据库管理器(首次注册时创建加密配置)
self.db_manager = DatabaseManager(password)
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO users (username, master_password, created_date, last_login)
VALUES (?, ?, ?, ?)
''', (username, hashed_password, created_date, created_date))
conn.commit()
conn.close()
return True, "注册成功"
except sqlite3.IntegrityError:
return False, "用户名已存在"
except Exception as e:
return False, f"注册失败: {str(e)}"
def login(self, username, password):
"""用户登录"""
try:
# 先尝试不加密连接获取用户信息
temp_db = DatabaseManager()
conn = temp_db.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT id, master_password FROM users WHERE username = ?
''', (username,))
result = cursor.fetchone()
conn.close()
if not result:
return False, "用户不存在"
user_id, stored_password = result
if stored_password == self.hash_password(password):
# 密码验证成功,使用主密码初始化数据库管理器
self.db_manager = DatabaseManager(password)
self.current_user = username
self.current_user_id = user_id
# 更新最后登录时间
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
UPDATE users SET last_login = ? WHERE id = ?
''', (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), user_id))
conn.commit()
conn.close()
# 加载用户数据到内存
self.load_user_data()
return True, "登录成功"
else:
return False, "密码错误"
except Exception as e:
return False, f"登录失败: {str(e)}"
def load_user_data(self):
"""加载用户数据到内存"""
if not self.current_user_id:
return
try:
conn = self.get_connection()
cursor = conn.cursor()
# 加载单位表数据
cursor.execute('''
SELECT id, website_name, website_url, account, password, notes
FROM unit_table WHERE user_id = ? ORDER BY id
''', (self.current_user_id,))
unit_records = []
for row in cursor.fetchall():
# 解密密码
decrypted_password = self.db_manager.decrypt_password(row[4])
unit_records.append({
"id": row[0],
"网站名称": row[1],
"网址": row[2] or "",
"账号": row[3],
"密码": decrypted_password,
"备注": row[5] or ""
})
# 加载个人表数据
cursor.execute('''
SELECT id, website_name, website_url, account, password, notes
FROM personal_table WHERE user_id = ? ORDER BY id
''', (self.current_user_id,))
personal_records = []
for row in cursor.fetchall():
# 解密密码
decrypted_password = self.db_manager.decrypt_password(row[4])
personal_records.append({
"id": row[0],
"网站名称": row[1],
"网址": row[2] or "",
"账号": row[3],
"密码": decrypted_password,
"备注": row[5] or ""
})
conn.close()
# 更新内存中的数据
self.current_data["tables"]["单位表"] = unit_records
self.current_data["tables"]["个人表"] = personal_records
# 计算下一个ID
all_records = unit_records + personal_records
if all_records:
max_id = max(record["id"] for record in all_records)
self.current_data["next_id"] = max_id + 1
else:
self.current_data["next_id"] = 1
except Exception as e:
print(f"加载用户数据失败: {str(e)}")
def logout(self):
"""用户注销"""
if self.current_user:
self.current_user = None
self.current_user_id = None
self.db_manager = None
self.current_data = {
"tables": {
"单位表": [],
"个人表": []
},
"next_id": 1
}
return True
return False
def change_password(self, current_password, new_password):
"""修改用户密码(完整优化版)"""
if not self.current_user_id:
return False, "用户未登录"
# 验证密码强度
strength_valid, strength_msg = self.validate_password_strength(new_password)
if not strength_valid:
return False, strength_msg
# 检查新密码是否与当前密码相同
if current_password == new_password:
return False, "新密码不能与当前密码相同"
try:
# 获取数据库连接
conn = self.get_connection()
cursor = conn.cursor()
# 验证当前密码
cursor.execute('SELECT master_password FROM users WHERE id = ?', (self.current_user_id,))
result = cursor.fetchone()
if not result or result[0] != self.hash_password(current_password):
conn.close()
return False, "当前密码错误"
# 创建新的数据库管理器实例
new_db_manager = DatabaseManager(new_password)
# 备份当前数据库管理器
old_db_manager = self.db_manager
try:
# 开始事务
conn.execute('BEGIN TRANSACTION')
# 重新加密所有密码记录
self._reencrypt_all_passwords_in_transaction(old_db_manager, new_db_manager, cursor)
# 更新用户主密码哈希
cursor.execute('UPDATE users SET master_password = ? WHERE id = ?',
(self.hash_password(new_password), self.current_user_id))
# 提交事务
conn.commit()
# 更新当前数据库管理器
self.db_manager = new_db_manager
# 重新加载用户数据以确保一致性
self.load_user_data()
return True, "密码修改成功"
except Exception as e:
# 回滚事务
conn.rollback()
# 恢复原来的数据库管理器
self.db_manager = old_db_manager
return False, f"密码修改失败: {str(e)}"
finally:
conn.close()
except Exception as e:
return False, f"密码修改失败: {str(e)}"
def _reencrypt_all_passwords_in_transaction(self, old_db_manager, new_db_manager, cursor):
"""在事务中重新加密所有密码记录"""
# 重新加密单位表
cursor.execute('SELECT id, password FROM unit_table WHERE user_id = ?', (self.current_user_id,))
unit_records = cursor.fetchall()
for record_id, old_encrypted_password in unit_records:
try:
# 使用旧密钥解密
decrypted_password = old_db_manager.decrypt_password(old_encrypted_password)
# 使用新密钥加密
new_encrypted_password = new_db_manager.encrypt_password(decrypted_password)
# 更新数据库记录
cursor.execute('UPDATE unit_table SET password = ? WHERE id = ?',
(new_encrypted_password, record_id))
except Exception as e:
raise Exception(f"重新加密单位表记录 {record_id} 失败: {str(e)}")
# 重新加密个人表
cursor.execute('SELECT id, password FROM personal_table WHERE user_id = ?', (self.current_user_id,))
personal_records = cursor.fetchall()
for record_id, old_encrypted_password in personal_records:
try:
# 使用旧密钥解密
decrypted_password = old_db_manager.decrypt_password(old_encrypted_password)
# 使用新密钥加密
new_encrypted_password = new_db_manager.encrypt_password(decrypted_password)
# 更新数据库记录
cursor.execute('UPDATE personal_table SET password = ? WHERE id = ?',
(new_encrypted_password, record_id))
except Exception as e:
raise Exception(f"重新加密个人表记录 {record_id} 失败: {str(e)}")
def validate_password_strength(self, password):
"""验证密码强度"""
if len(password) < 8:
return False, "密码长度至少8位"
if len(password) > 128:
return False, "密码长度不能超过128位"
# 检查字符类型
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
if not (has_upper and has_lower and has_digit):
return False, "密码必须包含大写字母、小写字母和数字"
# 检查常见弱密码
weak_passwords = [
'password', '123456', 'qwerty', 'admin', 'welcome',
'password123', '12345678', '123456789', '123123',
str(self.current_user) if self.current_user else ''
]
if password.lower() in weak_passwords:
return False, "密码过于简单,请使用更复杂的密码"
return True, "密码强度符合要求"
def backup_data(self, backup_type="manual"):
"""备份数据到SQLite数据库"""
if not self.current_user_id:
return False, "用户未登录"
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if backup_type == "auto":
backup_file = os.path.join(self.backup_dir, f"{self.current_user}_auto_{timestamp}.db")
else:
backup_file = os.path.join(self.backup_dir, f"{self.current_user}_manual_{timestamp}.db")
# 创建备份数据库
backup_conn = sqlite3.connect(backup_file)
backup_cursor = backup_conn.cursor()
# 复制表结构
backup_cursor.execute('''
CREATE TABLE IF NOT EXISTS unit_table_backup (
id INTEGER PRIMARY KEY,
user_id INTEGER,
website_name TEXT,
website_url TEXT,
account TEXT,
password TEXT,
notes TEXT,
created_time TEXT,
updated_time TEXT
)
''')
backup_cursor.execute('''
CREATE TABLE IF NOT EXISTS personal_table_backup (
id INTEGER PRIMARY KEY,
user_id INTEGER,
website_name TEXT,
website_url TEXT,
account TEXT,
password TEXT,
notes TEXT,
created_time TEXT,
updated_time TEXT
)
''')
# 复制数据
conn = self.get_connection()
cursor = conn.cursor()
# 备份单位表
cursor.execute('SELECT * FROM unit_table WHERE user_id = ?', (self.current_user_id,))
for row in cursor.fetchall():
backup_cursor.execute('''
INSERT INTO unit_table_backup VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', row)
# 备份个人表
cursor.execute('SELECT * FROM personal_table WHERE user_id = ?', (self.current_user_id,))
for row in cursor.fetchall():
backup_cursor.execute('''
INSERT INTO personal_table_backup VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', row)
conn.close()
# 记录备份信息
record_count = len(self.current_data["tables"]["单位表"]) + len(self.current_data["tables"]["个人表"])
backup_cursor.execute('''
CREATE TABLE IF NOT EXISTS backup_info (
backup_time TEXT,
user_name TEXT,
record_count INTEGER,
backup_type TEXT
)
''')
backup_cursor.execute('''
INSERT INTO backup_info VALUES (?, ?, ?, ?)
''', (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self.current_user, record_count, backup_type))
backup_conn.commit()
backup_conn.close()
return True, f"备份成功: {os.path.basename(backup_file)}"
except Exception as e:
return False, f"备份失败: {str(e)}"
# 其他方法保持不变...
def verify_master_password(self, password):
"""验证主密码是否正确"""
if not self.current_user_id:
return False
try:
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT master_password FROM users WHERE id = ?
''', (self.current_user_id,))
result = cursor.fetchone()
conn.close()
return result and result[0] == self.hash_password(password)
except Exception as e:
return False
def add_record(self, table_name, record_data):
"""添加新记录"""
if not self.current_user_id:
return False
try:
conn = self.get_connection()
cursor = conn.cursor()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 加密密码
encrypted_password = self.db_manager.encrypt_password(record_data["密码"])
if table_name == "单位表":
cursor.execute('''
INSERT INTO unit_table
(user_id, website_name, website_url, account, password, notes, created_time, updated_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
self.current_user_id,
record_data["网站名称"],
record_data["网址"],
record_data["账号"],
encrypted_password,
record_data["备注"],
current_time,
current_time
))
else: # 个人表
cursor.execute('''
INSERT INTO personal_table
(user_id, website_name, website_url, account, password, notes, created_time, updated_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
self.current_user_id,
record_data["网站名称"],
record_data["网址"],
record_data["账号"],
encrypted_password,
record_data["备注"],
current_time,
current_time
))
conn.commit()
record_id = cursor.lastrowid
conn.close()
# 更新内存数据
record_data["id"] = record_id
self.current_data["tables"][table_name].append(record_data)
self.current_data["next_id"] = max(self.current_data["next_id"], record_id + 1)
return True
except Exception as e:
print(f"添加记录失败: {str(e)}")
return False
def update_record(self, table_name, record_id, new_data):
"""更新记录"""
if not self.current_user_id:
return False
try:
conn = self.get_connection()
cursor = conn.cursor()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 加密密码
encrypted_password = self.db_manager.encrypt_password(new_data["密码"])
if table_name == "单位表":
cursor.execute('''
UPDATE unit_table SET
website_name = ?, website_url = ?, account = ?, password = ?, notes = ?, updated_time = ?
WHERE id = ? AND user_id = ?
''', (
new_data["网站名称"],
new_data["网址"],
new_data["账号"],
encrypted_password,
new_data["备注"],
current_time,
record_id,
self.current_user_id
))
else: # 个人表
cursor.execute('''
UPDATE personal_table SET
website_name = ?, website_url = ?, account = ?, password = ?, notes = ?, updated_time = ?
WHERE id = ? AND user_id = ?
''', (
new_data["网站名称"],
new_data["网址"],
new_data["账号"],
encrypted_password,
new_data["备注"],
current_time,
record_id,
self.current_user_id
))
conn.commit()
conn.close()
# 更新内存数据
for record in self.current_data["tables"][table_name]:
if record["id"] == record_id:
record.update(new_data)
break
return True
except Exception as e:
print(f"更新记录失败: {str(e)}")
return False
def delete_record(self, table_name, record_id):
"""删除记录"""
if not self.current_user_id:
return False
try:
conn = self.get_connection()
cursor = conn.cursor()
if table_name == "单位表":
cursor.execute('''
DELETE FROM unit_table WHERE id = ? AND user_id = ?
''', (record_id, self.current_user_id))
else: # 个人表
cursor.execute('''
DELETE FROM personal_table WHERE id = ? AND user_id = ?
''', (record_id, self.current_user_id))
conn.commit()
conn.close()
# 更新内存数据
self.current_data["tables"][table_name] = [
record for record in self.current_data["tables"][table_name]
if record["id"] != record_id
]
return True
except Exception as e:
print(f"删除记录失败: {str(e)}")
return False
def search_records(self, table_name, search_term, search_field="所有字段"):
"""搜索记录"""
if not search_term:
return self.current_data["tables"][table_name]
results = []
for record in self.current_data["tables"][table_name]:
if search_field != "所有字段":
field_value = record.get(search_field, "")
if search_term.lower() in str(field_value).lower():
results.append(record)
else:
for value in record.values():
if search_term.lower() in str(value).lower():
results.append(record)
break
return results
def import_from_excel(self, file_path, target_table, sheet_name=None):
"""从Excel文件导入数据到指定表"""
if not self.current_user_id:
return False, "用户未登录"
if target_table not in ["单位表", "个人表"]:
return False, "目标表必须是'单位表'或'个人表'"
try:
df = _read_import_dataframe(file_path, sheet_name)
if df.empty:
return False, "文件为空或没有数据"
field_mapping = {
'网站名称': ['网站名称', '网站名', '网站', '名称', '站点名称', '平台名称'],
'网址': ['网址', '网站地址', 'URL', '链接', '地址', '网站链接'],
'账号': ['账号', '账户', '用户名', '用户', '登录名', '用户账号'],
'密码': ['密码', '登陆密码', '登录密码', 'pass', 'passwd'],
'备注': ['备注', '说明', '注释', '描述', 'note', 'description']
}
actual_mapping = {}
for target_field, possible_names in field_mapping.items():
for col in df.columns:
if any(name in str(col) for name in possible_names):
actual_mapping[target_field] = col
break
else:
actual_mapping[target_field] = None
imported_count = 0
skipped_count = 0
error_count = 0
for index, row in df.iterrows():
try:
website_name = row[actual_mapping['网站名称']] if actual_mapping['网站名称'] is not None else ""
website_url = row[actual_mapping['网址']] if actual_mapping['网址'] is not None else ""
account = row[actual_mapping['账号']] if actual_mapping['账号'] is not None else ""
password = row[actual_mapping['密码']] if actual_mapping['密码'] is not None else ""
notes = row[actual_mapping['备注']] if actual_mapping['备注'] is not None else ""
website_name = str(website_name).strip() if pd.notna(website_name) else ""
website_url = str(website_url).strip() if pd.notna(website_url) else ""
account = str(account).strip() if pd.notna(account) else ""
password = str(password).strip() if pd.notna(password) else ""
notes = str(notes).strip() if pd.notna(notes) else ""
if not website_name and not account:
skipped_count += 1
continue
record_data = {
"网站名称": website_name,
"网址": website_url,
"账号": account,
"密码": password,
"备注": notes
}
if self.add_record(target_table, record_data):
imported_count += 1
else:
error_count += 1
skipped_count += 1
except Exception as e:
error_count += 1
skipped_count += 1
continue
return True, f"导入完成:成功导入 {imported_count} 条记录,跳过 {skipped_count} 条记录,错误 {error_count} 条记录"
except Exception as e:
return False, f"导入失败: {str(e)}"
class SecurityDialog(QDialog):
"""安全验证对话框"""
def __init__(self, parent=None, title="安全验证", message="请输入您的主密码以继续操作"):
super().__init__(parent)
self.setWindowTitle(title)
self.setFixedSize(350, 200)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
title_label = QLabel(title)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 18px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 10px;
}
""")
layout.addWidget(title_label)
message_label = QLabel(message)
message_label.setAlignment(Qt.AlignCenter)
message_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853;")
message_label.setWordWrap(True)
layout.addWidget(message_label)
password_layout = QHBoxLayout()
password_label = QLabel("主密码:")
password_label.setFixedWidth(80)
password_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
password_layout.addWidget(password_label)
password_layout.addWidget(self.password_input)
layout.addLayout(password_layout)
button_layout = QHBoxLayout()
button_layout.addStretch()
ok_btn = ChineseStyle.create_gradient_button("验证", 80, 35)
ok_btn.clicked.connect(self.accept)
cancel_btn = ChineseStyle.create_gradient_button("取消", 80, 35, "#8C7853", "#A8967A")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(ok_btn)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout)
self.password_input.returnPressed.connect(self.accept)
def get_password(self):
return self.password_input.text()
class ChangePasswordDialog(QDialog):
"""修改密码对话框"""
def __init__(self, parent=None, password_manager=None):
super().__init__(parent)
self.pm = password_manager
self.init_ui()
def init_ui(self):
self.setWindowTitle("修改密码")
self.setFixedSize(500, 450)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(20)
# 标题
title_label = QLabel("修改主密码")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 18px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 10px;
}
""")
layout.addWidget(title_label)
# 当前密码
current_pwd_layout = QHBoxLayout()
current_pwd_label = QLabel("当前密码:")
current_pwd_label.setFixedWidth(100)
current_pwd_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.current_pwd_input = QLineEdit()
self.current_pwd_input.setEchoMode(QLineEdit.Password)
self.current_pwd_input.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
current_pwd_layout.addWidget(current_pwd_label)
current_pwd_layout.addWidget(self.current_pwd_input)
layout.addLayout(current_pwd_layout)
# 新密码
new_pwd_layout = QHBoxLayout()
new_pwd_label = QLabel("新密码:")
new_pwd_label.setFixedWidth(100)
new_pwd_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.new_pwd_input = QLineEdit()
self.new_pwd_input.setEchoMode(QLineEdit.Password)
self.new_pwd_input.textChanged.connect(self.validate_password_strength)
self.new_pwd_input.setStyleSheet(self.current_pwd_input.styleSheet())
new_pwd_layout.addWidget(new_pwd_label)
new_pwd_layout.addWidget(self.new_pwd_input)
layout.addLayout(new_pwd_layout)
# 密码强度指示器
self.strength_label = QLabel("")
self.strength_label.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 12px;")
layout.addWidget(self.strength_label)
# 确认新密码
confirm_pwd_layout = QHBoxLayout()
confirm_pwd_label = QLabel("确认新密码:")
confirm_pwd_label.setFixedWidth(100)
confirm_pwd_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.confirm_pwd_input = QLineEdit()
self.confirm_pwd_input.setEchoMode(QLineEdit.Password)
self.confirm_pwd_input.textChanged.connect(self.check_password_match)
self.confirm_pwd_input.setStyleSheet(self.current_pwd_input.styleSheet())
confirm_pwd_layout.addWidget(confirm_pwd_label)
confirm_pwd_layout.addWidget(self.confirm_pwd_input)
layout.addLayout(confirm_pwd_layout)
# 密码匹配指示器
self.match_label = QLabel("")
self.match_label.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 12px;")
layout.addWidget(self.match_label)
# 密码要求说明
requirements_label = QLabel("""
<span style='font-family: "Microsoft YaHei"; font-size: 11px; color: #8C7853;'>
• 密码长度至少8位<br>
• 包含大写字母、小写字母和数字<br>
• 建议使用特殊字符增强安全性
</span>
""")
requirements_label.setWordWrap(True)
layout.addWidget(requirements_label)
# 按钮布局
button_layout = QHBoxLayout()
button_layout.addStretch()
self.change_btn = ChineseStyle.create_gradient_button("确认修改", 100, 35)
self.change_btn.clicked.connect(self.change_password)
self.change_btn.setEnabled(False) # 初始禁用
cancel_btn = ChineseStyle.create_gradient_button("取消", 100, 35, "#8C7853", "#A8967A")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.change_btn)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout)
# 连接回车键
self.confirm_pwd_input.returnPressed.connect(self.change_password)
def validate_password_strength(self):
"""验证密码强度"""
password = self.new_pwd_input.text()
if len(password) < 8:
self.strength_label.setText("❌ 密码长度至少8位")
self.strength_label.setStyleSheet("color: #FF6B6B; font-family: 'Microsoft YaHei';")
return False
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
if not (has_upper and has_lower and has_digit):
self.strength_label.setText("❌ 需包含大小写字母和数字")
self.strength_label.setStyleSheet("color: #FF6B6B; font-family: 'Microsoft YaHei';")
return False
# 评估密码强度
strength = 0
if len(password) >= 12:
strength += 1
if any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?/' for c in password):
strength += 1
if strength >= 1:
self.strength_label.setText("✅ 密码强度:强")
self.strength_label.setStyleSheet("color: #51CF66; font-family: 'Microsoft YaHei';")
else:
self.strength_label.setText("⚠️ 密码强度:中")
self.strength_label.setStyleSheet("color: #FCC419; font-family: 'Microsoft YaHei';")
return True
def check_password_match(self):
"""检查密码是否匹配"""
new_pwd = self.new_pwd_input.text()
confirm_pwd = self.confirm_pwd_input.text()
if not new_pwd:
self.match_label.setText("")
self.change_btn.setEnabled(False)
return False
if new_pwd == confirm_pwd:
self.match_label.setText("✅ 密码匹配")
self.match_label.setStyleSheet("color: #51CF66; font-family: 'Microsoft YaHei';")
# 只有在密码强度合格且匹配时才启用按钮
if self.validate_password_strength():
self.change_btn.setEnabled(True)
return True
else:
self.match_label.setText("❌ 密码不匹配")
self.match_label.setStyleSheet("color: #FF6B6B; font-family: 'Microsoft YaHei';")
self.change_btn.setEnabled(False)
return False
def change_password(self):
"""执行密码修改"""
current_password = self.current_pwd_input.text().strip()
new_password = self.new_pwd_input.text().strip()
confirm_password = self.confirm_pwd_input.text().strip()
# 验证输入
if not current_password:
QMessageBox.warning(self, "输入错误", "请输入当前密码")
self.current_pwd_input.setFocus()
return
if not new_password:
QMessageBox.warning(self, "输入错误", "请输入新密码")
self.new_pwd_input.setFocus()
return
if new_password != confirm_password:
QMessageBox.warning(self, "输入错误", "新密码和确认密码不匹配")
self.confirm_pwd_input.setFocus()
return
if not self.validate_password_strength():
QMessageBox.warning(self, "密码强度不足", "请按照要求设置更强的密码")
self.new_pwd_input.setFocus()
return
# 验证当前密码
if not self.pm.verify_master_password(current_password):
QMessageBox.warning(self, "验证失败", "当前密码错误")
self.current_pwd_input.clear()
self.current_pwd_input.setFocus()
return
# 检查新密码是否与旧密码相同
if current_password == new_password:
QMessageBox.warning(self, "密码重复", "新密码不能与当前密码相同")
self.new_pwd_input.clear()
self.confirm_pwd_input.clear()
self.new_pwd_input.setFocus()
return
# 执行密码修改
try:
success, message = self.pm.change_password(current_password, new_password)
if success:
QMessageBox.information(self, "修改成功", "密码修改成功")
self.accept()
else:
QMessageBox.critical(self, "修改失败", message)
except Exception as e:
QMessageBox.critical(self, "错误", f"修改密码时发生错误: {str(e)}")
class LoginWindow(QDialog):
"""登录窗口"""
login_success = pyqtSignal(str) # 登录成功信号
def __init__(self, password_manager):
super().__init__()
self.pm = password_manager
self.init_ui()
def init_ui(self):
self.setWindowTitle("墨香 - 登录")
self.setFixedSize(420, 460)
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setWindowIcon(get_app_icon())
self.main_widget = QWidget(self)
self.main_widget.setObjectName("main_widget")
self.main_widget.setStyleSheet("""
#main_widget {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #F8F4E9, stop:1 #F0E9D9);
border-radius: 15px;
border: 1px solid #D9C7A7;
}
""")
layout = QVBoxLayout(self.main_widget)
layout.setContentsMargins(30, 25, 30, 25)
layout.setSpacing(15)
title_layout = QVBoxLayout()
title_layout.setAlignment(Qt.AlignCenter)
deco_label = QLabel("❀❀❀❀")
deco_label.setAlignment(Qt.AlignCenter)
deco_label.setStyleSheet("font-size: 32px; color: #A64D37;")
title_layout.addWidget(deco_label)
title_label = QLabel("墨香账号平台")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 24px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
margin-bottom: 2px;
}
""")
title_layout.addWidget(title_label)
subtitle_label = QLabel("安全存储 · 优雅管理")
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("font-size: 14px; color: #8C7853;")
title_layout.addWidget(subtitle_label)
layout.addLayout(title_layout)
form_layout = QVBoxLayout()
form_layout.setSpacing(12)
username_group = QGroupBox()
username_group.setStyleSheet("""
QGroupBox {
border: 1px solid #D9C7A7;
border-radius: 10px;
padding: 6px 10px;
background: rgba(255, 255, 255, 0.7);
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #8C7853;
}
""")
username_group.setTitle("用户名")
username_layout = QHBoxLayout(username_group)
self.username_input = QLineEdit()
self.username_input.setPlaceholderText("请输入用户名")
self.username_input.setStyleSheet("""
QLineEdit {
border: none;
background: transparent;
font-size: 14px;
color: #654321;
min-height: 26px;
max-height: 30px;
padding: 2px 0px;
}
""")
username_layout.addWidget(self.username_input)
form_layout.addWidget(username_group)
password_group = QGroupBox()
password_group.setStyleSheet(username_group.styleSheet())
password_group.setTitle("密码")
password_layout = QHBoxLayout(password_group)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("请输入密码")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setStyleSheet(self.username_input.styleSheet())
password_layout.addWidget(self.password_input)
form_layout.addWidget(password_group)
layout.addLayout(form_layout)
# 记住账号 / 记住密码(两级:默认只记账号,密码需额外勾选)
remember_layout = QHBoxLayout()
remember_layout.setSpacing(20)
self.remember_user_checkbox = QCheckBox("记住账号")
self.remember_user_checkbox.setChecked(True) # 默认勾选,只记账号
self.remember_user_checkbox.setStyleSheet(
"font-family: 'Microsoft YaHei'; color: #8C7853; spacing: 6px;")
self.remember_pwd_checkbox = QCheckBox("记住密码")
self.remember_pwd_checkbox.setStyleSheet(
"font-family: 'Microsoft YaHei'; color: #8C7853; spacing: 6px;")
self.remember_pwd_checkbox.setEnabled(False) # 未勾选记账号时禁用
remember_layout.addWidget(self.remember_user_checkbox, 0, Qt.AlignLeft)
remember_layout.addWidget(self.remember_pwd_checkbox, 0, Qt.AlignLeft)
remember_layout.addStretch()
layout.addLayout(remember_layout)
self.remember_user_checkbox.toggled.connect(self._on_remember_user_toggled)
button_layout = QHBoxLayout()
button_layout.setSpacing(20)
self.login_btn = ChineseStyle.create_gradient_button("登录", 120, 40)
self.login_btn.clicked.connect(self.login)
self.register_btn = ChineseStyle.create_gradient_button("注册", 120, 40, "#8C7853", "#A8967A")
self.register_btn.clicked.connect(self.register)
button_layout.addWidget(self.login_btn)
button_layout.addWidget(self.register_btn)
layout.addLayout(button_layout)
footer_label = QLabel("· 安全第一 · 隐私至上 ·")
footer_label.setAlignment(Qt.AlignCenter)
footer_label.setStyleSheet("font-size: 12px; color: #8C7853;")
layout.addWidget(footer_label)
main_layout = QVBoxLayout(self)
main_layout.addWidget(self.main_widget)
main_layout.setContentsMargins(15, 15, 15, 15)
self.password_input.returnPressed.connect(self.login)
self.center_window()
def center_window(self):
"""窗口居中显示"""
screen = QApplication.primaryScreen().availableGeometry()
size = self.geometry()
self.move((screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2)
def _on_remember_user_toggled(self, checked):
"""取消「记住账号」时,禁用并取消「记住密码」"""
self.remember_pwd_checkbox.setEnabled(checked)
if not checked:
self.remember_pwd_checkbox.setChecked(False)
def login(self):
username = self.username_input.text().strip()
password = self.password_input.text().strip()
if not username or not password:
QMessageBox.warning(self, "输入错误", "请输入用户名和密码")
return
success, message = self.pm.login(username, password)
if success:
if self.remember_user_checkbox.isChecked():
save_remembered(
username,
remember_pwd=self.remember_pwd_checkbox.isChecked(),
master_password=password)
else:
clear_remembered()
self.login_success.emit(username)
self.hide()
else:
QMessageBox.critical(self, "登录失败", message)
def register(self):
username = self.username_input.text().strip()
password = self.password_input.text().strip()
if not username or not password:
QMessageBox.warning(self, "输入错误", "请输入用户名和密码")
return
success, message = self.pm.register_user(username, password)
if success:
QMessageBox.information(self, "注册成功", "账号注册成功,请登录")
self.password_input.clear()
else:
QMessageBox.critical(self, "注册失败", message)
def show_login(self):
"""显示登录窗口;若已记住账号则自动填充(默认只记账号,密码需额外勾选)"""
self.username_input.clear()
self.password_input.clear()
self.remember_user_checkbox.setChecked(True) # 默认只记账号
self.remember_pwd_checkbox.setChecked(False)
uname, pwd = load_remembered()
if uname:
self.username_input.setText(uname)
self.remember_user_checkbox.setChecked(True)
if pwd:
self.password_input.setText(pwd)
self.remember_pwd_checkbox.setChecked(True)
# 同步「记住密码」可用状态(依赖「记住账号」是否勾选)
self.remember_pwd_checkbox.setEnabled(self.remember_user_checkbox.isChecked())
self.show()
self.raise_()
self.activateWindow()
class PasswordGeneratorDialog(QDialog):
"""强密码生成器对话框(功能2)"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("密码生成器")
self.setFixedSize(460, 330)
self.setWindowIcon(get_app_icon())
self.generated = ""
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(12)
title = QLabel("🔑 强密码生成器")
title.setStyleSheet("font-size: 18px; font-weight: bold; color: #654321; font-family: 'Microsoft YaHei';")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
# 长度
len_layout = QHBoxLayout()
len_label = QLabel("长度:")
len_label.setFixedWidth(50)
len_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.len_spin = QSpinBox()
self.len_spin.setRange(8, 64)
self.len_spin.setValue(16)
self.len_spin.setStyleSheet("font-family: 'Microsoft YaHei';")
len_layout.addWidget(len_label)
len_layout.addWidget(self.len_spin)
len_layout.addStretch()
layout.addLayout(len_layout)
# 字符类型
opt_label = QLabel("包含字符:")
opt_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold;")
layout.addWidget(opt_label)
self.cb_upper = QCheckBox("大写字母 (A-Z)")
self.cb_lower = QCheckBox("小写字母 (a-z)")
self.cb_digit = QCheckBox("数字 (0-9)")
self.cb_symbol = QCheckBox("符号 (!@#$...)")
self.cb_upper.setChecked(True)
self.cb_lower.setChecked(True)
self.cb_digit.setChecked(True)
self.cb_symbol.setChecked(True)
for cb in (self.cb_upper, self.cb_lower, self.cb_digit, self.cb_symbol):
cb.setStyleSheet("font-family: 'Microsoft YaHei';")
layout.addWidget(cb)
# 生成结果
self.result_edit = QLineEdit()
self.result_edit.setReadOnly(True)
self.result_edit.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 6px;
font-family: 'Consolas','Microsoft YaHei';
font-size: 13px;
color: #654321;
}
""")
layout.addWidget(self.result_edit)
# 按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
regen_btn = QPushButton("重新生成")
regen_btn.setStyleSheet(self.cb_upper.styleSheet())
regen_btn.clicked.connect(self.generate)
copy_btn = QPushButton("复制")
copy_btn.setStyleSheet(self.cb_upper.styleSheet())
copy_btn.clicked.connect(self.copy_password)
use_btn = ChineseStyle.create_gradient_button("使用此密码", 120, 32)
use_btn.clicked.connect(self.accept)
cancel_btn = ChineseStyle.create_gradient_button("取消", 100, 32, "#8C7853", "#A8967A")
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(regen_btn)
btn_layout.addWidget(copy_btn)
btn_layout.addWidget(use_btn)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
self.generate()
def generate(self):
"""使用密码学安全随机数生成密码"""
length = self.len_spin.value()
pool = ""
if self.cb_upper.isChecked():
pool += string.ascii_uppercase
if self.cb_lower.isChecked():
pool += string.ascii_lowercase
if self.cb_digit.isChecked():
pool += string.digits
if self.cb_symbol.isChecked():
pool += "!@#$%^&*()-_=+[]{};:,.<>?/"
if not pool:
pool = string.ascii_letters + string.digits
self.generated = "".join(secrets.choice(pool) for _ in range(length))
self.result_edit.setText(self.generated)
def copy_password(self):
cb = QApplication.clipboard()
if cb:
cb.setText(self.generated)
QMessageBox.information(self, "已复制", "密码已复制到剪贴板")
def get_password(self):
return self.generated
class RecordDialog(QDialog):
"""记录编辑对话框"""
def __init__(self, parent=None, title="", record=None, main_window=None):
super().__init__(parent)
self.record = record or {}
self.result_data = None
self.main_window = main_window
self.password_visible = False
self.init_ui(title)
def init_ui(self, title):
self.setWindowTitle(title)
self.setFixedSize(500, 450)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
title_label = QLabel(title)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 20px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 10px;
}
""")
layout.addWidget(title_label)
form_layout = QVBoxLayout()
site_name_layout = QHBoxLayout()
site_name_label = QLabel("网站名称:")
site_name_label.setFixedWidth(80)
site_name_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.site_name_input = QLineEdit()
self.site_name_input.setText(self.record.get("网站名称", ""))
self.site_name_input.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
site_name_layout.addWidget(site_name_label)
site_name_layout.addWidget(self.site_name_input)
form_layout.addLayout(site_name_layout)
url_layout = QHBoxLayout()
url_label = QLabel("网址:")
url_label.setFixedWidth(80)
url_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.url_input = QLineEdit()
self.url_input.setText(self.record.get("网址", ""))
self.url_input.setStyleSheet(self.site_name_input.styleSheet())
url_layout.addWidget(url_label)
url_layout.addWidget(self.url_input)
form_layout.addLayout(url_layout)
username_layout = QHBoxLayout()
username_label = QLabel("账号:")
username_label.setFixedWidth(80)
username_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.username_input = QLineEdit()
self.username_input.setText(self.record.get("账号", ""))
self.username_input.setStyleSheet(self.site_name_input.styleSheet())
username_layout.addWidget(username_label)
username_layout.addWidget(self.username_input)
form_layout.addLayout(username_layout)
password_layout = QHBoxLayout()
password_label = QLabel("密码:")
password_label.setFixedWidth(80)
password_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.password_input = QLineEdit()
self.password_input.setText(self.record.get("密码", ""))
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setStyleSheet(self.site_name_input.styleSheet())
self.toggle_password_btn = QPushButton("显示")
self.toggle_password_btn.setFixedWidth(60)
self.toggle_password_btn.setStyleSheet("""
QPushButton {
background: #8C7853;
color: white;
border-radius: 5px;
font-family: "Microsoft YaHei";
font-size: 12px;
}
QPushButton:hover {
background: #A8967A;
}
""")
self.toggle_password_btn.clicked.connect(self.toggle_password_visibility)
self.gen_password_btn = QPushButton("生成")
self.gen_password_btn.setFixedWidth(60)
self.gen_password_btn.setStyleSheet(self.toggle_password_btn.styleSheet())
self.gen_password_btn.clicked.connect(self.open_generator)
password_layout.addWidget(password_label)
password_layout.addWidget(self.password_input)
password_layout.addWidget(self.toggle_password_btn)
password_layout.addWidget(self.gen_password_btn)
form_layout.addLayout(password_layout)
notes_layout = QVBoxLayout()
notes_label = QLabel("备注:")
notes_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.notes_input = QTextEdit()
self.notes_input.setMaximumHeight(100)
self.notes_input.setStyleSheet("""
QTextEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
self.notes_input.setText(self.record.get("备注", ""))
notes_layout.addWidget(notes_label)
notes_layout.addWidget(self.notes_input)
form_layout.addLayout(notes_layout)
layout.addLayout(form_layout)
button_layout = QHBoxLayout()
button_layout.addStretch()
ok_btn = ChineseStyle.create_gradient_button("确定", 100, 35)
ok_btn.clicked.connect(self.accept)
cancel_btn = ChineseStyle.create_gradient_button("取消", 100, 35, "#8C7853", "#A8967A")
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(ok_btn)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout)
def toggle_password_visibility(self):
"""切换密码显示/隐藏状态"""
if not self.password_visible:
if self.main_window and self.record and "id" in self.record:
dialog = SecurityDialog(self, "安全验证", "查看密码需要验证您的主密码")
if dialog.exec_() == QDialog.Accepted:
if self.main_window.pm.verify_master_password(dialog.get_password()):
self.password_input.setEchoMode(QLineEdit.Normal)
self.toggle_password_btn.setText("隐藏")
self.password_visible = True
else:
QMessageBox.warning(self, "验证失败", "主密码错误,无法查看密码")
else:
return
else:
self.password_input.setEchoMode(QLineEdit.Normal)
self.toggle_password_btn.setText("隐藏")
self.password_visible = True
else:
self.password_input.setEchoMode(QLineEdit.Password)
self.toggle_password_btn.setText("显示")
self.password_visible = False
def open_generator(self):
"""打开密码生成器并把结果填入密码框"""
dialog = PasswordGeneratorDialog(self)
if dialog.exec_() == QDialog.Accepted:
pwd = dialog.get_password()
if pwd:
self.password_input.setText(pwd)
self.password_input.setEchoMode(QLineEdit.Normal)
self.toggle_password_btn.setText("隐藏")
self.password_visible = True
def accept(self):
site_name = self.site_name_input.text().strip()
if not site_name:
QMessageBox.warning(self, "输入错误", "请输入网站名称")
return
self.result_data = {
"网站名称": site_name,
"网址": self.url_input.text().strip(),
"账号": self.username_input.text().strip(),
"密码": self.password_input.text(),
"备注": self.notes_input.toPlainText().strip()
}
super().accept()
class ExcelImportDialog(QDialog):
"""Excel导入对话框"""
import_completed = pyqtSignal(bool, str)
def __init__(self, parent=None, password_manager=None):
super().__init__(parent)
self.pm = password_manager
self.init_ui()
def init_ui(self):
self.setWindowTitle("Excel数据导入")
self.setFixedSize(500, 600)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
title_label = QLabel("Excel数据导入")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 20px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 10px;
}
""")
layout.addWidget(title_label)
info_label = QLabel("请选择Excel文件,并指定要导入的目标数据表(单位表或个人表)")
info_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853; font-size: 12px;")
layout.addWidget(info_label)
file_group = QGroupBox("选择Excel文件")
file_group.setStyleSheet("""
QGroupBox {
border: 1px solid #D9C7A7;
border-radius: 8px;
padding: 10px;
background: rgba(255, 255, 255, 0.7);
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #8C7853;
}
""")
file_layout = QVBoxLayout(file_group)
file_path_layout = QHBoxLayout()
self.file_path_input = QLineEdit()
self.file_path_input.setPlaceholderText("请选择Excel或CSV文件...")
self.file_path_input.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
browse_btn = ChineseStyle.create_gradient_button("浏览", 80, 30)
browse_btn.clicked.connect(self.browse_file)
file_path_layout.addWidget(self.file_path_input)
file_path_layout.addWidget(browse_btn)
file_layout.addLayout(file_path_layout)
sheet_layout = QHBoxLayout()
sheet_label = QLabel("工作表:")
sheet_label.setFixedWidth(80)
sheet_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
self.sheet_combo = QComboBox()
self.sheet_combo.setStyleSheet("""
QComboBox {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
min-width: 120px;
}
""")
self.sheet_combo.addItem("自动选择(第一个工作表)")
sheet_layout.addWidget(sheet_label)
sheet_layout.addWidget(self.sheet_combo)
sheet_layout.addStretch()
file_layout.addLayout(sheet_layout)
file_layout.addStretch()
layout.addWidget(file_group)
target_group = QGroupBox("选择目标数据表")
target_group.setStyleSheet(file_group.styleSheet())
target_layout = QVBoxLayout(target_group)
self.target_radio_group = QButtonGroup()
self.unit_radio = QRadioButton("单位表")
self.unit_radio.setChecked(True)
self.personal_radio = QRadioButton("个人表")
self.target_radio_group.addButton(self.unit_radio)
self.target_radio_group.addButton(self.personal_radio)
target_layout.addWidget(self.unit_radio)
target_layout.addWidget(self.personal_radio)
target_layout.addStretch()
layout.addWidget(target_group)
options_group = QGroupBox("导入选项")
options_group.setStyleSheet(file_group.styleSheet())
options_layout = QVBoxLayout(options_group)
mapping_label = QLabel("字段映射说明:")
mapping_label.setStyleSheet(
"font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold; font-size: 12px;")
options_layout.addWidget(mapping_label)
mapping_text = QLabel("""
• 系统会自动匹配常见的Excel/CSV列名,如:网站名称、网站名、网址、URL、账号、账户、密码等
• 如果列名不匹配,系统会按照顺序尝试匹配
• 必填字段:网站名称或账号至少填写一项
• 支持.xlsx、.xls 格式的Excel文件,以及 .csv 文件(UTF-8编码)
""")
mapping_text.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853; font-size: 11px;")
mapping_text.setWordWrap(True)
options_layout.addWidget(mapping_text)
options_layout.addStretch()
layout.addWidget(options_group)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(True)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #D9C7A7;
border-radius: 5px;
text-align: center;
font-family: "Microsoft YaHei";
color: #654321;
}
QProgressBar::chunk {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #A64D37, stop:1 #C46C4E);
border-radius: 3px;
}
""")
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
button_layout = QHBoxLayout()
button_layout.addStretch()
self.import_btn = ChineseStyle.create_gradient_button("开始导入", 100, 35)
self.import_btn.clicked.connect(self.start_import)
button_layout.addWidget(self.import_btn)
self.cancel_btn = ChineseStyle.create_gradient_button("取消", 100, 35, "#8C7853", "#A8967A")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def browse_file(self):
"""浏览选择Excel或CSV文件"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择Excel/CSV文件",
"",
"Excel/CSV文件 (*.xlsx *.xls *.csv);;所有文件 (*)"
)
if file_path:
self.file_path_input.setText(file_path)
self.load_sheet_info(file_path)
def load_sheet_info(self, file_path):
"""加载Excel文件的工作表信息;CSV文件无工作表"""
self.sheet_combo.clear()
if file_path.lower().endswith('.csv'):
self.sheet_combo.addItem("CSV文件(无工作表)")
self.sheet_combo.setEnabled(False)
return
self.sheet_combo.setEnabled(True)
self.sheet_combo.addItem("自动选择(第一个工作表)")
try:
xls = pd.ExcelFile(file_path)
sheets = xls.sheet_names
for sheet in sheets:
self.sheet_combo.addItem(sheet)
except Exception as e:
QMessageBox.warning(self, "文件错误", f"无法读取Excel文件: {str(e)}")
def start_import(self):
"""开始导入过程"""
file_path = self.file_path_input.text().strip()
if not file_path:
QMessageBox.warning(self, "选择文件", "请先选择Excel文件")
return
if not os.path.exists(file_path):
QMessageBox.warning(self, "文件不存在", "所选的Excel文件不存在")
return
if not (file_path.lower().endswith('.xlsx') or file_path.lower().endswith('.xls') or file_path.lower().endswith('.csv')):
QMessageBox.warning(self, "文件格式", "请选择.xlsx、.xls或.csv格式的文件")
return
if self.unit_radio.isChecked():
target_table = "单位表"
else:
target_table = "个人表"
sheet_selection = self.sheet_combo.currentText()
sheet_name = None
if sheet_selection not in ("自动选择(第一个工作表)", "CSV文件(无工作表)"):
sheet_name = sheet_selection
# 验证主密码
dialog = SecurityDialog(self, "安全验证", "导入数据需要验证您的主密码")
if dialog.exec_() == QDialog.Accepted:
if self.pm.verify_master_password(dialog.get_password()):
self.set_interaction_enabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
# 在线程中执行导入
self.import_thread = ImportThread(file_path, target_table, sheet_name, self.pm)
self.import_thread.progress_updated.connect(self.update_progress)
self.import_thread.import_completed.connect(self.on_import_completed)
self.import_thread.start()
else:
QMessageBox.warning(self, "验证失败", "主密码错误,无法导入数据")
def set_interaction_enabled(self, enabled):
"""设置界面元素交互状态"""
self.file_path_input.setEnabled(enabled)
self.sheet_combo.setEnabled(enabled)
self.unit_radio.setEnabled(enabled)
self.personal_radio.setEnabled(enabled)
self.import_btn.setEnabled(enabled)
self.cancel_btn.setEnabled(enabled)
def update_progress(self, value):
"""更新进度条"""
self.progress_bar.setValue(value)
def on_import_completed(self, success, message):
"""导入完成回调"""
self.set_interaction_enabled(True)
self.progress_bar.setVisible(False)
if success:
QMessageBox.information(self, "导入成功", message)
self.import_completed.emit(True, message)
self.accept()
else:
QMessageBox.critical(self, "导入失败", message)
self.import_completed.emit(False, message)
class ExportDialog(QDialog):
"""数据导出选项对话框(功能6,支持勾选记录)"""
def __init__(self, parent=None, current_table="单位表", data=None):
super().__init__(parent)
self.current_table = current_table
self.data = data or {} # {表名: [记录dict, ...]}
self.setWindowTitle("导出数据")
self.setFixedSize(460, 520)
self.setWindowIcon(get_app_icon())
layout = QVBoxLayout(self)
layout.setSpacing(10)
fmt_label = QLabel("导出格式:")
fmt_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold;")
layout.addWidget(fmt_label)
self.excel_rb = QRadioButton("Excel 文件 (.xlsx)")
self.csv_rb = QRadioButton("CSV 文件 (.csv)")
self.excel_rb.setChecked(True)
for rb in (self.excel_rb, self.csv_rb):
rb.setStyleSheet("font-family: 'Microsoft YaHei';")
layout.addWidget(rb)
scope_label = QLabel("导出范围:")
scope_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold;")
layout.addWidget(scope_label)
self.current_rb = QRadioButton(f"仅当前表({current_table})")
self.all_rb = QRadioButton("全部(单位表 + 个人表)")
self.current_rb.setChecked(True)
for rb in (self.current_rb, self.all_rb):
rb.setStyleSheet("font-family: 'Microsoft YaHei';")
layout.addWidget(rb)
rb.toggled.connect(self._load_records)
# 显式按钮组,保证格式/范围各自互斥
self.fmt_group = QButtonGroup(self)
self.fmt_group.addButton(self.excel_rb)
self.fmt_group.addButton(self.csv_rb)
self.scope_group = QButtonGroup(self)
self.scope_group.addButton(self.current_rb)
self.scope_group.addButton(self.all_rb)
# 记录勾选列表
list_tip = QLabel("选择要导出的记录(可勾选 / 全选):")
list_tip.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold;")
layout.addWidget(list_tip)
self.record_list = QListWidget()
self.record_list.setStyleSheet(
"font-family: 'Microsoft YaHei'; border: 1px solid #D9C7A7; "
"border-radius: 6px; background: #FFFEF9;")
layout.addWidget(self.record_list, 1)
sel_layout = QHBoxLayout()
self.select_all_btn = ChineseStyle.create_gradient_button("全选", 80, 30)
self.select_all_btn.clicked.connect(self._select_all)
self.select_none_btn = ChineseStyle.create_gradient_button("全不选", 80, 30, "#8C7853", "#A8967A")
self.select_none_btn.clicked.connect(self._select_none)
sel_layout.addStretch()
sel_layout.addWidget(self.select_all_btn)
sel_layout.addWidget(self.select_none_btn)
layout.addLayout(sel_layout)
self._load_records()
mp_label = QLabel("主密码(导出须验证,导出内容为明文):")
mp_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321; font-weight: bold;")
layout.addWidget(mp_label)
self.mp_input = QLineEdit()
self.mp_input.setEchoMode(QLineEdit.Password)
self.mp_input.setPlaceholderText("请输入主密码以授权导出")
self.mp_input.setStyleSheet(
"font-family: 'Microsoft YaHei'; padding: 6px; "
"border: 1px solid #D9C7A7; border-radius: 6px;")
layout.addWidget(self.mp_input)
btn_layout = QHBoxLayout()
btn_layout.addStretch()
ok_btn = ChineseStyle.create_gradient_button("确定", 100, 35)
ok_btn.clicked.connect(self.accept)
cancel_btn = ChineseStyle.create_gradient_button("取消", 100, 35, "#8C7853", "#A8967A")
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(ok_btn)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
def _load_records(self):
"""根据范围把记录加载到列表(每项带勾选框,默认全选)"""
self.record_list.clear()
tables = [self.current_table] if self.scope_group.checkedButton() is self.current_rb else ["单位表", "个人表"]
multi = len(tables) > 1
for t in tables:
for rec in self.data.get(t, []):
name = rec.get("网站名称", "")
account = rec.get("账号", "")
tag = f"[{t}] " if multi else ""
item = QListWidgetItem(f"{tag}{name} | {account}")
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, {"table": t, "record": rec})
self.record_list.addItem(item)
def _select_all(self):
for i in range(self.record_list.count()):
self.record_list.item(i).setCheckState(Qt.Checked)
def _select_none(self):
for i in range(self.record_list.count()):
self.record_list.item(i).setCheckState(Qt.Unchecked)
def get_selected(self):
"""返回勾选记录列表:[{'table':.., 'record':..}, ...]"""
result = []
for i in range(self.record_list.count()):
item = self.record_list.item(i)
if item.checkState() == Qt.Checked:
result.append(item.data(Qt.UserRole))
return result
def get_choice(self):
return "excel" if self.excel_rb.isChecked() else "csv"
def get_scope(self):
return "current" if self.current_rb.isChecked() else "all"
def get_master_password(self):
return self.mp_input.text().strip()
class IdleSettingsDialog(QDialog):
"""空闲自动锁定时间设置(功能3 可配置,从欢迎菜单进入)"""
def __init__(self, parent=None, current_minutes=DEFAULT_IDLE_MINUTES):
super().__init__(parent)
self.setWindowTitle("空闲自动锁定设置")
self.setFixedSize(360, 180)
self.setWindowIcon(get_app_icon())
layout = QVBoxLayout(self)
layout.setSpacing(14)
tip = QLabel("设置无操作后自动锁定的时间(锁定时需重新输入主密码进入):")
tip.setStyleSheet("font-family: 'Microsoft YaHei'; color: #654321;")
tip.setWordWrap(True)
layout.addWidget(tip)
row = QHBoxLayout()
row.addStretch()
self.spin = QSpinBox()
self.spin.setRange(1, 120)
self.spin.setValue(current_minutes)
self.spin.setSuffix(" 分钟")
self.spin.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 16px;")
row.addWidget(self.spin)
row.addStretch()
layout.addLayout(row)
layout.addStretch()
btn = QHBoxLayout()
btn.addStretch()
ok = ChineseStyle.create_gradient_button("确定", 100, 35)
ok.clicked.connect(self.accept)
cancel = ChineseStyle.create_gradient_button("取消", 100, 35, "#8C7853", "#A8967A")
cancel.clicked.connect(self.reject)
btn.addWidget(ok)
btn.addWidget(cancel)
layout.addLayout(btn)
def get_minutes(self):
return self.spin.value()
class ShareDialog(QDialog):
"""分享记录对话框"""
def __init__(self, parent=None, record=None):
super().__init__(parent)
self.record = record or {}
self.init_ui()
def init_ui(self):
self.setWindowTitle("分享账号信息")
self.setFixedSize(500, 400)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
# 标题
title_label = QLabel("分享账号信息")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
QLabel {
font-size: 20px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 10px;
}
""")
layout.addWidget(title_label)
# 分享格式说明
info_label = QLabel("以下信息已按标准格式生成,可直接复制使用:")
info_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853;")
layout.addWidget(info_label)
# 分享内容文本框
self.share_text = QTextEdit()
self.share_text.setStyleSheet("""
QTextEdit {
border: 1px solid #D9C7A7;
border-radius: 8px;
padding: 10px;
font-family: "Microsoft YaHei";
font-size: 14px;
background-color: #FFFEF9;
selection-background-color: #E8D0B0;
}
""")
self.share_text.setReadOnly(True)
layout.addWidget(self.share_text)
# 生成分享内容
self.generate_share_content()
# 按钮布局
button_layout = QHBoxLayout()
button_layout.addStretch()
copy_btn = ChineseStyle.create_gradient_button("复制内容", 100, 35)
copy_btn.clicked.connect(self.copy_to_clipboard)
close_btn = ChineseStyle.create_gradient_button("关闭", 100, 35, "#8C7853", "#A8967A")
close_btn.clicked.connect(self.accept)
button_layout.addWidget(copy_btn)
button_layout.addWidget(close_btn)
layout.addLayout(button_layout)
def generate_share_content(self):
"""生成分享内容"""
website_name = self.record.get("网站名称", "未知网站")
website_url = self.record.get("网址", "")
account = self.record.get("账号", "")
password = self.record.get("密码", "")
description = self.record.get("备注", "")
# 创建一个列表,用于存储非空的字段信息
fields = []
# 检查每个字段是否为空,非空则添加到列表中
if website_name and website_name != "未知网站":
fields.append(f"网站名称:{website_name}")
if website_url:
fields.append(f"网址:{website_url}")
if account:
fields.append(f"账号:{account}")
if password:
fields.append(f"密码:{password}")
if description:
fields.append(f"备注:{description}")
# 将列表中的字段用换行符连接起来
share_content = "\n".join(fields)
self.share_text.setPlainText(share_content)
def copy_to_clipboard(self):
"""复制内容到剪贴板"""
clipboard = QApplication.clipboard()
clipboard.setText(self.share_text.toPlainText())
# 显示复制成功提示
QMessageBox.information(self, "复制成功", "分享内容已复制到剪贴板!")
class ImportThread(QThread):
"""导入数据的后台线程"""
progress_updated = pyqtSignal(int)
import_completed = pyqtSignal(bool, str)
def __init__(self, file_path, target_table, sheet_name, password_manager):
super().__init__()
self.file_path = file_path
self.target_table = target_table
self.sheet_name = sheet_name
self.pm = password_manager
def run(self):
"""线程运行函数"""
try:
df = _read_import_dataframe(self.file_path, self.sheet_name)
if df.empty:
self.import_completed.emit(False, "文件为空或没有数据")
return
field_mapping = {
'网站名称': ['网站名称', '网站名', '网站', '名称', '站点名称', '平台名称'],
'网址': ['网址', '网站地址', 'URL', '链接', '地址', '网站链接'],
'账号': ['账号', '账户', '用户名', '用户', '登录名', '用户账号'],
'密码': ['密码', '登陆密码', '登录密码', 'pass', 'passwd'],
'备注': ['备注', '说明', '注释', '描述', 'note', 'description']
}
actual_mapping = {}
for target_field, possible_names in field_mapping.items():
for col in df.columns:
if any(name in str(col) for name in possible_names):
actual_mapping[target_field] = col
break
else:
actual_mapping[target_field] = None
total_rows = len(df)
imported_count = 0
skipped_count = 0
error_count = 0
for index, row in df.iterrows():
try:
# 更新进度
progress = int((index + 1) / total_rows * 100)
self.progress_updated.emit(progress)
website_name = row[actual_mapping['网站名称']] if actual_mapping['网站名称'] is not None else ""
website_url = row[actual_mapping['网址']] if actual_mapping['网址'] is not None else ""
account = row[actual_mapping['账号']] if actual_mapping['账号'] is not None else ""
password = row[actual_mapping['密码']] if actual_mapping['密码'] is not None else ""
notes = row[actual_mapping['备注']] if actual_mapping['备注'] is not None else ""
website_name = str(website_name).strip() if pd.notna(website_name) else ""
website_url = str(website_url).strip() if pd.notna(website_url) else ""
account = str(account).strip() if pd.notna(account) else ""
password = str(password).strip() if pd.notna(password) else ""
notes = str(notes).strip() if pd.notna(notes) else ""
if not website_name and not account:
skipped_count += 1
continue
record_data = {
"网站名称": website_name,
"网址": website_url,
"账号": account,
"密码": password,
"备注": notes
}
if self.pm.add_record(self.target_table, record_data):
imported_count += 1
else:
error_count += 1
skipped_count += 1
except Exception as e:
error_count += 1
skipped_count += 1
continue
self.progress_updated.emit(100)
self.import_completed.emit(True,
f"导入完成:成功导入 {imported_count} 条记录,跳过 {skipped_count} 条记录,错误 {error_count} 条记录")
except Exception as e:
self.import_completed.emit(False, f"导入过程中发生错误: {str(e)}")
class MainWindow(QMainWindow):
"""主窗口 - 添加右键菜单功能"""
def __init__(self, password_manager, login_window, app_controller): # 修改:添加 app_controller 参数
super().__init__()
self.pm = password_manager
self.login_window = login_window
self.app_controller = app_controller # 新增:保存应用程序控制器引用
self.current_table = "单位表"
self.current_records = []
self.init_ui()
def init_ui(self):
self.setWindowTitle(f"墨香 - {self.pm.current_user}")
self.setGeometry(100, 100, 1200, 800)
self.setWindowIcon(get_app_icon())
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(10)
main_layout.setContentsMargins(15, 15, 15, 15)
# 标题栏
title_layout = QHBoxLayout()
title_label = QLabel("墨香")
title_label.setStyleSheet("""
QLabel {
font-size: 24px;
font-weight: bold;
color: #654321;
font-family: "Microsoft YaHei";
padding: 5px;
}
""")
title_layout.addWidget(title_label)
title_layout.addStretch()
# 用户信息标签 - 设置为支持右键菜单
self.user_info_label = QLabel(f"欢迎,{self.pm.current_user}")
self.user_info_label.setStyleSheet("""
QLabel {
font-family: 'Microsoft YaHei';
color: #8C7853;
padding: 5px;
border: 1px solid transparent;
border-radius: 5px;
}
QLabel:hover {
background-color: rgba(217, 199, 167, 0.2);
border: 1px solid #D9C7A7;
}
""")
self.user_info_label.setContextMenuPolicy(Qt.CustomContextMenu)
self.user_info_label.customContextMenuRequested.connect(self.show_account_menu)
title_layout.addWidget(self.user_info_label)
main_layout.addLayout(title_layout)
# 控制面板
control_frame = QGroupBox("控制面板")
control_frame.setStyleSheet("""
QGroupBox {
border: 1px solid #D9C7A7;
border-radius: 8px;
margin-top: 10px;
padding-top: 10px;
font-family: "Microsoft YaHei";
color: #654321;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
control_layout = QVBoxLayout(control_frame)
# 第一行控制按钮
control_row1 = QHBoxLayout()
table_label = QLabel("数据表:")
table_label.setStyleSheet("font-family: 'Microsoft YaHei';")
control_row1.addWidget(table_label)
self.table_combo = QComboBox()
self.table_combo.addItems(["单位表", "个人表"])
self.table_combo.setCurrentText(self.current_table)
self.table_combo.currentTextChanged.connect(self.on_table_changed)
self.table_combo.setStyleSheet("""
QComboBox {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
min-width: 100px;
}
""")
control_row1.addWidget(self.table_combo)
control_row1.addSpacing(20)
self.add_btn = ChineseStyle.create_gradient_button("新增记录", 100, 30)
self.add_btn.clicked.connect(self.add_record)
control_row1.addWidget(self.add_btn)
self.edit_btn = ChineseStyle.create_gradient_button("编辑记录", 100, 30)
self.edit_btn.clicked.connect(self.edit_record)
control_row1.addWidget(self.edit_btn)
self.delete_btn = ChineseStyle.create_gradient_button("删除记录", 100, 30)
self.delete_btn.clicked.connect(self.delete_record)
control_row1.addWidget(self.delete_btn)
self.view_password_btn = ChineseStyle.create_gradient_button("查看密码", 100, 30)
self.view_password_btn.clicked.connect(self.view_password)
control_row1.addWidget(self.view_password_btn)
self.refresh_btn = ChineseStyle.create_gradient_button("刷新数据", 100, 30)
self.refresh_btn.clicked.connect(self.refresh_table)
control_row1.addWidget(self.refresh_btn)
self.import_btn = ChineseStyle.create_gradient_button("Excel导入", 100, 30)
self.import_btn.clicked.connect(self.excel_import)
control_row1.addWidget(self.import_btn)
self.share_btn = ChineseStyle.create_gradient_button("分享记录", 100, 30)
self.share_btn.clicked.connect(self.share_record)
control_row1.addWidget(self.share_btn)
self.export_btn = ChineseStyle.create_gradient_button("导出数据", 100, 30, "#8C7853", "#A8967A")
self.export_btn.clicked.connect(self.export_data)
control_row1.addWidget(self.export_btn)
control_row1.addStretch()
control_layout.addLayout(control_row1)
# 第二行搜索功能
control_row2 = QHBoxLayout()
search_label = QLabel("搜 索:")
search_label.setStyleSheet("font-family: 'Microsoft YaHei';")
control_row2.addWidget(search_label)
self.search_field_combo = QComboBox()
self.search_field_combo.addItems(["所有字段", "网站名称", "网址", "账号", "备注"])
self.search_field_combo.setStyleSheet(self.table_combo.styleSheet())
control_row2.addWidget(self.search_field_combo)
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("输入搜索关键词...")
self.search_input.textChanged.connect(self.on_search)
self.search_input.setStyleSheet("""
QLineEdit {
border: 1px solid #D9C7A7;
border-radius: 5px;
padding: 5px;
font-family: "Microsoft YaHei";
}
""")
control_row2.addWidget(self.search_input)
self.clear_search_btn = ChineseStyle.create_gradient_button("清空搜索", 80, 30, "#8C7853", "#A8967A")
self.clear_search_btn.clicked.connect(self.clear_search)
control_row2.addWidget(self.clear_search_btn)
control_row2.addStretch()
control_layout.addLayout(control_row2)
main_layout.addWidget(control_frame)
# 数据表格
table_frame = QGroupBox("数据记录")
table_frame.setStyleSheet(control_frame.styleSheet())
table_layout = QVBoxLayout(table_frame)
self.table_widget = QTableWidget()
self.table_widget.setColumnCount(6)
self.table_widget.setHorizontalHeaderLabels(["ID", "网站名称", "网址", "账号", "密码", "备注"])
self.table_widget.setStyleSheet("""
QTableWidget {
border: 1px solid #D9C7A7;
border-radius: 5px;
background-color: #FFFEF9;
alternate-background-color: #F8F4E9;
font-family: "Microsoft YaHei";
gridline-color: #D9C7A7;
}
QTableWidget::item {
padding: 5px;
border-bottom: 1px solid #E8E0D0;
}
QTableWidget::item:selected {
background-color: #E8D0B0;
color: #654321;
}
QHeaderView::section {
background-color: #D9C7A7;
color: #654321;
font-weight: bold;
padding: 5px;
border: none;
font-family: "Microsoft YaHei";
}
""")
header = self.table_widget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Stretch)
header.setSectionResizeMode(2, QHeaderView.Stretch)
header.setSectionResizeMode(3, QHeaderView.Stretch)
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
header.setSectionResizeMode(5, QHeaderView.Stretch)
self.table_widget.setAlternatingRowColors(True)
self.table_widget.setSelectionBehavior(QTableWidget.SelectRows)
self.table_widget.itemDoubleClicked.connect(self.on_item_double_clicked)
table_layout.addWidget(self.table_widget)
main_layout.addWidget(table_frame)
# 状态栏
status_bar = QHBoxLayout()
self.status_label = QLabel("就绪")
self.status_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853;")
status_bar.addWidget(self.status_label)
status_bar.addStretch()
self.record_count_label = QLabel("")
self.record_count_label.setStyleSheet("font-family: 'Microsoft YaHei'; color: #8C7853;")
status_bar.addWidget(self.record_count_label)
main_layout.addLayout(status_bar)
self.refresh_table()
self.update_status("系统就绪")
# ===== 空闲自动锁定(功能3,空闲时长可配置)=====
self.idle_timeout = load_config().get("idle_minutes", DEFAULT_IDLE_MINUTES) * 60
self.last_activity = time.time()
self.is_locked = False
self._relocking = False
self.installEventFilter(self)
self.idle_timer = QTimer(self)
self.idle_timer.timeout.connect(self.check_idle)
self.idle_timer.start(1000) # 每秒检测一次空闲
def eventFilter(self, obj, event):
"""监听用户鼠标/键盘活动,重置空闲计时"""
if event.type() in (QEvent.MouseMove, QEvent.MouseButtonPress,
QEvent.MouseButtonRelease, QEvent.KeyPress,
QEvent.KeyRelease, QEvent.Wheel):
self.last_activity = time.time()
return super().eventFilter(obj, event)
def check_idle(self):
"""空闲超时则自动锁定"""
if self.is_locked:
return
# 有模态对话框打开时视为活动中,避免误锁
if QApplication.activeModalWidget() is not None:
self.last_activity = time.time()
return
if time.time() - self.last_activity >= self.idle_timeout:
self.auto_lock()
def auto_lock(self):
"""空闲自动锁定:隐藏主窗口并回到登录界面(需主密码重新进入)"""
self.is_locked = True
self.idle_timer.stop()
self._relocking = True
if self.app_controller.tray_icon:
self.app_controller.tray_icon.showMessage(
"墨香密码管理器", "已因长时间无操作自动锁定", QSystemTrayIcon.Information, 2000)
self.hide()
self.login_window.show_login()
def show_account_menu(self, pos):
"""显示账号右键菜单(含空闲锁定设置入口)"""
menu = QMenu(self)
backup_action = QAction("📊 手动备份", self)
change_pwd_action = QAction("🔑 修改密码", self)
idle_action = QAction("⏲ 空闲锁定设置", self)
logout_action = QAction("🚪 注销账号", self)
menu.addAction(backup_action)
menu.addAction(change_pwd_action)
menu.addAction(idle_action)
menu.addSeparator()
menu.addAction(logout_action)
backup_action.triggered.connect(self.manual_backup)
change_pwd_action.triggered.connect(self.change_password)
idle_action.triggered.connect(self.open_idle_settings)
logout_action.triggered.connect(self.logout)
menu.exec_(self.user_info_label.mapToGlobal(pos))
def open_idle_settings(self):
"""打开空闲自动锁定时间设置,并持久化到配置"""
minutes = int(self.idle_timeout // 60)
dlg = IdleSettingsDialog(self, minutes)
if dlg.exec_() == QDialog.Accepted:
new_min = dlg.get_minutes()
self.idle_timeout = new_min * 60
cfg = load_config()
cfg["idle_minutes"] = new_min
save_config(cfg)
self.update_status(f"空闲自动锁定已设为 {new_min} 分钟")
QMessageBox.information(self, "设置已保存",
f"空闲自动锁定时间已设为 {new_min} 分钟。\n下次登录生效(当前会话立即生效)。")
def on_item_double_clicked(self, item):
"""处理表格项双击事件"""
row = item.row()
column = item.column()
if column == 2: # 网址列
url_item = self.table_widget.item(row, 2)
if url_item and url_item.text():
self.open_url_in_browser(url_item.text())
elif column == 4: # 密码列
self.view_password()
else: # 其他列
self.edit_record()
def open_url_in_browser(self, url):
"""在默认浏览器中打开网址"""
if not url:
QMessageBox.warning(self, "网址为空", "该记录的网址为空,无法打开。")
return
if not url.startswith(('http://', 'https://')):
url = 'http://' + url
try:
webbrowser.open(url)
self.update_status(f"正在打开网址: {url}")
except Exception as e:
QMessageBox.critical(self, "打开失败", f"无法打开网址: {str(e)}")
def on_table_changed(self, table_name):
self.current_table = table_name
self.refresh_table()
self.update_status(f"已切换到 {self.current_table}")
def on_search(self):
search_term = self.search_input.text()
search_field = self.search_field_combo.currentText()
self.current_records = self.pm.search_records(self.current_table, search_term, search_field)
self.update_table()
self.update_status(f"搜索完成,找到 {len(self.current_records)} 条记录")
def clear_search(self):
self.search_input.clear()
self.refresh_table()
def refresh_table(self):
self.current_records = self.pm.current_data["tables"][self.current_table]
self.update_table()
self.update_status("数据已刷新")
def update_table(self):
"""更新表格显示"""
self.table_widget.setRowCount(0)
for record in self.current_records:
row = self.table_widget.rowCount()
self.table_widget.insertRow(row)
# 密码显示为星号
display_pwd = "•" * 8 if record.get("密码") else ""
self.table_widget.setItem(row, 0, QTableWidgetItem(str(record["id"])))
self.table_widget.setItem(row, 1, QTableWidgetItem(record.get("网站名称", "")))
# 网址列特殊处理:添加超链接样式
url_item = QTableWidgetItem(record.get("网址", ""))
url_item.setForeground(QColor(166, 77, 55)) # 朱红色
url_item.setToolTip("双击打开此网址")
self.table_widget.setItem(row, 2, url_item)
self.table_widget.setItem(row, 3, QTableWidgetItem(record.get("账号", "")))
self.table_widget.setItem(row, 4, QTableWidgetItem(display_pwd))
self.table_widget.setItem(row, 5, QTableWidgetItem(record.get("备注", "")))
# 更新记录计数
total_records = len(self.pm.current_data["tables"]["单位表"]) + len(self.pm.current_data["tables"]["个人表"])
self.record_count_label.setText(f"总记录数: {total_records}")
def add_record(self):
"""添加新记录"""
dialog = RecordDialog(self, "新增记录", main_window=self)
if dialog.exec_() == QDialog.Accepted and dialog.result_data:
try:
if self.pm.add_record(self.current_table, dialog.result_data):
self.refresh_table()
QMessageBox.information(self, "成功", "记录添加成功")
self.update_status("记录添加成功")
else:
QMessageBox.critical(self, "错误", "添加记录失败")
except Exception as e:
QMessageBox.critical(self, "错误", f"添加记录时出错: {str(e)}")
def edit_record(self):
"""编辑记录"""
selected = self.table_widget.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请选择要编辑的记录")
return
row = self.table_widget.currentRow()
record_id = int(self.table_widget.item(row, 0).text())
# 查找原始记录
original_record = None
for record in self.pm.current_data["tables"][self.current_table]:
if record["id"] == record_id:
original_record = record
break
if not original_record:
QMessageBox.critical(self, "错误", "未找到记录")
return
# 验证主密码
dialog = SecurityDialog(self, "安全验证", "编辑记录需要验证您的主密码")
if dialog.exec_() == QDialog.Accepted:
if self.pm.verify_master_password(dialog.get_password()):
dialog = RecordDialog(self, "编辑记录", original_record, main_window=self)
if dialog.exec_() == QDialog.Accepted and dialog.result_data:
if self.pm.update_record(self.current_table, record_id, dialog.result_data):
self.refresh_table()
QMessageBox.information(self, "成功", "记录更新成功")
self.update_status("记录更新成功")
else:
QMessageBox.critical(self, "错误", "更新记录失败")
else:
QMessageBox.warning(self, "验证失败", "主密码错误,无法编辑记录")
def view_password(self):
"""查看密码功能"""
selected = self.table_widget.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请选择要查看密码的记录")
return
row = self.table_widget.currentRow()
record_id = int(self.table_widget.item(row, 0).text())
# 查找原始记录
original_record = None
for record in self.pm.current_data["tables"][self.current_table]:
if record["id"] == record_id:
original_record = record
break
if not original_record:
QMessageBox.critical(self, "错误", "未找到记录")
return
# 验证主密码
dialog = SecurityDialog(self, "安全验证", "查看密码需要验证您的主密码")
if dialog.exec_() == QDialog.Accepted:
if self.pm.verify_master_password(dialog.get_password()):
site_name = original_record.get("网站名称", "")
username = original_record.get("账号", "")
password = original_record.get("密码", "")
# 显示密码信息
msg = QMessageBox(self)
msg.setWindowTitle("密码详情")
msg.setText(f"""
<div style="font-family: 'Microsoft YaHei'; color: #654321;">
<h3>密码详情</h3>
<p><b>网站名称:</b> {site_name}</p>
<p><b>账号:</b> {username}</p>
<p><b>密码:</b> <span style="color: #A64D37; font-weight: bold;">{password}</span></p>
</div>
""")
msg.setStandardButtons(QMessageBox.Ok)
msg.exec_()
else:
QMessageBox.warning(self, "验证失败", "主密码错误,无法查看密码")
def delete_record(self):
"""删除记录"""
selected = self.table_widget.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请选择要删除的记录")
return
row = self.table_widget.currentRow()
record_id = int(self.table_widget.item(row, 0).text())
reply = QMessageBox.question(self, "确认删除",
"确定要删除选中的记录吗?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
if self.pm.delete_record(self.current_table, record_id):
self.refresh_table()
QMessageBox.information(self, "成功", "记录删除成功")
self.update_status("记录删除成功")
else:
QMessageBox.critical(self, "错误", "删除记录失败")
def share_record(self):
"""分享选中的记录"""
selected = self.table_widget.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请选择要分享的记录")
return
row = self.table_widget.currentRow()
record_id = int(self.table_widget.item(row, 0).text())
# 查找原始记录
original_record = None
for record in self.pm.current_data["tables"][self.current_table]:
if record["id"] == record_id:
original_record = record
break
if not original_record:
QMessageBox.critical(self, "错误", "未找到记录")
return
# 验证主密码
dialog = SecurityDialog(self, "安全验证", "分享记录需要验证您的主密码")
if dialog.exec_() == QDialog.Accepted:
if self.pm.verify_master_password(dialog.get_password()):
self.show_share_dialog(original_record)
else:
QMessageBox.warning(self, "验证失败", "主密码错误,无法分享记录")
def show_share_dialog(self, record):
"""显示分享对话框"""
dialog = ShareDialog(self, record)
dialog.exec_()
def manual_backup(self):
"""手动备份数据"""
success, message = self.pm.backup_data("manual")
if success:
QMessageBox.information(self, "备份成功", message)
self.update_status("数据备份完成")
else:
QMessageBox.critical(self, "备份失败", message)
def change_password(self):
"""打开修改密码对话框"""
dialog = ChangePasswordDialog(self, self.pm)
result = dialog.exec_()
if result == QDialog.Accepted:
# 密码修改成功,可以执行一些后续操作
self.update_status("密码修改成功")
# 可选:询问用户是否重新登录
reply = QMessageBox.question(self, "修改成功",
"密码修改成功,建议重新登录以确保安全。\n是否立即重新登录?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.logout()
# def change_password(self):
# """修改密码"""
# dialog = ChangePasswordDialog(self, self.pm)
# dialog.exec_()
def logout(self):
"""注销当前账号"""
reply = QMessageBox.question(self, "确认注销",
"确定要注销当前账号吗?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.pm.logout()
self.hide()
self.login_window.show_login()
def update_status(self, message):
"""更新状态栏"""
self.status_label.setText(message)
def excel_import(self):
"""打开Excel导入对话框"""
dialog = ExcelImportDialog(self, self.pm)
dialog.import_completed.connect(self.on_excel_import_completed)
dialog.exec_()
def on_excel_import_completed(self, success, message):
"""Excel导入完成回调"""
if success:
self.refresh_table()
total_records = len(self.pm.current_data["tables"]["单位表"]) + len(self.pm.current_data["tables"]["个人表"])
self.record_count_label.setText(f"总记录数: {total_records}")
self.update_status(message)
def export_data(self):
"""导出数据(Excel / CSV,导出内容为明文,须输入主密码授权,可勾选记录)"""
data = self.pm.current_data.get("tables", {})
dlg = ExportDialog(self, self.current_table, data)
if dlg.exec_() != QDialog.Accepted:
return
fmt = dlg.get_choice()
# 导出涉及明文密码,必须输入并验证主密码
master_pwd = dlg.get_master_password()
if not master_pwd:
QMessageBox.warning(self, "需要主密码", "导出明文密码必须先输入主密码")
return
if not self.pm.verify_master_password(master_pwd):
QMessageBox.warning(self, "验证失败", "主密码错误,已取消导出")
return
selected = dlg.get_selected()
if not selected:
QMessageBox.warning(self, "未选择记录", "请至少勾选一条要导出的记录")
return
scope = dlg.get_scope()
if fmt == "excel":
file_filter = "Excel 文件 (*.xlsx)"
default_name = "墨香密码导出.xlsx"
else:
if scope == "all":
file_filter = "ZIP 文件 (*.zip)"
default_name = "墨香密码导出.zip"
else:
file_filter = "CSV 文件 (*.csv)"
default_name = "墨香密码导出.csv"
save_path, _ = QFileDialog.getSaveFileName(
self, "导出数据", os.path.join(BASE_DIR, default_name), file_filter)
if not save_path:
return
reply = QMessageBox.warning(
self, "安全提醒",
"导出的文件将包含【明文密码】。\n请勿通过微信/邮件等不安全渠道发送,也不要长期存放在公共电脑上。\n\n确定导出吗?",
QMessageBox.Yes | QMessageBox.No)
if reply != QMessageBox.Yes:
return
# 与导入格式对齐的列(去掉 id,导入时会重新分配)
columns = ["网站名称", "网址", "账号", "密码", "备注"]
try:
# 按表分组勾选的记录
by_table = {}
for sel in selected:
by_table.setdefault(sel["table"], []).append(sel["record"])
if fmt == "excel":
with pd.ExcelWriter(save_path, engine="openpyxl") as writer:
for t, rows in by_table.items():
df = pd.DataFrame(
[{c: r.get(c, "") for c in columns} for r in rows],
columns=columns)
df.to_excel(writer, sheet_name=t, index=False)
else:
if len(by_table) == 1:
# 单表:直接写 CSV,可被导入功能重新导入
rows = list(by_table.values())[0]
df = pd.DataFrame(
[{c: r.get(c, "") for c in columns} for r in rows],
columns=columns)
df.to_csv(save_path, index=False, encoding="utf-8-sig")
else:
# 多表:打包成 zip,内含 单位表.csv / 个人表.csv,解压后可逐表导入
with zipfile.ZipFile(save_path, "w", zipfile.ZIP_DEFLATED) as zf:
for t, rows in by_table.items():
df = pd.DataFrame(
[{c: r.get(c, "") for c in columns} for r in rows],
columns=columns)
buffer = io.StringIO()
df.to_csv(buffer, index=False, encoding="utf-8-sig")
zf.writestr(f"{t}.csv", buffer.getvalue())
QMessageBox.information(self, "导出成功", f"已导出 {len(selected)} 条记录到:\n{save_path}")
self.update_status("数据导出完成")
except Exception as e:
QMessageBox.critical(self, "导出失败", f"导出数据时出错:{str(e)}")
# 新增:重写关闭事件处理
def closeEvent(self, event):
"""重写关闭事件,询问用户操作"""
if getattr(self, '_relocking', False):
event.accept()
return
if not self.app_controller.is_hidden_to_tray:
reply = QMessageBox.question(
self,
"确认退出",
"您确定要退出程序吗?\n\n选择'是'将退出程序,选择'否'将最小化到系统托盘。",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# 退出程序
event.accept()
self.app_controller.quit_application()
elif reply == QMessageBox.No:
# 最小化到托盘
event.ignore()
self.app_controller.hide_to_tray()
else:
# 取消关闭
event.ignore()
else:
event.accept()
def get_app_icon():
"""返回应用图标(优先 枫叶.ico,兼容打包环境),缺失时回退内置图标"""
candidates = []
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
candidates.append(os.path.join(sys._MEIPASS, '枫叶.ico'))
candidates.append(os.path.join(BASE_DIR, '枫叶.ico'))
candidates.append(os.path.join(BASE_DIR, '枫叶.png'))
for path in candidates:
if os.path.exists(path):
icon = QIcon(path)
if not icon.isNull():
return icon
from PyQt5.QtWidgets import QStyle
return QApplication.style().standardIcon(QStyle.SP_DriveHDIcon)
class ApplicationController:
"""应用程序控制器,管理窗口切换"""
def __init__(self):
self.pm = PasswordManager()
self.login_window = LoginWindow(self.pm)
self.main_window = None
self.tray_icon = None
self.is_hidden_to_tray = False
# 连接信号
self.login_window.login_success.connect(self.show_main_window)
# 初始化系统托盘
self.init_tray_icon()
def create_tray_icon(self):
"""创建托盘图标(统一使用 get_app_icon)"""
return get_app_icon()
def init_tray_icon(self):
"""初始化系统托盘图标"""
if not QSystemTrayIcon.isSystemTrayAvailable():
print("系统不支持托盘功能")
return
self.tray_icon = QSystemTrayIcon(self.login_window)
# 设置托盘图标
icon = self.create_tray_icon()
self.tray_icon.setIcon(icon)
self.tray_icon.setToolTip("墨香密码管理器")
# 创建托盘菜单
tray_menu = QMenu()
show_action = QAction("显示主窗口", self.login_window)
show_action.triggered.connect(self.show_main_window_from_tray)
tray_menu.addAction(show_action)
tray_menu.addSeparator()
exit_action = QAction("退出", self.login_window)
exit_action.triggered.connect(self.quit_application)
tray_menu.addAction(exit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.on_tray_icon_activated)
# 显示托盘图标
self.tray_icon.show()
print("系统托盘图标已初始化")
def on_tray_icon_activated(self, reason):
"""托盘图标激活事件"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_main_window_from_tray()
elif reason == QSystemTrayIcon.Trigger:
# 单击也可以显示菜单
pass
def show_main_window_from_tray(self):
"""从托盘显示主窗口"""
if self.main_window:
self.main_window.show()
self.main_window.raise_()
self.main_window.activateWindow()
self.is_hidden_to_tray = False
print("从托盘恢复主窗口")
def show_main_window(self, username):
"""显示主窗口"""
if self.main_window:
self.main_window.close()
self.main_window = MainWindow(self.pm, self.login_window, self)
self.main_window.show()
self.is_hidden_to_tray = False
print("主窗口已显示")
def hide_to_tray(self):
"""隐藏到系统托盘"""
if self.main_window:
self.main_window.hide()
self.is_hidden_to_tray = True
if self.tray_icon:
self.tray_icon.showMessage(
"墨香密码管理器",
"程序已最小化到系统托盘",
QSystemTrayIcon.Information,
2000
)
print("已隐藏到系统托盘")
def quit_application(self):
"""退出应用程序"""
print("正在退出应用程序...")
if self.tray_icon:
self.tray_icon.hide()
if self.main_window:
self.main_window.close()
if self.login_window:
self.login_window.close()
QApplication.quit()
def start(self):
"""启动应用程序"""
self.login_window.show_login()
def main():
# 高 DPI 适配:必须在创建 QApplication 之前设置,避免 4K 屏模糊
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
# 修改:创建单实例应用程序
app = SingleApplication(sys.argv) # 使用 SingleApplication 替代 QApplication
# 新增:检查是否已有实例运行
if app.is_running():
QMessageBox.information(
None,
"程序已运行",
"墨香密码管理器已经在运行中!\n\n请检查系统托盘或任务栏。"
)
sys.exit(0)
# 设置中国风样式
ChineseStyle.setup_style(app)
# 检查并安装必要的加密库
try:
from Crypto.Cipher import AES
except ImportError:
reply = QMessageBox.question(
None, "缺少依赖库",
"系统需要pycryptodome库进行数据加密。是否要安装?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
try:
import subprocess
subprocess.check_call([sys.executable, "-m", "pip", "install", "pycryptodome"])
QMessageBox.information(None, "安装成功", "依赖库安装成功,请重新启动程序")
except Exception as e:
QMessageBox.critical(None, "安装失败", f"安装依赖库失败: {str(e)}")
sys.exit(0)
else:
QMessageBox.warning(None, "警告", "未安装加密库,数据将不会加密存储")
controller = ApplicationController()
controller.start()
sys.exit(app.exec_())
if __name__ == "__main__":
main()▌获取方式 1. 百度网盘 链接: https://pan.baidu.com/s/1S2DjUov0dfJwX2vUJNwGMA 提取码: byin 2. 夸克网盘 链接: https://pan.quark.cn/s/80928e23125e 免费评分 | |||||||||||||||||||||||||||||||||||||
|
发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。 |
|||||||||||||||||||||||||||||||||||||
来自 2#
|
发表于 2026-8-25 11:46
|楼主
| ||
|
3#
发表于 2026-8-25 11:47
| ||
|
4#
发表于 2026-8-25 13:17
| |
|
5#
发表于 2026-8-25 13:25
| ||
|
6#
发表于 2026-8-25 13:49
| ||
RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )
GMT+8, 2026-9-10 05:02
Powered by Discuz!
Copyright © 2001-2020, Tencent Cloud.