mirror of
https://github.com/goldenfishs/MRobot.git
synced 2026-03-23 17:03:57 +08:00
添加了开发票
This commit is contained in:
parent
e2e275b6e4
commit
8f4636ab5a
@ -1,123 +0,0 @@
|
||||
# 财务管理模块 - 分类功能使用指南
|
||||
|
||||
## 功能概览
|
||||
|
||||
财务模块现已支持完整的分类功能,用户可以:
|
||||
|
||||
1. **新建分类** - 在做账标签页左侧点击"新建分类"按钮
|
||||
2. **选择分类** - 在创建/编辑交易时选择或输入分类
|
||||
3. **按分类查询** - 在查询标签页通过分类进行筛选
|
||||
|
||||
## 功能详细说明
|
||||
|
||||
### 1. 新建分类
|
||||
|
||||
**操作步骤:**
|
||||
1. 打开财务模块,进入"做账"标签页
|
||||
2. 点击左上方"新建分类"按钮
|
||||
3. 在弹出对话框中输入分类名称(如"工作支出"、"投资收益"等)
|
||||
4. 点击"创建"按钮
|
||||
|
||||
**注意:**
|
||||
- 系统默认提供10个基础分类:其他、工资、奖金、投资、生活、交通、饮食、娱乐、医疗、教育
|
||||
- 无法删除默认分类,但可以添加自定义分类
|
||||
- 同一账户不能有重复的分类名称
|
||||
|
||||
### 2. 创建/编辑交易记录
|
||||
|
||||
**创建新交易:**
|
||||
1. 点击"新建记录"按钮
|
||||
2. 填写交易信息:
|
||||
- **交易类型**:选择"入账"或"支出"
|
||||
- **分类**:从下拉框中选择或确认分类
|
||||
- **日期**:选择交易日期
|
||||
- **金额**:输入交易金额(正数)
|
||||
- **交易人**:输入交易对方名称
|
||||
- **备注**:添加备注说明(可选)
|
||||
- **相关文件**:上传发票、支付记录、购买记录等(可选)
|
||||
3. 点击"保存"按钮
|
||||
|
||||
**编辑现有交易:**
|
||||
1. 在记录表中选择要编辑的交易
|
||||
2. 点击"编辑"按钮
|
||||
3. 修改任意字段(包括分类)
|
||||
4. 点击"保存"按钮
|
||||
|
||||
### 3. 按分类查询
|
||||
|
||||
**操作步骤:**
|
||||
1. 进入"查询"标签页
|
||||
2. 设置查询条件:
|
||||
- **日期范围**:选择起始和结束日期
|
||||
- **交易类型**:选择"全部"、"收入(正数)"或"支出(负数)"
|
||||
- **分类**:从下拉框选择要查询的分类("全部"表示不按分类筛选)
|
||||
- **金额范围**:输入最小和最大金额范围
|
||||
- **交易人**:输入交易人名称(支持模糊匹配)
|
||||
3. 点击"查询"按钮
|
||||
|
||||
**查询结果显示:**
|
||||
- 表格列包括:日期、交易人、**分类**、金额、备注
|
||||
- 结果按日期倒序排列(最新的在上)
|
||||
|
||||
## 数据持久化
|
||||
|
||||
所有分类信息被保存到:
|
||||
```
|
||||
assets/Finance_Data/accounts/{account_id}/metadata.json
|
||||
```
|
||||
|
||||
交易记录(包括分类)被保存到:
|
||||
```
|
||||
assets/Finance_Data/accounts/{account_id}/{transaction_id}/data.json
|
||||
```
|
||||
|
||||
## 导出与导入
|
||||
|
||||
### 导出为CSV
|
||||
- 打开"导出"标签页
|
||||
- 点击"导出CSV"按钮
|
||||
- 选择保存位置
|
||||
- CSV文件将包含:日期、金额、交易人、**分类**、备注、创建时间
|
||||
|
||||
### 导出/导入账户
|
||||
- 支持完整的账户数据迁移(包括所有分类和交易记录)
|
||||
- 导出为ZIP包后可在其他地方导入
|
||||
|
||||
## API 接口
|
||||
|
||||
如果需要在代码中直接使用分类功能:
|
||||
|
||||
```python
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
fm = FinanceManager()
|
||||
|
||||
# 添加分类
|
||||
fm.add_category(account_id, "新分类名称")
|
||||
|
||||
# 删除自定义分类
|
||||
fm.delete_category(account_id, "分类名称")
|
||||
|
||||
# 获取所有分类
|
||||
categories = fm.get_categories(account_id)
|
||||
|
||||
# 按分类查询交易
|
||||
results = fm.query_transactions(
|
||||
account_id,
|
||||
category="工资" # 指定分类
|
||||
)
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 为什么我新建的分类不显示在"新建记录"对话框中?**
|
||||
A: 确保已点击"创建"按钮完成分类创建,然后再打开新建记录对话框。系统会自动从账户中读取最新的分类列表。
|
||||
|
||||
**Q: 能否修改分类名称?**
|
||||
A: 目前不支持修改分类名称。建议先新建一个新分类,然后为已有交易重新分配分类。
|
||||
|
||||
**Q: 删除交易记录后,其分类会被删除吗?**
|
||||
A: 不会。分类是账户级别的属性,删除交易不会影响分类列表。
|
||||
|
||||
**Q: 如何导入包含分类的交易数据?**
|
||||
A: 使用"导出"标签页的"导入账户"功能,选择之前导出的ZIP包即可恢复所有分类和交易记录。
|
||||
@ -1,163 +0,0 @@
|
||||
# 财务模块分类功能 - 实现总结
|
||||
|
||||
## 已完成的功能
|
||||
|
||||
### 1. 数据模型扩展
|
||||
|
||||
**Transaction 类**
|
||||
- 添加了 `category` 字段(默认值为"其他")
|
||||
- 更新了 `to_dict()` 和 `from_dict()` 方法以支持分类序列化
|
||||
|
||||
**Account 类**
|
||||
- 添加了 `categories` 列表(包含10个默认分类)
|
||||
- 更新了 `to_dict()` 和 `from_dict()` 方法以支持分类列表序列化
|
||||
|
||||
### 2. FinanceManager 新增方法
|
||||
|
||||
```python
|
||||
# 分类管理方法
|
||||
add_category(account_id, category) # 添加新分类
|
||||
delete_category(account_id, category) # 删除自定义分类
|
||||
get_categories(account_id) # 获取所有分类
|
||||
|
||||
# 查询方法扩展
|
||||
query_transactions(..., category=None) # 支持按分类查询
|
||||
```
|
||||
|
||||
### 3. UI 界面更新
|
||||
|
||||
**做账标签页(Bookkeeping Tab)**
|
||||
- 左上方添加"新建分类"按钮
|
||||
- 记录表新增"分类"列(第3列)
|
||||
- 表格列顺序:日期、交易人、**分类**、金额、备注
|
||||
|
||||
**新建/编辑交易对话框**
|
||||
- 在"交易类型"下方添加"分类"下拉框
|
||||
- 支持从账户的分类列表中选择
|
||||
- 编辑时自动加载原交易的分类
|
||||
|
||||
**查询标签页(Query Tab)**
|
||||
- 新增"分类"下拉框筛选条件
|
||||
- 查询结果表新增"分类"列
|
||||
- 支持按分类进行精确查询
|
||||
- 查询前自动更新分类下拉框以显示最新的账户分类
|
||||
|
||||
### 4. 分类创建功能
|
||||
|
||||
- 点击"新建分类"打开对话框
|
||||
- 输入分类名称后创建
|
||||
- 新分类立即保存到账户元数据中
|
||||
- 创建后对话框自动关闭
|
||||
|
||||
### 5. 数据持久化
|
||||
|
||||
所有分类信息被保存在账户元数据中:
|
||||
```
|
||||
assets/Finance_Data/accounts/{account_id}/metadata.json
|
||||
```
|
||||
|
||||
包含内容:
|
||||
- 账户基本信息(ID、名称、描述)
|
||||
- 所有分类列表
|
||||
- 创建和更新时间戳
|
||||
|
||||
### 6. CSV 导出更新
|
||||
|
||||
导出的 CSV 文件现在包含"分类"列:
|
||||
- 日期、金额、交易人、**分类**、备注、创建时间
|
||||
|
||||
### 7. 技术改进
|
||||
|
||||
- 修改了 `CreateTransactionDialog` 的初始化方式,支持传入现有的 `FinanceManager` 实例
|
||||
- 确保分类列表始终与最新的账户数据同步
|
||||
- 在 `create_new_record()` 和 `edit_record()` 中传入 `finance_manager` 参数
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 修改的文件
|
||||
1. `/Users/lvzucheng/Documents/R/MRobot/app/tools/finance_manager.py`
|
||||
- Transaction 类:添加 category 字段
|
||||
- Account 类:添加 categories 列表
|
||||
- FinanceManager 类:新增分类管理方法、更新查询和导出方法
|
||||
|
||||
2. `/Users/lvzucheng/Documents/R/MRobot/app/finance_interface.py`
|
||||
- CreateTransactionDialog:添加分类选择
|
||||
- FinanceInterface:新增分类创建UI、更新表格显示、更新查询功能
|
||||
|
||||
### 新建的文件
|
||||
1. `/Users/lvzucheng/Documents/R/MRobot/test_category.py` - 单元测试(已验证通过)
|
||||
2. `/Users/lvzucheng/Documents/R/MRobot/CATEGORY_GUIDE.md` - 使用指南
|
||||
|
||||
## 功能验证
|
||||
|
||||
✅ 数据模型测试通过
|
||||
✅ 分类创建功能正常
|
||||
✅ 交易记录分类存储正确
|
||||
✅ 按分类查询功能正常
|
||||
✅ CSV 导出包含分类信息
|
||||
✅ 账户导入导出保留分类信息
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 创建新分类
|
||||
1. 点击做账标签页的"新建分类"按钮
|
||||
2. 输入分类名称(如"房租")
|
||||
3. 点击"创建"
|
||||
|
||||
### 创建分类交易
|
||||
1. 点击"新建记录"
|
||||
2. 选择分类(如"生活")
|
||||
3. 填写其他信息并保存
|
||||
|
||||
### 按分类查询
|
||||
1. 进入查询标签页
|
||||
2. 从"分类"下拉框选择要查询的分类
|
||||
3. 点击"查询"
|
||||
|
||||
## 默认分类列表
|
||||
|
||||
系统为每个新账户预设以下分类:
|
||||
- 其他
|
||||
- 工资
|
||||
- 奖金
|
||||
- 投资
|
||||
- 生活
|
||||
- 交通
|
||||
- 饮食
|
||||
- 娱乐
|
||||
- 医疗
|
||||
- 教育
|
||||
|
||||
用户可以根据需要添加自定义分类。
|
||||
|
||||
## 技术架构
|
||||
|
||||
```
|
||||
财务管理系统架构
|
||||
├── FinanceManager(数据层)
|
||||
│ ├── 分类管理:add_category, delete_category, get_categories
|
||||
│ ├── 交易查询:query_transactions(category=None)
|
||||
│ └── 数据持久化
|
||||
│
|
||||
├── CreateTransactionDialog(UI层 - 交易编辑)
|
||||
│ ├── 分类选择下拉框
|
||||
│ └── 支持创建和编辑时选择分类
|
||||
│
|
||||
├── FinanceInterface(UI层 - 主界面)
|
||||
│ ├── 新建分类功能
|
||||
│ ├── 做账标签页(显示分类列)
|
||||
│ └── 查询标签页(按分类筛选)
|
||||
│
|
||||
└── 数据存储
|
||||
├── 账户元数据:metadata.json(包含分类列表)
|
||||
└── 交易记录:transaction/data.json(包含分类字段)
|
||||
```
|
||||
|
||||
## 后续可能的改进
|
||||
|
||||
- [ ] 支持修改分类名称
|
||||
- [ ] 支持删除已使用的分类(需要迁移交易记录)
|
||||
- [ ] 为分类配置颜色标签
|
||||
- [ ] 按分类生成统计报表
|
||||
- [ ] 分类快速切换功能
|
||||
- [ ] 分类使用频率排序
|
||||
@ -1,105 +0,0 @@
|
||||
# 财务模块分类功能改进总结
|
||||
|
||||
## 已完成的功能
|
||||
|
||||
### 1. 删除默认分类
|
||||
- ✅ 移除了硬编码的默认分类列表("其他", "工资", "奖金", "投资", "生活", "交通", "饮食", "娱乐", "医疗", "教育")
|
||||
- ✅ 新建账户时,分类列表为空
|
||||
- ✅ 用户需要手动创建所需的分类
|
||||
|
||||
### 2. 自动创建Admin账户
|
||||
- ✅ FinanceManager初始化时,如果没有任何账户,自动创建名为"admin"的默认账户
|
||||
- ✅ 如果admin账户已存在,则直接使用
|
||||
- ✅ FinanceInterface初始化时,优先选择admin账户
|
||||
|
||||
### 3. 分类管理功能
|
||||
- ✅ 在做账页面左侧添加"新建分类"按钮
|
||||
- ✅ 用户可以通过对话框输入新分类名称
|
||||
- ✅ 用户可以删除自定义分类
|
||||
- ✅ 所有分类操作都会持久化保存
|
||||
|
||||
### 4. 交易记录与分类关联
|
||||
- ✅ 创建交易时必须选择分类
|
||||
- ✅ 如果没有分类,则禁用分类下拉框并提示"请先在做账页创建分类"
|
||||
- ✅ 交易记录显示分类信息
|
||||
- ✅ 支持按分类查询交易
|
||||
- ✅ CSV导出包含分类列
|
||||
|
||||
## 数据结构变化
|
||||
|
||||
### Transaction类
|
||||
```python
|
||||
class Transaction:
|
||||
# ...其他字段...
|
||||
category: str = "" # 默认为空字符串,用户自定义
|
||||
```
|
||||
|
||||
### Account类
|
||||
```python
|
||||
class Account:
|
||||
# ...其他字段...
|
||||
categories: List[str] = [] # 默认为空列表,用户自定义分类
|
||||
```
|
||||
|
||||
## UI改进
|
||||
|
||||
### 做账页面
|
||||
1. **记录表格**:新增"分类"列显示交易分类
|
||||
2. **操作按钮**:在"新建记录"按钮左侧添加"新建分类"按钮
|
||||
3. **新建分类对话框**:输入分类名称并创建
|
||||
|
||||
### 新建/编辑交易对话框
|
||||
1. **分类选择框**:
|
||||
- 如果有分类,显示所有可用分类
|
||||
- 如果没有分类,禁用并显示提示信息
|
||||
2. **验证**:保存前检查是否选择了有效分类
|
||||
|
||||
### 查询页面
|
||||
1. **分类过滤**:新增"分类"过滤条件
|
||||
2. **结果表格**:显示交易的分类信息
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 首次使用
|
||||
1. 应用启动时自动创建"admin"账户(无默认分类)
|
||||
2. 在做账页面点击"新建分类"按钮
|
||||
3. 输入所需的分类名称(如"工资"、"房租"等)
|
||||
4. 点击"创建"按钮
|
||||
5. 现在可以创建交易记录并选择相应分类
|
||||
|
||||
### 后续使用
|
||||
1. 在做账页面点击"新建分类"添加新分类
|
||||
2. 在"新建记录"对话框中选择分类
|
||||
3. 在查询页面可以按分类过滤交易
|
||||
|
||||
## 文件修改
|
||||
|
||||
### app/tools/finance_manager.py
|
||||
- ✅ 修改Transaction.__init__:category默认值改为""
|
||||
- ✅ 修改Account.__init__:categories默认值改为[]
|
||||
- ✅ 修改FinanceManager.__init__:自动创建admin账户
|
||||
- ✅ 修改load_all_accounts():从元数据加载分类
|
||||
- ✅ 修改delete_category():允许删除任何分类
|
||||
- ✅ 修改query_transactions():支持按分类查询
|
||||
- ✅ 修改export_to_csv():包含分类列
|
||||
|
||||
### app/finance_interface.py
|
||||
- ✅ 修改CreateTransactionDialog.init_ui():分类下拉框显示提示
|
||||
- ✅ 修改CreateTransactionDialog.save_transaction():验证分类
|
||||
- ✅ 修改CreateTransactionDialog.load_transaction_data():加载分类
|
||||
- ✅ 修改create_bookkeeping_tab():添加"新建分类"按钮
|
||||
- ✅ 修改记录表格列:添加分类列
|
||||
- ✅ 修改create_query_tab():添加分类过滤
|
||||
- ✅ 修改perform_query():支持分类查询
|
||||
- ✅ 新增on_create_category_clicked():处理新建分类
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
- ✅ test_admin_account.py:验证admin账户自动创建
|
||||
- ✅ test_no_default_categories.py:验证删除默认分类和分类管理功能
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
- ✅ 现有账户数据可以正常加载
|
||||
- ✅ 分类为空的旧交易可以继续显示
|
||||
- ✅ CSV导出保持兼容
|
||||
@ -1,424 +0,0 @@
|
||||
"""
|
||||
财务模块 - 编程接口文档
|
||||
|
||||
本文档说明如何在代码中使用财务模块的API
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# 1. 基础数据模型
|
||||
# ============================================================================
|
||||
|
||||
from app.tools.finance_manager import FinanceManager, Transaction, Account, TransactionType
|
||||
from datetime import datetime
|
||||
|
||||
# 创建财务管理器实例
|
||||
fm = FinanceManager() # 使用默认路径 assets/Finance_Data
|
||||
# 或指定自定义路径
|
||||
# fm = FinanceManager(data_root="/custom/path/to/finance_data")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 2. 账户操作
|
||||
# ============================================================================
|
||||
|
||||
# 创建新账户
|
||||
account = fm.create_account(
|
||||
account_name="2024年项目经费",
|
||||
description="用于记录项目开发期间的所有支出"
|
||||
)
|
||||
print(f"创建账户: {account.id}")
|
||||
|
||||
# 获取所有账户
|
||||
all_accounts = fm.get_all_accounts()
|
||||
for acc in all_accounts:
|
||||
print(f"账户: {acc.name} (ID: {acc.id})")
|
||||
|
||||
# 获取单个账户
|
||||
account = fm.get_account(account.id)
|
||||
print(f"账户名称: {account.name}")
|
||||
print(f"账户描述: {account.description}")
|
||||
|
||||
# 更新账户信息
|
||||
fm.update_account(
|
||||
account.id,
|
||||
account_name="2024年项目经费V2",
|
||||
description="更新的描述"
|
||||
)
|
||||
|
||||
# 删除账户(删除所有相关数据)
|
||||
# fm.delete_account(account.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 3. 交易记录操作
|
||||
# ============================================================================
|
||||
|
||||
# 创建交易记录
|
||||
transaction = Transaction(
|
||||
date="2024-01-15",
|
||||
amount=1500.50,
|
||||
trader="张三",
|
||||
notes="购买办公用品和文具"
|
||||
)
|
||||
|
||||
# 添加到账户
|
||||
fm.add_transaction(account.id, transaction)
|
||||
print(f"交易记录ID: {transaction.id}")
|
||||
|
||||
# 获取交易记录
|
||||
trans = fm.get_transaction(account.id, transaction.id)
|
||||
print(f"交易人: {trans.trader}")
|
||||
print(f"金额: ¥{trans.amount}")
|
||||
|
||||
# 更新交易记录
|
||||
fm.update_transaction(
|
||||
account.id,
|
||||
transaction.id,
|
||||
amount=1600.50,
|
||||
notes="购买办公用品、文具和咖啡机"
|
||||
)
|
||||
|
||||
# 删除交易记录
|
||||
# fm.delete_transaction(account.id, transaction.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 4. 图片管理
|
||||
# ============================================================================
|
||||
|
||||
# 保存交易相关的图片
|
||||
# 参数:账户ID、交易ID、图片类型、本地图片路径
|
||||
relative_path = fm.save_image_for_transaction(
|
||||
account.id,
|
||||
transaction.id,
|
||||
TransactionType.INVOICE, # 发票图片
|
||||
"/path/to/invoice.jpg"
|
||||
)
|
||||
print(f"保存发票图片: {relative_path}")
|
||||
|
||||
# 获取图片的完整路径(用于显示或处理)
|
||||
full_path = fm.get_transaction_image_path(account.id, relative_path)
|
||||
print(f"完整路径: {full_path}")
|
||||
|
||||
# 一次性保存多个图片
|
||||
image_types_and_paths = {
|
||||
TransactionType.INVOICE: "/path/to/invoice.jpg",
|
||||
TransactionType.PAYMENT: "/path/to/payment_screenshot.png",
|
||||
TransactionType.PURCHASE: "/path/to/order.jpg"
|
||||
}
|
||||
|
||||
for image_type, image_path in image_types_and_paths.items():
|
||||
fm.save_image_for_transaction(
|
||||
account.id,
|
||||
transaction.id,
|
||||
image_type,
|
||||
image_path
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 5. 查询功能
|
||||
# ============================================================================
|
||||
|
||||
# 基础查询 - 获取账户的所有交易
|
||||
all_transactions = account.transactions
|
||||
print(f"总交易数: {len(all_transactions)}")
|
||||
|
||||
# 高级查询 - 带条件过滤
|
||||
results = fm.query_transactions(
|
||||
account.id,
|
||||
date_start="2024-01-01", # 开始日期
|
||||
date_end="2024-12-31", # 结束日期
|
||||
amount_min=100.0, # 最小金额
|
||||
amount_max=5000.0, # 最大金额
|
||||
trader="张三" # 交易人(模糊匹配)
|
||||
)
|
||||
print(f"查询结果: {len(results)} 条记录")
|
||||
|
||||
# 按日期倒序排列(默认已排序)
|
||||
for trans in results:
|
||||
print(f"{trans.date} - {trans.trader}: ¥{trans.amount}")
|
||||
|
||||
# 只查询特定日期范围
|
||||
january_records = fm.query_transactions(
|
||||
account.id,
|
||||
date_start="2024-01-01",
|
||||
date_end="2024-01-31"
|
||||
)
|
||||
|
||||
# 只查询特定金额范围
|
||||
expensive_records = fm.query_transactions(
|
||||
account.id,
|
||||
amount_min=1000.0
|
||||
)
|
||||
|
||||
# 只查询特定交易人
|
||||
zhang_records = fm.query_transactions(
|
||||
account.id,
|
||||
trader="张" # 支持模糊匹配,不区分大小写
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 6. 账户汇总
|
||||
# ============================================================================
|
||||
|
||||
# 获取账户汇总信息
|
||||
summary = fm.get_account_summary(account.id)
|
||||
print(f"账户名: {summary['account_name']}")
|
||||
print(f"总金额: ¥{summary['total_amount']:.2f}")
|
||||
print(f"交易数: {summary['transaction_count']}")
|
||||
print(f"创建时间: {summary['created_at']}")
|
||||
print(f"更新时间: {summary['updated_at']}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 7. 导入导出功能
|
||||
# ============================================================================
|
||||
|
||||
# 导出账户为ZIP包(用于转移)
|
||||
success = fm.export_account_package(
|
||||
account.id,
|
||||
export_path="/Users/username/Desktop"
|
||||
)
|
||||
if success:
|
||||
print("账户导出成功")
|
||||
|
||||
# 导入账户ZIP包
|
||||
imported_account_id = fm.import_account_package(
|
||||
zip_path="/Users/username/Desktop/2024年项目经费_xxx.zip"
|
||||
)
|
||||
if imported_account_id:
|
||||
print(f"账户导入成功,ID: {imported_account_id}")
|
||||
|
||||
# 导出为CSV格式(用于Excel分析)
|
||||
success = fm.export_to_csv(
|
||||
account.id,
|
||||
csv_path="/Users/username/Desktop/report.csv"
|
||||
)
|
||||
if success:
|
||||
print("CSV导出成功")
|
||||
|
||||
# 备份所有账户
|
||||
success = fm.backup_all_accounts()
|
||||
if success:
|
||||
print("备份创建成功,位置: assets/Finance_Data/backups/")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 8. 数据序列化
|
||||
# ============================================================================
|
||||
|
||||
# 将交易记录转换为字典(用于JSON序列化)
|
||||
trans_dict = transaction.to_dict()
|
||||
print(f"交易记录字典: {trans_dict}")
|
||||
|
||||
# 从字典创建交易记录
|
||||
new_trans = Transaction.from_dict(trans_dict)
|
||||
|
||||
# 将账户转换为字典
|
||||
account_dict = account.to_dict()
|
||||
print(f"账户字典(包含所有交易)")
|
||||
|
||||
# 从字典创建账户
|
||||
new_account = Account.from_dict(account_dict)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 9. 实用示例
|
||||
# ============================================================================
|
||||
|
||||
# 示例1: 计算月度支出
|
||||
def get_monthly_expense(finance_manager, account_id, month_str):
|
||||
"""
|
||||
获取指定月份的总支出
|
||||
month_str: 格式为 "2024-01"
|
||||
"""
|
||||
date_start = f"{month_str}-01"
|
||||
# 计算月末日期
|
||||
year, month = map(int, month_str.split('-'))
|
||||
if month == 12:
|
||||
date_end = f"{year + 1}-01-01"
|
||||
else:
|
||||
date_end = f"{year}-{month + 1:02d}-01"
|
||||
|
||||
transactions = finance_manager.query_transactions(
|
||||
account_id,
|
||||
date_start=date_start,
|
||||
date_end=date_end
|
||||
)
|
||||
total = sum(t.amount for t in transactions)
|
||||
return total
|
||||
|
||||
monthly_total = get_monthly_expense(fm, account.id, "2024-01")
|
||||
print(f"1月份总支出: ¥{monthly_total:.2f}")
|
||||
|
||||
|
||||
# 示例2: 按交易人统计支出
|
||||
def get_trader_total(finance_manager, account_id, trader_name):
|
||||
"""获取特定交易人的总支出"""
|
||||
transactions = finance_manager.query_transactions(
|
||||
account_id,
|
||||
trader=trader_name
|
||||
)
|
||||
total = sum(t.amount for t in transactions)
|
||||
count = len(transactions)
|
||||
return total, count
|
||||
|
||||
total, count = get_trader_total(fm, account.id, "张三")
|
||||
print(f"与张三的交易: {count}笔,总额 ¥{total:.2f}")
|
||||
|
||||
|
||||
# 示例3: 查找最大支出
|
||||
def get_max_transaction(finance_manager, account_id):
|
||||
"""找到金额最大的交易记录"""
|
||||
account = finance_manager.get_account(account_id)
|
||||
if not account.transactions:
|
||||
return None
|
||||
return max(account.transactions, key=lambda t: t.amount)
|
||||
|
||||
max_trans = get_max_transaction(fm, account.id)
|
||||
if max_trans:
|
||||
print(f"最大支出: {max_trans.trader} - ¥{max_trans.amount}")
|
||||
|
||||
|
||||
# 示例4: 导出月度报告
|
||||
def export_monthly_report(finance_manager, account_id, month_str, output_path):
|
||||
"""
|
||||
导出指定月份的报告
|
||||
"""
|
||||
date_start = f"{month_str}-01"
|
||||
year, month = map(int, month_str.split('-'))
|
||||
if month == 12:
|
||||
date_end = f"{year + 1}-01-01"
|
||||
else:
|
||||
date_end = f"{year}-{month + 1:02d}-01"
|
||||
|
||||
transactions = finance_manager.query_transactions(
|
||||
account_id,
|
||||
date_start=date_start,
|
||||
date_end=date_end
|
||||
)
|
||||
|
||||
# 创建CSV内容
|
||||
import csv
|
||||
with open(output_path, 'w', newline='', encoding='utf-8-sig') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['日期', '交易人', '金额', '备注'])
|
||||
|
||||
for trans in transactions:
|
||||
writer.writerow([
|
||||
trans.date,
|
||||
trans.trader,
|
||||
trans.amount,
|
||||
trans.notes
|
||||
])
|
||||
|
||||
export_monthly_report(fm, account.id, "2024-01", "/tmp/report_2024-01.csv")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 10. 错误处理
|
||||
# ============================================================================
|
||||
|
||||
try:
|
||||
# 尝试获取不存在的账户
|
||||
result = fm.get_account("non-existent-id")
|
||||
if result is None:
|
||||
print("账户不存在")
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
try:
|
||||
# 验证交易金额
|
||||
if transaction.amount <= 0:
|
||||
raise ValueError("金额必须大于0")
|
||||
except ValueError as e:
|
||||
print(f"验证错误: {e}")
|
||||
|
||||
try:
|
||||
# 删除交易前检查
|
||||
if not fm.delete_transaction(account.id, "invalid-id"):
|
||||
print("删除失败,交易记录不存在")
|
||||
except Exception as e:
|
||||
print(f"删除错误: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 11. 性能优化建议
|
||||
# ============================================================================
|
||||
|
||||
"""
|
||||
1. 缓存:
|
||||
- 使用 fm.get_account() 获取账户后,交易记录已加载到内存
|
||||
- 避免频繁重新加载同一账户
|
||||
|
||||
2. 查询优化:
|
||||
- 使用查询条件过滤,而不是加载所有后手动过滤
|
||||
- 日期范围会显著提升查询性能
|
||||
|
||||
3. 批量操作:
|
||||
- 需要添加多条记录时,推荐循环调用 add_transaction()
|
||||
- 每次操作都会自动保存到磁盘
|
||||
|
||||
4. 大数据量:
|
||||
- 单个账户建议不超过10000条记录
|
||||
- 考虑按时间周期分账户
|
||||
- 定期归档和备份
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 12. 完整工作流示例
|
||||
# ============================================================================
|
||||
|
||||
def complete_workflow_example():
|
||||
"""完整的财务做账工作流"""
|
||||
|
||||
# 1. 初始化
|
||||
fm = FinanceManager()
|
||||
|
||||
# 2. 创建账户
|
||||
account = fm.create_account(
|
||||
account_name="2024年度账目",
|
||||
description="全年财务记录"
|
||||
)
|
||||
|
||||
# 3. 添加多条记录
|
||||
records_data = [
|
||||
("2024-01-10", 500.0, "王五", "办公桌购买"),
|
||||
("2024-01-15", 1200.0, "李四", "电脑软件许可"),
|
||||
("2024-02-05", 300.0, "张三", "文具采购"),
|
||||
]
|
||||
|
||||
for date, amount, trader, notes in records_data:
|
||||
trans = Transaction(
|
||||
date=date,
|
||||
amount=amount,
|
||||
trader=trader,
|
||||
notes=notes
|
||||
)
|
||||
fm.add_transaction(account.id, trans)
|
||||
|
||||
# 4. 查询数据
|
||||
jan_records = fm.query_transactions(
|
||||
account.id,
|
||||
date_start="2024-01-01",
|
||||
date_end="2024-01-31"
|
||||
)
|
||||
|
||||
# 5. 统计分析
|
||||
total = sum(t.amount for t in jan_records)
|
||||
print(f"1月份支出: ¥{total}")
|
||||
|
||||
# 6. 生成报告
|
||||
account_summary = fm.get_account_summary(account.id)
|
||||
print(f"账户总计: ¥{account_summary['total_amount']}")
|
||||
|
||||
# 7. 备份数据
|
||||
fm.backup_all_accounts()
|
||||
|
||||
return account.id
|
||||
|
||||
# 执行工作流
|
||||
# account_id = complete_workflow_example()
|
||||
@ -1,338 +0,0 @@
|
||||
# 财务做账模块 - 实现完成报告
|
||||
|
||||
## 项目交付清单
|
||||
|
||||
### ✅ 已完成的功能
|
||||
|
||||
#### 1. 核心数据管理系统
|
||||
- [x] 交易记录数据模型 (`Transaction` 类)
|
||||
- [x] 账户数据模型 (`Account` 类)
|
||||
- [x] 财务管理器核心类 (`FinanceManager`)
|
||||
- [x] 本地文件系统组织结构
|
||||
- [x] JSON序列化和持久化存储
|
||||
|
||||
#### 2. 做账功能模块
|
||||
- [x] 多账户管理
|
||||
- [x] 创建新账户
|
||||
- [x] 删除账户
|
||||
- [x] 更新账户信息
|
||||
- [x] 账户列表展示
|
||||
|
||||
- [x] 交易记录管理
|
||||
- [x] 新建交易记录
|
||||
- [x] 编辑交易记录
|
||||
- [x] 删除交易记录
|
||||
- [x] 记录详情查看
|
||||
|
||||
- [x] 图片附件支持
|
||||
- [x] 发票图片保存
|
||||
- [x] 支付记录图片保存
|
||||
- [x] 购买记录图片保存
|
||||
- [x] 图片预览功能
|
||||
- [x] 图片自动组织存储
|
||||
|
||||
- [x] 交易记录显示
|
||||
- [x] 表格显示所有记录
|
||||
- [x] 实时统计总额
|
||||
- [x] 实时统计记录数
|
||||
- [x] 记录排序(按日期倒序)
|
||||
|
||||
#### 3. 查询功能模块
|
||||
- [x] 多条件查询
|
||||
- [x] 日期范围查询
|
||||
- [x] 金额范围查询
|
||||
- [x] 交易人模糊搜索
|
||||
- [x] 条件组合查询
|
||||
|
||||
- [x] 查询结果展示
|
||||
- [x] 结果表格显示
|
||||
- [x] 实时统计结果数
|
||||
- [x] 结果排序
|
||||
|
||||
- [x] 图片预览功能
|
||||
- [x] 在详情对话框中预览
|
||||
- [x] 支持多种图片格式
|
||||
|
||||
#### 4. 导出导入功能
|
||||
- [x] 账户转移
|
||||
- [x] 导出为ZIP包
|
||||
- [x] 导入ZIP包
|
||||
- [x] 完整数据迁移
|
||||
|
||||
- [x] 数据备份
|
||||
- [x] CSV导出功能
|
||||
- [x] 完整账户备份
|
||||
- [x] 备份时间戳命名
|
||||
- [x] 备份文件组织
|
||||
|
||||
- [x] 数据恢复
|
||||
- [x] 从ZIP导入
|
||||
- [x] 自动ID冲突处理
|
||||
|
||||
#### 5. 用户界面
|
||||
- [x] 主界面布局
|
||||
- [x] 账户选择下拉框
|
||||
- [x] 新建/删除账户按钮
|
||||
- [x] 三个标签页:做账、查询、导出
|
||||
|
||||
- [x] 做账标签页
|
||||
- [x] 交易记录表格
|
||||
- [x] 新建记录按钮
|
||||
- [x] 编辑、删除、查看功能
|
||||
- [x] 统计信息显示
|
||||
|
||||
- [x] 查询标签页
|
||||
- [x] 日期范围选择器
|
||||
- [x] 金额范围输入框
|
||||
- [x] 交易人搜索框
|
||||
- [x] 查询按钮
|
||||
- [x] 结果表格显示
|
||||
|
||||
- [x] 导出标签页
|
||||
- [x] 导出为ZIP包按钮
|
||||
- [x] 导出为CSV按钮
|
||||
- [x] 导入账户按钮
|
||||
- [x] 创建备份按钮
|
||||
- [x] 功能说明文本
|
||||
|
||||
- [x] 对话框
|
||||
- [x] 创建/编辑交易对话框
|
||||
- [x] 图片选择功能
|
||||
- [x] 数据验证
|
||||
- [x] 记录详情预览对话框
|
||||
- [x] 图片缩略图预览
|
||||
|
||||
#### 6. 主应用集成
|
||||
- [x] 导入财务模块到主窗口
|
||||
- [x] 添加财务界面到导航栏
|
||||
- [x] 使用合适的图标 (FIF.DOCUMENT)
|
||||
- [x] 导航项文本 ("财务做账")
|
||||
|
||||
### 📁 交付文件清单
|
||||
|
||||
```
|
||||
MRobot/
|
||||
├── app/
|
||||
│ ├── finance_interface.py [新增] UI界面模块 (800+ 行)
|
||||
│ │ ├── CreateTransactionDialog 创建/编辑对话框
|
||||
│ │ ├── RecordViewDialog 查看详情对话框
|
||||
│ │ └── FinanceInterface 主界面
|
||||
│ │
|
||||
│ ├── tools/
|
||||
│ │ └── finance_manager.py [新增] 数据管理模块 (700+ 行)
|
||||
│ │ ├── TransactionType 交易类型枚举
|
||||
│ │ ├── Transaction 交易记录类
|
||||
│ │ ├── Account 账户类
|
||||
│ │ └── FinanceManager 核心管理类
|
||||
│ │
|
||||
│ └── main_window.py [修改] 添加财务模块集成
|
||||
│
|
||||
├── assets/Finance_Data/ [新增] 数据存储目录结构
|
||||
│ ├── accounts/ 账户存储
|
||||
│ ├── backups/ 备份存储
|
||||
│ ├── images/ 临时存储
|
||||
│ └── [其他现有文件] 保持不变
|
||||
│
|
||||
└── 文档文件
|
||||
├── FINANCE_README.md [新增] 项目总结 (250+ 行)
|
||||
├── FINANCE_QUICK_START.md [新增] 快速开始 (300+ 行)
|
||||
├── FINANCE_MODULE_GUIDE.md [新增] 详细指南 (350+ 行)
|
||||
└── FINANCE_API_EXAMPLES.py [新增] API示例 (450+ 行)
|
||||
```
|
||||
|
||||
### 🔧 技术实现细节
|
||||
|
||||
#### 数据存储架构
|
||||
- **位置**: `assets/Finance_Data/`
|
||||
- **格式**: JSON + 图片文件
|
||||
- **组织**: 按账户ID → 交易ID → 数据类型组织
|
||||
- **安全**: 原子操作,避免数据损坏
|
||||
|
||||
#### 关键类的接口设计
|
||||
```python
|
||||
# FinanceManager (18个公共方法)
|
||||
- 账户管理: 4个方法
|
||||
- 交易管理: 4个方法
|
||||
- 图片处理: 2个方法
|
||||
- 查询统计: 2个方法
|
||||
- 导入导出: 4个方法
|
||||
- 备份恢复: 1个方法
|
||||
- 辅助方法: 内部使用
|
||||
|
||||
# UI层 (3个主要对话框 + 1个主界面)
|
||||
- CreateTransactionDialog: 300+ 行
|
||||
- RecordViewDialog: 150+ 行
|
||||
- FinanceInterface: 600+ 行
|
||||
```
|
||||
|
||||
#### 性能优化
|
||||
- 内存缓存所有账户和交易
|
||||
- 按日期倒序排列加速查询
|
||||
- 图片延迟加载
|
||||
- 批量操作优化
|
||||
|
||||
### 📊 代码统计
|
||||
|
||||
| 模块 | 代码行数 | 类数 | 方法数 |
|
||||
|------|---------|------|--------|
|
||||
| finance_manager.py | 700+ | 4 | 40+ |
|
||||
| finance_interface.py | 800+ | 4 | 60+ |
|
||||
| 主窗口集成 | 5 | - | - |
|
||||
| 文档 | 1500+ | - | - |
|
||||
| **总计** | **3000+** | - | - |
|
||||
|
||||
### ✨ 核心特性总结
|
||||
|
||||
1. **完整性**
|
||||
- ✅ 做账、查询、导出三大功能完整
|
||||
- ✅ 支持图片附件和本地存储
|
||||
- ✅ 支持数据转移和备份
|
||||
|
||||
2. **易用性**
|
||||
- ✅ 直观的UI设计
|
||||
- ✅ 流畅的操作流程
|
||||
- ✅ 详细的帮助文档
|
||||
|
||||
3. **可靠性**
|
||||
- ✅ 数据持久化存储
|
||||
- ✅ 完整的错误处理
|
||||
- ✅ 数据备份和恢复机制
|
||||
|
||||
4. **可扩展性**
|
||||
- ✅ 清晰的代码结构
|
||||
- ✅ 分离的UI和数据层
|
||||
- ✅ 易于添加新功能
|
||||
|
||||
5. **性能**
|
||||
- ✅ 快速查询 (< 100ms)
|
||||
- ✅ 高效的内存使用
|
||||
- ✅ 支持大数据量 (10000+ 记录)
|
||||
|
||||
### 🎯 功能完成度
|
||||
|
||||
```
|
||||
做账功能: ████████████████████ 100%
|
||||
- 账户管理: ✅
|
||||
- 记录管理: ✅
|
||||
- 图片附件: ✅
|
||||
- 统计显示: ✅
|
||||
|
||||
查询功能: ████████████████████ 100%
|
||||
- 多条件查询: ✅
|
||||
- 结果展示: ✅
|
||||
- 图片预览: ✅
|
||||
|
||||
导出导入: ████████████████████ 100%
|
||||
- ZIP转移: ✅
|
||||
- CSV导出: ✅
|
||||
- 数据导入: ✅
|
||||
- 完整备份: ✅
|
||||
|
||||
用户界面: ████████████████████ 100%
|
||||
- 主界面: ✅
|
||||
- 对话框: ✅
|
||||
- 表格显示: ✅
|
||||
- 按钮操作: ✅
|
||||
|
||||
主应用集成: ████████████████████ 100%
|
||||
- 导航栏添加: ✅
|
||||
- 图标配置: ✅
|
||||
- 菜单项: ✅
|
||||
|
||||
文档完整: ████████████████████ 100%
|
||||
- 快速开始: ✅
|
||||
- 详细指南: ✅
|
||||
- API文档: ✅
|
||||
- 示例代码: ✅
|
||||
```
|
||||
|
||||
### 🚀 部署检查清单
|
||||
|
||||
- [x] 代码语法检查 (通过)
|
||||
- [x] 导入依赖检查 (通过)
|
||||
- [x] 文件路径检查 (通过)
|
||||
- [x] 应用启动测试 (通过)
|
||||
- [x] 基础功能测试 (通过)
|
||||
- [x] UI响应性测试 (通过)
|
||||
- [x] 数据持久化测试 (通过)
|
||||
|
||||
### 📝 使用入门
|
||||
|
||||
1. **启动应用**
|
||||
```bash
|
||||
python MRobot.py
|
||||
```
|
||||
|
||||
2. **打开财务模块**
|
||||
- 点击左侧导航栏"财务做账"
|
||||
|
||||
3. **创建账户**
|
||||
- 点击"新建账户"
|
||||
- 输入账户名称
|
||||
|
||||
4. **添加交易**
|
||||
- 点击"新建记录"
|
||||
- 填写交易信息
|
||||
- 上传图片(可选)
|
||||
|
||||
5. **查询数据**
|
||||
- 切换"查询"标签页
|
||||
- 设置过滤条件
|
||||
- 查看结果
|
||||
|
||||
6. **导出备份**
|
||||
- 切换"导出"标签页
|
||||
- 选择导出方式
|
||||
|
||||
### 🔮 建议的后续改进
|
||||
|
||||
**短期 (v1.1)**
|
||||
- [ ] 交易分类系统
|
||||
- [ ] 自定义字段支持
|
||||
- [ ] 批量导入功能
|
||||
|
||||
**中期 (v2.0)**
|
||||
- [ ] 统计报表生成
|
||||
- [ ] 图表可视化
|
||||
- [ ] 预算管理
|
||||
|
||||
**长期 (v3.0)**
|
||||
- [ ] 云同步功能
|
||||
- [ ] 多用户协作
|
||||
- [ ] 移动端应用
|
||||
|
||||
### 📚 文档清单
|
||||
|
||||
| 文档 | 用途 | 类型 |
|
||||
|------|------|------|
|
||||
| FINANCE_README.md | 项目总体介绍 | 项目文档 |
|
||||
| FINANCE_QUICK_START.md | 快速入门指南 | 用户手册 |
|
||||
| FINANCE_MODULE_GUIDE.md | 详细功能说明 | 用户手册 |
|
||||
| FINANCE_API_EXAMPLES.py | API编程示例 | 开发文档 |
|
||||
|
||||
### 🎓 学习资源
|
||||
|
||||
- **快速上手**: 5分钟了解基本功能
|
||||
- **详细指南**: 学习所有功能细节
|
||||
- **API文档**: 了解如何二次开发
|
||||
- **代码注释**: 源代码中的详细注释
|
||||
|
||||
### ✅ 交付完成
|
||||
|
||||
该财务做账模块已完全开发并集成到MRobot应用中。所有计划的功能都已实现,代码质量良好,文档完善。
|
||||
|
||||
**项目状态: 🟢 完成就绪**
|
||||
|
||||
---
|
||||
|
||||
## 项目统计
|
||||
|
||||
- **开发时间**: 2024年11月
|
||||
- **代码量**: 3000+ 行
|
||||
- **文档量**: 1500+ 行
|
||||
- **总投入**: 完整、生产级质量
|
||||
- **测试覆盖**: 核心功能已测试
|
||||
|
||||
---
|
||||
|
||||
**感谢使用本财务做账模块!** 🎉
|
||||
@ -1,274 +0,0 @@
|
||||
# 财务做账模块 - 完成总结
|
||||
|
||||
## 项目完成情况
|
||||
|
||||
### ✅ 全部功能已实现
|
||||
|
||||
本财务做账模块已成功开发并集成到MRobot应用中,所有计划的功能都已完成:
|
||||
|
||||
#### 1️⃣ 做账功能 (100%)
|
||||
- ✅ 多账户管理系统
|
||||
- ✅ 交易记录添加/编辑/删除
|
||||
- ✅ 三种图片附件支持
|
||||
- ✅ 实时统计显示
|
||||
|
||||
#### 2️⃣ 查询功能 (100%)
|
||||
- ✅ 多条件过滤查询
|
||||
- ✅ 日期/金额/交易人查询
|
||||
- ✅ 模糊搜索支持
|
||||
- ✅ 图片预览功能
|
||||
|
||||
#### 3️⃣ 导出功能 (100%)
|
||||
- ✅ ZIP格式转移
|
||||
- ✅ CSV导出分析
|
||||
- ✅ 账户导入还原
|
||||
- ✅ 完整备份功能
|
||||
|
||||
#### 4️⃣ 本地存储 (100%)
|
||||
- ✅ 清晰的文件夹结构
|
||||
- ✅ 数据持久化保存
|
||||
- ✅ 支持大数据量
|
||||
- ✅ 安全的删除恢复
|
||||
|
||||
## 交付物清单
|
||||
|
||||
### 代码文件
|
||||
```
|
||||
✅ app/tools/finance_manager.py (700+ 行代码)
|
||||
└─ 完整的数据管理模块
|
||||
|
||||
✅ app/finance_interface.py (860+ 行代码)
|
||||
└─ 完整的用户界面模块
|
||||
|
||||
✅ app/main_window.py (已修改)
|
||||
└─ 集成财务模块到主应用
|
||||
```
|
||||
|
||||
### 数据存储
|
||||
```
|
||||
✅ assets/Finance_Data/ (自动创建)
|
||||
├─ accounts/ 账户数据
|
||||
├─ backups/ 备份数据
|
||||
└─ images/ 临时文件
|
||||
```
|
||||
|
||||
### 文档文件
|
||||
```
|
||||
✅ FINANCE_USER_MANUAL.md 用户手册
|
||||
✅ FINANCE_QUICK_START.md 快速开始
|
||||
✅ FINANCE_MODULE_GUIDE.md 详细指南
|
||||
✅ FINANCE_API_EXAMPLES.py API示例
|
||||
✅ FINANCE_README.md 项目介绍
|
||||
✅ FINANCE_COMPLETION_REPORT.md 完成报告
|
||||
✅ debug_finance.py 调试工具
|
||||
```
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 💰 完整的财务记录
|
||||
每条记录包含:
|
||||
- 📅 交易日期
|
||||
- 💵 交易金额
|
||||
- 👤 交易人名称
|
||||
- 📝 备注说明
|
||||
- 🖼️ 三种凭证图片
|
||||
|
||||
### 🔍 强大的查询能力
|
||||
支持按以下条件查询:
|
||||
- 日期范围
|
||||
- 金额范围
|
||||
- 交易人(模糊搜索)
|
||||
- 自由组合条件
|
||||
|
||||
### 📤 灵活的导出转移
|
||||
支持以下导出方式:
|
||||
- ZIP压缩包转移
|
||||
- CSV表格导出
|
||||
- 完整备份创建
|
||||
- 数据导入还原
|
||||
|
||||
### 📊 实时的统计信息
|
||||
每个账户显示:
|
||||
- 总交易金额
|
||||
- 交易笔数
|
||||
- 账户创建时间
|
||||
- 最后更新时间
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 分层设计
|
||||
```
|
||||
UI层 (PyQt5 + qfluentwidgets)
|
||||
↓
|
||||
业务层 (FinanceManager 核心类)
|
||||
↓
|
||||
数据层 (JSON + 本地文件系统)
|
||||
```
|
||||
|
||||
### 数据模型
|
||||
- `TransactionType`: 交易类型枚举
|
||||
- `Transaction`: 交易记录类
|
||||
- `Account`: 账户类
|
||||
- `FinanceManager`: 管理器类
|
||||
|
||||
### API接口
|
||||
```python
|
||||
FinanceManager 提供 30+ 个方法
|
||||
- 账户管理: create, get, delete, update, list
|
||||
- 交易处理: add, get, delete, update, query
|
||||
- 图片管理: save, get
|
||||
- 导入导出: export, import, backup
|
||||
- 统计汇总: summary, query
|
||||
```
|
||||
|
||||
## 使用验证
|
||||
|
||||
### ✅ 功能测试已通过
|
||||
```
|
||||
✅ 账户创建 - 正常
|
||||
✅ 交易添加 - 正常
|
||||
✅ 图片保存 - 正常
|
||||
✅ 数据查询 - 正常
|
||||
✅ 统计汇总 - 正常
|
||||
✅ 数据备份 - 正常
|
||||
✅ ZIP导出 - 正常
|
||||
✅ CSV导出 - 正常
|
||||
```
|
||||
|
||||
### ✅ 调试工具验证
|
||||
```
|
||||
运行: python debug_finance.py
|
||||
|
||||
✅ 初始化成功
|
||||
✅ 获取账户成功
|
||||
✅ 创建账户成功
|
||||
✅ 添加交易成功
|
||||
✅ 查询账户成功
|
||||
✅ 获取汇总成功
|
||||
✅ 测试查询成功
|
||||
✅ 创建备份成功
|
||||
```
|
||||
|
||||
## 现有数据
|
||||
|
||||
系统中已存在的账户:
|
||||
1. 账户ID: `c8c53f15-bf70-4abe-8600-d42a73ace8ad` - 名称: "1"
|
||||
2. 账户ID: `992f0c19-ba3d-4444-8995-c694adda2e9e` - 名称: "吕祖成"
|
||||
|
||||
可以直接使用这些账户,或者创建新账户。
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 快速开始(5分钟)
|
||||
1. 启动 MRobot: `python MRobot.py`
|
||||
2. 点击左侧"财务做账"
|
||||
3. 选择或创建账户
|
||||
4. 点击"新建记录"
|
||||
5. 填写交易信息并保存
|
||||
|
||||
### 完整功能(15分钟)
|
||||
1. 做账标签页 - 添加和管理交易
|
||||
2. 查询标签页 - 查询和统计数据
|
||||
3. 导出标签页 - 备份和转移数据
|
||||
|
||||
### 详细参考
|
||||
- 快速开始: `FINANCE_QUICK_START.md`
|
||||
- 详细指南: `FINANCE_MODULE_GUIDE.md`
|
||||
- API文档: `FINANCE_API_EXAMPLES.py`
|
||||
- 用户手册: `FINANCE_USER_MANUAL.md`
|
||||
|
||||
## 项目质量
|
||||
|
||||
### 代码质量
|
||||
- ✅ 代码注释完善
|
||||
- ✅ 函数文档齐全
|
||||
- ✅ 错误处理完整
|
||||
- ✅ 类型提示规范
|
||||
|
||||
### 功能完整性
|
||||
- ✅ 所有计划功能实现
|
||||
- ✅ 额外功能增强
|
||||
- ✅ 边界情况处理
|
||||
- ✅ 用户体验优化
|
||||
|
||||
### 文档完善度
|
||||
- ✅ 用户手册详尽
|
||||
- ✅ 开发文档清晰
|
||||
- ✅ API示例丰富
|
||||
- ✅ 快速开始指南
|
||||
|
||||
### 数据安全
|
||||
- ✅ 本地存储保护
|
||||
- ✅ 备份恢复机制
|
||||
- ✅ 数据验证检查
|
||||
- ✅ 异常处理完善
|
||||
|
||||
## 性能指标
|
||||
|
||||
### 响应时间
|
||||
- 账户加载: < 100ms
|
||||
- 记录查询: < 100ms
|
||||
- 数据保存: < 50ms
|
||||
- 统计计算: < 50ms
|
||||
|
||||
### 容量支持
|
||||
- 账户数: 无限制
|
||||
- 单账户记录: 推荐 ≤ 10000
|
||||
- 图片大小: 支持 ≤ 2MB
|
||||
- 备份文件: 无限制
|
||||
|
||||
### 内存使用
|
||||
- 基础占用: ~ 50MB
|
||||
- 加载10000记录: ~ 100MB
|
||||
- 图片缓存: 按需加载
|
||||
|
||||
## 扩展方向
|
||||
|
||||
### 可能的改进 (v1.1)
|
||||
- [ ] 交易分类管理
|
||||
- [ ] 自定义字段
|
||||
- [ ] 批量导入
|
||||
- [ ] 统计图表
|
||||
|
||||
### 后期计划 (v2.0)
|
||||
- [ ] 云同步功能
|
||||
- [ ] 多用户协作
|
||||
- [ ] OCR识别
|
||||
- [ ] 移动应用
|
||||
|
||||
## 支持和反馈
|
||||
|
||||
### 如何获取帮助
|
||||
1. 阅读相关文档
|
||||
2. 查看API示例
|
||||
3. 运行调试工具
|
||||
4. 提交问题反馈
|
||||
|
||||
### 文档索引
|
||||
- 📖 **用户手册** → `FINANCE_USER_MANUAL.md`
|
||||
- 🚀 **快速开始** → `FINANCE_QUICK_START.md`
|
||||
- 📚 **详细指南** → `FINANCE_MODULE_GUIDE.md`
|
||||
- 💻 **API示例** → `FINANCE_API_EXAMPLES.py`
|
||||
- 📊 **项目介绍** → `FINANCE_README.md`
|
||||
- ✅ **完成报告** → `FINANCE_COMPLETION_REPORT.md`
|
||||
- 🐛 **调试工具** → `debug_finance.py`
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 📧 Email: [项目邮箱]
|
||||
- 🐙 GitHub: [项目地址]
|
||||
- 💬 讨论: [社区论坛]
|
||||
|
||||
---
|
||||
|
||||
## 项目状态
|
||||
|
||||
🟢 **完全就绪 (Production Ready)**
|
||||
|
||||
所有功能已实现、测试通过、文档完善、可以安心使用!
|
||||
|
||||
---
|
||||
|
||||
**感谢使用MRobot财务做账模块!** 🎉
|
||||
|
||||
如有任何问题或建议,欢迎随时反馈。
|
||||
@ -1,236 +0,0 @@
|
||||
# 导入账户查询问题 - 解决方案文档
|
||||
|
||||
## 🐛 问题描述
|
||||
|
||||
用户反馈:导入的账户无法进行查询操作。
|
||||
|
||||
### 问题表现
|
||||
|
||||
1. 账户成功导入(UI显示导入成功)
|
||||
2. 账户出现在账户列表中
|
||||
3. 但是使用查询功能时无法显示结果
|
||||
4. 做账页面也可能无法显示导入账户的记录
|
||||
|
||||
## 🔍 根本原因
|
||||
|
||||
问题源于 `import_account_package` 方法中的两个bug:
|
||||
|
||||
### Bug #1: 元数据ID不同步
|
||||
```
|
||||
问题流程:
|
||||
1. ZIP包中存储原始账户ID (例如: f78adb43-cf2c-49be-8e36-361908db6d68)
|
||||
2. 导入时如果账户已存在,创建新ID (例如: 1c4b2c9f-1629-463d-b7a0-cbcff93c99)
|
||||
3. 新ID用于创建文件夹
|
||||
4. 但metadata.json中的ID仍然是原始ID
|
||||
5. 加载时,目录名(新ID)与metadata中的ID(原始ID)不匹配
|
||||
6. load_all_accounts() 无法正确识别账户
|
||||
```
|
||||
|
||||
### Bug #2: UI刷新不完整
|
||||
```
|
||||
问题流程:
|
||||
1. 导入后只调用 refresh_account_list()
|
||||
2. 但没有设置导入账户为当前选中账户
|
||||
3. 用户可能仍在查看旧账户的数据
|
||||
4. 查询页面没有被清空
|
||||
```
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 修复1: 同步metadata中的账户ID
|
||||
|
||||
**文件**: `app/tools/finance_manager.py`
|
||||
|
||||
**修改**:
|
||||
```python
|
||||
def import_account_package(self, zip_path: str) -> Optional[str]:
|
||||
"""导入账户压缩包,返回导入的账户ID"""
|
||||
try:
|
||||
zip_path = Path(zip_path)
|
||||
if not zip_path.exists():
|
||||
return None
|
||||
|
||||
# 先加载元数据以获取账户ID
|
||||
with zipfile.ZipFile(zip_path, 'r') as zipf:
|
||||
metadata_content = zipf.read('metadata.json')
|
||||
metadata = json.loads(metadata_content)
|
||||
account_id = metadata['id']
|
||||
|
||||
# 如果账户已存在,创建新ID
|
||||
if account_id in self.accounts:
|
||||
account_id = str(uuid.uuid4())
|
||||
# ✅ 关键修复: 更新元数据中的ID
|
||||
metadata['id'] = account_id
|
||||
|
||||
# 解压到临时目录
|
||||
temp_dir = self.data_root / f"_temp_{uuid.uuid4()}"
|
||||
with zipfile.ZipFile(zip_path, 'r') as zipf:
|
||||
zipf.extractall(temp_dir)
|
||||
|
||||
# ✅ 关键修复: 更新临时目录中的元数据文件
|
||||
metadata_file = temp_dir / 'metadata.json'
|
||||
with open(metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 移动到正式目录
|
||||
account_dir = self._get_account_dir(account_id)
|
||||
if account_dir.exists():
|
||||
shutil.rmtree(account_dir)
|
||||
|
||||
shutil.move(str(temp_dir), str(account_dir))
|
||||
|
||||
# 重新加载账户
|
||||
self.load_all_accounts()
|
||||
return account_id
|
||||
except Exception as e:
|
||||
print(f"导入账户出错: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### 修复2: 完整的UI刷新流程
|
||||
|
||||
**文件**: `app/finance_interface.py`
|
||||
|
||||
**修改**:
|
||||
```python
|
||||
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.refresh_account_list()
|
||||
|
||||
# ✅ 关键修复: 找到新导入的账户并设置为当前账户
|
||||
for i in range(self.account_combo.count()):
|
||||
if self.account_combo.itemData(i) == account_id:
|
||||
self.account_combo.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
# ✅ 关键修复: 清空查询结果以显示新导入账户的数据
|
||||
self.query_result_table.setRowCount(0)
|
||||
|
||||
InfoBar.success("账户导入成功", "", duration=2000, parent=self)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "导入账户失败")
|
||||
```
|
||||
|
||||
### 修复3: 账户切换时清空查询结果
|
||||
|
||||
**文件**: `app/finance_interface.py`
|
||||
|
||||
**修改**:
|
||||
```python
|
||||
def on_account_changed(self):
|
||||
"""账户改变时刷新显示"""
|
||||
account_id = self.get_current_account_id()
|
||||
if account_id:
|
||||
self.refresh_records_display()
|
||||
# ✅ 关键修复: 切换账户时清空查询结果
|
||||
self.query_result_table.setRowCount(0)
|
||||
```
|
||||
|
||||
## 🧪 验证
|
||||
|
||||
运行测试脚本 `test_import_query.py` 验证修复:
|
||||
|
||||
```bash
|
||||
python test_import_query.py
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
✅ 找到有数据的测试账户: '测试账户'
|
||||
✅ 导出成功
|
||||
✅ 导入成功
|
||||
✅ 导入的账户信息: 名称: 测试账户, 交易数: 1
|
||||
✅ 无条件查询: 1 条记录
|
||||
✅ 按交易人'测试商家'查询: 1 条记录
|
||||
```
|
||||
|
||||
## 📝 使用流程
|
||||
|
||||
修复后,导入账户的完整流程:
|
||||
|
||||
1. **打开财务做账模块**
|
||||
- 点击左侧"财务做账"导航
|
||||
|
||||
2. **点击"导出"标签页**
|
||||
- 选择要导入的ZIP文件
|
||||
- 点击"导入账户"按钮
|
||||
|
||||
3. **验证导入结果**
|
||||
- 新账户自动出现在账户列表中
|
||||
- 新账户自动设置为当前选中账户
|
||||
- 做账页显示导入的交易记录
|
||||
- 可以立即在查询页进行查询
|
||||
|
||||
## 🔧 技术细节
|
||||
|
||||
### 数据结构
|
||||
```
|
||||
assets/Finance_Data/
|
||||
└── accounts/
|
||||
└── [账户ID]/
|
||||
├── metadata.json ← 关键: ID必须与文件夹名一致
|
||||
└── [交易ID]/
|
||||
├── data.json
|
||||
├── invoice/
|
||||
├── payment/
|
||||
└── purchase/
|
||||
```
|
||||
|
||||
### 关键对象关系
|
||||
```
|
||||
FinanceManager.accounts = {
|
||||
'账户ID': Account(...),
|
||||
...
|
||||
}
|
||||
|
||||
Account.id ↔ 文件夹名称 ↔ metadata.json中的id
|
||||
```
|
||||
|
||||
### 加载流程
|
||||
```
|
||||
load_all_accounts()
|
||||
├─ 遍历 accounts/ 目录
|
||||
├─ 加载每个目录下的 metadata.json
|
||||
├─ 使用 metadata['id'] 作为关键字
|
||||
└─ 如果ID与文件夹名不匹配 → 账户加载失败
|
||||
```
|
||||
|
||||
## ⚠️ 重要提醒
|
||||
|
||||
1. **ID一致性**: 账户ID必须在三个地方一致:
|
||||
- 文件夹名称
|
||||
- metadata.json中的id字段
|
||||
- FinanceManager.accounts字典的键
|
||||
|
||||
2. **元数据更新**: 创建新ID时必须更新元数据文件
|
||||
|
||||
3. **重新加载**: 导入后必须调用load_all_accounts()刷新内存中的账户
|
||||
|
||||
## 📊 测试结果
|
||||
|
||||
| 测试项 | 修复前 | 修复后 |
|
||||
|-------|--------|--------|
|
||||
| 账户导入 | ✅ 成功 | ✅ 成功 |
|
||||
| 账户识别 | ❌ 失败 | ✅ 成功 |
|
||||
| 数据查询 | ❌ 无结果 | ✅ 正常 |
|
||||
| UI更新 | ❌ 不完整 | ✅ 完整 |
|
||||
| 做账显示 | ❌ 无数据 | ✅ 显示数据 |
|
||||
|
||||
---
|
||||
|
||||
**更新日期**: 2025-11-25
|
||||
|
||||
**修复状态**: ✅ 完成
|
||||
|
||||
**测试状态**: ✅ 通过
|
||||
|
||||
**生产准备**: ✅ 就绪
|
||||
@ -1,207 +0,0 @@
|
||||
# 财务做账模块使用指南
|
||||
|
||||
## 功能概述
|
||||
|
||||
MRobot 财务做账模块是一个完整的财务管理系统,提供做账、查询、导出三大功能。支持多账户管理、图片附件、本地数据存储等功能。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **多账户管理**: 支持创建和管理多个独立的财务账户
|
||||
- **完整的交易记录**: 每条记录包含日期、金额、交易人、备注和三种类型的图片证明
|
||||
- **图片管理**: 支持为每条记录关联发票、支付记录、购买记录三张图片
|
||||
- **本地存储**: 所有数据和图片存储在本地,组织清晰
|
||||
- **强大的查询**: 支持按日期、金额、交易人多条件查询
|
||||
- **数据导入导出**: 支持ZIP打包转移、CSV导出、完整备份
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
assets/Finance_Data/
|
||||
├── accounts/ # 账户数据目录
|
||||
│ ├── [账户ID]/ # 每个账户的独立目录
|
||||
│ │ ├── metadata.json # 账户元数据
|
||||
│ │ ├── [交易ID]/ # 每条交易记录的目录
|
||||
│ │ │ ├── data.json # 交易记录数据
|
||||
│ │ │ ├── invoice/ # 发票图片
|
||||
│ │ │ ├── payment/ # 支付记录图片
|
||||
│ │ │ └── purchase/ # 购买记录图片
|
||||
├── backups/ # 自动备份目录
|
||||
├── images/ # 临时图片缓存(可选)
|
||||
└── [其他文件]
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 1. 做账标签页
|
||||
|
||||
#### 创建账户
|
||||
1. 点击"新建账户"按钮
|
||||
2. 输入账户名称和描述(可选)
|
||||
3. 点击确定
|
||||
|
||||
#### 创建新交易记录
|
||||
1. 选择账户
|
||||
2. 点击"新建记录"按钮
|
||||
3. 填写表单信息:
|
||||
- **日期**: 默认为当前日期,可修改
|
||||
- **金额**: 必须大于0
|
||||
- **交易人**: 必填项,填写交易对象
|
||||
- **备注**: 可选,补充说明
|
||||
- **图片**: 可选上传发票、支付记录、购买记录三张图片
|
||||
|
||||
#### 管理交易记录
|
||||
- **编辑**: 点击"编辑"按钮修改记录
|
||||
- **删除**: 点击"删除"按钮删除记录
|
||||
- **查看**: 点击"查看"按钮查看完整详情和图片预览
|
||||
|
||||
#### 统计信息
|
||||
- 实时显示总金额(人民币)
|
||||
- 显示记录总数
|
||||
|
||||
### 2. 查询标签页
|
||||
|
||||
#### 设置查询条件
|
||||
- **日期范围**: 默认查询最近一个月,可自定义
|
||||
- **金额范围**: 可设置最小和最大金额
|
||||
- **交易人**: 支持模糊搜索(不区分大小写)
|
||||
|
||||
#### 执行查询
|
||||
1. 调整过滤条件
|
||||
2. 点击"查询"按钮
|
||||
3. 结果列表显示匹配的记录
|
||||
|
||||
#### 查看记录详情
|
||||
- 点击结果列表中的"查看详情"按钮
|
||||
- 显示完整信息和图片预览
|
||||
|
||||
### 3. 导出标签页
|
||||
|
||||
#### 导出账户为ZIP包
|
||||
- 用途: 转移账户给他人使用
|
||||
- 操作: 点击"导出为ZIP包",选择保存位置
|
||||
- 文件名格式: `账户名_账户ID.zip`
|
||||
- 包含所有交易记录和附加图片
|
||||
|
||||
#### 导出为CSV格式
|
||||
- 用途: 用Excel或其他工具分析
|
||||
- 操作: 点击"导出CSV",选择保存位置
|
||||
- 内容: 日期、金额、交易人、备注、创建时间
|
||||
|
||||
#### 导入账户
|
||||
- 用途: 导入他人共享的账户
|
||||
- 操作: 点击"导入账户",选择ZIP文件
|
||||
- 系统自动提取并加载账户
|
||||
|
||||
#### 创建备份
|
||||
- 用途: 备份所有账户
|
||||
- 操作: 点击"创建备份"
|
||||
- 保存位置: `assets/Finance_Data/backups/backup_[时间戳].zip`
|
||||
- 自动压缩所有账户数据
|
||||
|
||||
## 数据模式
|
||||
|
||||
### 交易记录数据结构
|
||||
```json
|
||||
{
|
||||
"id": "唯一ID",
|
||||
"date": "2024-01-15",
|
||||
"amount": 1500.50,
|
||||
"trader": "张三",
|
||||
"notes": "购买办公用品",
|
||||
"invoice_path": "accounts/xxx/yyy/invoice/receipt.jpg",
|
||||
"payment_path": "accounts/xxx/yyy/payment/wechat.jpg",
|
||||
"purchase_path": "accounts/xxx/yyy/purchase/order.jpg",
|
||||
"created_at": "ISO格式时间戳",
|
||||
"updated_at": "ISO格式时间戳"
|
||||
}
|
||||
```
|
||||
|
||||
### 账户元数据结构
|
||||
```json
|
||||
{
|
||||
"id": "唯一ID",
|
||||
"name": "账户名称",
|
||||
"description": "账户描述",
|
||||
"created_at": "ISO格式时间戳",
|
||||
"updated_at": "ISO格式时间戳"
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 大数据量管理
|
||||
- **分账户**: 按业务类型或时间周期创建多个账户
|
||||
- **定期备份**: 定期使用"创建备份"功能
|
||||
- **及时查询**: 不要一次加载过多数据,使用查询功能精确定位
|
||||
|
||||
### 图片管理
|
||||
- **推荐格式**: PNG、JPG、BMP、JPEG
|
||||
- **推荐大小**: 不超过2MB
|
||||
- **文件组织**: 三张图片分别存储,避免重复
|
||||
|
||||
### 数据安全
|
||||
- 所有数据存储在本地,不上传到云端
|
||||
- 定期备份重要数据
|
||||
- 谨慎删除账户和记录
|
||||
|
||||
## 文件结构详解
|
||||
|
||||
### finance_manager.py
|
||||
核心数据管理模块,包含:
|
||||
- `TransactionType`: 交易类型枚举
|
||||
- `Transaction`: 交易记录数据模型
|
||||
- `Account`: 账户数据模型
|
||||
- `FinanceManager`: 主要的财务管理类
|
||||
|
||||
### finance_interface.py
|
||||
用户界面模块,包含:
|
||||
- `CreateTransactionDialog`: 创建/编辑交易对话框
|
||||
- `RecordViewDialog`: 查看交易详情对话框
|
||||
- `FinanceInterface`: 主界面和三个标签页
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何转移账户给他人?
|
||||
A: 使用"导出为ZIP包"功能导出账户,将ZIP文件发送给对方,对方使用"导入账户"导入。
|
||||
|
||||
### Q: 删除记录后能恢复吗?
|
||||
A: 不能。请定期创建备份,以防误删。
|
||||
|
||||
### Q: 支持的最大数据量是多少?
|
||||
A: 理论上不限制,但建议单个账户不超过10000条记录以保持性能。
|
||||
|
||||
### Q: 图片能否修改?
|
||||
A: 不能。如需修改,请删除记录后重新创建。
|
||||
|
||||
### Q: 可以合并账户吗?
|
||||
A: 暂不支持。可以手动导出CSV后粘贴合并。
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 数据存储方式
|
||||
- 元数据: JSON格式
|
||||
- 交易记录: JSON格式
|
||||
- 图片: 原始格式(JPG、PNG等)
|
||||
|
||||
### 查询性能
|
||||
- 账户级别的内存缓存
|
||||
- 交易记录按日期倒序排列
|
||||
- 查询时进行内存过滤
|
||||
|
||||
### 文件安全
|
||||
- 所有写入操作使用原子性操作
|
||||
- 删除前自动备份(通过备份功能)
|
||||
- 支持恢复已备份数据
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- 初始发布
|
||||
- 支持基本的做账、查询、导出功能
|
||||
- 支持多账户管理
|
||||
- 支持图片附件
|
||||
- 支持备份和恢复
|
||||
|
||||
---
|
||||
|
||||
**使用此模块前,请确保 MRobot 已正确安装并配置好依赖环境。**
|
||||
@ -1,360 +0,0 @@
|
||||
# 🎉 财务做账模块 - 项目完成
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
已成功为MRobot应用开发并集成了一个**完整的财务做账管理系统**,包含做账、查询、导出三大功能模块。
|
||||
|
||||
## ✅ 完成清单
|
||||
|
||||
### 核心功能实现
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 多账户管理 | ✅ | 支持创建、删除、更新账户 |
|
||||
| 交易记录 | ✅ | 完整的记录管理系统 |
|
||||
| 图片附件 | ✅ | 支持3种类型图片保存 |
|
||||
| 本地存储 | ✅ | JSON + 文件系统存储 |
|
||||
| 多条件查询 | ✅ | 日期、金额、交易人查询 |
|
||||
| ZIP转移 | ✅ | 账户导出导入功能 |
|
||||
| CSV导出 | ✅ | 支持Excel分析 |
|
||||
| 完整备份 | ✅ | 一键备份所有数据 |
|
||||
| 图片预览 | ✅ | 查看交易凭证 |
|
||||
| 实时统计 | ✅ | 显示总额和记录数 |
|
||||
|
||||
### 代码交付物
|
||||
|
||||
```
|
||||
app/
|
||||
├── finance_interface.py ✅ 完成 (860+ 行)
|
||||
│ ├── CreateTransactionDialog 创建/编辑对话框
|
||||
│ ├── RecordViewDialog 查看详情对话框
|
||||
│ └── FinanceInterface 主界面(3个标签页)
|
||||
│
|
||||
├── tools/
|
||||
│ └── finance_manager.py ✅ 完成 (700+ 行)
|
||||
│ ├── TransactionType 交易类型枚举
|
||||
│ ├── Transaction 交易记录类
|
||||
│ ├── Account 账户类
|
||||
│ └── FinanceManager 核心管理类 (30+ 方法)
|
||||
│
|
||||
└── main_window.py ✅ 已修改
|
||||
└── 集成财务模块
|
||||
```
|
||||
|
||||
### 文档交付物
|
||||
|
||||
```
|
||||
✅ FINANCE_USER_MANUAL.md 用户手册
|
||||
✅ FINANCE_QUICK_START.md 快速开始指南
|
||||
✅ FINANCE_MODULE_GUIDE.md 详细功能指南
|
||||
✅ FINANCE_API_EXAMPLES.py API编程示例
|
||||
✅ FINANCE_README.md 项目总结
|
||||
✅ FINANCE_COMPLETION_REPORT.md 完成报告
|
||||
✅ FINANCE_COMPLETION_SUMMARY.md 完成总结
|
||||
✅ debug_finance.py 调试工具
|
||||
```
|
||||
|
||||
### 数据存储结构
|
||||
|
||||
```
|
||||
assets/Finance_Data/
|
||||
├── accounts/ ✅ 账户数据目录
|
||||
│ ├── [账户ID1]/
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── [交易ID]/
|
||||
│ │ ├── data.json
|
||||
│ │ ├── invoice/
|
||||
│ │ ├── payment/
|
||||
│ │ └── purchase/
|
||||
│ └── [账户ID2]/...
|
||||
├── backups/ ✅ 备份目录
|
||||
├── images/ ✅ 临时目录
|
||||
└── [其他文件] 保持不变
|
||||
```
|
||||
|
||||
## 🚀 快速使用
|
||||
|
||||
### 启动应用
|
||||
```bash
|
||||
python MRobot.py
|
||||
```
|
||||
|
||||
### 打开财务模块
|
||||
1. 点击左侧导航栏"财务做账"
|
||||
2. 选择或创建账户
|
||||
3. 开始记账
|
||||
|
||||
### 基本流程
|
||||
```
|
||||
新建账户 → 新建记录 → 上传图片 → 查询统计 → 导出备份
|
||||
```
|
||||
|
||||
## 📊 项目统计
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 代码行数 | 1600+ |
|
||||
| 文档行数 | 2000+ |
|
||||
| Python类 | 4个 |
|
||||
| 方法总数 | 60+ |
|
||||
| 功能模块 | 3个 |
|
||||
| 对话框 | 2个 |
|
||||
| 标签页 | 3个 |
|
||||
|
||||
## 🔍 主要特性
|
||||
|
||||
### 1. 做账功能
|
||||
- 📝 完整的交易记录系统
|
||||
- 💰 支持金额、日期、交易人
|
||||
- 🖼️ 三种凭证图片支持
|
||||
- ✏️ 编辑和删除功能
|
||||
- 📊 实时统计显示
|
||||
|
||||
### 2. 查询功能
|
||||
- 🔎 多条件灵活查询
|
||||
- 📅 日期范围筛选
|
||||
- 💵 金额范围筛选
|
||||
- 👤 交易人模糊搜索
|
||||
- 👁️ 详情和图片预览
|
||||
|
||||
### 3. 导出功能
|
||||
- 📦 ZIP压缩包转移
|
||||
- 📊 CSV表格导出
|
||||
- 💾 完整数据备份
|
||||
- 📥 账户数据导入
|
||||
- ⏰ 自动时间戳命名
|
||||
|
||||
## 💻 技术实现
|
||||
|
||||
### 框架和库
|
||||
- PyQt5: UI框架
|
||||
- qfluentwidgets: 流畅设计组件
|
||||
- pathlib: 路径管理
|
||||
- json: 数据序列化
|
||||
- zipfile: 压缩包处理
|
||||
- csv: 表格导出
|
||||
- uuid: 唯一ID生成
|
||||
- datetime: 时间处理
|
||||
|
||||
### 架构设计
|
||||
```
|
||||
业务逻辑层 (FinanceManager)
|
||||
↓
|
||||
UI展示层 (FinanceInterface)
|
||||
↓
|
||||
本地存储层 (JSON + 文件系统)
|
||||
```
|
||||
|
||||
### 数据模型
|
||||
- TransactionType: 枚举类型
|
||||
- Transaction: 交易记录
|
||||
- Account: 账户信息
|
||||
- FinanceManager: 管理类
|
||||
|
||||
## ✨ 亮点设计
|
||||
|
||||
### 1. 清晰的代码结构
|
||||
- 数据层和UI层分离
|
||||
- 每个类职责单一明确
|
||||
- 方法命名规范易理解
|
||||
- 注释文档完善详尽
|
||||
|
||||
### 2. 完善的错误处理
|
||||
- 参数验证检查
|
||||
- 异常捕获处理
|
||||
- 用户友好提示
|
||||
- 数据一致性保证
|
||||
|
||||
### 3. 优秀的用户体验
|
||||
- 直观的操作流程
|
||||
- 及时的反馈提示
|
||||
- 流畅的界面设计
|
||||
- 合理的默认值
|
||||
|
||||
### 4. 灵活的数据管理
|
||||
- 支持大数据量
|
||||
- 快速查询能力
|
||||
- 安全的备份恢复
|
||||
- 便捷的数据转移
|
||||
|
||||
## 🧪 测试验证
|
||||
|
||||
### 功能测试
|
||||
```
|
||||
✅ 账户创建 - 正常
|
||||
✅ 账户删除 - 正常
|
||||
✅ 记录添加 - 正常
|
||||
✅ 记录更新 - 正常
|
||||
✅ 记录删除 - 正常
|
||||
✅ 图片保存 - 正常
|
||||
✅ 数据查询 - 正常
|
||||
✅ CSV导出 - 正常
|
||||
✅ ZIP导出 - 正常
|
||||
✅ 数据导入 - 正常
|
||||
✅ 备份创建 - 正常
|
||||
```
|
||||
|
||||
### 调试工具验证
|
||||
```
|
||||
运行: python debug_finance.py
|
||||
|
||||
✅ 初始化财务管理器 - 成功
|
||||
✅ 获取现有账户 - 成功
|
||||
✅ 创建测试账户 - 成功
|
||||
✅ 添加交易记录 - 成功
|
||||
✅ 查询账户信息 - 成功
|
||||
✅ 获取账户汇总 - 成功
|
||||
✅ 测试查询功能 - 成功
|
||||
✅ 创建备份功能 - 成功
|
||||
```
|
||||
|
||||
## 📖 使用文档
|
||||
|
||||
### 快速参考
|
||||
- **5分钟上手**: 阅读 `FINANCE_QUICK_START.md`
|
||||
- **功能详解**: 阅读 `FINANCE_MODULE_GUIDE.md`
|
||||
- **API开发**: 参考 `FINANCE_API_EXAMPLES.py`
|
||||
- **用户手册**: 查看 `FINANCE_USER_MANUAL.md`
|
||||
|
||||
### 技术文档
|
||||
- **项目介绍**: 查看 `FINANCE_README.md`
|
||||
- **完成报告**: 查看 `FINANCE_COMPLETION_REPORT.md`
|
||||
- **完成总结**: 查看 `FINANCE_COMPLETION_SUMMARY.md`
|
||||
|
||||
## 🎯 性能指标
|
||||
|
||||
### 响应时间
|
||||
- 账户加载: < 100ms
|
||||
- 查询操作: < 100ms
|
||||
- 数据保存: < 50ms
|
||||
- 统计计算: < 50ms
|
||||
|
||||
### 容量支持
|
||||
- 单账户容量: 推荐 ≤ 10000 条记录
|
||||
- 图片大小: 支持 ≤ 2MB
|
||||
- 备份文件: 无限制
|
||||
- 账户数: 无限制
|
||||
|
||||
## 🚀 后续扩展方向
|
||||
|
||||
### 短期优化 (v1.1)
|
||||
- [ ] 交易分类管理
|
||||
- [ ] 自定义字段
|
||||
- [ ] 批量导入
|
||||
- [ ] 统计图表
|
||||
|
||||
### 中期功能 (v2.0)
|
||||
- [ ] 云同步支持
|
||||
- [ ] 多用户协作
|
||||
- [ ] OCR识别
|
||||
- [ ] 预算管理
|
||||
|
||||
### 长期计划 (v3.0)
|
||||
- [ ] 移动应用
|
||||
- [ ] 智能分类
|
||||
- [ ] 数据分析
|
||||
- [ ] API接口
|
||||
|
||||
## 📝 已知情况
|
||||
|
||||
### 现有数据
|
||||
系统中已存在两个测试账户:
|
||||
1. 账户名: "1"
|
||||
2. 账户名: "吕祖成"
|
||||
|
||||
可以直接使用或创建新账户。
|
||||
|
||||
### 系统状态
|
||||
- ✅ 应用启动正常
|
||||
- ✅ 所有功能运行正常
|
||||
- ✅ 数据保存成功
|
||||
- ✅ 备份功能完善
|
||||
|
||||
## 🎓 开发建议
|
||||
|
||||
### 如何使用本模块
|
||||
```python
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
# 初始化
|
||||
fm = FinanceManager()
|
||||
|
||||
# 创建账户
|
||||
account = fm.create_account("我的账户")
|
||||
|
||||
# 查看账户
|
||||
accounts = fm.get_all_accounts()
|
||||
```
|
||||
|
||||
### 如何扩展功能
|
||||
1. 在 `FinanceManager` 中添加新方法
|
||||
2. 在 `FinanceInterface` 中添加新UI
|
||||
3. 对应添加文档说明
|
||||
4. 编写单元测试验证
|
||||
|
||||
## 📞 支持信息
|
||||
|
||||
### 获取帮助
|
||||
1. 阅读相应文档
|
||||
2. 运行调试工具
|
||||
3. 查看代码注释
|
||||
4. 参考API示例
|
||||
|
||||
### 问题反馈
|
||||
- 提交Issue描述问题
|
||||
- 提供错误日志信息
|
||||
- 说明重现步骤
|
||||
- 建议改进方向
|
||||
|
||||
## ✅ 最终状态
|
||||
|
||||
### 项目完成度: 100%
|
||||
```
|
||||
做账功能 ████████████████████ 100%
|
||||
查询功能 ████████████████████ 100%
|
||||
导出功能 ████████████████████ 100%
|
||||
本地存储 ████████████████████ 100%
|
||||
UI设计 ████████████████████ 100%
|
||||
文档编写 ████████████████████ 100%
|
||||
代码测试 ████████████████████ 100%
|
||||
```
|
||||
|
||||
### 代码质量: 优秀
|
||||
```
|
||||
功能完整性 ████████████████████ 100%
|
||||
代码规范性 ████████████████████ 95%
|
||||
注释文档 ████████████████████ 95%
|
||||
错误处理 ████████████████████ 95%
|
||||
用户体验 ████████████████████ 90%
|
||||
```
|
||||
|
||||
### 交付物: 完整
|
||||
```
|
||||
源代码 ✅ 完成
|
||||
数据存储 ✅ 完成
|
||||
UI界面 ✅ 完成
|
||||
用户文档 ✅ 完成
|
||||
开发文档 ✅ 完成
|
||||
调试工具 ✅ 完成
|
||||
测试工具 ✅ 完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
本财务做账模块是一个**功能完整、设计优秀、文档完善**的生产级应用。所有计划的功能都已实现并通过测试,代码质量良好,用户文档详尽。
|
||||
|
||||
该模块已准备好投入使用,可以满足日常的财务记账需求。
|
||||
|
||||
---
|
||||
|
||||
**项目状态: 🟢 完成就绪**
|
||||
|
||||
**发布日期: 2024-11-25**
|
||||
|
||||
**版本: v1.0.0 (稳定版)**
|
||||
|
||||
---
|
||||
|
||||
感谢使用!如有任何问题,欢迎反馈。🙏
|
||||
@ -1,247 +0,0 @@
|
||||
# 财务模块 - 快速开始指南
|
||||
|
||||
## 什么是财务做账模块?
|
||||
|
||||
财务做账模块是MRobot内置的一个完整的财务管理系统,用于:
|
||||
- 📊 记录和管理财务交易
|
||||
- 📸 保存交易相关的凭证图片
|
||||
- 🔍 快速查询和统计
|
||||
- 📤 导出转移和备份数据
|
||||
|
||||
## 5分钟快速上手
|
||||
|
||||
### 第一步:打开财务做账页面
|
||||
|
||||
1. 启动 MRobot 应用
|
||||
2. 在左侧导航栏找到"财务做账"
|
||||
3. 点击进入财务模块
|
||||
|
||||
### 第二步:创建你的第一个账户
|
||||
|
||||
1. 点击"新建账户"按钮
|
||||
2. 输入账户名称(例如:2024年项目经费)
|
||||
3. 可选:输入账户描述
|
||||
4. 点击确定
|
||||
|
||||
> **提示**: 你可以为不同的项目、部门或时间周期创建多个账户
|
||||
|
||||
### 第三步:添加第一条交易记录
|
||||
|
||||
1. 确保已选择正确的账户
|
||||
2. 点击"新建记录"按钮
|
||||
3. 填写以下信息:
|
||||
- **日期**: 交易发生的日期
|
||||
- **金额**: 交易金额(必填,且必须 > 0)
|
||||
- **交易人**: 与谁进行的交易(例如:张三、供应商A)
|
||||
- **备注**: 关于这笔交易的说明(可选)
|
||||
4. 上传图片(可选):
|
||||
- 发票图片:上传发票照片
|
||||
- 支付记录:上传支付截图或凭证
|
||||
- 购买记录:上传订单或收据
|
||||
5. 点击"保存"
|
||||
|
||||
> **提示**: 图片会自动整理在账户目录中,无需手动管理文件
|
||||
|
||||
### 第四步:查看和管理记录
|
||||
|
||||
在"做账"标签页的表格中,你可以:
|
||||
- ✏️ **编辑**: 修改已有记录
|
||||
- 🗑️ **删除**: 删除不需要的记录
|
||||
- 👁️ **查看**: 查看完整详情和预览图片
|
||||
|
||||
## 常用功能速查
|
||||
|
||||
### 查询记录
|
||||
|
||||
1. 切换到"查询"标签页
|
||||
2. 设置查询条件:
|
||||
- 日期范围:默认最近一个月
|
||||
- 金额范围:可选
|
||||
- 交易人:输入名称进行搜索
|
||||
3. 点击"查询"按钮
|
||||
4. 查看结果列表
|
||||
|
||||
**查询小技巧**:
|
||||
- 交易人搜索支持模糊匹配(例如搜"张"可以找到"张三")
|
||||
- 不区分大小写
|
||||
- 可以只设置其中的某些条件
|
||||
|
||||
### 导出数据
|
||||
|
||||
#### 转移给他人
|
||||
1. 切换到"导出"标签页
|
||||
2. 点击"导出为ZIP包"
|
||||
3. 选择保存位置
|
||||
4. 生成的ZIP文件可以发给他人
|
||||
5. 他人可以用"导入账户"功能导入
|
||||
|
||||
#### 用Excel分析
|
||||
1. 切换到"导出"标签页
|
||||
2. 点击"导出CSV"
|
||||
3. 用Excel或Numbers打开CSV文件
|
||||
4. 可进行数据透视表、图表分析等
|
||||
|
||||
#### 备份数据
|
||||
1. 切换到"导出"标签页
|
||||
2. 点击"创建备份"
|
||||
3. 自动保存到应用目录
|
||||
4. 文件名:`backup_[日期时间].zip`
|
||||
|
||||
> **建议**: 定期创建备份以防数据丢失
|
||||
|
||||
## 重要的统计信息
|
||||
|
||||
在"做账"标签页,你可以看到:
|
||||
- **总额**: 当前账户的所有交易总金额
|
||||
- **记录数**: 当前账户的总交易笔数
|
||||
|
||||
这些信息实时更新,可以快速了解账户情况。
|
||||
|
||||
## 常见问题解答
|
||||
|
||||
### Q: 如何修改已有的交易记录?
|
||||
**A**: 在做账标签页的表格中,点击该记录所在行的"编辑"按钮,修改后保存即可。
|
||||
|
||||
### Q: 上传的图片存在哪里?
|
||||
**A**: 所有图片都存储在应用的本地数据目录中,位置为:
|
||||
```
|
||||
assets/Finance_Data/accounts/[账户ID]/[交易ID]/
|
||||
```
|
||||
- 发票图片:`invoice/`
|
||||
- 支付记录:`payment/`
|
||||
- 购买记录:`purchase/`
|
||||
|
||||
### Q: 可以删除账户吗?
|
||||
**A**: 可以。点击"删除账户"按钮,**注意这会删除该账户的所有数据,包括所有交易记录和图片**。建议先导出备份。
|
||||
|
||||
### Q: 删除的记录能恢复吗?
|
||||
**A**: 删除后无法恢复。建议定期创建备份。如果有备份,可以重新导入备份恢复数据。
|
||||
|
||||
### Q: 支持多少条记录?
|
||||
**A**: 理论上无限制,但为了性能考虑,建议单个账户不超过10000条记录。如果数据量大,可以按时间周期分账户。
|
||||
|
||||
### Q: 如何合并两个账户?
|
||||
**A**: 暂不支持自动合并。可以:
|
||||
1. 导出第一个账户为CSV
|
||||
2. 导出第二个账户为CSV
|
||||
3. 用Excel合并两个CSV
|
||||
4. 手动重新输入或联系开发者
|
||||
|
||||
### Q: 我的数据安全吗?
|
||||
**A**: 所有数据都存储在本地,不上传到任何云服务。数据安全性取决于:
|
||||
- 你的电脑安全
|
||||
- 定期备份
|
||||
- 避免误删除
|
||||
|
||||
### Q: 如何转移财务数据到新电脑?
|
||||
**A**:
|
||||
1. 在旧电脑上导出所需账户为ZIP包
|
||||
2. 将ZIP文件转移到新电脑
|
||||
3. 在新电脑上的MRobot中使用"导入账户"
|
||||
|
||||
### Q: 可以离线使用吗?
|
||||
**A**: 完全可以。所有功能都在本地运行,无需网络连接。
|
||||
|
||||
## 最佳实践建议
|
||||
|
||||
### 1. 账户组织
|
||||
- 按部门分账户
|
||||
- 按年度分账户
|
||||
- 按项目分账户
|
||||
|
||||
**示例**:
|
||||
```
|
||||
- 2024年项目A
|
||||
- 2024年项目B
|
||||
- 市场部日常支出
|
||||
- 技术部日常支出
|
||||
```
|
||||
|
||||
### 2. 记录命名
|
||||
- 交易人写清楚,便于后期查询
|
||||
- 备注要具体,说明用途或项目
|
||||
|
||||
**好的例子**:
|
||||
```
|
||||
日期: 2024-01-15
|
||||
交易人: 阿里巴巴(云服务)
|
||||
备注: 购买ECS服务器3台,用于生产环境
|
||||
```
|
||||
|
||||
**不好的例子**:
|
||||
```
|
||||
日期: 2024-01-15
|
||||
交易人: A
|
||||
备注: 购买
|
||||
```
|
||||
|
||||
### 3. 图片附件
|
||||
- 每条记录的三张图片位置不同,要对应正确
|
||||
- 定期检查图片是否完整
|
||||
- 大额交易务必保留完整凭证
|
||||
|
||||
### 4. 数据备份
|
||||
- 至少每周备份一次
|
||||
- 重大变化后立即备份
|
||||
- 备份文件保存到云盘或移动存储
|
||||
|
||||
### 5. 查询分析
|
||||
- 利用"查询"功能快速定位记录
|
||||
- 定期导出CSV做深度分析
|
||||
- 使用Excel数据透视表生成报表
|
||||
|
||||
## 工作流程举例
|
||||
|
||||
### 日常记账流程
|
||||
```
|
||||
1. 交易发生
|
||||
↓
|
||||
2. 保存凭证(发票、截图等)
|
||||
↓
|
||||
3. 打开MRobot财务模块
|
||||
↓
|
||||
4. 新建记录
|
||||
↓
|
||||
5. 上传凭证图片
|
||||
↓
|
||||
6. 保存记录
|
||||
```
|
||||
|
||||
### 月度对账流程
|
||||
```
|
||||
1. 打开"查询"标签页
|
||||
↓
|
||||
2. 设置日期为本月
|
||||
↓
|
||||
3. 查询所有记录
|
||||
↓
|
||||
4. 查看总额是否与银行对账
|
||||
↓
|
||||
5. 导出CSV做详细分析
|
||||
↓
|
||||
6. 生成月度报告
|
||||
```
|
||||
|
||||
### 数据转移流程
|
||||
```
|
||||
1. 选择需要转移的账户
|
||||
↓
|
||||
2. 点击"导出为ZIP包"
|
||||
↓
|
||||
3. 将ZIP文件发送给他人
|
||||
↓
|
||||
4. 他人接收后点击"导入账户"
|
||||
↓
|
||||
5. 数据完全转移到他人账户
|
||||
```
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 📖 详细文档:查看 `FINANCE_MODULE_GUIDE.md`
|
||||
- 💻 API示例:查看 `FINANCE_API_EXAMPLES.py`
|
||||
- 🐛 报告问题:提交Issue到GitHub
|
||||
- 💬 提交建议:欢迎反馈和建议
|
||||
|
||||
---
|
||||
|
||||
**祝你使用愉快!** 🎉
|
||||
@ -1,259 +0,0 @@
|
||||
# 财务做账模块 - 项目总结
|
||||
|
||||
## 项目概述
|
||||
|
||||
为MRobot应用添加了一个完整的**财务做账管理系统**,支持做账、查询、导出三大功能。该模块采用本地存储方式,支持多账户管理、图片附件、数据转移等功能。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 做账功能 ✏️
|
||||
- **创建账户**: 支持多个独立账户
|
||||
- **新建记录**: 记录日期、金额、交易人、备注
|
||||
- **图片附件**: 每条记录支持三种类型的图片(发票、支付、购买)
|
||||
- **记录管理**: 编辑、删除、查看记录
|
||||
- **实时统计**: 显示账户总额和记录数
|
||||
|
||||
### 2. 查询功能 🔍
|
||||
- **多条件过滤**: 按日期、金额、交易人查询
|
||||
- **模糊搜索**: 交易人支持模糊匹配
|
||||
- **灵活组合**: 支持多种查询条件组合
|
||||
- **图片预览**: 查看记录详情和图片预览
|
||||
|
||||
### 3. 导出功能 📤
|
||||
- **ZIP转移**: 导出账户为压缩包,方便转移给他人
|
||||
- **CSV导出**: 导出为Excel格式,便于数据分析
|
||||
- **账户导入**: 导入他人共享的账户
|
||||
- **完整备份**: 一键备份所有账户和数据
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
MRobot/
|
||||
├── app/
|
||||
│ ├── finance_interface.py # UI界面层
|
||||
│ │ ├── CreateTransactionDialog # 创建/编辑对话框
|
||||
│ │ ├── RecordViewDialog # 查看详情对话框
|
||||
│ │ └── FinanceInterface # 主界面(3个标签页)
|
||||
│ │
|
||||
│ └── tools/
|
||||
│ └── finance_manager.py # 数据管理层
|
||||
│ ├── TransactionType # 交易类型枚举
|
||||
│ ├── Transaction # 交易记录模型
|
||||
│ ├── Account # 账户模型
|
||||
│ └── FinanceManager # 核心管理类
|
||||
│
|
||||
├── assets/Finance_Data/ # 数据存储目录
|
||||
│ ├── accounts/
|
||||
│ │ └── [account_id]/
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── [transaction_id]/
|
||||
│ │ ├── data.json
|
||||
│ │ ├── invoice/
|
||||
│ │ ├── payment/
|
||||
│ │ └── purchase/
|
||||
│ └── backups/
|
||||
│
|
||||
├── FINANCE_QUICK_START.md # 快速开始指南
|
||||
├── FINANCE_MODULE_GUIDE.md # 详细使用指南
|
||||
└── FINANCE_API_EXAMPLES.py # API编程示例
|
||||
```
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### 交易记录 (Transaction)
|
||||
```python
|
||||
{
|
||||
'id': str, # 唯一标识符
|
||||
'date': str, # 日期 (YYYY-MM-DD)
|
||||
'amount': float, # 金额
|
||||
'trader': str, # 交易人
|
||||
'notes': str, # 备注
|
||||
'invoice_path': str, # 发票图片相对路径
|
||||
'payment_path': str, # 支付记录相对路径
|
||||
'purchase_path': str, # 购买记录相对路径
|
||||
'created_at': str, # 创建时间
|
||||
'updated_at': str # 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
#### 账户 (Account)
|
||||
```python
|
||||
{
|
||||
'id': str, # 唯一标识符
|
||||
'name': str, # 账户名称
|
||||
'description': str, # 账户描述
|
||||
'transactions': List[dict], # 包含的所有交易记录
|
||||
'created_at': str, # 创建时间
|
||||
'updated_at': str # 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
### 关键类说明
|
||||
|
||||
#### FinanceManager
|
||||
核心管理类,提供以下接口:
|
||||
|
||||
**账户操作**
|
||||
- `create_account(name, description)`: 创建账户
|
||||
- `get_account(account_id)`: 获取账户
|
||||
- `get_all_accounts()`: 获取所有账户
|
||||
- `delete_account(account_id)`: 删除账户
|
||||
- `update_account(account_id, name, description)`: 更新账户信息
|
||||
|
||||
**交易操作**
|
||||
- `add_transaction(account_id, transaction)`: 添加交易
|
||||
- `get_transaction(account_id, trans_id)`: 获取交易
|
||||
- `delete_transaction(account_id, trans_id)`: 删除交易
|
||||
- `update_transaction(account_id, trans_id, **kwargs)`: 更新交易
|
||||
|
||||
**查询功能**
|
||||
- `query_transactions(account_id, date_start, date_end, amount_min, amount_max, trader)`: 多条件查询
|
||||
- `get_account_summary(account_id)`: 获取账户统计
|
||||
|
||||
**图片管理**
|
||||
- `save_image_for_transaction(account_id, trans_id, image_type, image_path)`: 保存图片
|
||||
- `get_transaction_image_path(account_id, relative_path)`: 获取图片完整路径
|
||||
|
||||
**导入导出**
|
||||
- `export_account_package(account_id, export_path)`: 导出为ZIP
|
||||
- `import_account_package(zip_path)`: 导入ZIP
|
||||
- `export_to_csv(account_id, csv_path)`: 导出为CSV
|
||||
- `backup_all_accounts()`: 备份所有账户
|
||||
|
||||
## 集成方式
|
||||
|
||||
### 主窗口修改
|
||||
已修改 `app/main_window.py`:
|
||||
```python
|
||||
from .finance_interface import FinanceInterface
|
||||
|
||||
# 在 initInterface 中添加
|
||||
self.financeInterface = FinanceInterface(self)
|
||||
|
||||
# 在 initNavigation 中添加导航项
|
||||
self.addSubInterface(self.financeInterface, FIF.DOCUMENT, self.tr('财务做账'))
|
||||
```
|
||||
|
||||
### 依赖包
|
||||
- PyQt5: UI框架
|
||||
- qfluentwidgets: 流畅UI组件库
|
||||
- pathlib: 路径管理
|
||||
- json: 数据序列化
|
||||
- zipfile: 压缩包处理
|
||||
- csv: CSV导出
|
||||
- uuid: 唯一ID生成
|
||||
- datetime: 时间处理
|
||||
|
||||
## 性能特点
|
||||
|
||||
### 优化措施
|
||||
- **内存缓存**: 账户和交易记录在内存中缓存,避免频繁磁盘访问
|
||||
- **延迟加载**: 图片只在需要时加载
|
||||
- **索引优化**: 交易记录按日期倒序排列,加速查询
|
||||
- **增量更新**: 修改记录时只更新变化部分
|
||||
|
||||
### 性能指标
|
||||
- **单账户容量**: 推荐 ≤ 10,000 条记录
|
||||
- **加载速度**: 数千条记录 < 1s
|
||||
- **查询速度**: 完整查询 < 100ms
|
||||
- **导出速度**: 1000条记录 < 2s
|
||||
|
||||
## 数据安全性
|
||||
|
||||
### 存储方式
|
||||
- **完全本地存储**: 所有数据存储在用户电脑上,不上传云端
|
||||
- **原子操作**: 所有写入操作确保数据一致性
|
||||
- **自动备份**: 支持一键备份功能
|
||||
|
||||
### 恢复机制
|
||||
- **ZIP备份**: 完整的账户备份可以恢复
|
||||
- **版本控制**: 每次备份都生成新文件,保留历史
|
||||
- **导出转移**: 可以随时导出数据转移到其他设备
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 快速开始
|
||||
1. 打开MRobot应用
|
||||
2. 点击左侧导航"财务做账"
|
||||
3. 点击"新建账户"创建第一个账户
|
||||
4. 点击"新建记录"添加交易记录
|
||||
5. 使用"查询"功能快速定位记录
|
||||
6. 使用"导出"功能备份或转移数据
|
||||
|
||||
### 详细文档
|
||||
- **快速开始**: 查看 `FINANCE_QUICK_START.md`
|
||||
- **详细指南**: 查看 `FINANCE_MODULE_GUIDE.md`
|
||||
- **API示例**: 查看 `FINANCE_API_EXAMPLES.py`
|
||||
|
||||
## 扩展建议
|
||||
|
||||
### 可能的改进方向
|
||||
1. **统计报表**: 自动生成柱状图、饼图等报表
|
||||
2. **分类管理**: 为交易添加分类标签
|
||||
3. **预算管理**: 设置预算并提醒超支
|
||||
4. **多用户**: 支持多用户同步和协作
|
||||
5. **云同步**: 可选的云备份和同步功能
|
||||
6. **OCR识别**: 自动识别发票中的金额信息
|
||||
7. **智能分类**: 基于历史数据自动分类新交易
|
||||
8. **导出模板**: 自定义导出报表模板
|
||||
|
||||
### API扩展点
|
||||
```python
|
||||
# 可以添加的新功能示例
|
||||
- export_to_pdf() # 导出PDF报表
|
||||
- generate_statistics() # 生成统计数据
|
||||
- add_category() # 添加分类系统
|
||||
- set_budget() # 预算管理
|
||||
- get_trends() # 趋势分析
|
||||
```
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
### 已测试功能
|
||||
- ✅ 账户创建、获取、更新、删除
|
||||
- ✅ 交易记录增删改查
|
||||
- ✅ 图片保存和加载
|
||||
- ✅ 多条件查询
|
||||
- ✅ ZIP导入导出
|
||||
- ✅ CSV导出
|
||||
- ✅ 完整备份
|
||||
- ✅ 统计汇总
|
||||
|
||||
### 建议的测试场景
|
||||
1. **大数据量测试**: 10000+ 条记录的性能表现
|
||||
2. **并发测试**: 多个操作同时进行
|
||||
3. **异常恢复**: 中断操作后的数据一致性
|
||||
4. **图片处理**: 各种格式和大小的图片
|
||||
|
||||
## 贡献和反馈
|
||||
|
||||
### 如何贡献
|
||||
1. 提交Issue报告问题
|
||||
2. 提交PR改进代码
|
||||
3. 分享使用建议和反馈
|
||||
|
||||
### 联系方式
|
||||
- GitHub: [MRobot项目]
|
||||
- 邮件: [联系邮箱]
|
||||
- 讨论区: [社区讨论]
|
||||
|
||||
## 许可证
|
||||
|
||||
本模块遵循MRobot项目的许可证。
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
### v1.0.0 (2024-11)
|
||||
- ✨ 初始发布
|
||||
- 📊 完整的财务管理功能
|
||||
- 🎨 现代化UI界面
|
||||
- 📱 支持多账户管理
|
||||
- 🔒 本地数据存储
|
||||
- 📤 灵活的导入导出
|
||||
|
||||
---
|
||||
|
||||
**感谢使用MRobot财务做账模块!** 🎉
|
||||
@ -1,116 +0,0 @@
|
||||
# 财务界面 UI 改进总结
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 1. 统一使用 qfluentwidgets 组件
|
||||
|
||||
#### 标签控件替换
|
||||
|
||||
- **旧**: QTabWidget + QLabel
|
||||
- **新**: SegmentedWidget + BodyLabel/StrongBodyLabel
|
||||
|
||||
#### 消息提示替换
|
||||
|
||||
- **旧**: QMessageBox
|
||||
- **新**: InfoBar/Dialog (qfluentwidgets)
|
||||
|
||||
#### 其他组件替换
|
||||
|
||||
- QLabel → BodyLabel / StrongBodyLabel
|
||||
- QTableWidget 保留,但添加 qfluentwidgets 样式
|
||||
|
||||
### 2. 功能选择改进
|
||||
|
||||
使用 SegmentedWidget 替代 TabBar:
|
||||
|
||||
```python
|
||||
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)
|
||||
```
|
||||
|
||||
### 3. 表格优化
|
||||
|
||||
#### 做账标签页表格
|
||||
|
||||
- 列数: 5 (日期, 交易人, 金额, 备注, 操作)
|
||||
- 样式: 交替行颜色、自动调整列宽
|
||||
- 操作按钮: 查看、编辑、删除
|
||||
|
||||
#### 查询标签页
|
||||
|
||||
- 搜索过滤使用 CardWidget 包装
|
||||
- 表格显示查询结果
|
||||
- 操作按钮: 查看详情
|
||||
|
||||
#### 统计信息
|
||||
|
||||
- 使用 CardWidget 显示总额和记录数
|
||||
- 使用 StrongBodyLabel 强调显示
|
||||
|
||||
### 4. 对话框改进
|
||||
|
||||
#### CreateTransactionDialog
|
||||
|
||||
- 使用 BodyLabel 替代 QLabel
|
||||
- 保持现有布局和功能
|
||||
|
||||
#### RecordViewDialog
|
||||
|
||||
- 图片预览使用 BodyLabel
|
||||
- 支持图片显示
|
||||
|
||||
### 5. 消息提示改进
|
||||
|
||||
所有确认/警告/错误消息使用 InfoBar 或 Dialog:
|
||||
|
||||
```python
|
||||
InfoBar.success(
|
||||
title="成功",
|
||||
content="记录已添加",
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000,
|
||||
parent=self
|
||||
)
|
||||
```
|
||||
|
||||
删除确认使用 Dialog:
|
||||
|
||||
```python
|
||||
dialog = Dialog(
|
||||
title="确认删除",
|
||||
content="确定要删除这条记录吗?",
|
||||
parent=self
|
||||
)
|
||||
```
|
||||
|
||||
## 测试结果
|
||||
|
||||
✅ 所有 UI 组件验证通过
|
||||
✅ SegmentedWidget 正常工作
|
||||
✅ TableWidget 正常工作
|
||||
✅ 标签页切换功能正常
|
||||
|
||||
## 文件修改
|
||||
|
||||
- `/Users/lvzucheng/Documents/R/MRobot/app/finance_interface.py` - 主要修改
|
||||
- 导入调整
|
||||
- UI 组件替换
|
||||
- 样式优化
|
||||
- 消息提示更新
|
||||
|
||||
## 兼容性
|
||||
|
||||
- PyQt5 ✓
|
||||
- qfluentwidgets >= 0.10.0 ✓
|
||||
- Python >= 3.7 ✓
|
||||
|
||||
## 后续建议
|
||||
|
||||
1. 可考虑添加更多的快捷操作按钮
|
||||
2. 可以为表格添加上下文菜单
|
||||
3. 可以优化查询过滤的交互体验
|
||||
4. 可以添加数据导出的进度条显示
|
||||
@ -1,219 +0,0 @@
|
||||
# MRobot 财务做账模块 - 使用说明
|
||||
|
||||
## 🎉 完成情况
|
||||
|
||||
### 已成功实现的功能
|
||||
|
||||
✅ **做账功能**
|
||||
- 创建多个独立账户
|
||||
- 为每条交易记录添加日期、金额、交易人、备注
|
||||
- 上传3种类型的图片(发票、支付记录、购买记录)
|
||||
- 实时显示账户统计(总额、记录数)
|
||||
- 编辑、删除交易记录
|
||||
|
||||
✅ **查询功能**
|
||||
- 按日期范围查询
|
||||
- 按金额范围查询
|
||||
- 按交易人模糊搜索
|
||||
- 多条件组合查询
|
||||
- 图片预览功能
|
||||
|
||||
✅ **导出功能**
|
||||
- 导出账户为ZIP包(转移给他人)
|
||||
- 导出为CSV格式(用Excel分析)
|
||||
- 导入他人的ZIP包
|
||||
- 创建完整备份
|
||||
|
||||
✅ **本地存储**
|
||||
- 所有数据存储在 `assets/Finance_Data/` 目录
|
||||
- 清晰的文件夹结构,便于理解和维护
|
||||
- 支持大数据量(10000+ 记录)
|
||||
|
||||
## 🚀 使用步骤
|
||||
|
||||
### 1. 启动应用
|
||||
```bash
|
||||
python MRobot.py
|
||||
```
|
||||
|
||||
### 2. 打开财务做账模块
|
||||
- 点击左侧导航栏中的 **"财务做账"**
|
||||
|
||||
### 3. 创建账户(第一次使用)
|
||||
- 点击 **"新建账户"** 按钮
|
||||
- 输入账户名称(例如:2024年项目经费)
|
||||
- 输入账户描述(可选)
|
||||
- 点击确定
|
||||
|
||||
### 4. 新建交易记录
|
||||
- 确保已选择正确的账户
|
||||
- 点击 **"新建记录"** 按钮
|
||||
- 填写交易信息:
|
||||
- **日期**:交易发生的日期
|
||||
- **金额**:金额(必填,必须 > 0)
|
||||
- **交易人**:交易对象名称
|
||||
- **备注**:附加说明(可选)
|
||||
- 上传图片(可选):
|
||||
- 发票图片
|
||||
- 支付记录
|
||||
- 购买记录
|
||||
- 点击 **"保存"**
|
||||
|
||||
### 5. 查看和管理记录
|
||||
在表格中可以:
|
||||
- 点击 **"编辑"** 修改记录
|
||||
- 点击 **"删除"** 删除记录
|
||||
- 点击 **"查看"** 查看详情和图片
|
||||
|
||||
### 6. 查询记录
|
||||
- 切换到 **"查询"** 标签页
|
||||
- 设置查询条件:
|
||||
- 日期范围
|
||||
- 金额范围
|
||||
- 交易人名称
|
||||
- 点击 **"查询"** 查看结果
|
||||
|
||||
### 7. 导出和备份
|
||||
- 切换到 **"导出"** 标签页
|
||||
- **导出为ZIP包**:转移给他人使用
|
||||
- **导出CSV**:用Excel分析
|
||||
- **导入账户**:导入他人的数据
|
||||
- **创建备份**:自动备份所有数据
|
||||
|
||||
## 📁 数据存储位置
|
||||
|
||||
所有财务数据存储在项目的 `assets/Finance_Data/` 目录下:
|
||||
|
||||
```
|
||||
assets/Finance_Data/
|
||||
├── accounts/ # 账户数据
|
||||
│ ├── [账户ID1]/
|
||||
│ │ ├── metadata.json # 账户信息
|
||||
│ │ └── [交易ID]/ # 每条交易的文件夹
|
||||
│ │ ├── data.json
|
||||
│ │ ├── invoice/ # 发票图片
|
||||
│ │ ├── payment/ # 支付记录
|
||||
│ │ └── purchase/ # 购买记录
|
||||
│ └── [账户ID2]/
|
||||
│ └── ...
|
||||
├── backups/ # 备份文件
|
||||
│ └── backup_*.zip
|
||||
└── images/ # 临时文件(可选)
|
||||
```
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
### 账户管理
|
||||
- 为不同的项目创建不同的账户
|
||||
- 按年度/月度分账户便于统计
|
||||
- 可以随时删除不需要的账户(会删除所有记录)
|
||||
|
||||
### 记录输入
|
||||
- 交易人输入要清晰,便于后期查询
|
||||
- 备注字段可以写交易的具体用途
|
||||
- 务必上传清晰的凭证照片
|
||||
|
||||
### 查询技巧
|
||||
- 交易人搜索支持模糊匹配,无需输入完整名字
|
||||
- 可以只设置部分查询条件
|
||||
- 结果会按日期倒序显示
|
||||
|
||||
### 数据备份
|
||||
- 定期点击"创建备份"保存数据
|
||||
- 备份文件自动保存到 `assets/Finance_Data/backups/`
|
||||
- 备份文件名包含时间戳,便于管理
|
||||
|
||||
### 转移数据
|
||||
1. 选择要转移的账户
|
||||
2. 点击"导出为ZIP包"
|
||||
3. 将ZIP文件发送给他人
|
||||
4. 他人打开财务模块,点击"导入账户"
|
||||
|
||||
## 🔧 故障排查
|
||||
|
||||
### 问题1:无法新建记录
|
||||
**解决方案**:
|
||||
- 检查是否已经创建了账户
|
||||
- 确保账户已选中(下拉框显示账户名)
|
||||
- 尝试重新启动应用
|
||||
|
||||
### 问题2:图片无法上传
|
||||
**解决方案**:
|
||||
- 检查图片格式(支持PNG、JPG、BMP、JPEG)
|
||||
- 检查文件大小(建议不超过2MB)
|
||||
- 确保文件有读取权限
|
||||
|
||||
### 问题3:查询没有结果
|
||||
**解决方案**:
|
||||
- 检查日期范围是否正确
|
||||
- 尝试扩大查询范围
|
||||
- 检查交易人名称拼写
|
||||
|
||||
### 问题4:数据显示不正确
|
||||
**解决方案**:
|
||||
- 点击不同标签页再切换回来
|
||||
- 尝试刷新账户列表
|
||||
- 重新启动应用
|
||||
|
||||
## 📊 数据统计
|
||||
|
||||
每个账户显示两个统计数据:
|
||||
- **总额**:所有交易的总金额(红色显示)
|
||||
- **记录数**:交易记录的总笔数
|
||||
|
||||
## 🔐 数据安全
|
||||
|
||||
- 所有数据存储在本地,不上传到云端
|
||||
- 建议定期创建备份
|
||||
- 删除操作无法撤销,务必谨慎
|
||||
- 可以通过备份恢复已删除的数据
|
||||
|
||||
## 🎯 常见工作流
|
||||
|
||||
### 日常记账
|
||||
```
|
||||
1. 打开财务模块
|
||||
2. 新建记录
|
||||
3. 上传凭证
|
||||
4. 保存
|
||||
```
|
||||
|
||||
### 月度对账
|
||||
```
|
||||
1. 切换到查询标签页
|
||||
2. 设置日期为本月
|
||||
3. 查看所有记录
|
||||
4. 查看总额是否与银行对账
|
||||
5. 导出CSV做详细分析
|
||||
```
|
||||
|
||||
### 跨电脑转移
|
||||
```
|
||||
1. 点击"导出为ZIP包"
|
||||
2. 将ZIP发送到新电脑
|
||||
3. 在新电脑上点击"导入账户"
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 获取帮助
|
||||
- 查看详细文档:`FINANCE_MODULE_GUIDE.md`
|
||||
- 查看快速开始:`FINANCE_QUICK_START.md`
|
||||
- 查看API示例:`FINANCE_API_EXAMPLES.py`
|
||||
- 查看完成报告:`FINANCE_COMPLETION_REPORT.md`
|
||||
|
||||
### 反馈建议
|
||||
- 如发现问题,请提交Issue
|
||||
- 欢迎提供使用建议
|
||||
- 持续改进应用功能
|
||||
|
||||
## ✨ 版本信息
|
||||
|
||||
- **版本**: 1.0.0
|
||||
- **发布日期**: 2024-11-25
|
||||
- **状态**: 稳定版
|
||||
- **支持**: 完整功能测试通过
|
||||
|
||||
---
|
||||
|
||||
**祝你使用愉快!如有任何问题,请随时反馈。** 🎊
|
||||
Binary file not shown.
@ -2,9 +2,12 @@
|
||||
批量导出选项对话框
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QButtonGroup, QRadioButton
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QButtonGroup, QRadioButton, QFrame
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import BodyLabel, PushButton, PrimaryPushButton, SubtitleLabel
|
||||
from PyQt5.QtGui import QFont, QPalette
|
||||
from qfluentwidgets import (BodyLabel, PushButton, PrimaryPushButton, SubtitleLabel,
|
||||
TitleLabel, HorizontalSeparator, CardWidget, FluentIcon, StrongBodyLabel,
|
||||
theme, Theme)
|
||||
|
||||
|
||||
class BatchExportDialog(QDialog):
|
||||
@ -16,60 +19,186 @@ class BatchExportDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("导出选项")
|
||||
self.setGeometry(200, 200, 400, 250)
|
||||
self.setGeometry(200, 200, 680, 550)
|
||||
self.setMinimumWidth(640)
|
||||
self.setMinimumHeight(480)
|
||||
|
||||
# 设置背景色跟随主题
|
||||
if theme() == Theme.DARK:
|
||||
self.setStyleSheet("background-color: #232323;")
|
||||
else:
|
||||
self.setStyleSheet("background-color: #f7f9fc;")
|
||||
|
||||
self.export_type = self.EXPORT_NORMAL
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(20)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title_label = SubtitleLabel("选择导出方式")
|
||||
layout.addWidget(title_label)
|
||||
# 标题区域
|
||||
title_layout = QVBoxLayout()
|
||||
title_layout.setSpacing(8)
|
||||
|
||||
title_label = TitleLabel("选择导出方式")
|
||||
title_layout.addWidget(title_label)
|
||||
|
||||
desc_label = BodyLabel("选择最适合您的导出格式")
|
||||
title_layout.addWidget(desc_label)
|
||||
|
||||
layout.addLayout(title_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 选项组
|
||||
self.button_group = QButtonGroup()
|
||||
|
||||
# 普通导出选项
|
||||
normal_radio = QRadioButton("普通导出")
|
||||
# 普通导出选项卡
|
||||
normal_card = self._create_option_card(
|
||||
title="普通导出",
|
||||
description="将每个交易的图片导出到单独的文件夹",
|
||||
details="文件夹名称:日期_金额\n每个交易的图片保存在独立文件夹中,便于查看和管理",
|
||||
is_selected=True
|
||||
)
|
||||
normal_radio = normal_card.findChild(QRadioButton)
|
||||
normal_radio.setChecked(True)
|
||||
normal_radio.setToolTip("将每个交易的图片导出到单独的文件夹(文件夹名:日期_金额)")
|
||||
self.button_group.addButton(normal_radio, self.EXPORT_NORMAL)
|
||||
layout.addWidget(normal_radio)
|
||||
layout.addWidget(normal_card)
|
||||
|
||||
normal_desc = BodyLabel("每个交易的图片保存在独立文件夹中,便于查看和管理")
|
||||
layout.addWidget(normal_desc)
|
||||
|
||||
layout.addSpacing(15)
|
||||
|
||||
# MRobot 格式导出选项
|
||||
mrobot_radio = QRadioButton("MRobot 专用格式")
|
||||
mrobot_radio.setToolTip("导出为 .mrobot 文件(专用格式,用于数据转交)")
|
||||
# MRobot 格式导出选项卡
|
||||
mrobot_card = self._create_option_card(
|
||||
title="MRobot 专用格式",
|
||||
description="导出为 .mrobot 文件(ZIP 格式)",
|
||||
details="包含完整的交易数据和图片\n用于转交给他人或备份",
|
||||
is_selected=False
|
||||
)
|
||||
mrobot_radio = mrobot_card.findChild(QRadioButton)
|
||||
self.button_group.addButton(mrobot_radio, self.EXPORT_MROBOT)
|
||||
layout.addWidget(mrobot_radio)
|
||||
|
||||
mrobot_desc = BodyLabel("导出为 .mrobot 文件(ZIP 格式),包含完整的交易数据和图片,用于转交给他人")
|
||||
layout.addWidget(mrobot_desc)
|
||||
layout.addWidget(mrobot_card)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(12)
|
||||
btn_layout.addStretch()
|
||||
|
||||
cancel_btn = PushButton("取消")
|
||||
cancel_btn.setMinimumWidth(110)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
ok_btn = PrimaryPushButton("确定")
|
||||
ok_btn = PrimaryPushButton("确定导出")
|
||||
ok_btn.setMinimumWidth(110)
|
||||
ok_btn.clicked.connect(self.on_ok)
|
||||
btn_layout.addWidget(ok_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def _create_option_card(self, title, description, details, is_selected=False):
|
||||
"""创建导出选项卡片"""
|
||||
card = CardWidget()
|
||||
card_layout = QHBoxLayout()
|
||||
card_layout.setContentsMargins(16, 16, 16, 16)
|
||||
card_layout.setSpacing(16)
|
||||
|
||||
# 单选按钮
|
||||
radio = QRadioButton()
|
||||
radio.setMinimumWidth(40)
|
||||
card_layout.addWidget(radio)
|
||||
|
||||
# 内容区域
|
||||
content_layout = QVBoxLayout()
|
||||
content_layout.setSpacing(8)
|
||||
|
||||
# 标题行
|
||||
title_layout = QHBoxLayout()
|
||||
title_layout.setSpacing(10)
|
||||
|
||||
# 图标
|
||||
icon_label = BodyLabel()
|
||||
icon_label.setText("📁" if title == "普通导出" else "📦")
|
||||
icon_label.setStyleSheet("font-size: 20px;")
|
||||
title_layout.addWidget(icon_label)
|
||||
|
||||
# 标题
|
||||
title_label = StrongBodyLabel(title)
|
||||
title_layout.addWidget(title_label)
|
||||
title_layout.addStretch()
|
||||
content_layout.addLayout(title_layout)
|
||||
|
||||
# 描述
|
||||
desc_label = BodyLabel(description)
|
||||
desc_label.setWordWrap(True)
|
||||
# 使用 QPalette 来自适应主题
|
||||
from PyQt5.QtGui import QPalette
|
||||
content_layout.addWidget(desc_label)
|
||||
|
||||
# 详细信息
|
||||
details_label = BodyLabel(details)
|
||||
details_label.setWordWrap(True)
|
||||
# 使用相对颜色而不是硬编码
|
||||
content_layout.addWidget(details_label)
|
||||
|
||||
content_layout.addStretch()
|
||||
card_layout.addLayout(content_layout, 1)
|
||||
|
||||
# 设置卡片样式 - 不使用硬编码颜色,让 CardWidget 自适应主题
|
||||
# 只通过边框来显示选中状态
|
||||
self._update_card_style(card, is_selected)
|
||||
|
||||
card.setLayout(card_layout)
|
||||
card.setMinimumHeight(120)
|
||||
|
||||
# 点击卡片时选中单选按钮
|
||||
def on_card_clicked():
|
||||
radio.setChecked(True)
|
||||
# 更新卡片样式
|
||||
self._update_card_styles(radio)
|
||||
|
||||
radio.clicked.connect(on_card_clicked)
|
||||
card.mousePressEvent = lambda e: on_card_clicked()
|
||||
|
||||
return card
|
||||
|
||||
def _update_card_style(self, card, is_selected):
|
||||
"""更新单个卡片的样式"""
|
||||
if is_selected:
|
||||
card.setProperty("is_selected", True)
|
||||
card.setStyleSheet("""
|
||||
CardWidget[is_selected=true] {
|
||||
border: 2px solid palette(highlight);
|
||||
}
|
||||
CardWidget[is_selected=false] {
|
||||
border: 1px solid palette(mid);
|
||||
}
|
||||
CardWidget[is_selected=false]:hover {
|
||||
border: 2px solid palette(highlight);
|
||||
}
|
||||
""")
|
||||
else:
|
||||
card.setProperty("is_selected", False)
|
||||
card.setStyleSheet("""
|
||||
CardWidget[is_selected=false] {
|
||||
border: 1px solid palette(mid);
|
||||
}
|
||||
CardWidget[is_selected=false]:hover {
|
||||
border: 2px solid palette(highlight);
|
||||
}
|
||||
""")
|
||||
|
||||
def _update_card_styles(self, selected_radio):
|
||||
"""更新所有卡片的样式"""
|
||||
for button in self.button_group.buttons():
|
||||
card = button.parent()
|
||||
while card and not isinstance(card, CardWidget):
|
||||
card = card.parent()
|
||||
|
||||
if card:
|
||||
is_checked = button.isChecked()
|
||||
self._update_card_style(card, is_checked)
|
||||
|
||||
def on_ok(self):
|
||||
"""确定按钮点击"""
|
||||
checked_button = self.button_group.checkedButton()
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem)
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QMessageBox, QScrollArea, QWidget)
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QColor, QFont
|
||||
from qfluentwidgets import (BodyLabel, PushButton, PrimaryPushButton, LineEdit,
|
||||
InfoBar, InfoBarPosition)
|
||||
InfoBar, InfoBarPosition, SubtitleLabel, TitleLabel,
|
||||
HorizontalSeparator, CardWidget, FluentIcon, StrongBodyLabel, theme, Theme)
|
||||
from .tools.finance_manager import FinanceManager
|
||||
|
||||
|
||||
@ -19,49 +21,119 @@ class CategoryManagementDialog(QDialog):
|
||||
self.finance_manager = finance_manager
|
||||
self.account_id = account_id
|
||||
self.setWindowTitle("分类管理")
|
||||
self.setGeometry(100, 100, 500, 400)
|
||||
self.setGeometry(100, 100, 650, 550)
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(480)
|
||||
|
||||
# 设置背景色跟随主题
|
||||
if theme() == Theme.DARK:
|
||||
self.setStyleSheet("background-color: #232323;")
|
||||
else:
|
||||
self.setStyleSheet("background-color: #f7f9fc;")
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.setContentsMargins(24, 24, 24, 24)
|
||||
main_layout.setSpacing(16)
|
||||
|
||||
# 标签
|
||||
title_label = BodyLabel("选择分类进行管理:")
|
||||
main_layout.addWidget(title_label)
|
||||
# 标题区域
|
||||
title_layout = QVBoxLayout()
|
||||
title_layout.setSpacing(6)
|
||||
|
||||
title_label = TitleLabel("分类管理")
|
||||
title_layout.addWidget(title_label)
|
||||
|
||||
desc_label = BodyLabel("新增、编辑或删除您的交易分类")
|
||||
title_layout.addWidget(desc_label)
|
||||
|
||||
main_layout.addLayout(title_layout)
|
||||
main_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 内容区域(分类列表)
|
||||
content_layout = QHBoxLayout()
|
||||
content_layout.setSpacing(16)
|
||||
|
||||
# 左侧:分类列表卡片
|
||||
list_card = CardWidget()
|
||||
list_card_layout = QVBoxLayout()
|
||||
list_card_layout.setContentsMargins(0, 0, 0, 0)
|
||||
list_card_layout.setSpacing(0)
|
||||
|
||||
list_label = StrongBodyLabel("现有分类")
|
||||
list_label.setStyleSheet("padding: 12px 16px; border-bottom: 1px solid var(--border-color);")
|
||||
list_card_layout.addWidget(list_label)
|
||||
|
||||
# 分类列表
|
||||
self.category_list = QListWidget()
|
||||
self.category_list.itemSelectionChanged.connect(self.on_category_selected)
|
||||
main_layout.addWidget(self.category_list)
|
||||
self.category_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 2px 4px;
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: var(--highlight-color);
|
||||
color: var(--highlight-text-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
list_card_layout.addWidget(self.category_list, 1)
|
||||
list_card.setLayout(list_card_layout)
|
||||
list_card.setMinimumHeight(280)
|
||||
content_layout.addWidget(list_card, 1)
|
||||
|
||||
main_layout.addLayout(content_layout, 1)
|
||||
|
||||
# 加载分类
|
||||
self.load_categories()
|
||||
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
btn_layout.setSpacing(12)
|
||||
|
||||
# 新增按钮
|
||||
add_btn = PrimaryPushButton("新增")
|
||||
add_btn = PrimaryPushButton()
|
||||
add_btn.setIcon(FluentIcon.ADD)
|
||||
add_btn.setText("新增分类")
|
||||
add_btn.clicked.connect(self.on_add_category)
|
||||
add_btn.setMinimumWidth(120)
|
||||
btn_layout.addWidget(add_btn)
|
||||
|
||||
# 重命名按钮
|
||||
self.rename_btn = PushButton("重命名")
|
||||
self.rename_btn = PushButton()
|
||||
self.rename_btn.setIcon(FluentIcon.EDIT)
|
||||
self.rename_btn.setText("重命名")
|
||||
self.rename_btn.clicked.connect(self.on_rename_category)
|
||||
self.rename_btn.setEnabled(False)
|
||||
self.rename_btn.setMinimumWidth(110)
|
||||
btn_layout.addWidget(self.rename_btn)
|
||||
|
||||
# 删除按钮
|
||||
self.delete_btn = PushButton("删除")
|
||||
self.delete_btn = PushButton()
|
||||
self.delete_btn.setIcon(FluentIcon.DELETE)
|
||||
self.delete_btn.setText("删除")
|
||||
self.delete_btn.clicked.connect(self.on_delete_category)
|
||||
self.delete_btn.setEnabled(False)
|
||||
self.delete_btn.setMinimumWidth(110)
|
||||
btn_layout.addWidget(self.delete_btn)
|
||||
|
||||
btn_layout.addStretch()
|
||||
|
||||
# 关闭按钮
|
||||
close_btn = PushButton("关闭")
|
||||
close_btn.clicked.connect(self.accept)
|
||||
close_btn.setMinimumWidth(110)
|
||||
btn_layout.addWidget(close_btn)
|
||||
|
||||
main_layout.addLayout(btn_layout)
|
||||
@ -88,17 +160,33 @@ class CategoryManagementDialog(QDialog):
|
||||
"""新增分类"""
|
||||
# 弹出输入对话框
|
||||
from PyQt5.QtWidgets import QDialog as QStdDialog
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
dialog = QStdDialog(self)
|
||||
dialog.setWindowTitle("新增分类")
|
||||
dialog.setGeometry(150, 150, 400, 150)
|
||||
dialog.setGeometry(150, 150, 450, 220)
|
||||
dialog.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
layout.addWidget(BodyLabel("分类名称:"))
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title = StrongBodyLabel("创建新分类")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 说明文字
|
||||
desc = BodyLabel("请输入分类名称")
|
||||
desc.setStyleSheet("color: #606366;")
|
||||
layout.addWidget(desc)
|
||||
|
||||
# 输入框
|
||||
input_edit = LineEdit()
|
||||
input_edit.setPlaceholderText("例如:食品、交通、娱乐等")
|
||||
input_edit.setPlaceholderText("例如:食品、交通、娱乐、购物等")
|
||||
input_edit.setMinimumHeight(40)
|
||||
layout.addWidget(input_edit)
|
||||
|
||||
# 按钮
|
||||
@ -140,14 +228,21 @@ class CategoryManagementDialog(QDialog):
|
||||
)
|
||||
|
||||
cancel_btn = PushButton("取消")
|
||||
cancel_btn.setMinimumWidth(100)
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
create_btn = PrimaryPushButton("创建")
|
||||
create_btn.setMinimumWidth(100)
|
||||
create_btn.clicked.connect(on_create)
|
||||
btn_layout.addWidget(create_btn)
|
||||
|
||||
layout.addSpacing(12)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 回车快速创建
|
||||
input_edit.returnPressed.connect(on_create)
|
||||
|
||||
dialog.exec()
|
||||
|
||||
def on_rename_category(self):
|
||||
@ -163,15 +258,41 @@ class CategoryManagementDialog(QDialog):
|
||||
|
||||
dialog = QStdDialog(self)
|
||||
dialog.setWindowTitle("重命名分类")
|
||||
dialog.setGeometry(150, 150, 400, 150)
|
||||
dialog.setGeometry(150, 150, 450, 280)
|
||||
dialog.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
layout.addWidget(BodyLabel(f"原分类名: {old_name}"))
|
||||
layout.addWidget(BodyLabel("新分类名:"))
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title = StrongBodyLabel("重命名分类")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 原名称显示
|
||||
old_info_layout = QHBoxLayout()
|
||||
old_label = BodyLabel("原分类名:")
|
||||
old_label.setMinimumWidth(80)
|
||||
old_value = StrongBodyLabel(old_name)
|
||||
old_value.setStyleSheet("color: #1976d2;")
|
||||
old_info_layout.addWidget(old_label)
|
||||
old_info_layout.addWidget(old_value)
|
||||
old_info_layout.addStretch()
|
||||
layout.addLayout(old_info_layout)
|
||||
|
||||
# 新名称输入
|
||||
new_label = BodyLabel("新分类名:")
|
||||
new_label.setMinimumWidth(80)
|
||||
layout.addWidget(new_label)
|
||||
|
||||
input_edit = LineEdit()
|
||||
input_edit.setText(old_name)
|
||||
input_edit.selectAll()
|
||||
input_edit.setMinimumHeight(40)
|
||||
layout.addWidget(input_edit)
|
||||
|
||||
# 按钮
|
||||
@ -217,14 +338,21 @@ class CategoryManagementDialog(QDialog):
|
||||
)
|
||||
|
||||
cancel_btn = PushButton("取消")
|
||||
cancel_btn.setMinimumWidth(100)
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
rename_btn = PrimaryPushButton("重命名")
|
||||
rename_btn.setMinimumWidth(100)
|
||||
rename_btn.clicked.connect(on_rename)
|
||||
btn_layout.addWidget(rename_btn)
|
||||
|
||||
layout.addSpacing(12)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 回车快速重命名
|
||||
input_edit.returnPressed.connect(on_rename)
|
||||
|
||||
dialog.exec()
|
||||
|
||||
def on_delete_category(self):
|
||||
@ -236,8 +364,6 @@ class CategoryManagementDialog(QDialog):
|
||||
category_name = current_item.text()
|
||||
|
||||
# 确认删除
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认删除",
|
||||
|
||||
@ -36,7 +36,17 @@ class CreateTransactionDialog(QDialog):
|
||||
self.finance_manager = finance_manager if finance_manager else FinanceManager()
|
||||
|
||||
self.setWindowTitle("新建交易记录" if not transaction else "编辑交易记录")
|
||||
self.setGeometry(100, 100, 600, 500)
|
||||
self.setGeometry(100, 100, 700, 650)
|
||||
self.setMinimumWidth(650)
|
||||
self.setMinimumHeight(580)
|
||||
|
||||
# 设置背景色跟随主题
|
||||
from qfluentwidgets import theme, Theme
|
||||
if theme() == Theme.DARK:
|
||||
self.setStyleSheet("background-color: #232323;")
|
||||
else:
|
||||
self.setStyleSheet("background-color: #f7f9fc;")
|
||||
|
||||
self.init_ui()
|
||||
|
||||
if transaction:
|
||||
@ -48,23 +58,45 @@ class CreateTransactionDialog(QDialog):
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题区域
|
||||
title_layout = QVBoxLayout()
|
||||
title_layout.setSpacing(6)
|
||||
title_label = TitleLabel("交易记录" if not self.transaction else "编辑交易")
|
||||
title_layout.addWidget(title_label)
|
||||
desc_label = BodyLabel("请填写交易的相关信息")
|
||||
title_layout.addWidget(desc_label)
|
||||
layout.addLayout(title_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 使用ScrollArea实现可滚动的内容区
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QScrollArea.NoFrame)
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_widget)
|
||||
scroll_layout.setSpacing(12)
|
||||
|
||||
# 交易类型
|
||||
type_layout = QHBoxLayout()
|
||||
type_layout.addWidget(BodyLabel("交易类型:"))
|
||||
type_label = BodyLabel("交易类型:")
|
||||
type_label.setMinimumWidth(80)
|
||||
type_layout.addWidget(type_label)
|
||||
self.transaction_type_combo = ComboBox()
|
||||
self.transaction_type_combo.addItem("入账 (正数)")
|
||||
self.transaction_type_combo.addItem("支出 (负数)")
|
||||
self.transaction_type_combo.setMaximumWidth(200)
|
||||
self.transaction_type_combo.setMaximumWidth(250)
|
||||
type_layout.addWidget(self.transaction_type_combo)
|
||||
type_layout.addStretch()
|
||||
layout.addLayout(type_layout)
|
||||
scroll_layout.addLayout(type_layout)
|
||||
|
||||
# 分类
|
||||
category_layout = QHBoxLayout()
|
||||
category_layout.addWidget(BodyLabel("分类:"))
|
||||
cat_label = BodyLabel("分类:")
|
||||
cat_label.setMinimumWidth(80)
|
||||
category_layout.addWidget(cat_label)
|
||||
self.category_combo = ComboBox()
|
||||
# 从财务管理器获取分类列表
|
||||
categories = []
|
||||
@ -83,90 +115,116 @@ class CreateTransactionDialog(QDialog):
|
||||
self.category_combo.addItem(cat)
|
||||
self.category_combo.setEnabled(True)
|
||||
|
||||
self.category_combo.setMaximumWidth(200)
|
||||
self.category_combo.setMaximumWidth(250)
|
||||
category_layout.addWidget(self.category_combo)
|
||||
category_layout.addStretch()
|
||||
layout.addLayout(category_layout)
|
||||
scroll_layout.addLayout(category_layout)
|
||||
|
||||
# 日期
|
||||
date_layout = QHBoxLayout()
|
||||
date_layout.addWidget(BodyLabel("日期:"))
|
||||
date_label = BodyLabel("日期:")
|
||||
date_label.setMinimumWidth(80)
|
||||
date_layout.addWidget(date_label)
|
||||
self.date_edit = DateEdit()
|
||||
self.date_edit.setDate(QDate.currentDate())
|
||||
self.date_edit.setMaximumWidth(250)
|
||||
date_layout.addWidget(self.date_edit)
|
||||
date_layout.addStretch()
|
||||
layout.addLayout(date_layout)
|
||||
scroll_layout.addLayout(date_layout)
|
||||
|
||||
# 金额
|
||||
amount_layout = QHBoxLayout()
|
||||
amount_layout.addWidget(BodyLabel("金额 (元):"))
|
||||
amt_label = BodyLabel("金额 (元):")
|
||||
amt_label.setMinimumWidth(80)
|
||||
amount_layout.addWidget(amt_label)
|
||||
self.amount_spin = DoubleSpinBox()
|
||||
self.amount_spin.setRange(0, 999999999)
|
||||
self.amount_spin.setDecimals(2)
|
||||
self.amount_spin.setMaximumWidth(250)
|
||||
amount_layout.addWidget(self.amount_spin)
|
||||
amount_layout.addStretch()
|
||||
layout.addLayout(amount_layout)
|
||||
scroll_layout.addLayout(amount_layout)
|
||||
|
||||
# 交易人
|
||||
trader_layout = QHBoxLayout()
|
||||
trader_layout.addWidget(BodyLabel("交易人:"))
|
||||
trader_label = BodyLabel("交易人:")
|
||||
trader_label.setMinimumWidth(80)
|
||||
trader_layout.addWidget(trader_label)
|
||||
self.trader_edit = LineEdit()
|
||||
self.trader_edit.setMaximumWidth(250)
|
||||
trader_layout.addWidget(self.trader_edit)
|
||||
layout.addLayout(trader_layout)
|
||||
trader_layout.addStretch()
|
||||
scroll_layout.addLayout(trader_layout)
|
||||
|
||||
# 备注
|
||||
notes_layout = QHBoxLayout()
|
||||
notes_layout.addWidget(BodyLabel("备注:"))
|
||||
notes_layout = QVBoxLayout()
|
||||
notes_label = BodyLabel("备注:")
|
||||
notes_layout.addWidget(notes_label)
|
||||
self.notes_edit = TextEdit()
|
||||
self.notes_edit.setMaximumHeight(80)
|
||||
self.notes_edit.setMaximumHeight(100)
|
||||
self.notes_edit.setMinimumHeight(70)
|
||||
notes_layout.addWidget(self.notes_edit)
|
||||
layout.addLayout(notes_layout)
|
||||
scroll_layout.addLayout(notes_layout)
|
||||
|
||||
# 图片部分
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
layout.addWidget(SubtitleLabel("相关文件 (可选)"))
|
||||
scroll_layout.addWidget(HorizontalSeparator())
|
||||
scroll_layout.addWidget(SubtitleLabel("相关文件 (可选)"))
|
||||
|
||||
# 发票
|
||||
invoice_layout = QHBoxLayout()
|
||||
invoice_layout.addWidget(BodyLabel("发票图片:"))
|
||||
invoice_label = BodyLabel("发票图片:")
|
||||
invoice_label.setMinimumWidth(80)
|
||||
invoice_layout.addWidget(invoice_label)
|
||||
self.invoice_label = BodyLabel("未选择")
|
||||
invoice_layout.addWidget(self.invoice_label)
|
||||
invoice_layout.addWidget(self.invoice_label, 1)
|
||||
invoice_btn = PushButton("选择")
|
||||
invoice_btn.setMaximumWidth(100)
|
||||
invoice_btn.clicked.connect(lambda: self.select_image("invoice"))
|
||||
invoice_layout.addWidget(invoice_btn)
|
||||
layout.addLayout(invoice_layout)
|
||||
scroll_layout.addLayout(invoice_layout)
|
||||
|
||||
# 支付记录
|
||||
payment_layout = QHBoxLayout()
|
||||
payment_layout.addWidget(BodyLabel("支付记录:"))
|
||||
payment_label = BodyLabel("支付记录:")
|
||||
payment_label.setMinimumWidth(80)
|
||||
payment_layout.addWidget(payment_label)
|
||||
self.payment_label = BodyLabel("未选择")
|
||||
payment_layout.addWidget(self.payment_label)
|
||||
payment_layout.addWidget(self.payment_label, 1)
|
||||
payment_btn = PushButton("选择")
|
||||
payment_btn.setMaximumWidth(100)
|
||||
payment_btn.clicked.connect(lambda: self.select_image("payment"))
|
||||
payment_layout.addWidget(payment_btn)
|
||||
layout.addLayout(payment_layout)
|
||||
scroll_layout.addLayout(payment_layout)
|
||||
|
||||
# 购买记录
|
||||
purchase_layout = QHBoxLayout()
|
||||
purchase_layout.addWidget(BodyLabel("购买记录:"))
|
||||
purchase_label = BodyLabel("购买记录:")
|
||||
purchase_label.setMinimumWidth(80)
|
||||
purchase_layout.addWidget(purchase_label)
|
||||
self.purchase_label = BodyLabel("未选择")
|
||||
purchase_layout.addWidget(self.purchase_label)
|
||||
purchase_layout.addWidget(self.purchase_label, 1)
|
||||
purchase_btn = PushButton("选择")
|
||||
purchase_btn.setMaximumWidth(100)
|
||||
purchase_btn.clicked.connect(lambda: self.select_image("purchase"))
|
||||
purchase_layout.addWidget(purchase_btn)
|
||||
layout.addLayout(purchase_layout)
|
||||
scroll_layout.addLayout(purchase_layout)
|
||||
|
||||
layout.addStretch()
|
||||
scroll_layout.addStretch()
|
||||
scroll_area.setWidget(scroll_widget)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
# 按钮
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(12)
|
||||
btn_layout.addStretch()
|
||||
|
||||
cancel_btn = PushButton("取消")
|
||||
cancel_btn.setMinimumWidth(110)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
save_btn = PrimaryPushButton("保存")
|
||||
save_btn.setMinimumWidth(110)
|
||||
save_btn.clicked.connect(self.save_transaction)
|
||||
btn_layout.addWidget(save_btn)
|
||||
|
||||
@ -325,7 +383,17 @@ class RecordViewDialog(QDialog):
|
||||
self.finance_manager = FinanceManager()
|
||||
|
||||
self.setWindowTitle("记录详情")
|
||||
self.setGeometry(100, 100, 700, 600)
|
||||
self.setGeometry(100, 100, 750, 650)
|
||||
self.setMinimumWidth(700)
|
||||
self.setMinimumHeight(580)
|
||||
|
||||
# 设置背景色跟随主题
|
||||
from qfluentwidgets import theme, Theme
|
||||
if theme() == Theme.DARK:
|
||||
self.setStyleSheet("background-color: #232323;")
|
||||
else:
|
||||
self.setStyleSheet("background-color: #f7f9fc;")
|
||||
|
||||
self.init_ui()
|
||||
|
||||
if transaction:
|
||||
@ -334,52 +402,117 @@ class RecordViewDialog(QDialog):
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题区域
|
||||
title_layout = QVBoxLayout()
|
||||
title_layout.setSpacing(6)
|
||||
title_label = TitleLabel("交易记录详情")
|
||||
title_layout.addWidget(title_label)
|
||||
desc_label = BodyLabel("查看交易的完整信息和相关文件")
|
||||
title_layout.addWidget(desc_label)
|
||||
layout.addLayout(title_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 使用ScrollArea实现可滚动的内容区
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QScrollArea.NoFrame)
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_widget)
|
||||
scroll_layout.setSpacing(12)
|
||||
|
||||
# 基本信息
|
||||
info_layout = QVBoxLayout()
|
||||
|
||||
date_layout = QHBoxLayout()
|
||||
date_layout.addWidget(BodyLabel("日期:"))
|
||||
date_label = BodyLabel("日期:")
|
||||
date_label.setMinimumWidth(80)
|
||||
date_layout.addWidget(date_label)
|
||||
self.date_label = BodyLabel()
|
||||
date_layout.addWidget(self.date_label)
|
||||
date_layout.addWidget(self.date_label, 1)
|
||||
date_layout.addStretch()
|
||||
info_layout.addLayout(date_layout)
|
||||
scroll_layout.addLayout(date_layout)
|
||||
|
||||
amount_layout = QHBoxLayout()
|
||||
amount_layout.addWidget(BodyLabel("金额:"))
|
||||
amount_label = BodyLabel("金额:")
|
||||
amount_label.setMinimumWidth(80)
|
||||
amount_layout.addWidget(amount_label)
|
||||
self.amount_label = BodyLabel()
|
||||
amount_layout.addWidget(self.amount_label)
|
||||
amount_layout.addWidget(self.amount_label, 1)
|
||||
amount_layout.addStretch()
|
||||
info_layout.addLayout(amount_layout)
|
||||
scroll_layout.addLayout(amount_layout)
|
||||
|
||||
trader_layout = QHBoxLayout()
|
||||
trader_layout.addWidget(BodyLabel("交易人:"))
|
||||
trader_label = BodyLabel("交易人:")
|
||||
trader_label.setMinimumWidth(80)
|
||||
trader_layout.addWidget(trader_label)
|
||||
self.trader_label = BodyLabel()
|
||||
trader_layout.addWidget(self.trader_label)
|
||||
trader_layout.addWidget(self.trader_label, 1)
|
||||
trader_layout.addStretch()
|
||||
info_layout.addLayout(trader_layout)
|
||||
scroll_layout.addLayout(trader_layout)
|
||||
|
||||
notes_layout = QHBoxLayout()
|
||||
notes_layout.addWidget(BodyLabel("备注:"))
|
||||
notes_layout = QVBoxLayout()
|
||||
notes_title = BodyLabel("备注:")
|
||||
notes_layout.addWidget(notes_title)
|
||||
self.notes_label = BodyLabel()
|
||||
self.notes_label.setWordWrap(True)
|
||||
notes_layout.addWidget(self.notes_label)
|
||||
info_layout.addLayout(notes_layout)
|
||||
scroll_layout.addLayout(notes_layout)
|
||||
|
||||
layout.addLayout(info_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
scroll_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 图片预览
|
||||
layout.addWidget(SubtitleLabel("相关文件预览"))
|
||||
scroll_layout.addWidget(SubtitleLabel("相关文件预览"))
|
||||
|
||||
preview_layout = QHBoxLayout()
|
||||
preview_layout.setSpacing(16)
|
||||
|
||||
# 发票
|
||||
invoice_layout = QVBoxLayout()
|
||||
invoice_layout.addWidget(BodyLabel("发票:"))
|
||||
self.invoice_preview = BodyLabel("无")
|
||||
self.invoice_preview.setMinimumSize(140, 140)
|
||||
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(140, 140)
|
||||
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(140, 140)
|
||||
purchase_layout.addWidget(self.purchase_preview)
|
||||
preview_layout.addLayout(purchase_layout)
|
||||
|
||||
scroll_layout.addLayout(preview_layout)
|
||||
scroll_layout.addStretch()
|
||||
scroll_area.setWidget(scroll_widget)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(12)
|
||||
btn_layout.addStretch()
|
||||
|
||||
export_images_btn = PushButton()
|
||||
export_images_btn.setText("导出图片")
|
||||
export_images_btn.setMinimumWidth(120)
|
||||
export_images_btn.clicked.connect(self.on_export_images)
|
||||
btn_layout.addWidget(export_images_btn)
|
||||
|
||||
close_btn = PushButton("关闭")
|
||||
close_btn.setMinimumWidth(110)
|
||||
close_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(close_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
self.invoice_preview.setMinimumSize(150, 150)
|
||||
invoice_layout.addWidget(self.invoice_preview)
|
||||
preview_layout.addLayout(invoice_layout)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtCore import Qt, QSize, QTimer
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
with redirect_stdout(None):
|
||||
from qfluentwidgets import NavigationItemPosition, FluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, toggleTheme, setTheme, Theme, NavigationAvatarWidget, NavigationToolButton ,NavigationPushButton
|
||||
from qfluentwidgets import NavigationItemPosition, FluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, toggleTheme, setTheme, Theme, NavigationAvatarWidget, NavigationToolButton ,NavigationPushButton, theme
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
|
||||
@ -69,7 +69,7 @@ class MainWindow(FluentWindow):
|
||||
|
||||
|
||||
self.themeBtn = NavigationPushButton(FIF.BRUSH, "切换主题", False, self.navigationInterface)
|
||||
self.themeBtn.clicked.connect(lambda: toggleTheme(lazy=True))
|
||||
self.themeBtn.clicked.connect(self._safe_toggle_theme)
|
||||
self.navigationInterface.addWidget(
|
||||
'themeButton',
|
||||
self.themeBtn,
|
||||
@ -77,6 +77,36 @@ class MainWindow(FluentWindow):
|
||||
NavigationItemPosition.BOTTOM
|
||||
)
|
||||
|
||||
def _safe_toggle_theme(self):
|
||||
"""安全地切换主题,避免字典迭代异常"""
|
||||
def safe_toggle():
|
||||
try:
|
||||
import sys
|
||||
from io import StringIO
|
||||
|
||||
# 捕获 stderr 以抑制库内的异常消息
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = StringIO()
|
||||
|
||||
try:
|
||||
# 获取当前主题
|
||||
current_theme = theme()
|
||||
# 根据当前主题切换到另一个
|
||||
new_theme = Theme.LIGHT if current_theme == Theme.DARK else Theme.DARK
|
||||
setTheme(new_theme, save=True, lazy=True)
|
||||
finally:
|
||||
# 恢复 stderr
|
||||
sys.stderr = old_stderr
|
||||
|
||||
except Exception as e:
|
||||
# 其他异常仍然打印,但忽略字典迭代异常
|
||||
error_msg = str(e)
|
||||
if "dictionary changed size during iteration" not in error_msg:
|
||||
print(f"主题切换失败: {e}")
|
||||
|
||||
# 在下一个事件循环中执行切换,让 Qt 完成当前事件处理
|
||||
QTimer.singleShot(50, safe_toggle)
|
||||
|
||||
def check_updates_in_background(self):
|
||||
"""后台检查更新"""
|
||||
try:
|
||||
|
||||
BIN
assets/.DS_Store
vendored
BIN
assets/.DS_Store
vendored
Binary file not shown.
BIN
assets/Finance_Data/.DS_Store
vendored
BIN
assets/Finance_Data/.DS_Store
vendored
Binary file not shown.
@ -1,13 +0,0 @@
|
||||
{
|
||||
"id": "28ae4af8-67d9-431a-b2fe-312f587020b3",
|
||||
"date": "2025-01-01",
|
||||
"amount": 5000.0,
|
||||
"trader": "工作",
|
||||
"notes": "1月工资",
|
||||
"invoice_path": "accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/invoice/截屏2025-11-25 02.51.14.png",
|
||||
"payment_path": null,
|
||||
"purchase_path": null,
|
||||
"category": "NUC",
|
||||
"created_at": "2025-11-25T20:44:13.766681",
|
||||
"updated_at": "2025-11-25T21:11:13.405339"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
@ -1,13 +0,0 @@
|
||||
{
|
||||
"id": "5cf3dc01-8a1c-4418-86b6-1728f1ae47d1",
|
||||
"date": "2025-01-10",
|
||||
"amount": -200.0,
|
||||
"trader": "超市",
|
||||
"notes": "食材",
|
||||
"invoice_path": "accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png",
|
||||
"payment_path": null,
|
||||
"purchase_path": null,
|
||||
"category": "NUC",
|
||||
"created_at": "2025-11-25T20:44:13.766985",
|
||||
"updated_at": "2025-11-25T20:56:17.767227"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
@ -1,13 +0,0 @@
|
||||
{
|
||||
"id": "86232779-66fb-4e8e-b31e-477aef8e4ecf",
|
||||
"date": "2025-01-05",
|
||||
"amount": -1500.0,
|
||||
"trader": "房东",
|
||||
"notes": "房租",
|
||||
"invoice_path": null,
|
||||
"payment_path": null,
|
||||
"purchase_path": null,
|
||||
"category": "NUC",
|
||||
"created_at": "2025-11-25T20:44:13.766850",
|
||||
"updated_at": "2025-11-25T20:54:13.895024"
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "b87b752a-7fb6-40b6-a0cf-287df1d64e56",
|
||||
"name": "admin",
|
||||
"description": "默认管理账户",
|
||||
"categories": [
|
||||
"NUC"
|
||||
],
|
||||
"created_at": "2025-11-25T20:44:13.766008",
|
||||
"updated_at": "2025-11-25T20:53:48.533051"
|
||||
}
|
||||
BIN
assets/User_code/.DS_Store
vendored
BIN
assets/User_code/.DS_Store
vendored
Binary file not shown.
@ -22,8 +22,7 @@ static void (*SPI_Callback[BSP_SPI_NUM][BSP_SPI_CB_NUM])(void);
|
||||
|
||||
/* Private function -------------------------------------------------------- */
|
||||
static BSP_SPI_t SPI_Get(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI1)
|
||||
return BSP_SPI_BMI088;
|
||||
/* AUTO GENERATED SPI_GET */
|
||||
else
|
||||
return BSP_SPI_ERR;
|
||||
}
|
||||
@ -96,8 +95,7 @@ void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
SPI_HandleTypeDef *BSP_SPI_GetHandle(BSP_SPI_t spi) {
|
||||
switch (spi) {
|
||||
case BSP_SPI_BMI088:
|
||||
return &hspi1;
|
||||
/* AUTO GENERATED BSP_SPI_GET_HANDLE */
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
387
assets/User_code/component/cmd.c
Normal file
387
assets/User_code/component/cmd.c
Normal file
@ -0,0 +1,387 @@
|
||||
/*
|
||||
控制命令
|
||||
*/
|
||||
|
||||
#include "cmd.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* USER INCLUDE BEGIN */
|
||||
|
||||
/* USER INCLUDE END */
|
||||
|
||||
/* USER DEFINE BEGIN */
|
||||
|
||||
/* USER DEFINE END */
|
||||
|
||||
/**
|
||||
* @brief 行为转换为对应按键
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @param behavior 行为
|
||||
* @return uint16_t 行为对应的按键
|
||||
*/
|
||||
static inline CMD_KeyValue_t CMD_BehaviorToKey(CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
return cmd->param->map.key_map[behavior].key;
|
||||
}
|
||||
|
||||
static inline CMD_ActiveType_t CMD_BehaviorToActive(CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
return cmd->param->map.key_map[behavior].active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查按键是否按下
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param key 按键名称
|
||||
* @param stateful 是否为状态切换按键
|
||||
* @return true 按下
|
||||
* @return false 未按下
|
||||
*/
|
||||
static bool CMD_KeyPressedRc(const CMD_RC_t *rc, CMD_KeyValue_t key) {
|
||||
/* 按下按键为鼠标左、右键 */
|
||||
if (key == CMD_L_CLICK) {
|
||||
return rc->mouse.l_click;
|
||||
}
|
||||
if (key == CMD_R_CLICK) {
|
||||
return rc->mouse.r_click;
|
||||
}
|
||||
return rc->key & (1u << key);
|
||||
}
|
||||
|
||||
static bool CMD_BehaviorOccurredRc(const CMD_RC_t *rc, CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
CMD_KeyValue_t key = CMD_BehaviorToKey(cmd, behavior);
|
||||
CMD_ActiveType_t active = CMD_BehaviorToActive(cmd, behavior);
|
||||
|
||||
bool now_key_pressed, last_key_pressed;
|
||||
|
||||
/* 按下按键为鼠标左、右键 */
|
||||
if (key == CMD_L_CLICK) {
|
||||
now_key_pressed = rc->mouse.l_click;
|
||||
last_key_pressed = cmd->mouse_last.l_click;
|
||||
} else if (key == CMD_R_CLICK) {
|
||||
now_key_pressed = rc->mouse.r_click;
|
||||
last_key_pressed = cmd->mouse_last.r_click;
|
||||
} else {
|
||||
now_key_pressed = rc->key & (1u << key);
|
||||
last_key_pressed = cmd->key_last & (1u << key);
|
||||
}
|
||||
|
||||
switch (active) {
|
||||
case CMD_ACTIVE_PRESSING:
|
||||
return now_key_pressed && !last_key_pressed;
|
||||
case CMD_ACTIVE_RASING:
|
||||
return !now_key_pressed && last_key_pressed;
|
||||
case CMD_ACTIVE_PRESSED:
|
||||
return now_key_pressed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析pc行为逻辑
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 主结构体
|
||||
* @param dt_sec 两次解析的间隔
|
||||
*/
|
||||
static void CMD_PcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
|
||||
/* 云台设置为鼠标控制欧拉角的变化,底盘的控制向量设置为零 */
|
||||
cmd->gimbal.delta_eulr.yaw =
|
||||
(float)rc->mouse.x * dt_sec * cmd->param->sens_mouse;
|
||||
cmd->gimbal.delta_eulr.pit =
|
||||
(float)(-rc->mouse.y) * dt_sec * cmd->param->sens_mouse;
|
||||
cmd->chassis.ctrl_vec.vx = cmd->chassis.ctrl_vec.vy = 0.0f;
|
||||
cmd->shoot.reverse_trig = false;
|
||||
|
||||
/* 按键行为映射相关逻辑 */
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FORE)) {
|
||||
cmd->chassis.ctrl_vec.vy += cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BACK)) {
|
||||
cmd->chassis.ctrl_vec.vy -= cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_LEFT)) {
|
||||
cmd->chassis.ctrl_vec.vx -= cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_RIGHT)) {
|
||||
cmd->chassis.ctrl_vec.vx += cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ACCELERATE)) {
|
||||
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_fast_sense;
|
||||
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_fast_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_DECELEBRATE)) {
|
||||
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_slow_sense;
|
||||
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_slow_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE)) {
|
||||
/* 切换至开火模式,设置相应的射击频率和弹丸初速度 */
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = true;
|
||||
} else {
|
||||
/* 切换至准备模式,停止射击 */
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = false;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE_MODE)) {
|
||||
/* 每按一次依次切换开火下一个模式 */
|
||||
cmd->shoot.fire_mode++;
|
||||
cmd->shoot.fire_mode %= FIRE_MODE_NUM;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ROTOR)) {
|
||||
/* 切换到小陀螺模式 */
|
||||
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
|
||||
cmd->chassis.mode_rotor = ROTOR_MODE_RAND;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_OPENCOVER)) {
|
||||
/* 每按一次开、关弹舱盖 */
|
||||
cmd->shoot.cover_open = !cmd->shoot.cover_open;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BUFF)) {
|
||||
if (cmd->ai_status == AI_STATUS_HITSWITCH) {
|
||||
/* 停止ai的打符模式,停用host控制 */
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_STOP);
|
||||
cmd->host_overwrite = false;
|
||||
cmd->ai_status = AI_STATUS_STOP;
|
||||
} else if (cmd->ai_status == AI_STATUS_AUTOAIM) {
|
||||
/* 自瞄模式中切换失败提醒 */
|
||||
} else {
|
||||
/* ai切换至打符模式,启用host控制 */
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_START);
|
||||
cmd->ai_status = AI_STATUS_HITSWITCH;
|
||||
cmd->host_overwrite = true;
|
||||
}
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_AUTOAIM)) {
|
||||
if (cmd->ai_status == AI_STATUS_AUTOAIM) {
|
||||
/* 停止ai的自瞄模式,停用host控制 */
|
||||
cmd->host_overwrite = false;
|
||||
cmd->ai_status = AI_STATUS_STOP;
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_STOP);
|
||||
} else {
|
||||
/* ai切换至自瞄模式,启用host控制 */
|
||||
cmd->ai_status = AI_STATUS_AUTOAIM;
|
||||
cmd->host_overwrite = true;
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_START);
|
||||
}
|
||||
} else {
|
||||
cmd->host_overwrite = false;
|
||||
// TODO: 修复逻辑
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_REVTRIG)) {
|
||||
/* 按下拨弹反转 */
|
||||
cmd->shoot.reverse_trig = true;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FOLLOWGIMBAL35)) {
|
||||
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL_35;
|
||||
}
|
||||
/* 保存当前按下的键位状态 */
|
||||
cmd->key_last = rc->key;
|
||||
memcpy(&(cmd->mouse_last), &(rc->mouse), sizeof(cmd->mouse_last));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析rc行为逻辑
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 主结构体
|
||||
* @param dt_sec 两次解析的间隔
|
||||
*/
|
||||
static void CMD_RcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
switch (rc->sw_l) {
|
||||
/* 左拨杆相应行为选择和解析 */
|
||||
case CMD_SW_UP:
|
||||
cmd->chassis.mode = CHASSIS_MODE_BREAK;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
|
||||
cmd->chassis.mode_rotor = ROTOR_MODE_CW;
|
||||
break;
|
||||
|
||||
case CMD_SW_ERR:
|
||||
cmd->chassis.mode = CHASSIS_MODE_RELAX;
|
||||
break;
|
||||
}
|
||||
switch (rc->sw_r) {
|
||||
/* 右拨杆相应行为选择和解析*/
|
||||
case CMD_SW_UP:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.fire = false;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
|
||||
cmd->shoot.fire = true;
|
||||
break;
|
||||
/*
|
||||
case CMD_SW_UP:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.fire = false;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
|
||||
cmd->shoot.fire = true;
|
||||
break;
|
||||
*/
|
||||
case CMD_SW_ERR:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_RELAX;
|
||||
}
|
||||
/* 将操纵杆的对应值转换为底盘的控制向量和云台变化的欧拉角 */
|
||||
cmd->chassis.ctrl_vec.vx = rc->ch_l_x;
|
||||
cmd->chassis.ctrl_vec.vy = rc->ch_l_y;
|
||||
cmd->gimbal.delta_eulr.yaw = rc->ch_r_x * dt_sec * cmd->param->sens_rc;
|
||||
cmd->gimbal.delta_eulr.pit = rc->ch_r_y * dt_sec * cmd->param->sens_rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief rc失控时机器人恢复放松模式
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
*/
|
||||
static void CMD_RcLostLogic(CMD_t *cmd) {
|
||||
/* 机器人底盘、云台、射击运行模式恢复至放松模式 */
|
||||
cmd->chassis.mode = CHASSIS_MODE_RELAX;
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_RELAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化命令解析
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @param param 参数
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_Init(CMD_t *cmd, const CMD_Params_t *param) {
|
||||
/* 指针检测 */
|
||||
if (cmd == NULL) return -1;
|
||||
if (param == NULL) return -1;
|
||||
|
||||
/* 设置机器人的命令参数,初始化控制方式为rc控制 */
|
||||
cmd->pc_ctrl = false;
|
||||
cmd->param = param;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否启用上位机控制指令覆盖
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @return true 启用
|
||||
* @return false 不启用
|
||||
*/
|
||||
inline bool CMD_CheckHostOverwrite(CMD_t *cmd) { return cmd->host_overwrite; }
|
||||
|
||||
/**
|
||||
* @brief 解析命令
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseRc(CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
/* 指针检测 */
|
||||
if (rc == NULL) return -1;
|
||||
if (cmd == NULL) return -1;
|
||||
|
||||
/* 在pc控制和rc控制间切换 */
|
||||
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
|
||||
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_Q))
|
||||
cmd->pc_ctrl = true;
|
||||
|
||||
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
|
||||
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_E))
|
||||
cmd->pc_ctrl = false;
|
||||
/*c当rc丢控时,恢复机器人至默认状态 */
|
||||
if ((rc->sw_l == CMD_SW_ERR) || (rc->sw_r == CMD_SW_ERR)) {
|
||||
CMD_RcLostLogic(cmd);
|
||||
} else {
|
||||
if (cmd->pc_ctrl) {
|
||||
CMD_PcLogic(rc, cmd, dt_sec);
|
||||
} else {
|
||||
CMD_RcLogic(rc, cmd, dt_sec);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析上位机命令
|
||||
*
|
||||
* @param host host数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseHost(const CMD_Host_t *host, CMD_t *cmd, float dt_sec) {
|
||||
(void)dt_sec; /* 未使用dt_sec,消除警告 */
|
||||
/* 指针检测 */
|
||||
if (host == NULL) return -1;
|
||||
if (cmd == NULL) return -1;
|
||||
|
||||
/* 云台欧拉角设置为host相应的变化的欧拉角 */
|
||||
cmd->gimbal.delta_eulr.yaw = host->gimbal_delta.yaw;
|
||||
cmd->gimbal.delta_eulr.pit = host->gimbal_delta.pit;
|
||||
|
||||
/* host射击命令,设置不同的射击频率和弹丸初速度 */
|
||||
if (host->fire) {
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = true;
|
||||
} else {
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加向Referee发送的命令
|
||||
*
|
||||
* @param ref 命令队列
|
||||
* @param cmd 要添加的命令
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_RefereeAdd(CMD_RefereeCmd_t *ref, CMD_UI_t cmd) {
|
||||
/* 指针检测 */
|
||||
if (ref == NULL) return -1;
|
||||
/* 越界检测 */
|
||||
if (ref->counter >= CMD_REFEREE_MAX_NUM || ref->counter < 0) return -1;
|
||||
|
||||
/* 添加机器人当前行为状态到画图的命令队列中 */
|
||||
ref->cmd[ref->counter] = cmd;
|
||||
ref->counter++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* USER FUNCTION BEGIN */
|
||||
|
||||
/* USER FUNCTION END */
|
||||
318
assets/User_code/component/cmd.h
Normal file
318
assets/User_code/component/cmd.h
Normal file
@ -0,0 +1,318 @@
|
||||
/*
|
||||
控制命令
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "component/ahrs.h"
|
||||
|
||||
/* USER INCLUDE BEGIN */
|
||||
|
||||
/* USER INCLUDE END */
|
||||
|
||||
#define CMD_REFEREE_MAX_NUM (3) /* Lines 16 omitted */
|
||||
|
||||
/* USER DEFINE BEGIN */
|
||||
|
||||
/* USER DEFINE END */
|
||||
|
||||
/* 机器人型号 */
|
||||
typedef enum {
|
||||
ROBOT_MODEL_INFANTRY = 0, /* 步兵机器人 */
|
||||
ROBOT_MODEL_HERO, /* 英雄机器人 */
|
||||
ROBOT_MODEL_ENGINEER, /* 工程机器人 */
|
||||
ROBOT_MODEL_DRONE, /* 空中机器人 */
|
||||
ROBOT_MODEL_SENTRY, /* 哨兵机器人 */
|
||||
ROBOT_MODEL_NUM, /* 型号数量 */
|
||||
} CMD_RobotModel_t;
|
||||
|
||||
/* 底盘运行模式 */
|
||||
typedef enum {
|
||||
CHASSIS_MODE_RELAX, /* 放松模式,电机不输出。一般情况底盘初始化之后的模式 */
|
||||
CHASSIS_MODE_BREAK, /* 刹车模式,电机闭环控制保持静止。用于机器人停止状态 */
|
||||
CHASSIS_MODE_FOLLOW_GIMBAL, /* 通过闭环控制使车头方向跟随云台 */
|
||||
CHASSIS_MODE_FOLLOW_GIMBAL_35, /* 通过闭环控制使车头方向35度跟随云台 */
|
||||
CHASSIS_MODE_ROTOR, /* 小陀螺模式,通过闭环控制使底盘不停旋转 */
|
||||
CHASSIS_MODE_INDENPENDENT, /* 独立模式。底盘运行不受云台影响 */
|
||||
CHASSIS_MODE_OPEN, /* 开环模式。底盘运行不受PID控制,直接输出到电机 */
|
||||
} CMD_ChassisMode_t;
|
||||
|
||||
/* 云台运行模式 */
|
||||
typedef enum {
|
||||
GIMBAL_MODE_RELAX, /* 放松模式,电机不输出。一般情况云台初始化之后的模式 */
|
||||
GIMBAL_MODE_ABSOLUTE, /* 绝对坐标系控制,控制在空间内的绝对姿态 */
|
||||
GIMBAL_MODE_RELATIVE, /* 相对坐标系控制,控制相对于底盘的姿态 */
|
||||
} CMD_GimbalMode_t;
|
||||
|
||||
/* 射击运行模式 */
|
||||
typedef enum {
|
||||
SHOOT_MODE_RELAX, /* 放松模式,电机不输出 */
|
||||
SHOOT_MODE_SAFE, /* 保险模式,电机闭环控制保持静止 */
|
||||
SHOOT_MODE_LOADED, /* 上膛模式,摩擦轮开启。随时准备开火 */
|
||||
} CMD_ShootMode_t;
|
||||
|
||||
typedef enum {
|
||||
FIRE_MODE_SINGLE, /* 单发开火模式 */
|
||||
FIRE_MODE_BURST, /* N连发开火模式 */
|
||||
FIRE_MODE_CONT, /* 持续开火模式 */
|
||||
FIRE_MODE_NUM,
|
||||
} CMD_FireMode_t;
|
||||
|
||||
/* 小陀螺转动模式 */
|
||||
typedef enum {
|
||||
ROTOR_MODE_CW, /* 顺时针转动 */
|
||||
ROTOR_MODE_CCW, /* 逆时针转动 */
|
||||
ROTOR_MODE_RAND, /* 随机转动 */
|
||||
} CMD_RotorMode_t;
|
||||
|
||||
/* 底盘控制命令 */
|
||||
typedef struct {
|
||||
CMD_ChassisMode_t mode; /* 底盘运行模式 */
|
||||
CMD_RotorMode_t mode_rotor; /* 小陀螺转动模式 */
|
||||
MoveVector_t ctrl_vec; /* 底盘控制向量 */
|
||||
} CMD_ChassisCmd_t;
|
||||
|
||||
/* 云台控制命令 */
|
||||
typedef struct {
|
||||
CMD_GimbalMode_t mode; /* 云台运行模式 */
|
||||
AHRS_Eulr_t delta_eulr; /* 欧拉角变化角度 */
|
||||
} CMD_GimbalCmd_t;
|
||||
|
||||
/* 射击控制命令 */
|
||||
typedef struct {
|
||||
CMD_ShootMode_t mode; /* 射击运行模式 */
|
||||
CMD_FireMode_t fire_mode; /* 开火模式 */
|
||||
bool fire; /*开火*/
|
||||
bool cover_open; /* 弹舱盖开关 */
|
||||
bool reverse_trig; /* 拨弹电机状态 */
|
||||
} CMD_ShootCmd_t;
|
||||
|
||||
/* 拨杆位置 */
|
||||
typedef enum {
|
||||
CMD_SW_ERR = 0,
|
||||
CMD_SW_UP = 1,
|
||||
CMD_SW_MID = 3,
|
||||
CMD_SW_DOWN = 2,
|
||||
} CMD_SwitchPos_t;
|
||||
|
||||
/* 键盘按键值 */
|
||||
typedef enum {
|
||||
CMD_KEY_W = 0,
|
||||
CMD_KEY_S,
|
||||
CMD_KEY_A,
|
||||
CMD_KEY_D,
|
||||
CMD_KEY_SHIFT,
|
||||
CMD_KEY_CTRL,
|
||||
CMD_KEY_Q,
|
||||
CMD_KEY_E,
|
||||
CMD_KEY_R,
|
||||
CMD_KEY_F,
|
||||
CMD_KEY_G,
|
||||
CMD_KEY_Z,
|
||||
CMD_KEY_X,
|
||||
CMD_KEY_C,
|
||||
CMD_KEY_V,
|
||||
CMD_KEY_B,
|
||||
CMD_L_CLICK,
|
||||
CMD_R_CLICK,
|
||||
CMD_KEY_NUM,
|
||||
} CMD_KeyValue_t;
|
||||
|
||||
/* 行为值序列 */
|
||||
typedef enum {
|
||||
CMD_BEHAVIOR_FORE = 0, /* 向前 */
|
||||
CMD_BEHAVIOR_BACK, /* 向后 */
|
||||
CMD_BEHAVIOR_LEFT, /* 向左 */
|
||||
CMD_BEHAVIOR_RIGHT, /* 向右 */
|
||||
CMD_BEHAVIOR_ACCELERATE, /* 加速 */
|
||||
CMD_BEHAVIOR_DECELEBRATE, /* 减速 */
|
||||
CMD_BEHAVIOR_FIRE, /* 开火 */
|
||||
CMD_BEHAVIOR_FIRE_MODE, /* 切换开火模式 */
|
||||
CMD_BEHAVIOR_BUFF, /* 打符模式 */
|
||||
CMD_BEHAVIOR_AUTOAIM, /* 自瞄模式 */
|
||||
CMD_BEHAVIOR_OPENCOVER, /* 弹舱盖开关 */
|
||||
CMD_BEHAVIOR_ROTOR, /* 小陀螺模式 */
|
||||
CMD_BEHAVIOR_REVTRIG, /* 反转拨弹 */
|
||||
CMD_BEHAVIOR_FOLLOWGIMBAL35, /* 跟随云台呈35度 */
|
||||
CMD_BEHAVIOR_NUM,
|
||||
} CMD_Behavior_t;
|
||||
|
||||
typedef enum {
|
||||
CMD_ACTIVE_PRESSING, /* 按下时触发 */
|
||||
CMD_ACTIVE_RASING, /* 抬起时触发 */
|
||||
CMD_ACTIVE_PRESSED, /* 按住时触发 */
|
||||
} CMD_ActiveType_t;
|
||||
|
||||
typedef struct {
|
||||
CMD_ActiveType_t active;
|
||||
CMD_KeyValue_t key;
|
||||
} CMD_KeyMapItem_t;
|
||||
|
||||
/* 行为映射的对应按键数组 */
|
||||
typedef struct {
|
||||
CMD_KeyMapItem_t key_map[CMD_BEHAVIOR_NUM];
|
||||
} CMD_KeyMap_Params_t;
|
||||
|
||||
/* 位移灵敏度参数 */
|
||||
typedef struct {
|
||||
float move_sense; /* 移动灵敏度 */
|
||||
float move_fast_sense; /* 加速灵敏度 */
|
||||
float move_slow_sense; /* 减速灵敏度 */
|
||||
} CMD_Move_Params_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
} CMD_Screen_t;
|
||||
|
||||
/* 命令参数 */
|
||||
typedef struct {
|
||||
float sens_mouse; /* 鼠标灵敏度 */
|
||||
float sens_rc; /* 遥控器摇杆灵敏度 */
|
||||
CMD_KeyMap_Params_t map; /* 按键映射行为命令 */
|
||||
CMD_Move_Params_t move; /* 位移灵敏度参数 */
|
||||
CMD_Screen_t screen; /* 屏幕分辨率参数 */
|
||||
} CMD_Params_t;
|
||||
|
||||
/* AI行为状态 */
|
||||
typedef enum {
|
||||
AI_STATUS_STOP, /* 停止状态 */
|
||||
AI_STATUS_AUTOAIM, /* 自瞄状态 */
|
||||
AI_STATUS_HITSWITCH, /* 打符状态 */
|
||||
AI_STATUS_AUTOMATIC /* 自动状态 */
|
||||
} CMD_AI_Status_t;
|
||||
|
||||
/* UI所用行为状态 */
|
||||
typedef enum {
|
||||
CMD_UI_NOTHING, /* 当前无状态 */
|
||||
CMD_UI_AUTO_AIM_START, /* 自瞄状态开启 */
|
||||
CMD_UI_AUTO_AIM_STOP, /* 自瞄状态关闭 */
|
||||
CMD_UI_HIT_SWITCH_START, /* 打符状态开启 */
|
||||
CMD_UI_HIT_SWITCH_STOP /* 打符状态关闭 */
|
||||
} CMD_UI_t;
|
||||
|
||||
/*裁判系统发送的命令*/
|
||||
typedef struct {
|
||||
CMD_UI_t cmd[CMD_REFEREE_MAX_NUM]; /* 命令数组 */
|
||||
uint8_t counter; /* 命令计数 */
|
||||
} CMD_RefereeCmd_t;
|
||||
|
||||
typedef struct {
|
||||
bool pc_ctrl; /* 是否使用键鼠控制 */
|
||||
bool host_overwrite; /* 是否Host控制 */
|
||||
uint16_t key_last; /* 上次按键键值 */
|
||||
|
||||
struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
bool l_click; /* 左键 */
|
||||
bool r_click; /* 右键 */
|
||||
} mouse_last; /* 鼠标值 */
|
||||
|
||||
CMD_AI_Status_t ai_status; /* AI状态 */
|
||||
|
||||
const CMD_Params_t *param; /* 命令参数 */
|
||||
|
||||
CMD_ChassisCmd_t chassis; /* 底盘控制命令 */
|
||||
CMD_GimbalCmd_t gimbal; /* 云台控制命令 */
|
||||
CMD_ShootCmd_t shoot; /* 射击控制命令 */
|
||||
CMD_RefereeCmd_t referee; /* 裁判系统发送命令 */
|
||||
} CMD_t;
|
||||
|
||||
typedef struct {
|
||||
float ch_l_x; /* 遥控器左侧摇杆横轴值,上为正 */
|
||||
float ch_l_y; /* 遥控器左侧摇杆纵轴值,右为正 */
|
||||
float ch_r_x; /* 遥控器右侧摇杆横轴值,上为正 */
|
||||
float ch_r_y; /* 遥控器右侧摇杆纵轴值,右为正 */
|
||||
|
||||
float ch_res; /* 第五通道值 */
|
||||
|
||||
CMD_SwitchPos_t sw_r; /* 右侧拨杆位置 */
|
||||
CMD_SwitchPos_t sw_l; /* 左侧拨杆位置 */
|
||||
|
||||
struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
bool l_click; /* 左键 */
|
||||
bool r_click; /* 右键 */
|
||||
} mouse; /* 鼠标值 */
|
||||
|
||||
uint16_t key; /* 按键值 */
|
||||
|
||||
uint16_t res; /* 保留,未启用 */
|
||||
} CMD_RC_t;
|
||||
|
||||
typedef struct {
|
||||
AHRS_Eulr_t gimbal_delta; /* 欧拉角的变化量 */
|
||||
|
||||
struct {
|
||||
float vx; /* x轴移动速度 */
|
||||
float vy; /* y轴移动速度 */
|
||||
float wz; /* z轴转动速度 */
|
||||
} chassis_move_vec; /* 底盘移动向量 */
|
||||
|
||||
bool fire; /* 开火状态 */
|
||||
} CMD_Host_t;
|
||||
|
||||
/**
|
||||
* @brief 解析行为命令
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 主结构体
|
||||
*/
|
||||
int8_t CMD_Init(CMD_t *cmd, const CMD_Params_t *param);
|
||||
|
||||
/**
|
||||
* @brief 检查是否启用上位机控制指令覆盖
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @return true 启用
|
||||
* @return false 不启用
|
||||
*/
|
||||
bool CMD_CheckHostOverwrite(CMD_t *cmd);
|
||||
|
||||
/**
|
||||
* @brief 解析命令
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseRc(CMD_RC_t *rc, CMD_t *cmd, float dt_sec);
|
||||
|
||||
/**
|
||||
* @brief 解析上位机命令
|
||||
*
|
||||
* @param host host数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseHost(const CMD_Host_t *host, CMD_t *cmd, float dt_sec);
|
||||
|
||||
/**
|
||||
* @brief 添加向Referee发送的命令
|
||||
*
|
||||
* @param ref 命令队列
|
||||
* @param cmd 要添加的命令
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_RefereeAdd(CMD_RefereeCmd_t *ref, CMD_UI_t cmd);
|
||||
|
||||
/* USER FUNCTION BEGIN */
|
||||
|
||||
/* USER FUNCTION END */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -1,4 +1,4 @@
|
||||
bsp,can,dwt,gpio,i2c,mm,spi,uart,pwm,time
|
||||
component,ahrs,capacity,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
|
||||
device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,ai
|
||||
component,ahrs,capacity,cmd,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
|
||||
device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9
|
||||
module,config,
|
||||
|
@ -1,142 +0,0 @@
|
||||
/*
|
||||
AI
|
||||
*/
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "ai.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bsp/time.h"
|
||||
#include "bsp/uart.h"
|
||||
#include "component/ahrs.h"
|
||||
#include "component/crc16.h"
|
||||
#include "component/crc8.h"
|
||||
#include "component/user_math.h"
|
||||
#include "component/filter.h"
|
||||
|
||||
|
||||
/* Private define ----------------------------------------------------------- */
|
||||
#define AI_LEN_RX_BUFF (sizeof(AI_DownPackage_t))
|
||||
|
||||
/* Private macro ------------------------------------------------------------ */
|
||||
/* Private typedef ---------------------------------------------------------- */
|
||||
/* Private variables -------------------------------------------------------- */
|
||||
|
||||
static uint8_t rxbuf[AI_LEN_RX_BUFF];
|
||||
|
||||
static bool inited = false;
|
||||
|
||||
static osThreadId_t thread_alert;
|
||||
|
||||
static uint32_t drop_message = 0;
|
||||
|
||||
// uint16_t crc16;
|
||||
|
||||
/* Private function -------------------------------------------------------- */
|
||||
|
||||
static void Ai_RxCpltCallback(void) {
|
||||
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
static void Ai_IdleLineCallback(void) {
|
||||
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
int8_t AI_Init(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
if (inited) return DEVICE_ERR_INITED;
|
||||
thread_alert = osThreadGetId();
|
||||
|
||||
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_RX_CPLT_CB,
|
||||
Ai_RxCpltCallback);
|
||||
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_IDLE_LINE_CB,
|
||||
Ai_IdleLineCallback);
|
||||
|
||||
inited = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t AI_Restart(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
__HAL_UART_DISABLE(BSP_UART_GetHandle(BSP_UART_AI));
|
||||
__HAL_UART_ENABLE(BSP_UART_GetHandle(BSP_UART_AI));
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_StartReceiving(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
// if (HAL_UART_Receive_DMA(BSP_UART_GetHandle(BSP_UART_AI), rxbuf,
|
||||
// AI_LEN_RX_BUFF) == HAL_OK)
|
||||
if (BSP_UART_Receive(BSP_UART_AI, rxbuf,
|
||||
AI_LEN_RX_BUFF, true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
|
||||
bool AI_WaitDmaCplt(void) {
|
||||
return (osThreadFlagsWait(SIGNAL_AI_RAW_REDY, osFlagsWaitAll,0) ==
|
||||
SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
int8_t AI_ParseHost(AI_t *ai) {
|
||||
// crc16 = CRC16_Calc((const uint8_t *)&(rxbuf), sizeof(ai->from_host) - 2, CRC16_INIT);
|
||||
if (!CRC16_Verify((const uint8_t *)&(rxbuf), sizeof(ai->from_host)))
|
||||
goto error;
|
||||
ai->header.online = true;
|
||||
ai->header.last_online_time = BSP_TIME_Get();
|
||||
memcpy(&(ai->from_host), rxbuf, sizeof(ai->from_host));
|
||||
memset(rxbuf, 0, AI_LEN_RX_BUFF);
|
||||
return DEVICE_OK;
|
||||
|
||||
error:
|
||||
drop_message++;
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
|
||||
int8_t AI_PackMCU(AI_t *ai, const AHRS_Quaternion_t *data){
|
||||
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
|
||||
ai->to_host.mcu.id = AI_ID_MCU;
|
||||
ai->to_host.mcu.package.quat=*data;
|
||||
ai->to_host.mcu.package.notice = ai->status;
|
||||
ai->to_host.mcu.crc16 = CRC16_Calc((const uint8_t *)&(ai->to_host.mcu), sizeof(AI_UpPackageMCU_t) - 2, CRC16_INIT);
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_PackRef(AI_t *ai, const AI_UpPackageReferee_t *data) {
|
||||
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
|
||||
ai->to_host.ref = *data;
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_HandleOffline(AI_t *ai) {
|
||||
if (ai == NULL) return DEVICE_ERR_NULL;
|
||||
if (BSP_TIME_Get() - ai->header.last_online_time >
|
||||
100000) {
|
||||
ai->header.online = false;
|
||||
}
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_StartSend(AI_t *ai, bool ref_online){
|
||||
if (ai == NULL) return DEVICE_ERR_NULL;
|
||||
|
||||
if (ref_online) {
|
||||
// 发送裁判系统数据和MCU数据
|
||||
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host),
|
||||
sizeof(ai->to_host.ref) + sizeof(ai->to_host.mcu), true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
else
|
||||
return DEVICE_ERR;
|
||||
} else {
|
||||
// 只发送MCU数据
|
||||
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host.mcu),
|
||||
sizeof(ai->to_host.mcu), true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
else
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
/*
|
||||
AI
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/cdefs.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "component/ahrs.h"
|
||||
#include "component/filter.h"
|
||||
#include "component/user_math.h"
|
||||
#include "device/device.h"
|
||||
#include <cmsis_os2.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Exported constants ------------------------------------------------------- */
|
||||
/* Exported macro ----------------------------------------------------------- */
|
||||
#define AI_ID_MCU (0xC4)
|
||||
#define AI_ID_REF (0xA8)
|
||||
#define AI_ID_AI (0xA1)
|
||||
/* Exported types ----------------------------------------------------------- */
|
||||
|
||||
typedef enum {
|
||||
AI_ARMOR_HERO = 0, /*英雄机器人*/
|
||||
AI_ARMOR_INFANTRY, /*步兵机器人*/
|
||||
AI_ARMOR_SENTRY, /*哨兵机器人*/
|
||||
AI_ARMOR_ENGINEER, /*工程机器人*/
|
||||
AI_ARMOR_OUTPOST, /*前哨占*/
|
||||
AI_ARMOR_BASE, /*基地*/
|
||||
AI_ARMOR_NORMAL, /*由AI自动选择*/
|
||||
} AI_ArmorsType_t;
|
||||
|
||||
typedef enum {
|
||||
AI_STATUS_OFF = 0, /* 关闭 */
|
||||
AI_STATUS_AUTOAIM, /* 自瞄 */
|
||||
AI_STATUS_AUTOPICK, /* 自动取矿 */
|
||||
AI_STATUS_AUTOPUT, /* 自动兑矿 */
|
||||
AI_STATUS_AUTOHITBUFF, /* 自动打符 */
|
||||
AI_STATUS_AUTONAV,
|
||||
} AI_Status_t;
|
||||
|
||||
typedef enum {
|
||||
AI_NOTICE_NONE = 0,
|
||||
AI_NOTICE_SEARCH,
|
||||
AI_NOTICE_FIRE,
|
||||
}AI_Notice_t;
|
||||
|
||||
/* 电控 -> 视觉 MCU数据结构体*/
|
||||
typedef struct __packed {
|
||||
AHRS_Quaternion_t quat; /* 四元数 */
|
||||
// struct {
|
||||
// AI_ArmorsType_t armor_type;
|
||||
// AI_Status_t status;
|
||||
// }notice; /* 控制命令 */
|
||||
uint8_t notice;
|
||||
} AI_Protucol_UpDataMCU_t;
|
||||
|
||||
/* 电控 -> 视觉 裁判系统数据结构体*/
|
||||
typedef struct __packed {
|
||||
/* USER REFEREE BEGIN */
|
||||
uint16_t team; /* 本身队伍 */
|
||||
uint16_t time; /* 比赛开始时间 */
|
||||
/* USER REFEREE END */
|
||||
} AI_Protocol_UpDataReferee_t;
|
||||
|
||||
/* 视觉 -> 电控 数据包结构体*/
|
||||
typedef struct __packed {
|
||||
AHRS_Eulr_t eulr; /* 欧拉角 */
|
||||
MoveVector_t move_vec; /* 运动向量 */
|
||||
uint8_t notice; /* 控制命令 */
|
||||
} AI_Protocol_DownData_t;
|
||||
|
||||
/* 电控 -> 视觉 裁判系统数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id; /* 包ID */
|
||||
AI_Protocol_UpDataReferee_t package; /* 数据包 */
|
||||
uint16_t crc16; /* CRC16校验 */
|
||||
} AI_UpPackageReferee_t;
|
||||
|
||||
/* 电控 -> 视觉 MUC数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id;
|
||||
AI_Protucol_UpDataMCU_t package;
|
||||
uint16_t crc16;
|
||||
} AI_UpPackageMCU_t;
|
||||
|
||||
/* 视觉 -> 电控 数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id; /* 包ID */
|
||||
AI_Protocol_DownData_t package; /* 数据包 */
|
||||
uint16_t crc16; /* CRC16校验 */
|
||||
} AI_DownPackage_t;
|
||||
|
||||
typedef struct __packed {
|
||||
DEVICE_Header_t header; /* 设备通用头部 */
|
||||
AI_DownPackage_t from_host;
|
||||
AI_Status_t status;
|
||||
struct {
|
||||
AI_UpPackageReferee_t ref;
|
||||
AI_UpPackageMCU_t mcu;
|
||||
} to_host;
|
||||
} AI_t;
|
||||
|
||||
/* Exported functions prototypes -------------------------------------------- */
|
||||
|
||||
int8_t AI_Init(AI_t *ai);
|
||||
|
||||
int8_t AI_Restart(AI_t *ai);
|
||||
|
||||
int8_t AI_StartReceiving(AI_t *ai);
|
||||
|
||||
bool AI_WaitDmaCplt(void);
|
||||
|
||||
int8_t AI_ParseHost(AI_t *ai);
|
||||
|
||||
int8_t AI_PackMCU(AI_t *ai, const AHRS_Quaternion_t *quat);
|
||||
|
||||
int8_t AI_PackRef(AI_t *ai, const AI_UpPackageReferee_t *data);
|
||||
|
||||
int8_t AI_HandleOffline(AI_t *ai);
|
||||
|
||||
int8_t AI_StartSend(AI_t *ai, bool ref_online);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -56,12 +56,12 @@ static int8_t MOTOR_RM_GetLogicalIndex(uint16_t can_id, MOTOR_RM_Module_t module
|
||||
switch (module) {
|
||||
case MOTOR_M2006:
|
||||
case MOTOR_M3508:
|
||||
if (can_id >= M3508_M2006_FB_ID_BASE && can_id < M3508_M2006_FB_ID_BASE + 7) {
|
||||
if (can_id >= M3508_M2006_FB_ID_BASE && can_id <= M3508_M2006_FB_ID_BASE + 7) {
|
||||
return can_id - M3508_M2006_FB_ID_BASE;
|
||||
}
|
||||
break;
|
||||
case MOTOR_GM6020:
|
||||
if (can_id >= GM6020_FB_ID_BASE && can_id < GM6020_FB_ID_BASE + 6) {
|
||||
if (can_id >= GM6020_FB_ID_BASE && can_id <= GM6020_FB_ID_BASE + 6) {
|
||||
return can_id - GM6020_FB_ID_BASE + 4;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
导入查询问题修复 - 快速总结
|
||||
|
||||
问题: 导入的账户无法查询
|
||||
原因: 导入时元数据ID没有更新,导致ID不一致
|
||||
|
||||
解决:
|
||||
1. 修改 finance_manager.py 的 import_account_package() 方法
|
||||
- 创建新ID时,更新metadata.json中的id字段
|
||||
|
||||
2. 修改 finance_interface.py 的 import_account() 方法
|
||||
- 导入后自动设置为当前账户
|
||||
- 清空查询结果表格
|
||||
|
||||
3. 修改 on_account_changed() 方法
|
||||
- 切换账户时清空查询结果
|
||||
|
||||
结果:
|
||||
✅ 导入账户正常
|
||||
✅ 账户数据可查询
|
||||
✅ 所有交易记录可见
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
def main():
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ 导入查询问题 - 修复完成 ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📝 修复内容:
|
||||
|
||||
1️⃣ finance_manager.py - import_account_package()
|
||||
✅ 创建新ID时同步更新metadata.json中的id
|
||||
|
||||
2️⃣ finance_interface.py - import_account()
|
||||
✅ 导入后自动设置为当前账户
|
||||
✅ 自动清空查询结果表格
|
||||
|
||||
3️⃣ finance_interface.py - on_account_changed()
|
||||
✅ 切换账户时清空查询结果
|
||||
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ 验证结果:
|
||||
|
||||
""")
|
||||
|
||||
fm = FinanceManager()
|
||||
accounts = fm.get_all_accounts()
|
||||
|
||||
print(f"📊 当前账户数: {len(accounts)}\n")
|
||||
|
||||
for i, acc in enumerate(accounts, 1):
|
||||
trans_count = len(acc.transactions)
|
||||
print(f"[{i}] {acc.name}")
|
||||
print(f" ├─ ID: {acc.id}")
|
||||
print(f" └─ 交易数: {trans_count}")
|
||||
|
||||
if trans_count > 0:
|
||||
# 测试查询
|
||||
results = fm.query_transactions(acc.id)
|
||||
print(f" ✅ 查询测试: {len(results)} 条记录可查询")
|
||||
|
||||
print()
|
||||
|
||||
print("═══════════════════════════════════════════════════════════════════")
|
||||
print("🎉 问题已解决! 您现在可以:")
|
||||
print(" 1. 导入账户到本系统")
|
||||
print(" 2. 所有交易记录会自动显示")
|
||||
print(" 3. 可以正常进行查询操作")
|
||||
print("═══════════════════════════════════════════════════════════════════\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
6
config/config.json
Normal file
6
config/config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"QFluentWidgets": {
|
||||
"ThemeColor": "#fff18cb9",
|
||||
"ThemeMode": "Light"
|
||||
}
|
||||
}
|
||||
138
debug_finance.py
138
debug_finance.py
@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
财务模块调试脚本
|
||||
用于测试财务管理器的基本功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("财务模块调试")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 初始化财务管理器
|
||||
print("\n1. 初始化财务管理器...")
|
||||
try:
|
||||
fm = FinanceManager()
|
||||
print(f"✅ 初始化成功")
|
||||
print(f" 数据目录: {fm.data_root}")
|
||||
print(f" 目录存在: {fm.data_root.exists()}")
|
||||
except Exception as e:
|
||||
print(f"❌ 初始化失败: {e}")
|
||||
return
|
||||
|
||||
# 2. 获取现有账户
|
||||
print("\n2. 获取现有账户...")
|
||||
try:
|
||||
accounts = fm.get_all_accounts()
|
||||
print(f"✅ 获取成功")
|
||||
print(f" 账户数: {len(accounts)}")
|
||||
for acc in accounts:
|
||||
print(f" - {acc.name} (ID: {acc.id})")
|
||||
except Exception as e:
|
||||
print(f"❌ 获取失败: {e}")
|
||||
return
|
||||
|
||||
# 3. 创建测试账户
|
||||
print("\n3. 创建测试账户...")
|
||||
try:
|
||||
account = fm.create_account(
|
||||
account_name="测试账户",
|
||||
description="用于调试的测试账户"
|
||||
)
|
||||
print(f"✅ 创建成功")
|
||||
print(f" 账户ID: {account.id}")
|
||||
print(f" 账户名: {account.name}")
|
||||
except Exception as e:
|
||||
print(f"❌ 创建失败: {e}")
|
||||
return
|
||||
|
||||
# 4. 添加交易记录
|
||||
print("\n4. 添加交易记录...")
|
||||
try:
|
||||
transaction = Transaction(
|
||||
date="2024-11-25",
|
||||
amount=100.0,
|
||||
trader="测试商家",
|
||||
notes="测试交易记录"
|
||||
)
|
||||
fm.add_transaction(account.id, transaction)
|
||||
print(f"✅ 添加成功")
|
||||
print(f" 交易ID: {transaction.id}")
|
||||
print(f" 日期: {transaction.date}")
|
||||
print(f" 金额: ¥{transaction.amount}")
|
||||
except Exception as e:
|
||||
print(f"❌ 添加失败: {e}")
|
||||
return
|
||||
|
||||
# 5. 查询账户
|
||||
print("\n5. 查询账户信息...")
|
||||
try:
|
||||
acc = fm.get_account(account.id)
|
||||
print(f"✅ 查询成功")
|
||||
print(f" 账户名: {acc.name}")
|
||||
print(f" 记录数: {len(acc.transactions)}")
|
||||
|
||||
for trans in acc.transactions:
|
||||
print(f" - {trans.date}: {trans.trader} ¥{trans.amount}")
|
||||
except Exception as e:
|
||||
print(f"❌ 查询失败: {e}")
|
||||
return
|
||||
|
||||
# 6. 获取账户汇总
|
||||
print("\n6. 获取账户汇总...")
|
||||
try:
|
||||
summary = fm.get_account_summary(account.id)
|
||||
print(f"✅ 获取成功")
|
||||
print(f" 账户名: {summary['account_name']}")
|
||||
print(f" 总额: ¥{summary['total_amount']:.2f}")
|
||||
print(f" 记录数: {summary['transaction_count']}")
|
||||
except Exception as e:
|
||||
print(f"❌ 获取失败: {e}")
|
||||
return
|
||||
|
||||
# 7. 查询功能
|
||||
print("\n7. 测试查询功能...")
|
||||
try:
|
||||
results = fm.query_transactions(
|
||||
account.id,
|
||||
date_start="2024-01-01",
|
||||
date_end="2024-12-31"
|
||||
)
|
||||
print(f"✅ 查询成功")
|
||||
print(f" 查询结果: {len(results)} 条记录")
|
||||
except Exception as e:
|
||||
print(f"❌ 查询失败: {e}")
|
||||
return
|
||||
|
||||
# 8. 备份功能
|
||||
print("\n8. 测试备份功能...")
|
||||
try:
|
||||
success = fm.backup_all_accounts()
|
||||
if success:
|
||||
print(f"✅ 备份成功")
|
||||
backup_dir = fm.data_root / "backups"
|
||||
if backup_dir.exists():
|
||||
backups = list(backup_dir.glob("*.zip"))
|
||||
print(f" 备份文件数: {len(backups)}")
|
||||
if backups:
|
||||
print(f" 最新备份: {backups[-1].name}")
|
||||
else:
|
||||
print(f"❌ 备份失败")
|
||||
except Exception as e:
|
||||
print(f"❌ 备份失败: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("调试完成!所有功能正常运行 ✅")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""调试导入后查询无法显示的问题"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
def debug_import_issue():
|
||||
"""调试导入问题"""
|
||||
fm = FinanceManager()
|
||||
|
||||
print("=" * 60)
|
||||
print("财务模块 - 导入查询调试")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 显示当前所有账户
|
||||
print("\n1️⃣ 当前所有账户:")
|
||||
all_accounts = fm.get_all_accounts()
|
||||
for i, account in enumerate(all_accounts, 1):
|
||||
print(f" [{i}] {account.name} (ID: {account.id})")
|
||||
print(f" 交易数: {len(account.transactions)}")
|
||||
if account.transactions:
|
||||
for j, trans in enumerate(account.transactions[:3], 1):
|
||||
print(f" - [{j}] {trans.date} | {trans.trader} | ¥{trans.amount:.2f}")
|
||||
if len(account.transactions) > 3:
|
||||
print(f" ... 还有 {len(account.transactions) - 3} 条记录")
|
||||
|
||||
if not all_accounts:
|
||||
print(" ❌ 没有账户")
|
||||
return
|
||||
|
||||
# 2. 针对每个账户进行详细检查
|
||||
for account in all_accounts:
|
||||
print(f"\n2️⃣ 账户 '{account.name}' 的详细信息:")
|
||||
print(f" - 账户 ID: {account.id}")
|
||||
print(f" - 创建时间: {account.created_at}")
|
||||
print(f" - 交易总数: {len(account.transactions)}")
|
||||
|
||||
if account.transactions:
|
||||
print(f" - 金额范围: ¥{min(t.amount for t in account.transactions):.2f} ~ ¥{max(t.amount for t in account.transactions):.2f}")
|
||||
|
||||
# 按日期排序显示
|
||||
sorted_trans = sorted(account.transactions, key=lambda x: x.date)
|
||||
print(f" - 日期范围: {sorted_trans[0].date} ~ {sorted_trans[-1].date}")
|
||||
|
||||
# 显示样本交易
|
||||
print(f"\n 📋 交易样本 (前5条):")
|
||||
for i, trans in enumerate(sorted_trans[:5], 1):
|
||||
print(f" [{i}] 日期: {trans.date}")
|
||||
print(f" 交易人: {trans.trader}")
|
||||
print(f" 金额: ¥{trans.amount:.2f}")
|
||||
print(f" 备注: {trans.notes or '(无)'}")
|
||||
print(f" ID: {trans.id}")
|
||||
|
||||
# 3. 测试查询功能
|
||||
print("\n3️⃣ 测试查询功能:")
|
||||
if all_accounts:
|
||||
test_account = all_accounts[0]
|
||||
print(f" 使用账户: '{test_account.name}'")
|
||||
|
||||
# 无条件查询
|
||||
print(f"\n a) 无条件查询:")
|
||||
results = fm.query_transactions(test_account.id)
|
||||
print(f" 结果数: {len(results)} 条")
|
||||
|
||||
# 有条件查询
|
||||
if test_account.transactions:
|
||||
first_trans = sorted(test_account.transactions, key=lambda x: x.date)[0]
|
||||
print(f"\n b) 按日期查询 (>= {first_trans.date}):")
|
||||
results = fm.query_transactions(test_account.id, date_start=first_trans.date)
|
||||
print(f" 结果数: {len(results)} 条")
|
||||
|
||||
# 金额查询
|
||||
min_amount = min(t.amount for t in test_account.transactions)
|
||||
print(f"\n c) 按金额查询 (>= ¥{min_amount:.2f}):")
|
||||
results = fm.query_transactions(test_account.id, amount_min=min_amount)
|
||||
print(f" 结果数: {len(results)} 条")
|
||||
|
||||
# 4. 检查数据文件
|
||||
print("\n4️⃣ 检查数据文件位置:")
|
||||
data_root = fm.data_root
|
||||
print(f" 数据根目录: {data_root}")
|
||||
|
||||
accounts_dir = data_root / 'accounts'
|
||||
if accounts_dir.exists():
|
||||
print(f" 账户目录存在: ✅")
|
||||
account_dirs = list(accounts_dir.iterdir())
|
||||
print(f" 账户文件夹数: {len(account_dirs)}")
|
||||
for acc_dir in account_dirs[:3]:
|
||||
print(f" - {acc_dir.name}/")
|
||||
metadata_file = acc_dir / 'metadata.json'
|
||||
if metadata_file.exists():
|
||||
print(f" metadata.json: ✅")
|
||||
else:
|
||||
print(f" 账户目录不存在: ❌")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("调试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug_import_issue()
|
||||
@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试 admin 账户自动创建功能
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
# 清除旧数据
|
||||
data_root = Path("assets/Finance_Data")
|
||||
if data_root.exists():
|
||||
shutil.rmtree(data_root)
|
||||
|
||||
# 测试1: 第一次初始化,应该创建 admin 账户
|
||||
print("测试1: 第一次初始化(应该创建 admin 账户)")
|
||||
fm1 = FinanceManager()
|
||||
accounts1 = fm1.get_all_accounts()
|
||||
print(f" 账户数量: {len(accounts1)}")
|
||||
if accounts1:
|
||||
print(f" 第一个账户: 名称={accounts1[0].name}, ID={accounts1[0].id}")
|
||||
print(f" 是否为 admin: {accounts1[0].name == 'admin'}")
|
||||
assert len(accounts1) == 1, "应该有1个账户"
|
||||
assert accounts1[0].name == "admin", "账户名称应该是 'admin'"
|
||||
print(" ✓ 通过!\n")
|
||||
|
||||
# 测试2: 再次初始化,应该加载现有的 admin 账户
|
||||
print("测试2: 再次初始化(应该加载现有的 admin 账户)")
|
||||
fm2 = FinanceManager()
|
||||
accounts2 = fm2.get_all_accounts()
|
||||
print(f" 账户数量: {len(accounts2)}")
|
||||
if accounts2:
|
||||
print(f" 第一个账户: 名称={accounts2[0].name}, ID={accounts2[0].id}")
|
||||
print(f" 账户ID是否相同: {accounts1[0].id == accounts2[0].id}")
|
||||
assert len(accounts2) == 1, "应该有1个账户"
|
||||
assert accounts2[0].name == "admin", "账户名称应该是 'admin'"
|
||||
assert accounts1[0].id == accounts2[0].id, "账户ID应该相同"
|
||||
print(" ✓ 通过!\n")
|
||||
|
||||
# 测试3: 添加新账户后,应该仍然能找到 admin 账户
|
||||
print("测试3: 添加新账户后,应该仍然能找到 admin 账户")
|
||||
fm2.create_account("test", "测试账户")
|
||||
accounts3 = fm2.get_all_accounts()
|
||||
print(f" 账户数量: {len(accounts3)}")
|
||||
admin_found = False
|
||||
for acc in accounts3:
|
||||
print(f" - {acc.name}")
|
||||
if acc.name == "admin":
|
||||
admin_found = True
|
||||
assert len(accounts3) == 2, "应该有2个账户"
|
||||
assert admin_found, "应该找到 admin 账户"
|
||||
print(" ✓ 通过!\n")
|
||||
|
||||
# 测试4: 测试 admin 账户的分类
|
||||
print("测试4: 测试 admin 账户的分类")
|
||||
admin_acc = None
|
||||
for acc in accounts3:
|
||||
if acc.name == "admin":
|
||||
admin_acc = acc
|
||||
break
|
||||
assert admin_acc is not None
|
||||
print(f" 默认分类: {admin_acc.categories}")
|
||||
assert len(admin_acc.categories) > 0, "应该有默认分类"
|
||||
print(" ✓ 通过!\n")
|
||||
|
||||
print("所有测试都通过了!✓")
|
||||
@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试分类功能"""
|
||||
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
# 创建财务管理器
|
||||
fm = FinanceManager()
|
||||
|
||||
# 创建一个测试账户
|
||||
account = fm.create_account("测试账户", "用于测试分类功能")
|
||||
print(f"创建账户: {account.name} (ID: {account.id})")
|
||||
print(f"初始分类: {account.categories}")
|
||||
|
||||
# 添加新分类
|
||||
print("\n添加新分类...")
|
||||
fm.add_category(account.id, "工作支出")
|
||||
fm.add_category(account.id, "个人投资")
|
||||
|
||||
account = fm.get_account(account.id)
|
||||
print(f"更新后的分类: {account.categories}")
|
||||
|
||||
# 创建交易记录
|
||||
print("\n创建交易记录...")
|
||||
trans1 = Transaction(
|
||||
date="2024-01-01",
|
||||
amount=1000,
|
||||
trader="张三",
|
||||
notes="工资",
|
||||
category="工资"
|
||||
)
|
||||
fm.add_transaction(account.id, trans1)
|
||||
|
||||
trans2 = Transaction(
|
||||
date="2024-01-02",
|
||||
amount=-500,
|
||||
trader="李四",
|
||||
notes="午餐",
|
||||
category="饮食"
|
||||
)
|
||||
fm.add_transaction(account.id, trans2)
|
||||
|
||||
trans3 = Transaction(
|
||||
date="2024-01-03",
|
||||
amount=-200,
|
||||
trader="公司",
|
||||
notes="项目费用",
|
||||
category="工作支出"
|
||||
)
|
||||
fm.add_transaction(account.id, trans3)
|
||||
|
||||
# 查询交易记录
|
||||
print("\n所有交易记录:")
|
||||
account = fm.get_account(account.id)
|
||||
for trans in account.transactions:
|
||||
print(f" {trans.date} | {trans.trader} | {trans.category} | ¥{trans.amount:.2f} | {trans.notes}")
|
||||
|
||||
# 按分类查询
|
||||
print("\n按分类查询 - 工资:")
|
||||
results = fm.query_transactions(account.id, category="工资")
|
||||
for trans in results:
|
||||
print(f" {trans.date} | {trans.trader} | {trans.category} | ¥{trans.amount:.2f}")
|
||||
|
||||
print("\n按分类查询 - 饮食:")
|
||||
results = fm.query_transactions(account.id, category="饮食")
|
||||
for trans in results:
|
||||
print(f" {trans.date} | {trans.trader} | {trans.category} | ¥{trans.amount:.2f}")
|
||||
|
||||
print("\nTest passed! ✓")
|
||||
@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试查询页面分类下拉框动态更新功能
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
# 清除旧数据
|
||||
data_root = Path("assets/Finance_Data")
|
||||
if data_root.exists():
|
||||
shutil.rmtree(data_root)
|
||||
|
||||
print("=" * 60)
|
||||
print("测试查询页面分类下拉框动态更新")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化财务管理器
|
||||
fm = FinanceManager()
|
||||
admin_acc = fm.get_all_accounts()[0]
|
||||
|
||||
print("\n[初始状态] 检查初始分类列表")
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 初始分类数: {len(categories)}")
|
||||
print(f" 初始分类: {categories}")
|
||||
assert len(categories) == 0, "初始应该没有分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试1] 创建第一个分类")
|
||||
success = fm.add_category(admin_acc.id, "工资")
|
||||
print(f" 添加 '工资': {'成功' if success else '失败'}")
|
||||
assert success, "应该成功添加分类"
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 当前分类: {categories}")
|
||||
assert "工资" in categories
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试2] 创建第二个分类")
|
||||
success = fm.add_category(admin_acc.id, "房租")
|
||||
print(f" 添加 '房租': {'成功' if success else '失败'}")
|
||||
assert success
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 当前分类: {categories}")
|
||||
assert len(categories) == 2
|
||||
assert "工资" in categories and "房租" in categories
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试3] 创建第三个分类")
|
||||
success = fm.add_category(admin_acc.id, "食品")
|
||||
print(f" 添加 '食品': {'成功' if success else '失败'}")
|
||||
assert success
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 当前分类: {categories}")
|
||||
assert len(categories) == 3
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试4] 验证分类持久化")
|
||||
# 重新加载账户
|
||||
fm2 = FinanceManager()
|
||||
admin_acc2 = None
|
||||
for acc in fm2.get_all_accounts():
|
||||
if acc.name == "admin":
|
||||
admin_acc2 = acc
|
||||
break
|
||||
|
||||
assert admin_acc2 is not None
|
||||
categories2 = fm2.get_categories(admin_acc2.id)
|
||||
print(f" 重新加载后的分类: {categories2}")
|
||||
assert len(categories2) == 3
|
||||
assert set(categories2) == {"工资", "房租", "食品"}
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试5] 创建交易并按分类查询")
|
||||
# 创建测试交易
|
||||
trans_data = [
|
||||
("2025-01-01", 5000, "工作", "工资", "1月工资"),
|
||||
("2025-01-05", -1500, "房东", "房租", "房租"),
|
||||
("2025-01-10", -200, "超市", "食品", "食材"),
|
||||
]
|
||||
|
||||
for date, amount, trader, category, notes in trans_data:
|
||||
trans = Transaction(date=date, amount=amount, trader=trader, category=category, notes=notes)
|
||||
fm2.add_transaction(admin_acc2.id, trans)
|
||||
|
||||
print(f" 创建了 {len(trans_data)} 条交易")
|
||||
|
||||
# 按各分类查询
|
||||
for cat in categories2:
|
||||
results = fm2.query_transactions(admin_acc2.id, category=cat)
|
||||
print(f" 查询 '{cat}' 分类: {len(results)} 条")
|
||||
assert len(results) == 1, f"应该有1条 {cat} 分类的交易"
|
||||
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有动态分类测试都通过了!✓")
|
||||
print("=" * 60)
|
||||
print("\n说明:")
|
||||
print("1. 新建的分类可以立即在下拉框中选择")
|
||||
print("2. 分类数据会被正确保存和加载")
|
||||
print("3. 创建的交易可以按分类进行查询")
|
||||
@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试财务模块的分类功能完整流程
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
# 清除旧数据
|
||||
data_root = Path("assets/Finance_Data")
|
||||
if data_root.exists():
|
||||
shutil.rmtree(data_root)
|
||||
|
||||
print("=" * 60)
|
||||
print("财务模块分类功能测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化财务管理器
|
||||
fm = FinanceManager()
|
||||
|
||||
# 测试1: 验证 admin 账户自动创建
|
||||
print("\n[测试1] admin 账户自动创建")
|
||||
accounts = fm.get_all_accounts()
|
||||
print(f" 账户数量: {len(accounts)}")
|
||||
assert len(accounts) == 1, "应该有1个账户"
|
||||
admin_acc = accounts[0]
|
||||
print(f" 账户名称: {admin_acc.name}")
|
||||
assert admin_acc.name == "admin", "账户名称应该是 'admin'"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试2: 验证默认分类
|
||||
print("\n[测试2] 默认分类")
|
||||
print(f" 分类数量: {len(admin_acc.categories)}")
|
||||
print(f" 分类列表: {admin_acc.categories}")
|
||||
assert len(admin_acc.categories) > 0, "应该有默认分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试3: 添加新分类
|
||||
print("\n[测试3] 添加新分类")
|
||||
result = fm.add_category(admin_acc.id, "房租")
|
||||
print(f" 添加 '房租' 分类: {'成功' if result else '失败'}")
|
||||
assert result, "应该成功添加分类"
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 分类数量: {len(categories)}")
|
||||
assert "房租" in categories, "应该包含 '房租' 分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试4: 创建带分类的交易记录
|
||||
print("\n[测试4] 创建带分类的交易记录")
|
||||
trans1 = Transaction(
|
||||
date="2025-01-01",
|
||||
amount=5000,
|
||||
trader="工作所得",
|
||||
notes="1月工资",
|
||||
category="工资"
|
||||
)
|
||||
fm.add_transaction(admin_acc.id, trans1)
|
||||
print(f" 交易1: {trans1.date} | {trans1.trader} | {trans1.category} | ¥{trans1.amount}")
|
||||
|
||||
trans2 = Transaction(
|
||||
date="2025-01-05",
|
||||
amount=-1500,
|
||||
trader="房东",
|
||||
notes="1月房租",
|
||||
category="房租"
|
||||
)
|
||||
fm.add_transaction(admin_acc.id, trans2)
|
||||
print(f" 交易2: {trans2.date} | {trans2.trader} | {trans2.category} | ¥{trans2.amount}")
|
||||
|
||||
# 重新加载以验证保存
|
||||
fm.load_all_accounts()
|
||||
admin_acc = fm.get_account(admin_acc.id)
|
||||
print(f" 账户交易总数: {len(admin_acc.transactions)}")
|
||||
assert len(admin_acc.transactions) == 2, "应该有2个交易"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试5: 按分类查询
|
||||
print("\n[测试5] 按分类查询")
|
||||
results_salary = fm.query_transactions(admin_acc.id, category="工资")
|
||||
print(f" '工salary' 分类的交易: {len(results_salary)} 条")
|
||||
assert len(results_salary) == 1, "应该有1个工资交易"
|
||||
assert results_salary[0].amount == 5000
|
||||
|
||||
results_rent = fm.query_transactions(admin_acc.id, category="房租")
|
||||
print(f" '房租' 分类的交易: {len(results_rent)} 条")
|
||||
assert len(results_rent) == 1, "应该有1个房租交易"
|
||||
assert results_rent[0].amount == -1500
|
||||
|
||||
results_all = fm.query_transactions(admin_acc.id)
|
||||
print(f" 全部分类的交易: {len(results_all)} 条")
|
||||
assert len(results_all) == 2, "应该有2个交易"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试6: 更新交易分类
|
||||
print("\n[测试6] 更新交易分类")
|
||||
fm.update_transaction(admin_acc.id, trans1.id, category="奖金")
|
||||
fm.load_all_accounts()
|
||||
admin_acc = fm.get_account(admin_acc.id)
|
||||
updated_trans = fm.get_transaction(admin_acc.id, trans1.id)
|
||||
print(f" 更新后的分类: {updated_trans.category}")
|
||||
assert updated_trans.category == "奖金", "分类应该更新为 '奖金'"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试7: CSV导出包含分类
|
||||
print("\n[测试7] CSV导出包含分类")
|
||||
csv_path = "/tmp/test_export.csv"
|
||||
result = fm.export_to_csv(admin_acc.id, csv_path)
|
||||
print(f" 导出结果: {'成功' if result else '失败'}")
|
||||
assert result, "应该成功导出CSV"
|
||||
# 验证CSV内容
|
||||
with open(csv_path, 'r', encoding='utf-8-sig') as f:
|
||||
lines = f.readlines()
|
||||
print(f" CSV行数: {len(lines)}")
|
||||
print(f" 标题行: {lines[0].strip()}")
|
||||
assert "分类" in lines[0], "CSV应该包含 '分类' 列"
|
||||
print(f" 数据行1: {lines[1].strip()}")
|
||||
print(f" 数据行2: {lines[2].strip()}")
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试都通过了!✓")
|
||||
print("=" * 60)
|
||||
@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
分类管理功能测试脚本
|
||||
测试新增、重命名、删除分类的功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 添加app目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
|
||||
def test_rename_category():
|
||||
"""测试重命名分类"""
|
||||
print("=== 测试重命名分类 ===")
|
||||
|
||||
# 创建测试用的FinanceManager
|
||||
test_data_root = Path("/tmp/test_finance_data_rename")
|
||||
fm = FinanceManager(str(test_data_root))
|
||||
|
||||
# 获取admin账户或创建一个测试账户
|
||||
account_ids = list(fm.accounts.keys())
|
||||
if not account_ids:
|
||||
account_id = fm.create_account("test_account", "测试账户")
|
||||
else:
|
||||
account_id = account_ids[0]
|
||||
|
||||
print(f"使用账户: {account_id}")
|
||||
|
||||
# 添加分类
|
||||
print("添加分类: 食品、交通、娱乐")
|
||||
assert fm.add_category(account_id, "食品"), "添加分类'食品'失败"
|
||||
assert fm.add_category(account_id, "交通"), "添加分类'交通'失败"
|
||||
assert fm.add_category(account_id, "娱乐"), "添加分类'娱乐'失败"
|
||||
|
||||
categories = fm.get_categories(account_id)
|
||||
print(f"当前分类: {categories}")
|
||||
assert len(categories) == 3, f"期望3个分类,实际{len(categories)}个"
|
||||
|
||||
# 添加一个使用"食品"分类的交易
|
||||
trans = Transaction(
|
||||
date="2024-01-01",
|
||||
amount=100.0,
|
||||
trader="超市",
|
||||
notes="购买食品",
|
||||
category="食品"
|
||||
)
|
||||
fm.add_transaction(account_id, trans)
|
||||
print(f"添加交易,分类为'食品': {trans.id}")
|
||||
|
||||
# 重命名分类
|
||||
print("重命名分类: 食品 -> 饮食")
|
||||
assert fm.rename_category(account_id, "食品", "饮食"), "重命名分类失败"
|
||||
|
||||
categories = fm.get_categories(account_id)
|
||||
print(f"重命名后分类: {categories}")
|
||||
assert "饮食" in categories, "重命名后分类中没有'饮食'"
|
||||
assert "食品" not in categories, "重命名后分类中仍有'食品'"
|
||||
|
||||
# 验证交易的分类也被更新了
|
||||
# 需要重新加载账户数据
|
||||
fm.load_all_accounts()
|
||||
account = fm.accounts[account_id]
|
||||
for t in account.transactions:
|
||||
if t.id == trans.id:
|
||||
print(f"交易分类已更新为: {t.category}")
|
||||
assert t.category == "饮食", f"交易分类应该是'饮食',实际是'{t.category}'"
|
||||
break
|
||||
|
||||
# 测试重命名失败的情况:新分类名已存在
|
||||
print("测试重命名失败情况: 新分类名'交通'已存在")
|
||||
assert not fm.rename_category(account_id, "饮食", "交通"), "应该重命名失败"
|
||||
|
||||
print("✓ 重命名分类测试通过\n")
|
||||
|
||||
|
||||
def test_delete_category():
|
||||
"""测试删除分类"""
|
||||
print("=== 测试删除分类 ===")
|
||||
|
||||
test_data_root = Path("/tmp/test_finance_data_delete")
|
||||
fm = FinanceManager(str(test_data_root))
|
||||
|
||||
account_ids = list(fm.accounts.keys())
|
||||
if not account_ids:
|
||||
account_id = fm.create_account("test_account", "测试账户")
|
||||
else:
|
||||
account_id = account_ids[0]
|
||||
|
||||
print(f"使用账户: {account_id}")
|
||||
|
||||
# 添加分类
|
||||
print("添加分类: 食品、交通、娱乐")
|
||||
fm.add_category(account_id, "食品")
|
||||
fm.add_category(account_id, "交通")
|
||||
fm.add_category(account_id, "娱乐")
|
||||
|
||||
# 添加使用"食品"分类的交易
|
||||
trans1 = Transaction(
|
||||
date="2024-01-01",
|
||||
amount=100.0,
|
||||
trader="超市",
|
||||
notes="购买食品",
|
||||
category="食品"
|
||||
)
|
||||
trans2 = Transaction(
|
||||
date="2024-01-02",
|
||||
amount=50.0,
|
||||
trader="出租车",
|
||||
notes="交通费用",
|
||||
category="交通"
|
||||
)
|
||||
|
||||
fm.add_transaction(account_id, trans1)
|
||||
fm.add_transaction(account_id, trans2)
|
||||
|
||||
print(f"添加交易1(分类'食品'): {trans1.id}")
|
||||
print(f"添加交易2(分类'交通'): {trans2.id}")
|
||||
|
||||
# 删除"食品"分类
|
||||
print("删除分类: 食品")
|
||||
assert fm.delete_category(account_id, "食品"), "删除分类失败"
|
||||
|
||||
categories = fm.get_categories(account_id)
|
||||
print(f"删除后分类: {categories}")
|
||||
assert "食品" not in categories, "删除后分类中仍有'食品'"
|
||||
|
||||
# 验证使用"食品"分类的交易分类被清空了
|
||||
fm.load_all_accounts()
|
||||
account = fm.accounts[account_id]
|
||||
for t in account.transactions:
|
||||
if t.id == trans1.id:
|
||||
print(f"使用已删除分类的交易,其分类现在为: '{t.category}'")
|
||||
assert t.category == "", f"交易分类应该被清空,实际是'{t.category}'"
|
||||
elif t.id == trans2.id:
|
||||
print(f"使用'交通'分类的交易,其分类仍为: '{t.category}'")
|
||||
assert t.category == "交通", f"交易分类应该保持'交通',实际是'{t.category}'"
|
||||
|
||||
print("✓ 删除分类测试通过\n")
|
||||
|
||||
|
||||
def test_add_category():
|
||||
"""测试添加分类"""
|
||||
print("=== 测试添加分类 ===")
|
||||
|
||||
test_data_root = Path("/tmp/test_finance_data_add")
|
||||
fm = FinanceManager(str(test_data_root))
|
||||
|
||||
account_ids = list(fm.accounts.keys())
|
||||
if not account_ids:
|
||||
account_id = fm.create_account("test_account", "测试账户")
|
||||
else:
|
||||
account_id = account_ids[0]
|
||||
|
||||
print(f"使用账户: {account_id}")
|
||||
|
||||
# 初始应该没有分类
|
||||
categories = fm.get_categories(account_id)
|
||||
print(f"初始分类: {categories}")
|
||||
|
||||
# 添加分类
|
||||
print("添加分类: 食品")
|
||||
assert fm.add_category(account_id, "食品"), "添加分类失败"
|
||||
|
||||
categories = fm.get_categories(account_id)
|
||||
print(f"添加后分类: {categories}")
|
||||
assert "食品" in categories, "分类中没有'食品'"
|
||||
|
||||
# 测试添加重复分类
|
||||
print("测试添加重复分类")
|
||||
assert not fm.add_category(account_id, "食品"), "应该返回False"
|
||||
|
||||
categories = fm.get_categories(account_id)
|
||||
assert len(categories) == 1, f"应该只有1个分类,实际{len(categories)}个"
|
||||
|
||||
print("✓ 添加分类测试通过\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""运行所有测试"""
|
||||
print("开始测试分类管理功能...\n")
|
||||
|
||||
try:
|
||||
test_add_category()
|
||||
test_rename_category()
|
||||
test_delete_category()
|
||||
|
||||
print("=" * 50)
|
||||
print("✓ 所有测试通过!")
|
||||
print("=" * 50)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试分类查询功能
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
# 清除旧数据
|
||||
data_root = Path("assets/Finance_Data")
|
||||
if data_root.exists():
|
||||
shutil.rmtree(data_root)
|
||||
|
||||
print("=" * 60)
|
||||
print("测试分类查询功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化财务管理器
|
||||
fm = FinanceManager()
|
||||
admin_acc = fm.get_all_accounts()[0]
|
||||
|
||||
# 创建测试分类
|
||||
fm.add_category(admin_acc.id, "工资")
|
||||
fm.add_category(admin_acc.id, "房租")
|
||||
fm.add_category(admin_acc.id, "食品")
|
||||
print("\n✓ 创建分类: 工资, 房租, 食品")
|
||||
|
||||
# 创建测试交易
|
||||
transactions_data = [
|
||||
("2025-01-01", 5000, "工作所得", "工资", "1月工资"),
|
||||
("2025-01-05", -1500, "房东", "房租", "1月房租"),
|
||||
("2025-01-10", -300, "超市", "食品", "日常食品"),
|
||||
("2025-01-15", 5000, "奖励", "工资", "绩效奖励"),
|
||||
("2025-01-20", -200, "便利店", "食品", "零食"),
|
||||
]
|
||||
|
||||
for date, amount, trader, category, notes in transactions_data:
|
||||
trans = Transaction(date=date, amount=amount, trader=trader, category=category, notes=notes)
|
||||
fm.add_transaction(admin_acc.id, trans)
|
||||
|
||||
print("✓ 创建5条交易记录")
|
||||
|
||||
# 测试按分类查询
|
||||
print("\n[测试1] 查询 '工资' 分类")
|
||||
results = fm.query_transactions(admin_acc.id, category="工资")
|
||||
print(f" 结果: {len(results)} 条")
|
||||
for t in results:
|
||||
print(f" - {t.date} | {t.trader} | {t.category} | ¥{t.amount}")
|
||||
assert len(results) == 2, "应该有2条工资交易"
|
||||
assert all(t.category == "工资" for t in results), "所有交易应该是工资分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试2] 查询 '房租' 分类")
|
||||
results = fm.query_transactions(admin_acc.id, category="房租")
|
||||
print(f" 结果: {len(results)} 条")
|
||||
for t in results:
|
||||
print(f" - {t.date} | {t.trader} | {t.category} | ¥{t.amount}")
|
||||
assert len(results) == 1, "应该有1条房租交易"
|
||||
assert results[0].category == "房租"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试3] 查询 '食品' 分类")
|
||||
results = fm.query_transactions(admin_acc.id, category="食品")
|
||||
print(f" 结果: {len(results)} 条")
|
||||
for t in results:
|
||||
print(f" - {t.date} | {t.trader} | {t.category} | ¥{t.amount}")
|
||||
assert len(results) == 2, "应该有2条食品交易"
|
||||
assert all(t.category == "食品" for t in results), "所有交易应该是食品分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试4] 查询所有分类 (category=None)")
|
||||
results = fm.query_transactions(admin_acc.id, category=None)
|
||||
print(f" 结果: {len(results)} 条")
|
||||
assert len(results) == 5, "应该返回所有5条交易"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试5] 按分类和金额范围查询")
|
||||
results = fm.query_transactions(admin_acc.id, category="工资", amount_min=0)
|
||||
print(f" 查询 '工资' 且金额>=0: {len(results)} 条")
|
||||
assert len(results) == 2, "应该有2条正数的工资交易"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n[测试6] 按分类和日期范围查询")
|
||||
results = fm.query_transactions(admin_acc.id, category="食品", date_start="2025-01-15")
|
||||
print(f" 查询 '食品' 且日期>=2025-01-15: {len(results)} 条")
|
||||
for t in results:
|
||||
print(f" - {t.date} | {t.trader}")
|
||||
assert len(results) == 1, "应该有1条符合条件的食品交易"
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有分类查询测试都通过了!✓")
|
||||
print("=" * 60)
|
||||
@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
财务界面UI测试脚本
|
||||
验证 SegmentedWidget 和 TableWidget 是否正常工作
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from app.finance_interface import FinanceInterface
|
||||
|
||||
def test_finance_ui():
|
||||
"""测试财务界面 UI"""
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 创建财务界面
|
||||
finance_interface = FinanceInterface()
|
||||
|
||||
# 验证组件存在
|
||||
assert hasattr(finance_interface, 'segmented_widget'), "SegmentedWidget 未创建"
|
||||
assert hasattr(finance_interface, 'stacked_widget'), "StackedWidget 未创建"
|
||||
assert hasattr(finance_interface, 'records_table'), "records_table 未创建"
|
||||
assert hasattr(finance_interface, 'query_result_table'), "query_result_table 未创建"
|
||||
|
||||
# 验证 SegmentedWidget 项目数
|
||||
assert len(finance_interface.segmented_widget.items) == 3, "SegmentedWidget 应该有3个选项卡"
|
||||
|
||||
# 验证表格列数
|
||||
assert finance_interface.records_table.columnCount() == 5, "records_table 应该有5列"
|
||||
assert finance_interface.query_result_table.columnCount() == 5, "query_result_table 应该有5列"
|
||||
|
||||
# 验证标签切换功能
|
||||
finance_interface.segmented_widget.setCurrentItem("query")
|
||||
assert finance_interface.stacked_widget.currentIndex() == 1, "标签页切换失败"
|
||||
|
||||
finance_interface.segmented_widget.setCurrentItem("export")
|
||||
assert finance_interface.stacked_widget.currentIndex() == 2, "标签页切换失败"
|
||||
|
||||
print("✓ 所有 UI 组件验证通过")
|
||||
print("✓ SegmentedWidget 正常工作")
|
||||
print("✓ TableWidget 正常工作")
|
||||
print("✓ 标签页切换功能正常")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if test_finance_ui():
|
||||
print("\n✅ 财务界面 UI 测试成功!")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试导入账户后查询功能"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
import shutil
|
||||
|
||||
# 添加项目根路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager
|
||||
|
||||
def test_import_and_query():
|
||||
"""测试导入和查询流程"""
|
||||
fm = FinanceManager()
|
||||
|
||||
print("=" * 70)
|
||||
print("测试: 导入账户后查询数据")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. 获取现有账户
|
||||
print("\n[步骤1] 获取现有账户")
|
||||
all_accounts = fm.get_all_accounts()
|
||||
print(f"当前有 {len(all_accounts)} 个账户")
|
||||
|
||||
if all_accounts:
|
||||
# 找到有数据的账户
|
||||
test_account = None
|
||||
for account in all_accounts:
|
||||
if account.transactions:
|
||||
test_account = account
|
||||
break
|
||||
|
||||
if test_account:
|
||||
print(f"\n✅ 找到有数据的测试账户: '{test_account.name}'")
|
||||
print(f" 账户ID: {test_account.id}")
|
||||
print(f" 交易数: {len(test_account.transactions)}")
|
||||
|
||||
# 2. 创建ZIP压缩包进行模拟导出/导入
|
||||
print("\n[步骤2] 导出账户为ZIP包")
|
||||
export_dir = Path(fm.data_root) / "temp_export"
|
||||
export_dir.mkdir(exist_ok=True)
|
||||
|
||||
success = fm.export_account_package(test_account.id, str(export_dir))
|
||||
if success:
|
||||
print(f"✅ 导出成功")
|
||||
|
||||
# 找到生成的ZIP文件
|
||||
zip_files = list(export_dir.glob("*.zip"))
|
||||
if zip_files:
|
||||
zip_file = zip_files[0]
|
||||
print(f" ZIP文件: {zip_file.name}")
|
||||
print(f" 文件大小: {zip_file.stat().st_size} bytes")
|
||||
|
||||
# 3. 导入账户
|
||||
print("\n[步骤3] 导入账户")
|
||||
imported_id = fm.import_account_package(str(zip_file))
|
||||
if imported_id:
|
||||
print(f"✅ 导入成功")
|
||||
print(f" 新账户ID: {imported_id}")
|
||||
|
||||
# 4. 验证导入的账户
|
||||
print("\n[步骤4] 验证导入的账户")
|
||||
imported_account = fm.get_account(imported_id)
|
||||
if imported_account:
|
||||
print(f"✅ 导入的账户信息:")
|
||||
print(f" 名称: {imported_account.name}")
|
||||
print(f" 交易数: {len(imported_account.transactions)}")
|
||||
|
||||
# 5. 测试查询导入的账户
|
||||
print("\n[步骤5] 测试查询导入的账户")
|
||||
|
||||
# 无条件查询
|
||||
results = fm.query_transactions(imported_id)
|
||||
print(f"✅ 无条件查询: {len(results)} 条记录")
|
||||
|
||||
if results:
|
||||
print(f"\n 查询结果样本:")
|
||||
for i, trans in enumerate(results[:3], 1):
|
||||
print(f" [{i}] {trans.date} | {trans.trader} | ¥{trans.amount:.2f}")
|
||||
|
||||
# 按交易人查询
|
||||
if imported_account.transactions:
|
||||
trader_name = imported_account.transactions[0].trader
|
||||
results = fm.query_transactions(imported_id, trader=trader_name)
|
||||
print(f"\n✅ 按交易人'{trader_name}'查询: {len(results)} 条记录")
|
||||
else:
|
||||
print(f"❌ 无法获取导入的账户")
|
||||
else:
|
||||
print(f"❌ 导入失败")
|
||||
|
||||
# 清理临时文件
|
||||
print("\n[步骤6] 清理临时文件")
|
||||
shutil.rmtree(export_dir)
|
||||
print("✅ 临时文件已清理")
|
||||
else:
|
||||
print(f"❌ 导出失败")
|
||||
else:
|
||||
print(f"❌ 没有找到有数据的账户用于测试")
|
||||
print(f" 现有账户:")
|
||||
for account in all_accounts:
|
||||
print(f" - {account.name}: {len(account.transactions)} 条交易")
|
||||
else:
|
||||
print(f"❌ 系统中没有账户")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("测试完成")
|
||||
print("=" * 70)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_import_and_query()
|
||||
@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试移除账户选择后的财务模块"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.tools.finance_manager import FinanceManager, Transaction
|
||||
|
||||
def test_without_account_selection():
|
||||
"""测试不需要账户选择的模式"""
|
||||
print("=" * 70)
|
||||
print("测试: 移除账户选择功能后的财务模块")
|
||||
print("=" * 70)
|
||||
|
||||
fm = FinanceManager()
|
||||
|
||||
# 1. 获取所有账户
|
||||
print("\n[1️⃣] 获取所有账户")
|
||||
all_accounts = fm.get_all_accounts()
|
||||
print(f"✅ 找到 {len(all_accounts)} 个账户")
|
||||
|
||||
if not all_accounts:
|
||||
print("❌ 系统中没有账户,无法测试")
|
||||
return False
|
||||
|
||||
# 2. 获取第一个账户作为默认账户
|
||||
print("\n[2️⃣] 获取默认账户(第一个账户)")
|
||||
default_account = all_accounts[0]
|
||||
print(f"✅ 默认账户: {default_account.name} (ID: {default_account.id})")
|
||||
print(f" 交易记录数: {len(default_account.transactions)}")
|
||||
|
||||
# 3. 测试做账功能(新建记录)
|
||||
print("\n[3️⃣] 测试做账功能")
|
||||
new_trans = Transaction(
|
||||
date="2025-11-25",
|
||||
amount=123.45,
|
||||
trader="测试商户",
|
||||
notes="这是一个测试交易"
|
||||
)
|
||||
if fm.add_transaction(default_account.id, new_trans):
|
||||
print(f"✅ 新建记录成功: {new_trans.id}")
|
||||
else:
|
||||
print(f"❌ 新建记录失败")
|
||||
return False
|
||||
|
||||
# 4. 刷新账户信息
|
||||
print("\n[4️⃣] 刷新账户信息")
|
||||
fm.load_all_accounts()
|
||||
updated_account = fm.get_account(default_account.id)
|
||||
if updated_account:
|
||||
print(f"✅ 账户已刷新")
|
||||
print(f" 新的交易记录数: {len(updated_account.transactions)}")
|
||||
|
||||
# 5. 测试查询功能
|
||||
print("\n[5️⃣] 测试查询功能")
|
||||
|
||||
# 无条件查询
|
||||
results = fm.query_transactions(default_account.id)
|
||||
print(f"✅ 无条件查询: {len(results)} 条记录")
|
||||
|
||||
# 按交易人查询
|
||||
results = fm.query_transactions(default_account.id, trader="测试")
|
||||
print(f"✅ 按交易人'测试'查询: {len(results)} 条记录")
|
||||
|
||||
# 按金额查询
|
||||
results = fm.query_transactions(default_account.id, amount_min=100, amount_max=200)
|
||||
print(f"✅ 按金额范围(100-200)查询: {len(results)} 条记录")
|
||||
|
||||
# 6. 测试导出功能
|
||||
print("\n[6️⃣] 测试导出功能")
|
||||
|
||||
# CSV导出
|
||||
csv_path = Path(fm.data_root) / "test_export.csv"
|
||||
if fm.export_to_csv(default_account.id, str(csv_path)):
|
||||
print(f"✅ CSV导出成功: {csv_path.name}")
|
||||
if csv_path.exists():
|
||||
print(f" 文件大小: {csv_path.stat().st_size} bytes")
|
||||
else:
|
||||
print(f"❌ CSV导出失败")
|
||||
|
||||
# 7. 测试备份功能
|
||||
print("\n[7️⃣] 测试备份功能")
|
||||
if fm.backup_all_accounts():
|
||||
print(f"✅ 备份成功")
|
||||
backup_dir = fm.data_root / 'backups'
|
||||
if backup_dir.exists():
|
||||
backup_files = list(backup_dir.glob("*.zip"))
|
||||
print(f" 备份文件数: {len(backup_files)}")
|
||||
else:
|
||||
print(f"❌ 备份失败")
|
||||
|
||||
# 8. 测试账户汇总
|
||||
print("\n[8️⃣] 测试账户汇总")
|
||||
summary = fm.get_account_summary(default_account.id)
|
||||
if summary:
|
||||
print(f"✅ 账户汇总:")
|
||||
print(f" 账户名称: {summary['account_name']}")
|
||||
print(f" 总金额: ¥{summary['total_amount']:.2f}")
|
||||
print(f" 交易笔数: {summary['transaction_count']}")
|
||||
else:
|
||||
print(f"❌ 获取汇总失败")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("✅ 所有测试完成!移除账户选择功能后,系统仍可正常工作")
|
||||
print("=" * 70)
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_without_account_selection()
|
||||
@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试删除默认分类功能
|
||||
"""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.tools.finance_manager import FinanceManager, Transaction, Account
|
||||
|
||||
# 清除旧数据
|
||||
data_root = Path("assets/Finance_Data")
|
||||
if data_root.exists():
|
||||
shutil.rmtree(data_root)
|
||||
|
||||
print("=" * 60)
|
||||
print("测试删除默认分类功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化财务管理器
|
||||
fm = FinanceManager()
|
||||
|
||||
# 测试1: 验证 admin 账户的分类为空
|
||||
print("\n[测试1] 新建账户的分类为空")
|
||||
accounts = fm.get_all_accounts()
|
||||
admin_acc = accounts[0]
|
||||
print(f" 账户名称: {admin_acc.name}")
|
||||
print(f" 分类数量: {len(admin_acc.categories)}")
|
||||
print(f" 分类列表: {admin_acc.categories}")
|
||||
assert len(admin_acc.categories) == 0, "新账户应该没有默认分类"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试2: 用户创建分类
|
||||
print("\n[测试2] 用户创建分类")
|
||||
fm.add_category(admin_acc.id, "工资")
|
||||
fm.add_category(admin_acc.id, "房租")
|
||||
fm.add_category(admin_acc.id, "娱乐")
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 创建的分类: {categories}")
|
||||
assert len(categories) == 3, "应该有3个分类"
|
||||
assert "工资" in categories
|
||||
assert "房租" in categories
|
||||
assert "娱乐" in categories
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试3: 创建交易时必须指定分类
|
||||
print("\n[测试3] 创建交易时分类不能为空")
|
||||
trans1 = Transaction(
|
||||
date="2025-01-01",
|
||||
amount=5000,
|
||||
trader="工作所得",
|
||||
notes="1月工资",
|
||||
category="工资"
|
||||
)
|
||||
assert trans1.category == "工资", "交易应该有分类"
|
||||
print(f" 交易分类: {trans1.category}")
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试4: 验证空分类的交易
|
||||
print("\n[测试4] 检查默认分类为空字符串")
|
||||
trans_no_cat = Transaction(
|
||||
date="2025-01-01",
|
||||
amount=100,
|
||||
trader="测试用户"
|
||||
)
|
||||
print(f" 未指定分类的交易分类: '{trans_no_cat.category}'")
|
||||
assert trans_no_cat.category == "", "未指定分类应该为空字符串"
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试5: 用户可以删除自定义分类
|
||||
print("\n[测试5] 用户可以删除自定义分类")
|
||||
success = fm.delete_category(admin_acc.id, "娱乐")
|
||||
print(f" 删除 '娱乐' 分类: {'成功' if success else '失败'}")
|
||||
assert success, "应该成功删除分类"
|
||||
categories = fm.get_categories(admin_acc.id)
|
||||
print(f" 删除后的分类: {categories}")
|
||||
assert "娱乐" not in categories
|
||||
assert len(categories) == 2
|
||||
print(" ✓ 通过")
|
||||
|
||||
# 测试6: 验证分类持久化
|
||||
print("\n[测试6] 分类数据持久化")
|
||||
# 创建新的财务管理器实例,应该重新加载分类
|
||||
fm2 = FinanceManager()
|
||||
accounts2 = fm2.get_all_accounts()
|
||||
admin_acc2 = accounts2[0]
|
||||
categories2 = fm2.get_categories(admin_acc2.id)
|
||||
print(f" 重新加载后的分类: {categories2}")
|
||||
assert len(categories2) == 2, "应该有2个分类"
|
||||
assert "工资" in categories2
|
||||
assert "房租" in categories2
|
||||
assert "娱乐" not in categories2
|
||||
print(" ✓ 通过")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试都通过了!✓")
|
||||
print("=" * 60)
|
||||
105
开发要求.md
105
开发要求.md
@ -1,105 +0,0 @@
|
||||
# 嵌入式 代码
|
||||
|
||||
## 软件功能介绍
|
||||
|
||||
中心思想:
|
||||
|
||||
- 利用好RTOS和中断,释放CPU性能,保证实时性。
|
||||
- 一个项目适配不同型号的机器人和不同的操作手。
|
||||
|
||||
减少维护的工作量,减少出错的可能性。
|
||||
|
||||
## 依赖&环境
|
||||
|
||||
- Windows平台下用CubeMX生成项目,然后用Keil uvesrion进行编辑、烧写和调试。
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 环境安装
|
||||
- [MDK-ARM](https://www.keil.com/) (必备)
|
||||
- [STM32CubeMX](https://www.st.com/zh/development-tools/stm32cubemx.html) (可选)
|
||||
|
||||
|
||||
- 针对不同板子需要到不同的CubeMX工程文件(DevA.ioc、DevC.ioc)。
|
||||
|
||||
- (可选)利用CubeMX生成对应的外设初始化代码和Keil工程文件。忽略CAN总线相关错误。
|
||||
|
||||
- 每次生成代码后,请利用Git丢弃Middlewares文件夹中的所有改变。原因如下。
|
||||
|
||||
1. 使用了AC6,与CubeMX默认不匹配,会影响到FreeRTOS的移植。
|
||||
2. 使用了比CubeMX更新的FreeRTOS版本,降版本会导致部分代码无法编译。
|
||||
|
||||
- 因为已经生成过Keil工程文件,所以只会覆盖以前生成的代码,而不会影响手写的代码。
|
||||
|
||||
- 每次生成代码后,请在HAL_InitTick函数中添加uwTickPrio = TickPriority;
|
||||
|
||||
- 打开MDK-ARM中的DevC.uvprojx即可进行编辑、烧写或调试。
|
||||
|
||||
- Keil工程中有两个Target,其中Debug用来调试,不包含编译器优化等;DevC/DevA用来编译输出最终固件。
|
||||
|
||||
## 文件目录结构&文件用途说明
|
||||
|
||||
| 文件夹 | 来源 | 内容 |
|
||||
| ---- | ---- | ---- |
|
||||
| Core | CubeMX | 包含核心代码,外设初始化,系统初始化等 |
|
||||
| Doc | 开发者 | 文档 |
|
||||
| Drivers | CubeMX | CMSIS相关库、STM32 HAL |
|
||||
| Image | 开发者 | 图片 |
|
||||
| MDK-ARM | CubeMX | Keil uversion 项目相关文件 |
|
||||
| Middlewares | 开发者 / CubeMX | 中间件 |
|
||||
| USB_DEVICE | CubeMX | USB相关文件 |
|
||||
| User | 开发者 | 手动编写的代码 |
|
||||
| Utils | 开发者 | 使用到的工具,如CubeMonitor, Matlab |
|
||||
|
||||
| User内 | 内容 |
|
||||
| ---- | ---- |
|
||||
| bsp | 文件夹内包含开发板信息,基于STM32 HAL对板载的外设进行控制|
|
||||
| component | 包含各种组件,自成一体,相互依赖,但不依赖于其他文件夹|
|
||||
| device | 独立于开发板的设备,依赖于HAL和bsp|
|
||||
| module | 对机器人各模块的抽象,各模块一起组成机器人|
|
||||
| task | 独立的任务,module的运行容器,也包含通信、姿态解算等 |
|
||||
|
||||
## 系统介绍
|
||||
|
||||
### 硬件系统框图
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| *步兵嵌入式硬件框图* |
|
||||
|
||||
### 软件流程图
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| *步兵嵌入式硬件框图* |
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| *嵌入式程序结构图* |
|
||||
|
||||
## 原理介绍
|
||||
|
||||
### 云台控制原理
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| *云台控制原理(与PX类似)* |
|
||||
|
||||
### 其他参考文献
|
||||
|
||||
- 软件架构参考[PX4 Architectural Overview](https://dev.px4.io/master/en/concept/architecture.html)
|
||||
|
||||
- 云台控制参考[PX4 Controller Diagrams](https://dev.px4.io/master/en/flight_stack/controller_diagrams.html)
|
||||
|
||||
- 底盘Mixer和CAN的Control Group参考[PX4 Mixing and Actuators](https://dev.px4.io/master/en/concept/mixing.html)
|
||||
|
||||
## TODO
|
||||
- 给BSP USB print加保护,允许不同进程的使用。
|
||||
- 给所有BSP加保护
|
||||
- device.c里面加上一个Device_Init(),在里面初始化所有mutex
|
||||
- CAN设备代码优化。消息解析发送方向。
|
||||
- CAN设备动态初始化,保存好几组配置。
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. 在步兵上完成所有功能。
|
||||
Loading…
Reference in New Issue
Block a user