mirror of
https://github.com/goldenfishs/MRobot.git
synced 2026-02-04 18:00:19 +08:00
1078 lines
40 KiB
Python
1078 lines
40 KiB
Python
"""
|
|
财务做账应用主界面
|
|
包含做账、查询、导出三个功能标签页
|
|
"""
|
|
|
|
from typing import Optional
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QStackedLayout, QStackedWidget,
|
|
QFileDialog, QScrollArea, QFrame,
|
|
QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTreeWidget, QTreeWidgetItem)
|
|
from PyQt5.QtCore import Qt, QDate, pyqtSignal, QMimeData, QRect, QSize, QItemSelectionModel
|
|
from PyQt5.QtGui import QIcon, QPixmap, QDrag, QFont, QColor
|
|
from qfluentwidgets import (TitleLabel, BodyLabel, SubtitleLabel, StrongBodyLabel,
|
|
HorizontalSeparator, CardWidget, PushButton, LineEdit,
|
|
SpinBox, CheckBox, TextEdit, PrimaryPushButton,
|
|
InfoBar, InfoBarPosition, FluentIcon as FIF, ComboBox,
|
|
DoubleSpinBox, DateEdit, SearchLineEdit, StateToolTip,
|
|
Dialog, SegmentedWidget, TreeWidget)
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
import os
|
|
|
|
from .tools.finance_manager import FinanceManager, TransactionType, Transaction, Account
|
|
|
|
|
|
class CreateTransactionDialog(QDialog):
|
|
"""创建/编辑交易记录对话框"""
|
|
|
|
def __init__(self, parent=None, transaction: Optional[Transaction] = None, account_id: Optional[str] = None):
|
|
super().__init__(parent)
|
|
self.transaction = transaction
|
|
self.account_id = account_id
|
|
self.finance_manager = FinanceManager()
|
|
|
|
self.setWindowTitle("新建交易记录" if not transaction else "编辑交易记录")
|
|
self.setGeometry(100, 100, 600, 500)
|
|
self.init_ui()
|
|
|
|
if transaction:
|
|
self.load_transaction_data(transaction)
|
|
|
|
def init_ui(self):
|
|
"""初始化UI"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
# 日期
|
|
date_layout = QHBoxLayout()
|
|
date_layout.addWidget(BodyLabel("日期:"))
|
|
self.date_edit = DateEdit()
|
|
self.date_edit.setDate(QDate.currentDate())
|
|
date_layout.addWidget(self.date_edit)
|
|
date_layout.addStretch()
|
|
layout.addLayout(date_layout)
|
|
|
|
# 金额
|
|
amount_layout = QHBoxLayout()
|
|
amount_layout.addWidget(BodyLabel("金额 (元):"))
|
|
self.amount_spin = DoubleSpinBox()
|
|
self.amount_spin.setRange(0, 999999999)
|
|
self.amount_spin.setDecimals(2)
|
|
amount_layout.addWidget(self.amount_spin)
|
|
amount_layout.addStretch()
|
|
layout.addLayout(amount_layout)
|
|
|
|
# 交易人
|
|
trader_layout = QHBoxLayout()
|
|
trader_layout.addWidget(BodyLabel("交易人:"))
|
|
self.trader_edit = LineEdit()
|
|
trader_layout.addWidget(self.trader_edit)
|
|
layout.addLayout(trader_layout)
|
|
|
|
# 备注
|
|
notes_layout = QHBoxLayout()
|
|
notes_layout.addWidget(BodyLabel("备注:"))
|
|
self.notes_edit = TextEdit()
|
|
self.notes_edit.setMaximumHeight(80)
|
|
notes_layout.addWidget(self.notes_edit)
|
|
layout.addLayout(notes_layout)
|
|
|
|
# 图片部分
|
|
layout.addWidget(HorizontalSeparator())
|
|
layout.addWidget(SubtitleLabel("相关文件 (可选)"))
|
|
|
|
# 发票
|
|
invoice_layout = QHBoxLayout()
|
|
invoice_layout.addWidget(BodyLabel("发票图片:"))
|
|
self.invoice_label = BodyLabel("未选择")
|
|
invoice_layout.addWidget(self.invoice_label)
|
|
invoice_btn = PushButton("选择")
|
|
invoice_btn.clicked.connect(lambda: self.select_image("invoice"))
|
|
invoice_layout.addWidget(invoice_btn)
|
|
layout.addLayout(invoice_layout)
|
|
|
|
# 支付记录
|
|
payment_layout = QHBoxLayout()
|
|
payment_layout.addWidget(BodyLabel("支付记录:"))
|
|
self.payment_label = BodyLabel("未选择")
|
|
payment_layout.addWidget(self.payment_label)
|
|
payment_btn = PushButton("选择")
|
|
payment_btn.clicked.connect(lambda: self.select_image("payment"))
|
|
payment_layout.addWidget(payment_btn)
|
|
layout.addLayout(payment_layout)
|
|
|
|
# 购买记录
|
|
purchase_layout = QHBoxLayout()
|
|
purchase_layout.addWidget(BodyLabel("购买记录:"))
|
|
self.purchase_label = BodyLabel("未选择")
|
|
purchase_layout.addWidget(self.purchase_label)
|
|
purchase_btn = PushButton("选择")
|
|
purchase_btn.clicked.connect(lambda: self.select_image("purchase"))
|
|
purchase_layout.addWidget(purchase_btn)
|
|
layout.addLayout(purchase_layout)
|
|
|
|
layout.addStretch()
|
|
|
|
# 按钮
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
|
|
cancel_btn = PushButton("取消")
|
|
cancel_btn.clicked.connect(self.reject)
|
|
btn_layout.addWidget(cancel_btn)
|
|
|
|
save_btn = PrimaryPushButton("保存")
|
|
save_btn.clicked.connect(self.save_transaction)
|
|
btn_layout.addWidget(save_btn)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
# 存储图片路径
|
|
self.selected_images: dict = {
|
|
'invoice': None,
|
|
'payment': None,
|
|
'purchase': None
|
|
}
|
|
|
|
def select_image(self, image_type: str):
|
|
"""选择图片"""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self, f"选择{image_type}图片",
|
|
"", "图片文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*)"
|
|
)
|
|
|
|
if file_path:
|
|
self.selected_images[image_type] = file_path
|
|
filename = Path(file_path).name
|
|
|
|
if image_type == 'invoice':
|
|
self.invoice_label.setText(filename)
|
|
elif image_type == 'payment':
|
|
self.payment_label.setText(filename)
|
|
elif image_type == 'purchase':
|
|
self.purchase_label.setText(filename)
|
|
|
|
def load_transaction_data(self, transaction: Transaction):
|
|
"""加载交易记录数据到表单"""
|
|
self.date_edit.setDate(QDate.fromString(transaction.date, "yyyy-MM-dd"))
|
|
self.amount_spin.setValue(transaction.amount)
|
|
self.trader_edit.setText(transaction.trader)
|
|
self.notes_edit.setText(transaction.notes)
|
|
|
|
if transaction.invoice_path:
|
|
self.invoice_label.setText(Path(transaction.invoice_path).name)
|
|
if transaction.payment_path:
|
|
self.payment_label.setText(Path(transaction.payment_path).name)
|
|
if transaction.purchase_path:
|
|
self.purchase_label.setText(Path(transaction.purchase_path).name)
|
|
|
|
def save_transaction(self):
|
|
"""保存交易记录"""
|
|
if not self.account_id:
|
|
dialog = Dialog(
|
|
title="错误",
|
|
content="账户ID未设置",
|
|
parent=self
|
|
)
|
|
dialog.yesButton.setText("确定")
|
|
dialog.cancelButton.hide()
|
|
dialog.exec()
|
|
return
|
|
|
|
date_str = self.date_edit.date().toString("yyyy-MM-dd")
|
|
amount = self.amount_spin.value()
|
|
trader = self.trader_edit.text().strip()
|
|
notes = self.notes_edit.toPlainText().strip()
|
|
|
|
if not trader:
|
|
dialog = Dialog(
|
|
title="验证错误",
|
|
content="请输入交易人",
|
|
parent=self
|
|
)
|
|
dialog.yesButton.setText("确定")
|
|
dialog.cancelButton.hide()
|
|
dialog.exec()
|
|
return
|
|
|
|
if amount <= 0:
|
|
dialog = Dialog(
|
|
title="验证错误",
|
|
content="金额必须大于0",
|
|
parent=self
|
|
)
|
|
dialog.yesButton.setText("确定")
|
|
dialog.cancelButton.hide()
|
|
dialog.exec()
|
|
return
|
|
|
|
if self.transaction:
|
|
# 编辑现有交易记录
|
|
trans_id = self.transaction.id
|
|
self.finance_manager.update_transaction(
|
|
self.account_id, trans_id,
|
|
date=date_str, amount=amount,
|
|
trader=trader, notes=notes
|
|
)
|
|
else:
|
|
# 创建新交易记录
|
|
transaction = Transaction(
|
|
date=date_str, amount=amount,
|
|
trader=trader, notes=notes
|
|
)
|
|
self.finance_manager.add_transaction(self.account_id, transaction)
|
|
trans_id = transaction.id
|
|
|
|
# 保存图片
|
|
for image_type_str, image_path in self.selected_images.items():
|
|
if image_path:
|
|
image_type = TransactionType[image_type_str.upper()]
|
|
relative_path = self.finance_manager.save_image_for_transaction(
|
|
self.account_id, trans_id, image_type, image_path
|
|
)
|
|
if relative_path:
|
|
self.finance_manager.update_transaction(
|
|
self.account_id, trans_id,
|
|
**{f"{image_type_str}_path": relative_path}
|
|
)
|
|
|
|
self.accept()
|
|
|
|
|
|
class RecordViewDialog(QDialog):
|
|
"""查看和编辑交易记录详情对话框"""
|
|
|
|
def __init__(self, parent=None, account_id: Optional[str] = None, transaction: Optional[Transaction] = None):
|
|
super().__init__(parent)
|
|
self.account_id = account_id
|
|
self.transaction = transaction
|
|
self.finance_manager = FinanceManager()
|
|
|
|
self.setWindowTitle("记录详情")
|
|
self.setGeometry(100, 100, 700, 600)
|
|
self.init_ui()
|
|
|
|
if transaction:
|
|
self.load_transaction_data()
|
|
|
|
def init_ui(self):
|
|
"""初始化UI"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
# 基本信息
|
|
info_layout = QVBoxLayout()
|
|
|
|
date_layout = QHBoxLayout()
|
|
date_layout.addWidget(BodyLabel("日期:"))
|
|
self.date_label = BodyLabel()
|
|
date_layout.addWidget(self.date_label)
|
|
date_layout.addStretch()
|
|
info_layout.addLayout(date_layout)
|
|
|
|
amount_layout = QHBoxLayout()
|
|
amount_layout.addWidget(BodyLabel("金额:"))
|
|
self.amount_label = BodyLabel()
|
|
amount_layout.addWidget(self.amount_label)
|
|
amount_layout.addStretch()
|
|
info_layout.addLayout(amount_layout)
|
|
|
|
trader_layout = QHBoxLayout()
|
|
trader_layout.addWidget(BodyLabel("交易人:"))
|
|
self.trader_label = BodyLabel()
|
|
trader_layout.addWidget(self.trader_label)
|
|
trader_layout.addStretch()
|
|
info_layout.addLayout(trader_layout)
|
|
|
|
notes_layout = QHBoxLayout()
|
|
notes_layout.addWidget(BodyLabel("备注:"))
|
|
self.notes_label = BodyLabel()
|
|
self.notes_label.setWordWrap(True)
|
|
notes_layout.addWidget(self.notes_label)
|
|
info_layout.addLayout(notes_layout)
|
|
|
|
layout.addLayout(info_layout)
|
|
layout.addWidget(HorizontalSeparator())
|
|
|
|
# 图片预览
|
|
layout.addWidget(SubtitleLabel("相关文件预览"))
|
|
|
|
preview_layout = QHBoxLayout()
|
|
|
|
# 发票
|
|
invoice_layout = QVBoxLayout()
|
|
invoice_layout.addWidget(BodyLabel("发票:"))
|
|
self.invoice_preview = BodyLabel("无")
|
|
self.invoice_preview.setMinimumSize(150, 150)
|
|
invoice_layout.addWidget(self.invoice_preview)
|
|
preview_layout.addLayout(invoice_layout)
|
|
|
|
# 支付记录
|
|
payment_layout = QVBoxLayout()
|
|
payment_layout.addWidget(BodyLabel("支付记录:"))
|
|
self.payment_preview = BodyLabel("无")
|
|
self.payment_preview.setMinimumSize(150, 150)
|
|
payment_layout.addWidget(self.payment_preview)
|
|
preview_layout.addLayout(payment_layout)
|
|
|
|
# 购买记录
|
|
purchase_layout = QVBoxLayout()
|
|
purchase_layout.addWidget(BodyLabel("购买记录:"))
|
|
self.purchase_preview = BodyLabel("无")
|
|
self.purchase_preview.setMinimumSize(150, 150)
|
|
purchase_layout.addWidget(self.purchase_preview)
|
|
preview_layout.addLayout(purchase_layout)
|
|
|
|
layout.addLayout(preview_layout)
|
|
layout.addStretch()
|
|
|
|
# 按钮
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
|
|
close_btn = PushButton("关闭")
|
|
close_btn.clicked.connect(self.reject)
|
|
btn_layout.addWidget(close_btn)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
def load_transaction_data(self):
|
|
"""加载交易记录数据"""
|
|
if not self.transaction:
|
|
return
|
|
|
|
self.date_label.setText(self.transaction.date)
|
|
self.amount_label.setText(f"¥ {self.transaction.amount:.2f}")
|
|
self.trader_label.setText(self.transaction.trader)
|
|
self.notes_label.setText(self.transaction.notes or "无")
|
|
|
|
# 加载图片预览
|
|
self._load_image_preview('invoice', self.transaction.invoice_path if self.transaction else None, self.invoice_preview)
|
|
self._load_image_preview('payment', self.transaction.payment_path if self.transaction else None, self.payment_preview)
|
|
self._load_image_preview('purchase', self.transaction.purchase_path if self.transaction else None, self.purchase_preview)
|
|
|
|
def _load_image_preview(self, image_type: str, relative_path: Optional[str], label: BodyLabel):
|
|
"""加载并显示图片预览"""
|
|
if not relative_path:
|
|
return
|
|
|
|
full_path = self.finance_manager.get_transaction_image_path(self.account_id or "", relative_path)
|
|
if full_path and full_path.exists():
|
|
pixmap = QPixmap(str(full_path))
|
|
scaled_pixmap = pixmap.scaledToWidth(150)
|
|
label.setPixmap(scaled_pixmap)
|
|
|
|
|
|
class FinanceInterface(QWidget):
|
|
"""财务做账主界面"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setObjectName("financeInterface")
|
|
self.finance_manager = FinanceManager()
|
|
|
|
self.layout_main = QVBoxLayout(self)
|
|
self.layout_main.setContentsMargins(0, 0, 0, 0)
|
|
self.layout_main.setSpacing(0)
|
|
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""初始化UI"""
|
|
# 创建顶部标签切换器
|
|
top_layout = QHBoxLayout()
|
|
top_layout.addStretch()
|
|
self.segmented_widget = SegmentedWidget()
|
|
self.segmented_widget.insertItem(0, "bookkeeping", "做账")
|
|
self.segmented_widget.insertItem(1, "query", "查询")
|
|
self.segmented_widget.insertItem(2, "export", "导出")
|
|
self.segmented_widget.currentItemChanged.connect(self.on_tab_changed)
|
|
top_layout.addWidget(self.segmented_widget)
|
|
top_layout.addStretch()
|
|
self.layout_main.addLayout(top_layout)
|
|
|
|
# 内容堆叠
|
|
self.stacked_widget = QStackedWidget()
|
|
|
|
# 做账标签页
|
|
self.bookkeeping_tab = self.create_bookkeeping_tab()
|
|
self.stacked_widget.addWidget(self.bookkeeping_tab)
|
|
|
|
# 查询标签页
|
|
self.query_tab = self.create_query_tab()
|
|
self.stacked_widget.addWidget(self.query_tab)
|
|
|
|
# 导出标签页
|
|
self.export_tab = self.create_export_tab()
|
|
self.stacked_widget.addWidget(self.export_tab)
|
|
|
|
self.layout_main.addWidget(self.stacked_widget)
|
|
|
|
# 初始化时获取默认账户
|
|
self.init_default_account()
|
|
|
|
def on_tab_changed(self, route_key: str):
|
|
"""标签切换时更新显示"""
|
|
tab_index = {"bookkeeping": 0, "query": 1, "export": 2}
|
|
index = tab_index.get(route_key, 0)
|
|
self.stacked_widget.setCurrentIndex(index)
|
|
|
|
def create_bookkeeping_tab(self) -> QWidget:
|
|
"""创建做账标签页"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
# 标题和操作按钮
|
|
title_layout = QHBoxLayout()
|
|
title_layout.addWidget(SubtitleLabel("交易记录"))
|
|
title_layout.addStretch()
|
|
|
|
new_record_btn = PrimaryPushButton("新建记录")
|
|
new_record_btn.clicked.connect(self.create_new_record)
|
|
title_layout.addWidget(new_record_btn)
|
|
|
|
layout.addLayout(title_layout)
|
|
|
|
# 记录表格 - 使用 TreeWidget 风格
|
|
self.records_table = TreeWidget()
|
|
self.records_table.setHeaderLabels(["日期", "交易人", "金额 (元)", "备注"])
|
|
self.records_table.setSelectionMode(self.records_table.SingleSelection)
|
|
self.records_table.setIndentation(0)
|
|
|
|
# 设置 TreeWidget 样式
|
|
header = self.records_table.header()
|
|
if header:
|
|
header.setStretchLastSection(False)
|
|
# 列宽自适应,可拖动调整
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
|
# 启用列宽可拖动
|
|
header.setSectionsMovable(False)
|
|
header.setStretchLastSection(False)
|
|
|
|
self.records_table.setCheckedColor("#0078d4", "#2d7d9a")
|
|
self.records_table.setBorderRadius(8)
|
|
self.records_table.setBorderVisible(True)
|
|
self.records_table.itemSelectionChanged.connect(self.on_record_selection_changed)
|
|
|
|
layout.addWidget(self.records_table)
|
|
|
|
# 操作按钮区
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
|
|
self.view_record_btn = PushButton("查看详情")
|
|
self.view_record_btn.setFixedWidth(90)
|
|
self.view_record_btn.clicked.connect(self.on_view_record_clicked)
|
|
self.view_record_btn.setEnabled(False)
|
|
btn_layout.addWidget(self.view_record_btn)
|
|
|
|
self.edit_record_btn = PushButton("编辑")
|
|
self.edit_record_btn.setFixedWidth(75)
|
|
self.edit_record_btn.clicked.connect(self.on_edit_record_clicked)
|
|
self.edit_record_btn.setEnabled(False)
|
|
btn_layout.addWidget(self.edit_record_btn)
|
|
|
|
self.delete_record_btn = PushButton("删除")
|
|
self.delete_record_btn.setFixedWidth(75)
|
|
self.delete_record_btn.clicked.connect(self.on_delete_record_clicked)
|
|
self.delete_record_btn.setEnabled(False)
|
|
btn_layout.addWidget(self.delete_record_btn)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
# 统计信息卡片
|
|
stats_card = CardWidget()
|
|
stats_layout = QHBoxLayout(stats_card)
|
|
stats_layout.setContentsMargins(20, 15, 20, 15)
|
|
stats_layout.setSpacing(30)
|
|
|
|
stats_layout.addWidget(BodyLabel("总额:"))
|
|
self.total_amount_label = StrongBodyLabel("¥ 0.00")
|
|
self.total_amount_label.setStyleSheet("color: #d32f2f;")
|
|
stats_layout.addWidget(self.total_amount_label)
|
|
|
|
stats_layout.addSpacing(30)
|
|
stats_layout.addWidget(BodyLabel("记录数:"))
|
|
self.record_count_label = StrongBodyLabel("0")
|
|
stats_layout.addWidget(self.record_count_label)
|
|
|
|
stats_layout.addStretch()
|
|
layout.addWidget(stats_card)
|
|
|
|
return widget
|
|
|
|
def create_query_tab(self) -> QWidget:
|
|
"""创建查询标签页"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
# 搜索过滤区 - 卡片样式
|
|
filter_card = CardWidget()
|
|
filter_layout = QVBoxLayout(filter_card)
|
|
filter_layout.setContentsMargins(20, 15, 20, 15)
|
|
filter_layout.setSpacing(12)
|
|
|
|
# 第一行:日期范围
|
|
date_layout = QHBoxLayout()
|
|
date_layout.addWidget(BodyLabel("日期范围:"))
|
|
self.query_date_start = DateEdit()
|
|
self.query_date_start.setDate(QDate.currentDate().addMonths(-1))
|
|
self.query_date_start.setMaximumWidth(150)
|
|
date_layout.addWidget(self.query_date_start)
|
|
date_layout.addWidget(BodyLabel("至"))
|
|
self.query_date_end = DateEdit()
|
|
self.query_date_end.setDate(QDate.currentDate())
|
|
self.query_date_end.setMaximumWidth(150)
|
|
date_layout.addWidget(self.query_date_end)
|
|
date_layout.addSpacing(20)
|
|
|
|
# 金额范围
|
|
date_layout.addWidget(BodyLabel("金额范围:"))
|
|
self.query_amount_min = DoubleSpinBox()
|
|
self.query_amount_min.setRange(0, 999999999)
|
|
self.query_amount_min.setPrefix("¥ ")
|
|
self.query_amount_min.setMaximumWidth(120)
|
|
date_layout.addWidget(self.query_amount_min)
|
|
date_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.setPrefix("¥ ")
|
|
self.query_amount_max.setMaximumWidth(120)
|
|
date_layout.addWidget(self.query_amount_max)
|
|
date_layout.addStretch()
|
|
filter_layout.addLayout(date_layout)
|
|
|
|
# 第二行:交易人搜索和查询按钮
|
|
trader_layout = QHBoxLayout()
|
|
trader_layout.addWidget(BodyLabel("交易人:"))
|
|
self.query_trader_edit = SearchLineEdit()
|
|
self.query_trader_edit.setPlaceholderText("输入交易人名称...")
|
|
self.query_trader_edit.setMaximumWidth(250)
|
|
trader_layout.addWidget(self.query_trader_edit)
|
|
|
|
query_btn = PrimaryPushButton("查询")
|
|
query_btn.clicked.connect(self.perform_query)
|
|
trader_layout.addWidget(query_btn)
|
|
trader_layout.addStretch()
|
|
filter_layout.addLayout(trader_layout)
|
|
|
|
layout.addWidget(filter_card)
|
|
layout.addWidget(HorizontalSeparator())
|
|
|
|
# 查询结果表格 - 使用 TreeWidget 风格
|
|
self.query_result_table = TreeWidget()
|
|
self.query_result_table.setHeaderLabels(["日期", "交易人", "金额 (元)", "备注"])
|
|
self.query_result_table.setSelectionMode(self.query_result_table.SingleSelection)
|
|
self.query_result_table.setIndentation(0)
|
|
|
|
# 设置 TreeWidget 样式
|
|
header = self.query_result_table.header()
|
|
if header:
|
|
header.setStretchLastSection(False)
|
|
# 列宽自适应,可拖动调整
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
|
# 启用列宽可拖动
|
|
header.setSectionsMovable(False)
|
|
header.setStretchLastSection(False)
|
|
|
|
self.query_result_table.setCheckedColor("#0078d4", "#2d7d9a")
|
|
self.query_result_table.setBorderRadius(8)
|
|
self.query_result_table.setBorderVisible(True)
|
|
self.query_result_table.itemSelectionChanged.connect(self.on_query_result_selection_changed)
|
|
|
|
layout.addWidget(self.query_result_table)
|
|
|
|
# 查询结果操作按钮
|
|
query_btn_layout = QHBoxLayout()
|
|
query_btn_layout.addStretch()
|
|
|
|
self.query_view_btn = PushButton("查看详情")
|
|
self.query_view_btn.setFixedWidth(90)
|
|
self.query_view_btn.clicked.connect(self.on_query_view_clicked)
|
|
self.query_view_btn.setEnabled(False)
|
|
query_btn_layout.addWidget(self.query_view_btn)
|
|
|
|
layout.addLayout(query_btn_layout)
|
|
|
|
return widget
|
|
|
|
def create_export_tab(self) -> QWidget:
|
|
"""创建导出标签页"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(20)
|
|
|
|
layout.addWidget(TitleLabel("数据导出和导入"))
|
|
layout.addWidget(HorizontalSeparator())
|
|
|
|
# 导出选项
|
|
layout.addWidget(SubtitleLabel("导出选项"))
|
|
|
|
export_layout = QVBoxLayout()
|
|
|
|
# 导出账户为压缩包
|
|
account_export_layout = QHBoxLayout()
|
|
account_export_layout.addWidget(BodyLabel("导出当前账户:"))
|
|
account_export_layout.addStretch()
|
|
export_account_btn = PrimaryPushButton("导出为ZIP包")
|
|
export_account_btn.clicked.connect(self.export_account)
|
|
account_export_layout.addWidget(export_account_btn)
|
|
export_layout.addLayout(account_export_layout)
|
|
|
|
# 导出为CSV
|
|
csv_export_layout = QHBoxLayout()
|
|
csv_export_layout.addWidget(BodyLabel("导出为Excel格式:"))
|
|
csv_export_layout.addStretch()
|
|
export_csv_btn = PrimaryPushButton("导出CSV")
|
|
export_csv_btn.clicked.connect(self.export_csv)
|
|
csv_export_layout.addWidget(export_csv_btn)
|
|
export_layout.addLayout(csv_export_layout)
|
|
|
|
# 备份所有账户
|
|
backup_layout = QHBoxLayout()
|
|
backup_layout.addWidget(BodyLabel("备份所有账户:"))
|
|
backup_layout.addStretch()
|
|
backup_btn = PrimaryPushButton("创建备份")
|
|
backup_btn.clicked.connect(self.backup_all)
|
|
backup_layout.addWidget(backup_btn)
|
|
export_layout.addLayout(backup_layout)
|
|
|
|
layout.addLayout(export_layout)
|
|
layout.addWidget(HorizontalSeparator())
|
|
|
|
# 导入选项
|
|
layout.addWidget(SubtitleLabel("导入选项"))
|
|
|
|
import_layout = QVBoxLayout()
|
|
|
|
# 导入账户
|
|
account_import_layout = QHBoxLayout()
|
|
account_import_layout.addWidget(BodyLabel("导入账户ZIP包:"))
|
|
account_import_layout.addStretch()
|
|
import_account_btn = PrimaryPushButton("导入账户")
|
|
import_account_btn.clicked.connect(self.import_account)
|
|
account_import_layout.addWidget(import_account_btn)
|
|
import_layout.addLayout(account_import_layout)
|
|
|
|
layout.addLayout(import_layout)
|
|
layout.addStretch()
|
|
|
|
return widget
|
|
|
|
def init_default_account(self):
|
|
"""初始化默认账户"""
|
|
accounts = self.finance_manager.get_all_accounts()
|
|
if accounts:
|
|
self.default_account_id = accounts[0].id
|
|
self.refresh_records_display()
|
|
else:
|
|
self.default_account_id = None
|
|
self.clear_records_table()
|
|
|
|
def refresh_account_list(self):
|
|
"""刷新账户列表(已移除,保留兼容性)"""
|
|
pass
|
|
|
|
def get_current_account_id(self) -> Optional[str]:
|
|
"""获取当前账户ID"""
|
|
if hasattr(self, 'default_account_id'):
|
|
return self.default_account_id
|
|
|
|
# 备用:如果还没初始化,从财务管理器获取第一个账户
|
|
accounts = self.finance_manager.get_all_accounts()
|
|
if accounts:
|
|
return accounts[0].id
|
|
return None
|
|
|
|
def on_account_changed(self):
|
|
"""账户改变时刷新显示(已移除,保留兼容性)"""
|
|
pass
|
|
|
|
def refresh_records_display(self):
|
|
"""刷新记录显示"""
|
|
account_id = self.get_current_account_id()
|
|
if not account_id:
|
|
return
|
|
|
|
# 重新加载所有账户,从磁盘获取最新数据
|
|
self.finance_manager.load_all_accounts()
|
|
account = self.finance_manager.get_account(account_id)
|
|
if not account:
|
|
return
|
|
|
|
self.clear_records_table()
|
|
|
|
for transaction in account.transactions:
|
|
# 创建树形项
|
|
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 "")
|
|
# 存储交易ID到第一列的 UserRole (Qt.UserRole = 32)
|
|
item.setData(0, 32, transaction.id)
|
|
|
|
self.records_table.addTopLevelItem(item)
|
|
|
|
# 更新统计信息
|
|
total_amount = sum(t.amount for t in account.transactions)
|
|
self.total_amount_label.setText(f"¥ {total_amount:.2f}")
|
|
self.record_count_label.setText(str(len(account.transactions)))
|
|
|
|
def clear_records_table(self):
|
|
"""清空记录表格"""
|
|
self.records_table.clear()
|
|
self.total_amount_label.setText("¥ 0.00")
|
|
self.record_count_label.setText("0")
|
|
|
|
def create_new_account(self):
|
|
"""创建新账户(已移除)"""
|
|
pass
|
|
|
|
def delete_current_account(self):
|
|
"""删除当前账户(已移除)"""
|
|
pass
|
|
|
|
def create_new_record(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
|
|
|
|
dialog = CreateTransactionDialog(self, account_id=account_id)
|
|
result = dialog.exec_()
|
|
# 无论是否成功,都尝试刷新表格
|
|
if result == QDialog.Accepted:
|
|
# 对话框正常关闭(保存)
|
|
self.refresh_records_display()
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="记录已添加",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
else:
|
|
# 对话框被取消,也刷新以防万一
|
|
self.refresh_records_display()
|
|
|
|
def edit_record(self, trans_id: str):
|
|
"""编辑记录"""
|
|
account_id = self.get_current_account_id()
|
|
if not account_id:
|
|
return
|
|
|
|
transaction = self.finance_manager.get_transaction(account_id, trans_id)
|
|
if not transaction:
|
|
return
|
|
|
|
dialog = CreateTransactionDialog(self, transaction=transaction, account_id=account_id)
|
|
result = dialog.exec_()
|
|
if result == QDialog.Accepted:
|
|
self.refresh_records_display()
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="记录已更新",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
else:
|
|
# 对话框被取消,也刷新以防万一
|
|
self.refresh_records_display()
|
|
|
|
def delete_record(self, trans_id: str):
|
|
"""删除记录"""
|
|
account_id = self.get_current_account_id()
|
|
if not account_id:
|
|
return
|
|
|
|
def on_delete_confirm():
|
|
self.finance_manager.delete_transaction(account_id, trans_id)
|
|
self.refresh_records_display()
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="记录已删除",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
|
|
dialog = Dialog(
|
|
title="确认删除",
|
|
content="确定要删除这条记录吗?",
|
|
parent=self
|
|
)
|
|
dialog.yesButton.setText("删除")
|
|
dialog.cancelButton.setText("取消")
|
|
dialog.yesButton.clicked.connect(on_delete_confirm)
|
|
dialog.exec()
|
|
|
|
def view_record(self, trans_id: str):
|
|
"""查看记录详情"""
|
|
account_id = self.get_current_account_id()
|
|
if not account_id:
|
|
return
|
|
|
|
transaction = self.finance_manager.get_transaction(account_id, trans_id)
|
|
if not transaction:
|
|
return
|
|
|
|
dialog = RecordViewDialog(self, account_id=account_id, transaction=transaction)
|
|
dialog.exec_()
|
|
|
|
def perform_query(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
|
|
|
|
# 重新加载最新数据
|
|
self.finance_manager.load_all_accounts()
|
|
|
|
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
|
|
|
|
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
|
|
)
|
|
|
|
self.query_result_table.clear()
|
|
|
|
for transaction in results:
|
|
# 创建树形项
|
|
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 "")
|
|
# 存储交易ID
|
|
item.setData(0, 32, transaction.id)
|
|
|
|
self.query_result_table.addTopLevelItem(item)
|
|
|
|
InfoBar.success(
|
|
title="查询成功",
|
|
content=f"找到 {len(results)} 条记录",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
|
|
def export_account(self):
|
|
"""导出账户为ZIP包"""
|
|
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
|
|
|
|
export_dir = QFileDialog.getExistingDirectory(self, "选择导出目录")
|
|
if export_dir:
|
|
if self.finance_manager.export_account_package(account_id, export_dir):
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="账户导出成功",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
else:
|
|
InfoBar.error(
|
|
title="失败",
|
|
content="导出账户失败",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=3000,
|
|
parent=self
|
|
)
|
|
|
|
def import_account(self):
|
|
"""导入账户ZIP包"""
|
|
zip_file, _ = QFileDialog.getOpenFileName(
|
|
self, "选择要导入的账户文件",
|
|
"", "ZIP文件 (*.zip)"
|
|
)
|
|
|
|
if zip_file:
|
|
account_id = self.finance_manager.import_account_package(zip_file)
|
|
if account_id:
|
|
# 重新初始化默认账户
|
|
self.init_default_account()
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="账户导入成功",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
else:
|
|
InfoBar.error(
|
|
title="失败",
|
|
content="导入账户失败",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=3000,
|
|
parent=self
|
|
)
|
|
|
|
def export_csv(self):
|
|
"""导出为CSV"""
|
|
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
|
|
|
|
account = self.finance_manager.get_account(account_id)
|
|
if not account:
|
|
return
|
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
self, "保存CSV文件",
|
|
f"{account.name}.csv",
|
|
"CSV文件 (*.csv)"
|
|
)
|
|
|
|
if file_path:
|
|
if self.finance_manager.export_to_csv(account_id, file_path):
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="已导出为CSV",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=2000,
|
|
parent=self
|
|
)
|
|
else:
|
|
InfoBar.error(
|
|
title="失败",
|
|
content="导出CSV失败",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=3000,
|
|
parent=self
|
|
)
|
|
|
|
def backup_all(self):
|
|
"""备份所有账户"""
|
|
if self.finance_manager.backup_all_accounts():
|
|
InfoBar.success(
|
|
title="成功",
|
|
content="备份创建成功,已保存到 assets/Finance_Data/backups",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=3000,
|
|
parent=self
|
|
)
|
|
else:
|
|
InfoBar.error(
|
|
title="失败",
|
|
content="创建备份失败",
|
|
isClosable=True,
|
|
position=InfoBarPosition.TOP,
|
|
duration=3000,
|
|
parent=self
|
|
)
|
|
|
|
def on_record_selection_changed(self):
|
|
"""记录表格选择改变时"""
|
|
selected_items = self.records_table.selectedItems()
|
|
has_selection = len(selected_items) > 0
|
|
self.view_record_btn.setEnabled(has_selection)
|
|
self.edit_record_btn.setEnabled(has_selection)
|
|
self.delete_record_btn.setEnabled(has_selection)
|
|
|
|
def on_query_result_selection_changed(self):
|
|
"""查询结果表格选择改变时"""
|
|
selected_items = self.query_result_table.selectedItems()
|
|
has_selection = len(selected_items) > 0
|
|
self.query_view_btn.setEnabled(has_selection)
|
|
|
|
def on_view_record_clicked(self):
|
|
"""查看记录按钮点击"""
|
|
current_item = self.records_table.currentItem()
|
|
if current_item:
|
|
trans_id = current_item.data(0, 32) # Qt.UserRole = 32
|
|
if trans_id:
|
|
self.view_record(trans_id)
|
|
|
|
def on_edit_record_clicked(self):
|
|
"""编辑记录按钮点击"""
|
|
current_item = self.records_table.currentItem()
|
|
if current_item:
|
|
trans_id = current_item.data(0, 32) # Qt.UserRole = 32
|
|
if trans_id:
|
|
self.edit_record(trans_id)
|
|
|
|
def on_delete_record_clicked(self):
|
|
"""删除记录按钮点击"""
|
|
current_item = self.records_table.currentItem()
|
|
if current_item:
|
|
trans_id = current_item.data(0, 32) # Qt.UserRole = 32
|
|
if trans_id:
|
|
self.delete_record(trans_id)
|
|
|
|
def on_query_view_clicked(self):
|
|
"""查询结果查看详情按钮点击"""
|
|
current_item = self.query_result_table.currentItem()
|
|
if current_item:
|
|
trans_id = current_item.data(0, 32) # Qt.UserRole = 32
|
|
if trans_id:
|
|
self.view_record(trans_id)
|