触发条件视图 — 账单页(BillPage)

页面:BillPage

路由/bill

数据来源:全局 DataContext(useData()),数据在 HomePage 选择账单时selectBill(billId) 触发预加载。BillPage 本身不直接调用 API(除了操作类触发)。

DataContext 数据挂载时机


页面进入时

无直接 API 调用(数据由 DataContext 预加载)

数据流

  1. fetchBillDetails(billId) 并行请求:
    • GET /bills/:billId → 返回 bill(账单基本信息)+ members(成员列表)+ categories(分类列表)+ myRole + myPerms
    • GET /transactions/:billId → 返回 transactions[] + uploadMeta(最近一次 CSV 上传元信息)
  2. DataContext 将数据写入全局 state:activeBill / members / transactions / categories / csvUploadMeta / myPerms

BillPage 读取的 Context 字段

字段 来源 说明
bills DataContext 用户所有账单列表
activeBill DataContext 当前选中账单
transactions DataContext 当前账单所有交易记录
members DataContext 当前账单所有成员
csvUploadMeta DataContext 最近一次 CSV 上传元信息
myPerms DataContext 当前用户角色和权限 { role, can_edit, can_delete }
loading DataContext 全局加载状态

点击「账单选择器」切换账单时

前端操作:点击账单下拉选择器 → 选择另一个账单

触发 API(通过 selectBill(billId)):

1. GET /bills/:billId

说明:获取新选中账单的详情 请求GET /api/bills/:id(billId 从选择器传递) 后端处理

  1. 校验账单存在且用户是成员
  2. 返回 bill + members + categories + myRole + myPerms 响应字段
{
  "bill": { "id", "name", "description", "owner_id", "created_at", "updated_at" },
  "members": [{ "user_id", "role", "joined_at", "nickname", "phone", "avatar_url" }],
  "categories": [{ "id", "name", "icon", "color", "type" }],
  "myRole": "owner | editor | viewer",
  "myPerms": { "can_edit": 1, "can_delete": 0 }
}

关联 DB 表bills, bill_members, users, categories

2. GET /transactions/:billId

说明:获取新账单的全部交易记录 请求GET /api/transactions/:billId 后端处理

  1. 校验用户是账单成员
  2. 查询 transactions WHERE bill_id = :billId,按 tx_time ASC 升序排列
  3. 查询最近一次 CSV 上传元信息(JOIN bill_csv_uploads + users响应字段
{
  "transactions": [{ "id", "bill_id", "tx_time", "category", "amount", "tx_type", ... }],
  "uploadMeta": { "file_name", "uploaded_at", "uploaded_by_nickname", "uploaded_by_phone" } | null
}

关联 DB 表transactions, bill_csv_uploads, users


点击「新建账单」按钮(输入框 + 提交)

前端操作:输入账单名称 → 按回车或点击「新建」按钮

触发 API

POST /bills

请求

{ "name": "账单名称", "description": "" }

约束

后端处理

  1. UUID 生成 billId(10次碰撞检测)
  2. 事务:插入 bills → 插入 bill_members(角色=owner)→ 插入7个默认分类
  3. 返回新建账单完整对象

成功响应

{ "bill": { "id": "uuid", "name": "...", "owner_id": "...", "created_at": "..." } }

DB 表bills, bill_members, categories

后续动作:创建成功后自动调用 selectBill(res.data.bill.id) 选中新账单(触发上文「切换账单时」的 API 流程)


点击「铅笔图标」→ 修改账单名称

前端操作:点击重命名按钮 → 弹出编辑弹窗 → 输入新名称 → 点击「保存修改」

触发 API

PUT /bills/:id

请求

{ "name": "新账单名称", "description": "当前账单描述" }

权限:需 ownereditor 角色 后端处理:更新 bills SET name = :name, updated_at = NOW() WHERE id = :id DB 表bills

后续动作:成功后调用 fetchBills() + selectBill(billId) 刷新账单列表和详情


点击「垃圾桶图标」→ 删除账单

前端操作:点击删除按钮 → 弹出二次确认弹窗(需手动输入账单名称匹配)→ 点击「确认删除」

触发 API

DELETE /bills/:id

权限:仅 owner 后端处理

  1. 校验操作者是 owner
  2. DELETE FROM bills WHERE id = :id(CASCADE 删除关联数据) DB 表bills(联动删除 bill_memberstransactionscategories 等)

后续动作:删除成功后:

  1. 调用 fetchBills() 刷新列表
  2. 自动选中剩余的第一个账单(若无剩余账单,activeBill = null 显示空状态)

⚠️ 警告:这是覆盖式删除,所有历史交易数据、成员权限设置均不可恢复。


CSV 文件上传(拖拽或点击上传)

前端操作:选择 CSV 文件 → 自动触发上传(无需点击确认按钮)

触发 API

POST /transactions/:billId/upload

Content-Typemultipart/form-data 请求体

file: <CSV File>(最大 10MB)

权限:需 ownereditorviewer 不可上传)

