diff --git a/.DS_Store b/.DS_Store index d7f26ce..683c4e5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/CATEGORY_GUIDE.md b/CATEGORY_GUIDE.md deleted file mode 100644 index c2a3178..0000000 --- a/CATEGORY_GUIDE.md +++ /dev/null @@ -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包即可恢复所有分类和交易记录。 diff --git a/CATEGORY_IMPLEMENTATION.md b/CATEGORY_IMPLEMENTATION.md deleted file mode 100644 index b2ba9e9..0000000 --- a/CATEGORY_IMPLEMENTATION.md +++ /dev/null @@ -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(包含分类字段) -``` - -## 后续可能的改进 - -- [ ] 支持修改分类名称 -- [ ] 支持删除已使用的分类(需要迁移交易记录) -- [ ] 为分类配置颜色标签 -- [ ] 按分类生成统计报表 -- [ ] 分类快速切换功能 -- [ ] 分类使用频率排序 diff --git a/CATEGORY_IMPROVEMENT_SUMMARY.md b/CATEGORY_IMPROVEMENT_SUMMARY.md deleted file mode 100644 index 90bd35f..0000000 --- a/CATEGORY_IMPROVEMENT_SUMMARY.md +++ /dev/null @@ -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导出保持兼容 diff --git a/FINANCE_API_EXAMPLES.py b/FINANCE_API_EXAMPLES.py deleted file mode 100644 index 2076083..0000000 --- a/FINANCE_API_EXAMPLES.py +++ /dev/null @@ -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() diff --git a/FINANCE_COMPLETION_REPORT.md b/FINANCE_COMPLETION_REPORT.md deleted file mode 100644 index f7e1261..0000000 --- a/FINANCE_COMPLETION_REPORT.md +++ /dev/null @@ -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+ 行 -- **总投入**: 完整、生产级质量 -- **测试覆盖**: 核心功能已测试 - ---- - -**感谢使用本财务做账模块!** 🎉 diff --git a/FINANCE_COMPLETION_SUMMARY.md b/FINANCE_COMPLETION_SUMMARY.md deleted file mode 100644 index 13dfbf2..0000000 --- a/FINANCE_COMPLETION_SUMMARY.md +++ /dev/null @@ -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财务做账模块!** 🎉 - -如有任何问题或建议,欢迎随时反馈。 diff --git a/FINANCE_IMPORT_FIX.md b/FINANCE_IMPORT_FIX.md deleted file mode 100644 index e3efc79..0000000 --- a/FINANCE_IMPORT_FIX.md +++ /dev/null @@ -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 - -**修复状态**: ✅ 完成 - -**测试状态**: ✅ 通过 - -**生产准备**: ✅ 就绪 diff --git a/FINANCE_MODULE_GUIDE.md b/FINANCE_MODULE_GUIDE.md deleted file mode 100644 index 5396c67..0000000 --- a/FINANCE_MODULE_GUIDE.md +++ /dev/null @@ -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 已正确安装并配置好依赖环境。** diff --git a/FINANCE_PROJECT_COMPLETE.md b/FINANCE_PROJECT_COMPLETE.md deleted file mode 100644 index a43d61a..0000000 --- a/FINANCE_PROJECT_COMPLETE.md +++ /dev/null @@ -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 (稳定版)** - ---- - -感谢使用!如有任何问题,欢迎反馈。🙏 diff --git a/FINANCE_QUICK_START.md b/FINANCE_QUICK_START.md deleted file mode 100644 index 44951fe..0000000 --- a/FINANCE_QUICK_START.md +++ /dev/null @@ -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 -- 💬 提交建议:欢迎反馈和建议 - ---- - -**祝你使用愉快!** 🎉 diff --git a/FINANCE_README.md b/FINANCE_README.md deleted file mode 100644 index d71f2f1..0000000 --- a/FINANCE_README.md +++ /dev/null @@ -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财务做账模块!** 🎉 diff --git a/FINANCE_UI_IMPROVEMENT.md b/FINANCE_UI_IMPROVEMENT.md deleted file mode 100644 index c344a97..0000000 --- a/FINANCE_UI_IMPROVEMENT.md +++ /dev/null @@ -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. 可以添加数据导出的进度条显示 diff --git a/FINANCE_USER_MANUAL.md b/FINANCE_USER_MANUAL.md deleted file mode 100644 index cd0aad3..0000000 --- a/FINANCE_USER_MANUAL.md +++ /dev/null @@ -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 -- **状态**: 稳定版 -- **支持**: 完整功能测试通过 - ---- - -**祝你使用愉快!如有任何问题,请随时反馈。** 🎊 diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc index a429031..9dd2ee6 100644 Binary files a/app/__pycache__/__init__.cpython-39.pyc and b/app/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/batch_export_dialog.py b/app/batch_export_dialog.py index e65dcfc..d195b3f 100644 --- a/app/batch_export_dialog.py +++ b/app/batch_export_dialog.py @@ -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() diff --git a/app/category_management_dialog.py b/app/category_management_dialog.py index d991e12..32e6912 100644 --- a/app/category_management_dialog.py +++ b/app/category_management_dialog.py @@ -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, "确认删除", diff --git a/app/finance_interface.py b/app/finance_interface.py index 0644b39..7f478f7 100644 --- a/app/finance_interface.py +++ b/app/finance_interface.py @@ -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) diff --git a/app/main_window.py b/app/main_window.py index 6252548..067e718 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -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: diff --git a/assets/.DS_Store b/assets/.DS_Store index ea98324..78d0e24 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/Finance_Data/.DS_Store b/assets/Finance_Data/.DS_Store deleted file mode 100644 index ad9e73c..0000000 Binary files a/assets/Finance_Data/.DS_Store and /dev/null differ diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json deleted file mode 100644 index 83a9df5..0000000 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/data.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/invoice/截屏2025-11-25 02.51.14.png b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/invoice/截屏2025-11-25 02.51.14.png deleted file mode 100644 index 5aa13d5..0000000 Binary files a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/28ae4af8-67d9-431a-b2fe-312f587020b3/invoice/截屏2025-11-25 02.51.14.png and /dev/null differ diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json deleted file mode 100644 index 9adea80..0000000 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/data.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png deleted file mode 100644 index 5aa13d5..0000000 Binary files a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/5cf3dc01-8a1c-4418-86b6-1728f1ae47d1/invoice/截屏2025-11-25 02.51.14.png and /dev/null differ diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json deleted file mode 100644 index 2989261..0000000 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/86232779-66fb-4e8e-b31e-477aef8e4ecf/data.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json b/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json deleted file mode 100644 index 9d8c53f..0000000 --- a/assets/Finance_Data/accounts/b87b752a-7fb6-40b6-a0cf-287df1d64e56/metadata.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/assets/User_code/.DS_Store b/assets/User_code/.DS_Store index aa72674..e391af5 100644 Binary files a/assets/User_code/.DS_Store and b/assets/User_code/.DS_Store differ diff --git a/assets/User_code/bsp/spi.c b/assets/User_code/bsp/spi.c index d8ac00e..84d0d85 100644 --- a/assets/User_code/bsp/spi.c +++ b/assets/User_code/bsp/spi.c @@ -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; } diff --git a/assets/User_code/component/cmd.c b/assets/User_code/component/cmd.c new file mode 100644 index 0000000..1effe69 --- /dev/null +++ b/assets/User_code/component/cmd.c @@ -0,0 +1,387 @@ +/* + 控制命令 +*/ + +#include "cmd.h" + +#include + +/* 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 */ diff --git a/assets/User_code/component/cmd.h b/assets/User_code/component/cmd.h new file mode 100644 index 0000000..df84538 --- /dev/null +++ b/assets/User_code/component/cmd.h @@ -0,0 +1,318 @@ +/* + 控制命令 +*/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#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 diff --git a/assets/User_code/config.csv b/assets/User_code/config.csv index db5a049..32290e1 100644 --- a/assets/User_code/config.csv +++ b/assets/User_code/config.csv @@ -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, \ No newline at end of file diff --git a/assets/User_code/device/ai.c b/assets/User_code/device/ai.c deleted file mode 100644 index 2632536..0000000 --- a/assets/User_code/device/ai.c +++ /dev/null @@ -1,142 +0,0 @@ -/* -AI -*/ - -/* Includes ----------------------------------------------------------------- */ -#include "ai.h" - -#include -#include - -#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; - } -} - diff --git a/assets/User_code/device/ai.h b/assets/User_code/device/ai.h deleted file mode 100644 index 24b2a31..0000000 --- a/assets/User_code/device/ai.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - AI -*/ - -#pragma once - -#include -#ifdef __cplusplus -extern "C" { -#endif - -/* Includes ----------------------------------------------------------------- */ -#include "component/ahrs.h" -#include "component/filter.h" -#include "component/user_math.h" -#include "device/device.h" -#include -#include -#include - -/* 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 diff --git a/assets/User_code/device/motor_rm.c b/assets/User_code/device/motor_rm.c index 1e78a47..88c1ef4 100644 --- a/assets/User_code/device/motor_rm.c +++ b/assets/User_code/device/motor_rm.c @@ -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; diff --git a/assets/User_code/module/gimbal/2_axis_gimbal/2_axis_gimbal.c b/assets/User_code/module/2_axis_gimbal.c similarity index 100% rename from assets/User_code/module/gimbal/2_axis_gimbal/2_axis_gimbal.c rename to assets/User_code/module/2_axis_gimbal.c diff --git a/assets/User_code/module/gimbal/2_axis_gimbal/2_axis_gimbal.h b/assets/User_code/module/2_axis_gimbal.h similarity index 100% rename from assets/User_code/module/gimbal/2_axis_gimbal/2_axis_gimbal.h rename to assets/User_code/module/2_axis_gimbal.h diff --git a/assets/User_code/module/gimbal/2_axis_gimbal/config.yaml b/assets/User_code/module/gimbal/2_axis_gimbal/config.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/check_import_fix.py b/check_import_fix.py deleted file mode 100644 index a9a0343..0000000 --- a/check_import_fix.py +++ /dev/null @@ -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() diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..984f7b9 --- /dev/null +++ b/config/config.json @@ -0,0 +1,6 @@ +{ + "QFluentWidgets": { + "ThemeColor": "#fff18cb9", + "ThemeMode": "Light" + } +} \ No newline at end of file diff --git a/debug_finance.py b/debug_finance.py deleted file mode 100644 index 7daf357..0000000 --- a/debug_finance.py +++ /dev/null @@ -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() diff --git a/debug_import_issue.py b/debug_import_issue.py deleted file mode 100644 index 01ef016..0000000 --- a/debug_import_issue.py +++ /dev/null @@ -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() diff --git a/test_admin_account.py b/test_admin_account.py deleted file mode 100644 index c2e4737..0000000 --- a/test_admin_account.py +++ /dev/null @@ -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("所有测试都通过了!✓") diff --git a/test_category.py b/test_category.py deleted file mode 100644 index 76b0426..0000000 --- a/test_category.py +++ /dev/null @@ -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! ✓") diff --git a/test_category_dropdown.py b/test_category_dropdown.py deleted file mode 100644 index 701a02c..0000000 --- a/test_category_dropdown.py +++ /dev/null @@ -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. 创建的交易可以按分类进行查询") diff --git a/test_category_features.py b/test_category_features.py deleted file mode 100644 index 59c35f3..0000000 --- a/test_category_features.py +++ /dev/null @@ -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) diff --git a/test_category_management.py b/test_category_management.py deleted file mode 100644 index 722b464..0000000 --- a/test_category_management.py +++ /dev/null @@ -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() diff --git a/test_category_query.py b/test_category_query.py deleted file mode 100644 index 226f937..0000000 --- a/test_category_query.py +++ /dev/null @@ -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) diff --git a/test_finance_ui.py b/test_finance_ui.py deleted file mode 100644 index f787da4..0000000 --- a/test_finance_ui.py +++ /dev/null @@ -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) diff --git a/test_import_query.py b/test_import_query.py deleted file mode 100644 index f616ed6..0000000 --- a/test_import_query.py +++ /dev/null @@ -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() diff --git a/test_no_account_selection.py b/test_no_account_selection.py deleted file mode 100644 index dde17b0..0000000 --- a/test_no_account_selection.py +++ /dev/null @@ -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() diff --git a/test_no_default_categories.py b/test_no_default_categories.py deleted file mode 100644 index 5ec1e1c..0000000 --- a/test_no_default_categories.py +++ /dev/null @@ -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) diff --git a/开发要求.md b/开发要求.md deleted file mode 100644 index 84970c1..0000000 --- a/开发要求.md +++ /dev/null @@ -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的运行容器,也包含通信、姿态解算等 | - -## 系统介绍 - -### 硬件系统框图 - -| ![步兵嵌入式硬件框图](./Image/步兵嵌入式硬件框图.png?raw=true "步兵嵌入式硬件框图") | -|:--:| -| *步兵嵌入式硬件框图* | - -### 软件流程图 - -| ![步兵嵌入式硬件框图](./Image/嵌入式程序流程图.png?raw=true "步兵嵌入式硬件框图") | -|:--:| -| *步兵嵌入式硬件框图* | - -| ![嵌入式程序结构图](./Image/嵌入式程序结构图.png?raw=true "嵌入式程序结构图") | -|:--:| -| *嵌入式程序结构图* | - -## 原理介绍 - -### 云台控制原理 - -| ![云台控制原理(与PX类似)](./Image/云台控制原理.png?raw=true "嵌入式程序结构图") | -|:--:| -| *云台控制原理(与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. 在步兵上完成所有功能。