有分类功能了

This commit is contained in:
2025-11-25 20:45:10 +08:00
parent dd801f5d6a
commit 77b9eb978d
164 changed files with 1283 additions and 13008 deletions

View File

@@ -26,11 +26,11 @@ from .tools.finance_manager import FinanceManager, TransactionType, Transaction,
class CreateTransactionDialog(QDialog):
"""创建/编辑交易记录对话框"""
def __init__(self, parent=None, transaction: Optional[Transaction] = None, account_id: Optional[str] = None):
def __init__(self, parent=None, transaction: Optional[Transaction] = None, account_id: Optional[str] = None, finance_manager=None):
super().__init__(parent)
self.transaction = transaction
self.account_id = account_id
self.finance_manager = FinanceManager()
self.finance_manager = finance_manager if finance_manager else FinanceManager()
self.setWindowTitle("新建交易记录" if not transaction else "编辑交易记录")
self.setGeometry(100, 100, 600, 500)
@@ -59,6 +59,32 @@ class CreateTransactionDialog(QDialog):
type_layout.addStretch()
layout.addLayout(type_layout)
# 分类
category_layout = QHBoxLayout()
category_layout.addWidget(BodyLabel("分类:"))
self.category_combo = ComboBox()
# 从财务管理器获取分类列表
categories = []
if self.account_id:
# 确保账户数据已加载
account = self.finance_manager.get_account(self.account_id)
if account:
categories = account.categories
# 如果没有分类,提示用户创建
if not categories:
self.category_combo.addItem("请先在做账页创建分类")
self.category_combo.setEnabled(False)
else:
for cat in categories:
self.category_combo.addItem(cat)
self.category_combo.setEnabled(True)
self.category_combo.setMaximumWidth(200)
category_layout.addWidget(self.category_combo)
category_layout.addStretch()
layout.addLayout(category_layout)
# 日期
date_layout = QHBoxLayout()
date_layout.addWidget(BodyLabel("日期:"))
@@ -176,6 +202,13 @@ class CreateTransactionDialog(QDialog):
else:
self.transaction_type_combo.setCurrentIndex(1) # 支出
# 设置分类
category_index = self.category_combo.findText(transaction.category)
if category_index >= 0:
self.category_combo.setCurrentIndex(category_index)
else:
self.category_combo.setCurrentIndex(0) # 默认为第一个
self.date_edit.setDate(QDate.fromString(transaction.date, "yyyy-MM-dd"))
# 显示绝对值
self.amount_spin.setValue(abs(transaction.amount))
@@ -206,6 +239,19 @@ class CreateTransactionDialog(QDialog):
amount = self.amount_spin.value()
trader = self.trader_edit.text().strip()
notes = self.notes_edit.toPlainText().strip()
category = self.category_combo.currentText()
# 检查分类是否有效
if not category or category == "请先在做账页创建分类":
dialog = Dialog(
title="验证错误",
content="请先创建分类后再添加交易",
parent=self
)
dialog.yesButton.setText("确定")
dialog.cancelButton.hide()
dialog.exec()
return
if not trader:
dialog = Dialog(
@@ -239,13 +285,13 @@ class CreateTransactionDialog(QDialog):
self.finance_manager.update_transaction(
self.account_id, trans_id,
date=date_str, amount=final_amount,
trader=trader, notes=notes
trader=trader, notes=notes, category=category
)
else:
# 创建新交易记录
transaction = Transaction(
date=date_str, amount=final_amount,
trader=trader, notes=notes
trader=trader, notes=notes, category=category
)
self.finance_manager.add_transaction(self.account_id, transaction)
trans_id = transaction.id
@@ -455,6 +501,13 @@ class FinanceInterface(QWidget):
# 标题和操作按钮
title_layout = QHBoxLayout()
title_layout.addWidget(SubtitleLabel("交易记录"))
# 新建分类按钮
new_category_btn = PushButton("新建分类")
new_category_btn.setFixedWidth(90)
new_category_btn.clicked.connect(self.on_create_category_clicked)
title_layout.addWidget(new_category_btn)
title_layout.addStretch()
new_record_btn = PrimaryPushButton("新建记录")
@@ -465,7 +518,7 @@ class FinanceInterface(QWidget):
# 记录表格 - 使用 TreeWidget 风格
self.records_table = TreeWidget()
self.records_table.setHeaderLabels(["日期", "交易人", "金额 (元)", "备注"])
self.records_table.setHeaderLabels(["日期", "交易人", "分类", "金额 (元)", "备注"])
self.records_table.setSelectionMode(self.records_table.SingleSelection)
self.records_table.setIndentation(0)
@@ -477,7 +530,8 @@ class FinanceInterface(QWidget):
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.Stretch)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.Stretch)
# 启用列宽可拖动
header.setSectionsMovable(False)
header.setStretchLastSection(False)
@@ -545,7 +599,7 @@ class FinanceInterface(QWidget):
filter_card = CardWidget()
filter_layout = QVBoxLayout(filter_card)
filter_layout.setContentsMargins(20, 15, 20, 15)
filter_layout.setSpacing(12)
filter_layout.setSpacing(15) # 增加间距为15
# 第一行:日期范围
date_layout = QHBoxLayout()
@@ -559,29 +613,44 @@ class FinanceInterface(QWidget):
date_layout.addWidget(self.query_date_end, 1)
filter_layout.addLayout(date_layout)
# 第二行:交易类型和金额范围
type_amount_layout = QHBoxLayout()
type_amount_layout.addWidget(BodyLabel("交易类型:"))
# 第二行:交易类型和分类
type_category_layout = QHBoxLayout()
type_category_layout.addWidget(BodyLabel("交易类型:"))
self.query_transaction_type = ComboBox()
self.query_transaction_type.addItem("全部")
self.query_transaction_type.addItem("收入 (正数)")
self.query_transaction_type.addItem("支出 (负数)")
type_amount_layout.addWidget(self.query_transaction_type, 1)
type_category_layout.addWidget(self.query_transaction_type, 1)
type_amount_layout.addWidget(BodyLabel("金额范围:"))
type_category_layout.addWidget(BodyLabel("分类:"))
self.query_category = ComboBox()
self.query_category.addItem("全部")
# 初始化时加载现有分类
account_id = self.get_current_account_id()
if account_id:
account = self.finance_manager.get_account(account_id)
if account:
for cat in account.categories:
self.query_category.addItem(cat)
type_category_layout.addWidget(self.query_category, 1)
filter_layout.addLayout(type_category_layout)
# 第三行:金额范围
amount_layout = QHBoxLayout()
amount_layout.addWidget(BodyLabel("金额范围:"))
self.query_amount_min = DoubleSpinBox()
self.query_amount_min.setRange(0, 999999999)
self.query_amount_min.setRange(0, 999999)
self.query_amount_min.setPrefix("¥ ")
type_amount_layout.addWidget(self.query_amount_min, 1)
type_amount_layout.addWidget(BodyLabel(""))
amount_layout.addWidget(self.query_amount_min, 1)
amount_layout.addWidget(BodyLabel(""))
self.query_amount_max = DoubleSpinBox()
self.query_amount_max.setRange(0, 999999999)
self.query_amount_max.setValue(999999999)
self.query_amount_max.setRange(0, 999999)
self.query_amount_max.setValue(999999)
self.query_amount_max.setPrefix("¥ ")
type_amount_layout.addWidget(self.query_amount_max, 1)
filter_layout.addLayout(type_amount_layout)
amount_layout.addWidget(self.query_amount_max, 1)
filter_layout.addLayout(amount_layout)
# 第行:交易人搜索和查询按钮
# 第行:交易人搜索和查询按钮
trader_layout = QHBoxLayout()
trader_layout.addWidget(BodyLabel("交易人:"))
self.query_trader_edit = SearchLineEdit()
@@ -598,7 +667,7 @@ class FinanceInterface(QWidget):
# 查询结果表格 - 使用 TreeWidget 风格
self.query_result_table = TreeWidget()
self.query_result_table.setHeaderLabels(["日期", "交易人", "金额 (元)", "备注"])
self.query_result_table.setHeaderLabels(["日期", "交易人", "分类", "金额 (元)", "备注"])
self.query_result_table.setSelectionMode(self.query_result_table.SingleSelection)
self.query_result_table.setIndentation(0)
@@ -610,7 +679,8 @@ class FinanceInterface(QWidget):
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.Stretch)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.Stretch)
# 启用列宽可拖动
header.setSectionsMovable(False)
header.setStretchLastSection(False)
@@ -701,14 +771,31 @@ class FinanceInterface(QWidget):
return widget
def init_default_account(self):
"""初始化默认账户"""
"""初始化默认账户'admin'"""
accounts = self.finance_manager.get_all_accounts()
if accounts:
# 查找 admin 账户(通常应该存在,因为 FinanceManager 会自动创建)
admin_account = None
for account in accounts:
if account.name == "admin":
admin_account = account
break
# 设置为当前账户
if admin_account:
self.default_account_id = admin_account.id
elif accounts:
# 备用方案:如果找不到 admin使用第一个账户
self.default_account_id = accounts[0].id
self.refresh_records_display()
else:
self.default_account_id = None
self.clear_records_table()
# 如果没有任何账户(不应该发生),创建 admin
admin_account = self.finance_manager.create_account(
account_name="admin",
description="默认管理账户"
)
self.default_account_id = admin_account.id
self.refresh_records_display()
def refresh_account_list(self):
"""刷新账户列表(已移除,保留兼容性)"""
@@ -748,8 +835,9 @@ class FinanceInterface(QWidget):
item = QTreeWidgetItem()
item.setText(0, transaction.date)
item.setText(1, transaction.trader)
item.setText(2, f"¥ {transaction.amount:.2f}")
item.setText(3, transaction.notes or "")
item.setText(2, transaction.category)
item.setText(3, f"¥ {transaction.amount:.2f}")
item.setText(4, transaction.notes or "")
# 存储交易ID到第一列的 UserRole (Qt.UserRole = 32)
item.setData(0, 32, transaction.id)
@@ -788,7 +876,7 @@ class FinanceInterface(QWidget):
)
return
dialog = CreateTransactionDialog(self, account_id=account_id)
dialog = CreateTransactionDialog(self, account_id=account_id, finance_manager=self.finance_manager)
result = dialog.exec_()
# 无论是否成功,都尝试刷新表格
if result == QDialog.Accepted:
@@ -816,7 +904,7 @@ class FinanceInterface(QWidget):
if not transaction:
return
dialog = CreateTransactionDialog(self, transaction=transaction, account_id=account_id)
dialog = CreateTransactionDialog(self, transaction=transaction, account_id=account_id, finance_manager=self.finance_manager)
result = dialog.exec_()
if result == QDialog.Accepted:
self.refresh_records_display()
@@ -890,17 +978,42 @@ class FinanceInterface(QWidget):
# 重新加载最新数据
self.finance_manager.load_all_accounts()
# 更新分类下拉框(如果分类列表发生变化)
account = self.finance_manager.get_account(account_id)
if account:
# 检查分类是否已经存在于下拉框中
current_count = self.query_category.count()
expected_count = len(account.categories) + 1 # +1 是因为有"全部"选项
# 只有在分类数量变化时才重新加载
if current_count != expected_count:
current_category = self.query_category.currentText()
self.query_category.clear()
self.query_category.addItem("全部")
for cat in account.categories:
self.query_category.addItem(cat)
# 尽量恢复之前的选择
index = self.query_category.findText(current_category)
if index >= 0:
self.query_category.setCurrentIndex(index)
else:
self.query_category.setCurrentIndex(0) # 默认选择"全部"
date_start = self.query_date_start.date().toString("yyyy-MM-dd")
date_end = self.query_date_end.date().toString("yyyy-MM-dd")
amount_min = self.query_amount_min.value() if self.query_amount_min.value() > 0 else None
amount_max = self.query_amount_max.value() if self.query_amount_max.value() < 999999999 else None
trader = self.query_trader_edit.text().strip() or None
# 分类查询 - 获取当前选择的分类
category_text = self.query_category.currentText()
category = None if category_text == "全部" else category_text
results = self.finance_manager.query_transactions(
account_id,
date_start=date_start, date_end=date_end,
amount_min=amount_min, amount_max=amount_max,
trader=trader
trader=trader, category=category
)
# 根据交易类型过滤
@@ -918,8 +1031,9 @@ class FinanceInterface(QWidget):
item = QTreeWidgetItem()
item.setText(0, transaction.date)
item.setText(1, transaction.trader)
item.setText(2, f"¥ {transaction.amount:.2f}")
item.setText(3, transaction.notes or "")
item.setText(2, transaction.category)
item.setText(3, f"¥ {transaction.amount:.2f}")
item.setText(4, transaction.notes or "")
# 存储交易ID
item.setData(0, 32, transaction.id)
@@ -1109,3 +1223,86 @@ class FinanceInterface(QWidget):
trans_id = current_item.data(0, 32) # Qt.UserRole = 32
if trans_id:
self.view_record(trans_id)
def on_create_category_clicked(self):
"""新建分类按钮点击"""
account_id = self.get_current_account_id()
if not account_id:
InfoBar.warning(
title="提示",
content="请先创建或选择一个账户",
isClosable=True,
position=InfoBarPosition.TOP,
duration=3000,
parent=self
)
return
# 创建自定义对话框
from PyQt5.QtWidgets import QDialog as QStdDialog, QLabel
category_dialog = QStdDialog(self)
category_dialog.setWindowTitle("新建分类")
category_dialog.setGeometry(100, 100, 400, 150)
layout = QVBoxLayout(category_dialog)
layout.addWidget(BodyLabel("分类名称:"))
input_edit = LineEdit()
input_edit.setPlaceholderText("例如:食品、交通、娱乐等")
layout.addWidget(input_edit)
# 按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
def on_create():
category_name = input_edit.text().strip()
if not category_name:
InfoBar.warning(
title="提示",
content="分类名称不能为空",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
return
if self.finance_manager.add_category(account_id, category_name):
InfoBar.success(
title="成功",
content=f"分类 '{category_name}' 创建成功",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
# 更新查询页面的分类下拉框
if hasattr(self, 'query_category'):
# 检查是否已经存在
if self.query_category.findText(category_name) < 0:
self.query_category.addItem(category_name)
category_dialog.accept()
else:
InfoBar.warning(
title="提示",
content="分类已存在",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
cancel_btn = PushButton("取消")
cancel_btn.clicked.connect(category_dialog.reject)
btn_layout.addWidget(cancel_btn)
create_btn = PrimaryPushButton("创建")
create_btn.clicked.connect(on_create)
btn_layout.addWidget(create_btn)
layout.addLayout(btn_layout)
category_dialog.exec()

View File

@@ -26,7 +26,8 @@ class Transaction:
def __init__(self, trans_id: Optional[str] = None, date: Optional[str] = None, amount: float = 0.0,
trader: str = "", notes: str = "", invoice_path: Optional[str] = None,
payment_path: Optional[str] = None, purchase_path: Optional[str] = None):
payment_path: Optional[str] = None, purchase_path: Optional[str] = None,
category: str = ""):
self.id = trans_id or str(uuid.uuid4())
self.date = date or datetime.now().strftime("%Y-%m-%d")
self.amount = amount
@@ -35,6 +36,7 @@ class Transaction:
self.invoice_path = invoice_path # 相对路径
self.payment_path = payment_path
self.purchase_path = purchase_path
self.category = category # 交易分类,用户自定义
self.created_at = datetime.now().isoformat()
self.updated_at = datetime.now().isoformat()
@@ -49,6 +51,7 @@ class Transaction:
'invoice_path': self.invoice_path,
'payment_path': self.payment_path,
'purchase_path': self.purchase_path,
'category': self.category,
'created_at': self.created_at,
'updated_at': self.updated_at
}
@@ -64,7 +67,8 @@ class Transaction:
notes=data.get('notes', ''),
invoice_path=data.get('invoice_path'),
payment_path=data.get('payment_path'),
purchase_path=data.get('purchase_path')
purchase_path=data.get('purchase_path'),
category=data.get('category', '')
)
if 'created_at' in data:
trans.created_at = data['created_at']
@@ -81,6 +85,7 @@ class Account:
self.name = account_name
self.description = description
self.transactions: List[Transaction] = []
self.categories: List[str] = [] # 空列表,用户自定义分类
self.created_at = datetime.now().isoformat()
self.updated_at = datetime.now().isoformat()
@@ -111,6 +116,7 @@ class Account:
'id': self.id,
'name': self.name,
'description': self.description,
'categories': self.categories,
'transactions': [t.to_dict() for t in self.transactions],
'created_at': self.created_at,
'updated_at': self.updated_at
@@ -124,6 +130,7 @@ class Account:
account_name=data.get('name', ''),
description=data.get('description', '')
)
account.categories = data.get('categories', []) # 使用存储的分类,如果没有则为空列表
account.transactions = [Transaction.from_dict(t) for t in data.get('transactions', [])]
if 'created_at' in data:
account.created_at = data['created_at']
@@ -152,6 +159,13 @@ class FinanceManager:
self._ensure_directory_structure()
self.accounts: Dict[str, Account] = {}
self.load_all_accounts()
# 如果没有账户,自动创建 admin 账户
if len(self.accounts) == 0:
self.create_account(
account_name="admin",
description="默认管理账户"
)
def _ensure_directory_structure(self) -> None:
"""确保目录结构完整"""
@@ -183,6 +197,7 @@ class FinanceManager:
'id': account.id,
'name': account.name,
'description': account.description,
'categories': account.categories,
'created_at': account.created_at,
'updated_at': account.updated_at
}
@@ -325,7 +340,7 @@ class FinanceManager:
# 更新允许的字段
allowed_fields = ['date', 'amount', 'trader', 'notes', 'invoice_path',
'payment_path', 'purchase_path']
'payment_path', 'purchase_path', 'category']
for field, value in kwargs.items():
if field in allowed_fields:
setattr(transaction, field, value)
@@ -343,7 +358,8 @@ class FinanceManager:
def query_transactions(self, account_id: str, date_start: Optional[str] = None,
date_end: Optional[str] = None, amount_min: Optional[float] = None,
amount_max: Optional[float] = None, trader: Optional[str] = None) -> List[Transaction]:
amount_max: Optional[float] = None, trader: Optional[str] = None,
category: Optional[str] = None) -> List[Transaction]:
"""查询交易记录(支持多条件筛选)"""
account = self.accounts.get(account_id)
if not account:
@@ -367,6 +383,10 @@ class FinanceManager:
if trader and trader.lower() not in trans.trader.lower():
continue
# 分类筛选
if category and trans.category != category:
continue
results.append(trans)
# 按日期排序
@@ -498,6 +518,7 @@ class FinanceManager:
account_name=metadata['name'],
description=metadata.get('description', '')
)
account.categories = metadata.get('categories', []) # 从元数据加载分类
account.created_at = metadata.get('created_at')
account.updated_at = metadata.get('updated_at')
@@ -525,13 +546,14 @@ class FinanceManager:
import csv
with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
writer.writerow(['日期', '金额', '交易人', '备注', '创建时间'])
writer.writerow(['日期', '金额', '交易人', '分类', '备注', '创建时间'])
for trans in account.transactions:
writer.writerow([
trans.date,
trans.amount,
trans.trader,
trans.category,
trans.notes,
trans.created_at
])
@@ -540,3 +562,36 @@ class FinanceManager:
except Exception as e:
print(f"导出CSV出错: {e}")
return False
def add_category(self, account_id: str, category: str) -> bool:
"""添加交易分类"""
account = self.accounts.get(account_id)
if not account:
return False
if category not in account.categories:
account.categories.append(category)
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
return False
def delete_category(self, account_id: str, category: str) -> bool:
"""删除交易分类"""
account = self.accounts.get(account_id)
if not account:
return False
if category in account.categories:
account.categories.remove(category)
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
return False
def get_categories(self, account_id: str) -> List[str]:
"""获取账户的所有分类"""
account = self.accounts.get(account_id)
if not account:
return []
return account.categories