CSV 必填列时间分类类型金额 CSV 选填列二级分类ID币种账户1账户2备注已报销手续费优惠券记账者账单标记标签账单图片关联单笔

后端处理(关键行为)

  1. 校验文件存在(≤ 10MB)
  2. 校验角色权限
  3. PapaParse 解析 CSV
  4. 事务
    • DELETE FROM transactions WHERE bill_id = :billId全量覆盖,旧数据全部删除
    • 批量 INSERT 新数据(每批 40 行)
    • UPSERT bill_csv_uploads(保留最新上传记录)
    • 更新 bills.updated_at
  5. 返回 { success: true, count: 导入行数 }

DB 表transactions(全量清空后重新插入)、bill_csv_uploadsbills

前端进度回调

格式错误处理:若后端返回 code: 'FORMAT_ERROR',前端弹出格式错误详情弹窗(缺失列 + 实际列名对照)


邀请成员(输入手机号 → 点击「邀请」)

前端操作:选择国际区号 → 输入手机号 → 点击「邀请」按钮

触发 API

POST /bills/:id/invite

请求

{ "phone": "+86138xxxxxxxx", "role": "viewer" }

权限:仅 owner 可邀请

后端处理

  1. 校验操作者是 owner
  2. 校验手机号格式
  3. users 表查找该手机号(必须已注册,否则返回错误)
  4. 校验被邀请人还不是账单成员
  5. 插入 bill_invitations(24小时有效期)

⚠️ 已知缺陷

DB 表users, bill_members, bill_invitations

后续动作:成功后清空输入框 + 调用 selectBill(billId) 刷新成员列表


修改成员角色(点击角色下拉 → 选择新角色)

前端操作:owner 点击某成员的「角色」下拉 → 选择「编辑者」或「查看者」

触发 API

PUT /bills/:billId/members/:memberId

请求

{ "role": "editor" }

权限:仅 owner 可操作 约束

后端处理

  1. 校验操作者是 owner
  2. 校验目标不是 owner
  3. 更新 bill_members SET role = :role, updated_at = NOW() DB 表bill_members

后续动作:成功后调用 selectBill(billId) 刷新成员列表


移除成员(点击「移除」按钮 → 输入成员昵称确认)

前端操作:点击移除按钮 → 弹出二次确认弹窗 → 手动输入成员昵称匹配 → 点击「确认移除」

触发 API

DELETE /bills/:billId/members/:memberId

权限规则

约束:不能移除 owner(返回 400)

后端处理

  1. 校验目标成员存在
  2. 校验不是 owner
  3. 校验操作者权限
  4. DELETE FROM bill_members WHERE id = :memberId

DB 表bill_members

后续动作:成功后调用 selectBill(billId) 刷新成员列表,显示成功提示


下载 CSV 模板(点击「下载模板」)

前端操作:点击「下载标准模板」按钮

触发:无 API 调用,纯前端生成 CSV 文件并触发浏览器下载

模板内容(硬编码示例行):

ID,时间,分类,二级分类,类型,金额,币种,账户1,账户2,备注,已报销,手续费,优惠券,记账者,账单标记,标签,账单图片,关联单笔
1,2026-03-01 12:00:00,餐饮,外卖,支出,25.5,CNY,微信支付,,午餐外卖,否,0,0,我,个人,,,

导入试用数据(点击「导入试用数据」)

前端操作:点击「导入试用数据」按钮

触发 API:与「CSV 文件上传」相同,走 uploadCSV(file)

差异:文件不是用户选择,而是前端 generateSampleCsvFile() 动态生成(从 2024-01-30 到"当前日期前4天"的模拟数据)


API 上传说明弹窗(点击「API 导出」)

前端操作:点击「API 导出」子卡片

触发:无 API 调用,纯前端弹窗显示 cURL 示例代码

弹窗内容


统计信息展示(无需 API)

页面头部统计卡


权限判断汇总

操作 所需角色 API 端点
查看账单/成员/交易 任意成员(owner/editor/viewer) GET /bills/:id, GET /transactions/:billId
创建新账单 非游客(is_guest=0) POST /bills
修改账单名称 owner / editor PUT /bills/:id
删除账单 owner DELETE /bills/:id
上传 CSV owner / editor(viewer 不可) POST /transactions/:billId/upload
邀请成员 owner POST /bills/:id/invite
修改成员角色 owner PUT /bills/:id/members/:memberId
移除成员 owner(移除他人)/ 任意(移除自己) DELETE /bills/:id/members/:memberId