Compare commits
2 Commits
4a5b7003ca
...
4138164bd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 4138164bd8 | |||
| 03434feb35 |
@@ -0,0 +1,427 @@
|
|||||||
|
# 小程序订单与账单接口文档
|
||||||
|
|
||||||
|
> 适用端:微信小程序(h5 仓库,Taro)
|
||||||
|
> 覆盖接口:订单列表 / 订单详情、账单列表 / 账单详情
|
||||||
|
> 更新日期:2026-08-14
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php`) |
|
||||||
|
| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` |
|
||||||
|
| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` |
|
||||||
|
| 分页结构 | `{ data: T[], total: number, pageSize: number, current: number }`(账单列表在此结构上附加 `summary`) |
|
||||||
|
| 失败处理 | `success=false` 时 `msg` 为中文错误信息(含参数校验失败);HTTP 401 表示登录过期,需重新登录 |
|
||||||
|
| 金额字段 | 一律为**字符串**(decimal 序列化,如 `"140.00"`),直接展示即可;参与计算需自行转 number |
|
||||||
|
| 门店隔离 | 两个列表均强制按当前用户绑定门店过滤,无需也不能传门店参数 |
|
||||||
|
|
||||||
|
## 状态枚举
|
||||||
|
|
||||||
|
**订单状态(status)**
|
||||||
|
|
||||||
|
| 值 | 名称 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | 待接单 | 下单成功待后台接单,**仅此状态可取消** |
|
||||||
|
| 1 | 已接单 | 后台已接单,等待归集生成采购单 |
|
||||||
|
| 2 | 采购中 | 已归集进采购单 |
|
||||||
|
| 3 | 配送中 | |
|
||||||
|
| 4 | 已完成 | |
|
||||||
|
| 9 | 已取消 | |
|
||||||
|
|
||||||
|
**账单支付进度(pay_state)**——由后端推导,覆盖原始支付状态(status)的展示口径
|
||||||
|
|
||||||
|
| 值 | 名称 | 推导条件 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | 待支付 | status=0 且 payment_id=0 | `can_pay=true`,可发起合并付款 |
|
||||||
|
| 1 | 审核中 | status=0 且 payment_id>0 | 已提交合并付款凭证,待后台审核;**审核拒绝后自动回到待支付** |
|
||||||
|
| 2 | 已支付 | status=1 | 线上审核通过或后台线下收款登记 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GET /mini/order
|
||||||
|
|
||||||
|
门店历史订单列表,按订货日期倒序(同日期按 ID 倒序)分页。行数据附带**商品预览**(前 3 条明细),完整明细请调订单详情。
|
||||||
|
|
||||||
|
- **权限**:需登录且已绑定正常门店
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| status | number | 否 | 订单状态(见枚举表),不传=全部 |
|
||||||
|
| start_date | string | 否 | 订货日期起,`Y-m-d`;可配合 `/mini/order/summary` 返回的 `period_label` 下钻 |
|
||||||
|
| end_date | string | 否 | 订货日期止,`Y-m-d`,不得早于 start_date |
|
||||||
|
| page | number | 否 | 页码,默认 1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认 10,**最大 50** |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1024,
|
||||||
|
"order_no": "SO202608140001",
|
||||||
|
"order_date": "2026-08-14",
|
||||||
|
"status": 0,
|
||||||
|
"status_name": "待接单",
|
||||||
|
"can_cancel": true,
|
||||||
|
"total_quantity": 8,
|
||||||
|
"total_weight": "0.000",
|
||||||
|
"total_amount": "44.00",
|
||||||
|
"remark": "下午送到",
|
||||||
|
"purchase_id": 0,
|
||||||
|
"bill_id": 0,
|
||||||
|
"created_at": "2026-08-14 09:30:00",
|
||||||
|
"item_count": 4,
|
||||||
|
"items": [
|
||||||
|
{ "product_name": "白菜", "quantity": 2, "unit": "件" },
|
||||||
|
{ "product_name": "土豆", "quantity": 2, "unit": "件" },
|
||||||
|
{ "product_name": "番茄", "quantity": 2, "unit": "件" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 36,
|
||||||
|
"pageSize": 10,
|
||||||
|
"current": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明(列表行)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| id | number | 订单 ID |
|
||||||
|
| order_no | string | 订单编号(SO 前缀) |
|
||||||
|
| order_date | string | 订货日期 `Y-m-d` |
|
||||||
|
| status | number | 订单状态(见枚举表) |
|
||||||
|
| **status_name** | string | 状态中文名,**直接展示,前端不要再硬编码映射** |
|
||||||
|
| **can_cancel** | boolean | 是否可取消(=待接单);取消按钮据此渲染 |
|
||||||
|
| total_quantity | number | 订货总数量(件/包数) |
|
||||||
|
| total_weight | string | 总称重(斤,手动录入参考值,常为 0) |
|
||||||
|
| total_amount | string | 订单金额 |
|
||||||
|
| remark | string | 订单备注,可能为空字符串 |
|
||||||
|
| purchase_id | number | 关联采购单 ID,0=未归集 |
|
||||||
|
| bill_id | number | 关联账单 ID,0=未出账(>0 可跳账单详情) |
|
||||||
|
| created_at | string | 下单时间 `Y-m-d H:i:s` |
|
||||||
|
| **item_count** | number | 明细种数(如「共 4 种」) |
|
||||||
|
| **items** | array | 商品预览,**仅前 3 条**(`product_name` / `quantity` / `unit`),完整明细走详情接口 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GET /mini/order/{id}
|
||||||
|
|
||||||
|
订单详情(校验本店归属),返回订单全部字段 + 完整明细(`items` 数组,含下单时商品快照)。
|
||||||
|
|
||||||
|
- **权限**:需登录且已绑定正常门店;非本店订单返回 `success=false`「订单不存在」
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"id": 1024,
|
||||||
|
"order_no": "SO202608140001",
|
||||||
|
"store_id": 3,
|
||||||
|
"order_date": "2026-08-14",
|
||||||
|
"total_quantity": 8,
|
||||||
|
"total_weight": "0.000",
|
||||||
|
"total_amount": "44.00",
|
||||||
|
"status": 0,
|
||||||
|
"remark": "下午送到",
|
||||||
|
"purchase_id": 0,
|
||||||
|
"bill_id": 0,
|
||||||
|
"created_at": "2026-08-14 09:30:00",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 501,
|
||||||
|
"order_id": 1024,
|
||||||
|
"product_id": 11,
|
||||||
|
"product_name": "白菜",
|
||||||
|
"product_spec": "约30斤/件",
|
||||||
|
"unit": "件",
|
||||||
|
"price": "5.50",
|
||||||
|
"quantity": 2,
|
||||||
|
"weight": "0.000",
|
||||||
|
"amount": "11.00",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明
|
||||||
|
|
||||||
|
订单本体字段同列表行(无 `status_name` / `can_cancel` / 预览字段,状态展示沿用列表行数据或自行映射);`items` 为完整明细:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| product_name / product_spec / unit | string | 下单时商品快照(品名/规格包规/单位),商品档案后续变更不影响 |
|
||||||
|
| price | string | 下单时门店等级实际价快照 |
|
||||||
|
| quantity | number | 订货量(件/包数) |
|
||||||
|
| weight | string | 称重(斤,参考值) |
|
||||||
|
| amount | string | 明细金额 = price × quantity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GET /mini/bill
|
||||||
|
|
||||||
|
门店账单列表(采购单完成后由后台按门店生成,门店端只读),按账单日期倒序分页。响应在分页结构上**附加 `summary` 待支付汇总**(仅按门店口径统计,不受筛选参数影响),供页面头部与合并付款入口展示。
|
||||||
|
|
||||||
|
- **权限**:需登录且已绑定正常门店
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| status | number | 否 | 原始支付状态:0 未支付(含审核中)/ 1 已支付,不传=全部 |
|
||||||
|
| payable | number | 否 | 传 `1` = 仅可发起付款的账单(未支付且未在审核中),**合并付款选择页专用** |
|
||||||
|
| start_date | string | 否 | 账单日期起,`Y-m-d` |
|
||||||
|
| end_date | string | 否 | 账单日期止,`Y-m-d`,不得早于 start_date |
|
||||||
|
| page | number | 否 | 页码,默认 1 |
|
||||||
|
| pageSize | number | 否 | 每页数量,默认 10,**最大 50** |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"bill_no": "ZD202608140001",
|
||||||
|
"bill_date": "2026-08-14",
|
||||||
|
"purchase_id": 31,
|
||||||
|
"purchase": { "id": 31, "purchase_no": "PO202608130001", "purchase_date": "2026-08-13" },
|
||||||
|
"product_amount": "100.00",
|
||||||
|
"delivery_fee": "10.00",
|
||||||
|
"box_num": 2,
|
||||||
|
"tray_num": 1,
|
||||||
|
"box_price": "5.00",
|
||||||
|
"tray_price": "20.00",
|
||||||
|
"added_amount": "30.00",
|
||||||
|
"total_amount": "140.00",
|
||||||
|
"status": 0,
|
||||||
|
"status_name": "未支付",
|
||||||
|
"pay_state": 0,
|
||||||
|
"pay_state_name": "待支付",
|
||||||
|
"can_pay": true,
|
||||||
|
"payment_id": 0,
|
||||||
|
"settlement_date": "2026-08-17",
|
||||||
|
"paid_at": null,
|
||||||
|
"pay_remark": "",
|
||||||
|
"remark": "",
|
||||||
|
"created_at": "2026-08-14 03:17:01"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 12,
|
||||||
|
"pageSize": 10,
|
||||||
|
"current": 1,
|
||||||
|
"summary": { "unpaid_count": 3, "unpaid_amount": "420.50" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明(列表行)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| id | number | 账单 ID |
|
||||||
|
| bill_no | string | 账单编号(ZD 前缀) |
|
||||||
|
| bill_date | string | 账单日期(出账日)`Y-m-d` |
|
||||||
|
| purchase_id | number | 关联采购单 ID |
|
||||||
|
| purchase | object \| null | 关联采购单 `{ id, purchase_no, purchase_date }` |
|
||||||
|
| product_amount | string | 商品金额(订单汇总快照,不可修改) |
|
||||||
|
| delivery_fee | string | 配送费 |
|
||||||
|
| box_num / tray_num | number | 周转筐 / 周转托盘数量 |
|
||||||
|
| box_price / tray_price | string | 筐 / 托盘单价(出账时快照) |
|
||||||
|
| added_amount | string | 附加金额 = box_num×box_price + tray_num×tray_price |
|
||||||
|
| total_amount | string | **账单总金额 = 商品金额 + 配送费 + 附加金额** |
|
||||||
|
| status | number | 原始支付状态:0 未支付 / 1 已支付(展示用 pay_state 系列字段) |
|
||||||
|
| status_name | string | 支付状态中文名 |
|
||||||
|
| **pay_state** | number | 支付进度:0 待支付 / 1 审核中 / 2 已支付(见枚举表) |
|
||||||
|
| **pay_state_name** | string | 支付进度中文名,**列表状态标签直接用它** |
|
||||||
|
| **can_pay** | boolean | 是否可勾选发起合并付款(=待支付) |
|
||||||
|
| payment_id | number | 关联支付记录 ID,0=未发起付款;审核中时可跳支付记录详情查看进度 |
|
||||||
|
| **settlement_date** | string | 应结算日期 = 账单日期 + 门店回款周期天数 |
|
||||||
|
| paid_at | string \| null | 付款时间(已支付时非空) |
|
||||||
|
| pay_remark | string | 付款备注(如「微信支付(支付单号 ZF…)」、线下收款说明) |
|
||||||
|
| remark | string | 账单备注 |
|
||||||
|
| created_at | string | 出账时间 |
|
||||||
|
|
||||||
|
### summary 字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| unpaid_count | number | 待支付账单笔数(**含审核中**) |
|
||||||
|
| unpaid_amount | string | 待支付金额合计(含审核中) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GET /mini/bill/{id}
|
||||||
|
|
||||||
|
账单详情(校验本店归属):账单信息(字段同列表行)+ 合并后的商品明细 + 关联订单。
|
||||||
|
|
||||||
|
- **权限**:需登录且已绑定正常门店;非本店账单返回 `success=false`「账单不存在」
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"bill": { "id": 88, "bill_no": "ZD202608140001", "...": "(字段同列表行)" },
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"product_id": 11,
|
||||||
|
"product_name": "白菜",
|
||||||
|
"product_spec": "约30斤/件",
|
||||||
|
"unit": "件",
|
||||||
|
"price": "5.50",
|
||||||
|
"quantity": 20,
|
||||||
|
"weight": "0.000",
|
||||||
|
"amount": "110.00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orders": [
|
||||||
|
{
|
||||||
|
"id": 1024,
|
||||||
|
"order_no": "SO202608130001",
|
||||||
|
"order_date": "2026-08-13",
|
||||||
|
"total_quantity": 8,
|
||||||
|
"total_weight": "0.000",
|
||||||
|
"total_amount": "44.00",
|
||||||
|
"status": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段说明
|
||||||
|
|
||||||
|
- **bill**:与列表行完全一致(含 `pay_state_name` / `can_pay` / `settlement_date` 等派生字段)
|
||||||
|
- **items**:按商品聚合的合并明细(跨本账单全部订单),`price` 为加权平均口径(Σ金额÷Σ数量,保证 单价×数量=金额);`quantity` 为合计数量(number)、`amount` 为合计金额
|
||||||
|
- **orders**:本账单关联的门店订单(单价明细可继续下钻 `GET /mini/order/{id}`),`status` 为订单状态枚举
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端接入示例
|
||||||
|
|
||||||
|
`src/types/order.ts` 关键类型(列表行替换为接口返回,**删除本地 `ORDER_STATUS_MAP` 硬编码**):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 订单列表行(状态名/可取消/商品预览均由后端给出) */
|
||||||
|
export interface OrderListItem {
|
||||||
|
id: number
|
||||||
|
order_no: string
|
||||||
|
order_date: string
|
||||||
|
status: number
|
||||||
|
status_name: string
|
||||||
|
can_cancel: boolean
|
||||||
|
total_quantity: number
|
||||||
|
total_weight: string
|
||||||
|
total_amount: string
|
||||||
|
remark: string
|
||||||
|
purchase_id: number
|
||||||
|
bill_id: number
|
||||||
|
created_at: string
|
||||||
|
item_count: number
|
||||||
|
items: Array<{ product_name: string; quantity: number; unit: string }>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/services/bill.ts`(**替代旧 `statement.ts`——旧 `/mini/statement` 已下线**):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { get } from '@/utils/request'
|
||||||
|
import type { PaginatedData } from '@/types/api'
|
||||||
|
|
||||||
|
/** 账单(列表行与详情的 bill 字段一致) */
|
||||||
|
export interface Bill {
|
||||||
|
id: number
|
||||||
|
bill_no: string
|
||||||
|
bill_date: string
|
||||||
|
purchase_id: number
|
||||||
|
purchase: { id: number; purchase_no: string; purchase_date: string } | null
|
||||||
|
product_amount: string
|
||||||
|
delivery_fee: string
|
||||||
|
box_num: number
|
||||||
|
tray_num: number
|
||||||
|
box_price: string
|
||||||
|
tray_price: string
|
||||||
|
added_amount: string
|
||||||
|
total_amount: string
|
||||||
|
status: 0 | 1
|
||||||
|
status_name: string
|
||||||
|
/** 支付进度:0 待支付 / 1 审核中 / 2 已支付 */
|
||||||
|
pay_state: 0 | 1 | 2
|
||||||
|
pay_state_name: string
|
||||||
|
can_pay: boolean
|
||||||
|
payment_id: number
|
||||||
|
settlement_date: string
|
||||||
|
paid_at: string | null
|
||||||
|
pay_remark: string
|
||||||
|
remark: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BillSummary {
|
||||||
|
unpaid_count: number
|
||||||
|
unpaid_amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BillDetail {
|
||||||
|
bill: Bill
|
||||||
|
items: Array<{
|
||||||
|
product_id: number; product_name: string; product_spec: string
|
||||||
|
unit: string; price: string; quantity: number; weight: string; amount: string
|
||||||
|
}>
|
||||||
|
orders: Array<{
|
||||||
|
id: number; order_no: string; order_date: string
|
||||||
|
total_quantity: number; total_weight: string; total_amount: string; status: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单列表:GET /mini/bill(payable=1 为合并付款选择页口径) */
|
||||||
|
export function getBillListApi(
|
||||||
|
params: { status?: number; payable?: 1; start_date?: string; end_date?: string; page?: number; pageSize?: number } = {},
|
||||||
|
) {
|
||||||
|
return get<PaginatedData<Bill> & { summary: BillSummary }>('/mini/bill', { data: params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单详情:GET /mini/bill/{id} */
|
||||||
|
export function getBillDetailApi(id: number) {
|
||||||
|
return get<BillDetail>(`/mini/bill/${id}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
订单列表行商品预览渲染建议:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Text className='order-item__preview'>
|
||||||
|
{order.items.map(i => `${i.product_name}×${i.quantity}`).join('、')}
|
||||||
|
{order.item_count > 3 ? ` 等${order.item_count}种` : ''}
|
||||||
|
</Text>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **状态文案一律用接口返回的 `status_name` / `pay_state_name`**:订单状态为 6 态(0/1/2/3/4/9),h5 现有 `ORDER_STATUS_MAP` 是旧 5 态(缺采购中/配送中),继续硬编码会显示异常。
|
||||||
|
2. **账单状态标签用 `pay_state_name` 而非 `status`**:`status=0` 同时覆盖「待支付」与「审核中」两种展示;`payable=1` 筛选与 `can_pay` 同口径(待支付),合并付款页直接用它。
|
||||||
|
3. **审核拒绝后账单自动回到待支付**(payment_id 归零),列表刷新即可重新勾选付款,无需额外处理。
|
||||||
|
4. `summary` 统计的是**全部门店口径**(含审核中),与列表筛选参数无关;筛选「已支付」时 summary 仍返回待支付汇总。
|
||||||
|
5. 订单列表 `items` 仅为前 3 条预览(`item_count` 为总种数),完整明细必须调详情接口;两个详情接口均校验门店归属,跨店访问返回「不存在」。
|
||||||
|
6. `settlement_date` 由门店回款周期实时计算(门店改周期后历史账单随之变化),如需固化口径请后续提需求加快照列。
|
||||||
|
7. 参数校验失败与业务失败一样返回 `success=false` + 中文 `msg`(HTTP 200),无需特殊分支处理。
|
||||||
+2
-2
@@ -6,8 +6,8 @@ export default defineAppConfig({
|
|||||||
'pages/message/index',
|
'pages/message/index',
|
||||||
'pages/profile/index',
|
'pages/profile/index',
|
||||||
'pages/order-list/index',
|
'pages/order-list/index',
|
||||||
'pages/statement/index',
|
'pages/bill/index',
|
||||||
'pages/statement-detail/index',
|
'pages/bill-detail/index',
|
||||||
'pages/settings/index',
|
'pages/settings/index',
|
||||||
'pages/login/index',
|
'pages/login/index',
|
||||||
'pages/register/index',
|
'pages/register/index',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.bill-detail {
|
.bill-detail {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
padding: 20rpx 24rpx 120rpx;
|
padding: 20rpx 24rpx 60rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.bill-card {
|
.bill-card {
|
||||||
@@ -26,13 +26,20 @@
|
|||||||
&__status {
|
&__status {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
|
||||||
&--unpaid {
|
// 0 待支付 / 1 审核中 / 2 已支付
|
||||||
color: #ee0a24;
|
&--0 { color: #ee0a24; }
|
||||||
}
|
&--1 { color: #ff976a; }
|
||||||
|
&--2 { color: #07c160; }
|
||||||
|
}
|
||||||
|
|
||||||
&--paid {
|
&__tip {
|
||||||
color: #07c160;
|
display: block;
|
||||||
}
|
margin: -4rpx 0 16rpx;
|
||||||
|
padding: 12rpx 16rpx;
|
||||||
|
background: #fffbe8;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ed6a0c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__row {
|
&__row {
|
||||||
@@ -51,15 +58,15 @@
|
|||||||
&__label {
|
&__label {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #969799;
|
color: #969799;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__value {
|
&__value {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #323233;
|
color: #323233;
|
||||||
|
flex-shrink: 0;
|
||||||
&--credit {
|
|
||||||
color: #07c160;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__total {
|
&__total {
|
||||||
@@ -118,6 +125,7 @@
|
|||||||
&__side {
|
&__side {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-left: 16rpx;
|
margin-left: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__qty {
|
&__qty {
|
||||||
@@ -145,35 +153,112 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
&__no {
|
&__no {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #323233;
|
color: #323233;
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__date {
|
&__date {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #969799;
|
color: #969799;
|
||||||
margin: 0 16rpx;
|
margin: 0 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__amount {
|
&__amount {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #323233;
|
color: #323233;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bill-pay {
|
// ===== 订单明细弹层 =====
|
||||||
position: fixed;
|
.order-popup {
|
||||||
left: 0;
|
padding: 32rpx 32rpx 24rpx;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&__btn {
|
&__title {
|
||||||
border: none;
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
color: #ee0a24;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
max-height: 480rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f2f3f5;
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #323233;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-spec {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-amount {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #323233;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__total {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #ee0a24;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Empty, Popup } from '@antmjs/vantui'
|
||||||
|
import { getBillDetailApi } from '@/services/bill'
|
||||||
|
import { getOrderDetailApi } from '@/services/order'
|
||||||
|
import { ORDER_STATUS_TEXT } from '@/types/order'
|
||||||
|
import type { OrderDetail } from '@/types/order'
|
||||||
|
import type { BillDetail } from '@/services/bill'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单详情
|
||||||
|
* 账单信息 + 金额构成(商品/配送费/筐托盘附加)+ 合并商品明细 + 关联订单(可下钻订单明细)
|
||||||
|
*/
|
||||||
|
export default function BillDetailPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const id = Number(router.params.id ?? 0)
|
||||||
|
|
||||||
|
const [detail, setDetail] = useState<BillDetail | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
/** 关联订单明细弹层 */
|
||||||
|
const [showOrder, setShowOrder] = useState(false)
|
||||||
|
const [orderDetail, setOrderDetail] = useState<OrderDetail | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return
|
||||||
|
setLoading(true)
|
||||||
|
getBillDetailApi(id)
|
||||||
|
.then(res => setDetail(res.data))
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
/** 下钻关联订单明细 */
|
||||||
|
const handleOrderTap = useCallback(async (orderId: number) => {
|
||||||
|
try {
|
||||||
|
const res = await getOrderDetailApi(orderId)
|
||||||
|
setOrderDetail(res.data)
|
||||||
|
setShowOrder(true)
|
||||||
|
} catch {
|
||||||
|
// 错误已由 request 层 toast
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading && !detail) {
|
||||||
|
return <View className='bill-detail'><Empty description='加载中...' /></View>
|
||||||
|
}
|
||||||
|
if (!detail) {
|
||||||
|
return <View className='bill-detail'><Empty description='账单不存在' /></View>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { bill, items, orders } = detail
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='bill-detail'>
|
||||||
|
{/* ===== 账单信息 ===== */}
|
||||||
|
<View className='bill-card'>
|
||||||
|
<View className='bill-card__header'>
|
||||||
|
<Text className='bill-card__no'>{bill.bill_no}</Text>
|
||||||
|
<Text className={`bill-card__status bill-card__status--${bill.pay_state}`}>
|
||||||
|
{bill.pay_state_name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{bill.pay_state === 1 && (
|
||||||
|
<Text className='bill-card__tip'>已提交付款凭证,待后台审核;审核拒绝后将回到待支付</Text>
|
||||||
|
)}
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>账单日期</Text>
|
||||||
|
<Text className='bill-card__value'>{bill.bill_date}</Text>
|
||||||
|
</View>
|
||||||
|
{bill.purchase && (
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>关联采购单</Text>
|
||||||
|
<Text className='bill-card__value'>
|
||||||
|
{bill.purchase.purchase_no}({bill.purchase.purchase_date})
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>应结算日期</Text>
|
||||||
|
<Text className='bill-card__value'>{bill.settlement_date}</Text>
|
||||||
|
</View>
|
||||||
|
{bill.pay_state === 2 && bill.paid_at && (
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>付款时间</Text>
|
||||||
|
<Text className='bill-card__value'>{bill.paid_at}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{bill.pay_remark && (
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>付款备注</Text>
|
||||||
|
<Text className='bill-card__value'>{bill.pay_remark}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{bill.remark && (
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>账单备注</Text>
|
||||||
|
<Text className='bill-card__value'>{bill.remark}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 金额构成 ===== */}
|
||||||
|
<View className='bill-card'>
|
||||||
|
<Text className='bill-section__title'>金额构成</Text>
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>商品金额</Text>
|
||||||
|
<Text className='bill-card__value'>¥{bill.product_amount}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>配送费</Text>
|
||||||
|
<Text className='bill-card__value'>¥{bill.delivery_fee}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-card__row'>
|
||||||
|
<Text className='bill-card__label'>
|
||||||
|
附加金额(筐 {bill.box_num}×¥{bill.box_price},托盘 {bill.tray_num}×¥{bill.tray_price})
|
||||||
|
</Text>
|
||||||
|
<Text className='bill-card__value'>¥{bill.added_amount}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-card__row bill-card__row--total'>
|
||||||
|
<Text className='bill-card__label'>账单总额</Text>
|
||||||
|
<Text className='bill-card__total'>¥{bill.total_amount}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 商品明细(跨订单按商品合并) ===== */}
|
||||||
|
<View className='bill-card'>
|
||||||
|
<Text className='bill-section__title'>商品明细({items?.length ?? 0})</Text>
|
||||||
|
<Text className='bill-section__desc'>同一商品跨订单合并,单价为加权平均价</Text>
|
||||||
|
{(items ?? []).map(item => (
|
||||||
|
<View key={item.product_id} className='bill-goods'>
|
||||||
|
<View className='bill-goods__main'>
|
||||||
|
<Text className='bill-goods__name'>{item.product_name}</Text>
|
||||||
|
<Text className='bill-goods__spec'>
|
||||||
|
{item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-goods__side'>
|
||||||
|
<Text className='bill-goods__qty'>×{item.quantity}</Text>
|
||||||
|
<Text className='bill-goods__amount'>¥{item.amount}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{(items ?? []).length === 0 && <Empty description='暂无商品记录' />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 关联订单 ===== */}
|
||||||
|
<View className='bill-card'>
|
||||||
|
<Text className='bill-section__title'>关联订单({orders?.length ?? 0})</Text>
|
||||||
|
{(orders ?? []).map(order => (
|
||||||
|
<View key={order.id} className='bill-order' onClick={() => handleOrderTap(order.id)}>
|
||||||
|
<View className='bill-order__main'>
|
||||||
|
<Text className='bill-order__no'>{order.order_no}</Text>
|
||||||
|
<Text className='bill-order__date'>{order.order_date}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='bill-order__status'>{ORDER_STATUS_TEXT[order.status] || ''}</Text>
|
||||||
|
<Text className='bill-order__amount'>¥{order.total_amount}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{(orders ?? []).length === 0 && <Empty description='暂无订单记录' />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 订单明细弹层 ===== */}
|
||||||
|
<Popup
|
||||||
|
show={showOrder}
|
||||||
|
position='bottom'
|
||||||
|
round
|
||||||
|
closeable
|
||||||
|
closeOnClickOverlay
|
||||||
|
safeAreaInsetBottom
|
||||||
|
onClose={() => setShowOrder(false)}
|
||||||
|
>
|
||||||
|
{orderDetail && (
|
||||||
|
<View className='order-popup'>
|
||||||
|
<Text className='order-popup__title'>{orderDetail.order_no}</Text>
|
||||||
|
<View className='order-popup__meta'>
|
||||||
|
<Text>{orderDetail.order_date}</Text>
|
||||||
|
<Text className='order-popup__status'>
|
||||||
|
{ORDER_STATUS_TEXT[orderDetail.status] || ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<ScrollView scrollY className='order-popup__list'>
|
||||||
|
{(orderDetail.items ?? []).map(item => (
|
||||||
|
<View key={item.id} className='order-popup__item'>
|
||||||
|
<View className='order-popup__item-info'>
|
||||||
|
<Text className='order-popup__item-name'>{item.product_name}</Text>
|
||||||
|
<Text className='order-popup__item-spec'>
|
||||||
|
{item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='order-popup__item-amount'>¥{item.amount}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
<View className='order-popup__footer'>
|
||||||
|
<Text className='order-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
.bill-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding: 20rpx 24rpx 60rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
// 多选模式为底部导出栏留出空间
|
||||||
|
&--select {
|
||||||
|
padding-bottom: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 待支付汇总 =====
|
||||||
|
.bill-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 状态筛选 + 导出入口 =====
|
||||||
|
.bill-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.status-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__export {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff;
|
||||||
|
border: 1rpx solid #ee0a24;
|
||||||
|
color: #ee0a24;
|
||||||
|
font-size: 26rpx;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #ee0a24;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #646566;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #ee0a24;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-empty {
|
||||||
|
padding-top: 120rpx;
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 14rpx 60rpx;
|
||||||
|
background: #ee0a24;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-loading {
|
||||||
|
padding: 30rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 账单单项 =====
|
||||||
|
.bill-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&--select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #323233;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
font-size: 24rpx;
|
||||||
|
|
||||||
|
// 0 待支付 / 1 审核中 / 2 已支付
|
||||||
|
&--0 { color: #ee0a24; }
|
||||||
|
&--1 { color: #ff976a; }
|
||||||
|
&--2 { color: #07c160; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__purchase {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #323233;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__settle {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__paid {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 勾选圆圈 =====
|
||||||
|
.bill-check {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #c8c9cc;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background: #ee0a24;
|
||||||
|
border-color: #ee0a24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 导出操作栏 =====
|
||||||
|
.export-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #646566;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__all {
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
border: 1rpx solid #dcdee0;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
padding: 14rpx 40rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #ee0a24;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 分类选择弹层 =====
|
||||||
|
.category-popup {
|
||||||
|
padding: 32rpx 32rpx 24rpx;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
max-height: 560rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 8rpx;
|
||||||
|
border-bottom: 1rpx solid #f2f3f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react'
|
||||||
|
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Empty, Icon, Popup } from '@antmjs/vantui'
|
||||||
|
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||||
|
import { getBillExportUrl, getBillListApi } from '@/services/bill'
|
||||||
|
import { getCategoriesApi } from '@/services/product'
|
||||||
|
import { downloadExportFile } from '@/utils/download'
|
||||||
|
import type { Bill, BillSummary } from '@/services/bill'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
/** 单次导出上限(后端限制 1~100 张) */
|
||||||
|
const EXPORT_MAX = 100
|
||||||
|
|
||||||
|
/** 状态筛选(接口 status 为原始支付状态:0 未支付含审核中 / 1 已支付) */
|
||||||
|
const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [
|
||||||
|
{ value: undefined, label: '全部' },
|
||||||
|
{ value: 0, label: '未支付' },
|
||||||
|
{ value: 1, label: '已支付' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 导出分类选项(id=0 全部分类) */
|
||||||
|
interface CategoryOption {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单列表页
|
||||||
|
* 采购单完成后由后台按门店生成(只读);头部 summary 为门店口径待支付汇总(含审核中,不受筛选影响)
|
||||||
|
* 支持多选账单合并导出 Excel(可按一级分类过滤商品明细)
|
||||||
|
*/
|
||||||
|
export default function BillListPage() {
|
||||||
|
const token = useAuthStore(s => s.token)
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<0 | 1 | undefined>(undefined)
|
||||||
|
const [bills, setBills] = useState<Bill[]>([])
|
||||||
|
const [summary, setSummary] = useState<BillSummary | null>(null)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [finished, setFinished] = useState(false)
|
||||||
|
const loadingRef = useRef(false)
|
||||||
|
|
||||||
|
/** 导出:多选模式 + 已选账单 + 分类弹层 */
|
||||||
|
const [selectMode, setSelectMode] = useState(false)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||||
|
const [showCategory, setShowCategory] = useState(false)
|
||||||
|
const [categories, setCategories] = useState<CategoryOption[]>([])
|
||||||
|
|
||||||
|
const loggedIn = !!token
|
||||||
|
|
||||||
|
/** 拉取账单列表(summary 每次随响应刷新) */
|
||||||
|
const loadList = useCallback(
|
||||||
|
async (pageNum: number, reset: boolean, statusParam?: 0 | 1) => {
|
||||||
|
if (!loggedIn || loadingRef.current) return
|
||||||
|
loadingRef.current = true
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getBillListApi({ status: statusParam, page: pageNum, pageSize: PAGE_SIZE })
|
||||||
|
const { data, total, summary: sum } = res.data
|
||||||
|
setBills(prev => (reset ? data : [...prev, ...data]))
|
||||||
|
setSummary(sum)
|
||||||
|
setPage(pageNum)
|
||||||
|
setFinished(pageNum * PAGE_SIZE >= total)
|
||||||
|
} catch {
|
||||||
|
// 错误已由 request 层 toast
|
||||||
|
} finally {
|
||||||
|
loadingRef.current = false
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loggedIn],
|
||||||
|
)
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
loadList(1, true, status)
|
||||||
|
})
|
||||||
|
|
||||||
|
useReachBottom(() => {
|
||||||
|
if (!finished && !loadingRef.current && loggedIn) {
|
||||||
|
loadList(page + 1, false, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 切换状态筛选 */
|
||||||
|
const handleStatusTap = useCallback(
|
||||||
|
(value?: 0 | 1) => {
|
||||||
|
setStatus(value)
|
||||||
|
setFinished(false)
|
||||||
|
loadList(1, true, value)
|
||||||
|
},
|
||||||
|
[loadList],
|
||||||
|
)
|
||||||
|
|
||||||
|
const goLogin = useCallback(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const goDetail = useCallback((id: number) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${id}` })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** 进入/退出多选导出模式 */
|
||||||
|
const toggleSelectMode = useCallback(() => {
|
||||||
|
setSelectMode(prev => !prev)
|
||||||
|
setSelectedIds([])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** 账单点击:多选模式切换勾选,否则进详情 */
|
||||||
|
const handleItemTap = useCallback(
|
||||||
|
(bill: Bill) => {
|
||||||
|
if (!selectMode) {
|
||||||
|
goDetail(bill.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectedIds(prev => {
|
||||||
|
if (prev.includes(bill.id)) return prev.filter(i => i !== bill.id)
|
||||||
|
if (prev.length >= EXPORT_MAX) {
|
||||||
|
Taro.showToast({ title: `最多导出 ${EXPORT_MAX} 张`, icon: 'none' })
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
return [...prev, bill.id]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[selectMode, goDetail],
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 全选当前已加载账单(受导出上限约束) */
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
setSelectedIds(prev => {
|
||||||
|
if (prev.length === bills.length) return []
|
||||||
|
if (bills.length > EXPORT_MAX) {
|
||||||
|
Taro.showToast({ title: `最多导出 ${EXPORT_MAX} 张,已选前 ${EXPORT_MAX} 张`, icon: 'none' })
|
||||||
|
return bills.slice(0, EXPORT_MAX).map(b => b.id)
|
||||||
|
}
|
||||||
|
return bills.map(b => b.id)
|
||||||
|
})
|
||||||
|
}, [bills])
|
||||||
|
|
||||||
|
/** 打开分类选择弹层(首次加载分类树根节点) */
|
||||||
|
const handleExportTap = useCallback(async () => {
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
Taro.showToast({ title: '请先勾选要导出的账单', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (categories.length === 0) {
|
||||||
|
try {
|
||||||
|
const res = await getCategoriesApi()
|
||||||
|
setCategories([
|
||||||
|
{ id: 0, name: '全部分类' },
|
||||||
|
...res.data.map(c => ({ id: c.id, name: c.name })),
|
||||||
|
])
|
||||||
|
} catch {
|
||||||
|
return // 错误已由 request 层 toast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowCategory(true)
|
||||||
|
}, [selectedIds, categories])
|
||||||
|
|
||||||
|
/** 按所选分类导出合并 Excel */
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async (categoryId: number) => {
|
||||||
|
const isH5 = process.env.TARO_ENV === 'h5'
|
||||||
|
Taro.showLoading({ title: '导出中...', mask: true })
|
||||||
|
try {
|
||||||
|
await downloadExportFile(getBillExportUrl(selectedIds, categoryId), '门店账单.xlsx')
|
||||||
|
setShowCategory(false)
|
||||||
|
toggleSelectMode()
|
||||||
|
if (isH5) {
|
||||||
|
Taro.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Taro.showToast({ title: e?.message || '导出失败,请稍后重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
Taro.hideLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedIds, toggleSelectMode],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={`bill-page ${selectMode ? 'bill-page--select' : ''}`}>
|
||||||
|
{/* ========== 待支付汇总(门店口径,含审核中) ========== */}
|
||||||
|
{loggedIn && summary && summary.unpaid_count > 0 && (
|
||||||
|
<View className='bill-summary'>
|
||||||
|
<View className='bill-summary__info'>
|
||||||
|
<Text className='bill-summary__label'>待支付账单(含审核中)</Text>
|
||||||
|
<Text className='bill-summary__count'>{summary.unpaid_count} 笔</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='bill-summary__amount'>¥{summary.unpaid_amount}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========== 状态筛选 + 导出入口 ========== */}
|
||||||
|
<View className='bill-toolbar'>
|
||||||
|
<ScrollView scrollX className='status-scroll'>
|
||||||
|
{STATUS_FILTERS.map(item => (
|
||||||
|
<View
|
||||||
|
key={item.label}
|
||||||
|
className={`status-chip ${status === item.value ? 'active' : ''}`}
|
||||||
|
onClick={() => handleStatusTap(item.value)}
|
||||||
|
>
|
||||||
|
<Text>{item.label}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
{loggedIn && bills.length > 0 && (
|
||||||
|
<View className={`bill-toolbar__export ${selectMode ? 'active' : ''}`} onClick={toggleSelectMode}>
|
||||||
|
<Text>{selectMode ? '取消' : '导出'}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ========== 账单列表 ========== */}
|
||||||
|
{!loggedIn ? (
|
||||||
|
<Empty description='登录后查看账单' className='bill-empty'>
|
||||||
|
<View className='bill-empty__btn' onClick={goLogin}>去登录</View>
|
||||||
|
</Empty>
|
||||||
|
) : bills.length === 0 ? (
|
||||||
|
loading ? (
|
||||||
|
<View className='bill-loading'><Text>加载中...</Text></View>
|
||||||
|
) : (
|
||||||
|
<Empty description='暂无账单' className='bill-empty' />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
bills.map(bill => {
|
||||||
|
const checked = selectMode && selectedIds.includes(bill.id)
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={bill.id}
|
||||||
|
className={`bill-item ${selectMode ? 'bill-item--select' : ''}`}
|
||||||
|
onClick={() => handleItemTap(bill)}
|
||||||
|
>
|
||||||
|
{selectMode && (
|
||||||
|
<View className={`bill-check ${checked ? 'on' : ''}`}>
|
||||||
|
{checked && <Icon name='success' size={14} color='#fff' />}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='bill-item__content'>
|
||||||
|
<View className='bill-item__header'>
|
||||||
|
<Text className='bill-item__no'>{bill.bill_no}</Text>
|
||||||
|
<Text className={`bill-item__status bill-item__status--${bill.pay_state}`}>
|
||||||
|
{bill.pay_state_name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-item__body'>
|
||||||
|
<View className='bill-item__meta'>
|
||||||
|
<Text className='bill-item__date'>账单日期 {bill.bill_date}</Text>
|
||||||
|
{bill.purchase && (
|
||||||
|
<Text className='bill-item__purchase'>采购单 {bill.purchase.purchase_no}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text className='bill-item__amount'>¥{bill.total_amount}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-item__footer'>
|
||||||
|
<Text className='bill-item__settle'>应结算 {bill.settlement_date}</Text>
|
||||||
|
{bill.pay_state === 2 && bill.paid_at && (
|
||||||
|
<Text className='bill-item__paid'>已于 {bill.paid_at} 支付</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loggedIn && finished && bills.length > 0 && (
|
||||||
|
<View className='bill-loading'><Text>没有更多了</Text></View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========== 导出操作栏 ========== */}
|
||||||
|
{selectMode && (
|
||||||
|
<View className='export-bar'>
|
||||||
|
<Text className='export-bar__count'>已选 {selectedIds.length} 张</Text>
|
||||||
|
<View className='export-bar__all' onClick={handleSelectAll}>
|
||||||
|
{selectedIds.length === bills.length && bills.length > 0 ? '取消全选' : '全选'}
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`export-bar__btn ${selectedIds.length === 0 ? 'disabled' : ''}`}
|
||||||
|
onClick={handleExportTap}
|
||||||
|
>
|
||||||
|
导出 Excel
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========== 分类选择弹层 ========== */}
|
||||||
|
<Popup
|
||||||
|
show={showCategory}
|
||||||
|
position='bottom'
|
||||||
|
round
|
||||||
|
closeable
|
||||||
|
closeOnClickOverlay
|
||||||
|
safeAreaInsetBottom
|
||||||
|
onClose={() => setShowCategory(false)}
|
||||||
|
>
|
||||||
|
<View className='category-popup'>
|
||||||
|
<Text className='category-popup__title'>选择商品分类</Text>
|
||||||
|
<Text className='category-popup__desc'>
|
||||||
|
仅过滤商品明细行,配送费与附加金额仍全额汇总
|
||||||
|
</Text>
|
||||||
|
<ScrollView scrollY className='category-popup__list'>
|
||||||
|
{categories.map(c => (
|
||||||
|
<View key={c.id} className='category-popup__item' onClick={() => handleExport(c.id)}>
|
||||||
|
<Text className='category-popup__name'>{c.name}</Text>
|
||||||
|
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -66,7 +66,24 @@
|
|||||||
|
|
||||||
&__status {
|
&__status {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #ee0a24;
|
|
||||||
|
// 0 待接单 / 1 已接单 / 2 采购中 / 3 配送中 / 4 已完成 / 9 已取消
|
||||||
|
&--0 { color: #ee0a24; }
|
||||||
|
&--1 { color: #1989fa; }
|
||||||
|
&--2 { color: #1989fa; }
|
||||||
|
&--3 { color: #ff976a; }
|
||||||
|
&--4 { color: #07c160; }
|
||||||
|
&--9 { color: #969799; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #646566;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
@@ -105,8 +122,23 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cancel {
|
&__footer {
|
||||||
margin-top: 16rpx;
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bill {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 8rpx 24rpx;
|
||||||
|
border: 1rpx solid #1989fa;
|
||||||
|
color: #1989fa;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cancel {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 8rpx 24rpx;
|
padding: 8rpx 24rpx;
|
||||||
border: 1rpx solid #ee0a24;
|
border: 1rpx solid #ee0a24;
|
||||||
@@ -180,6 +212,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__remark {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,46 +4,46 @@ import { View, Text, ScrollView } from '@tarojs/components'
|
|||||||
import { Empty, Popup } from '@antmjs/vantui'
|
import { Empty, Popup } from '@antmjs/vantui'
|
||||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||||
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
||||||
import { ORDER_NAV_ITEMS, ORDER_STATUS_MAP } from '@/types/order'
|
import { ORDER_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order'
|
||||||
import type { Order, OrderStatus } from '@/types/order'
|
import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
|
||||||
import './index.less'
|
import './index.less'
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单列表页(框架)
|
* 订单列表页
|
||||||
* 入口:/pages/order-list/index?status=0|1|2|3|9|all
|
* 入口:/pages/order-list/index?status=0|1|2|3|4|9|all
|
||||||
* status 与 ORDER_NAV_ITEMS 映射(业务语言 → 后端枚举),缺省为全部
|
* 状态文案一律使用接口返回的 status_name(详情接口无该字段时沿用列表行)
|
||||||
*/
|
*/
|
||||||
export default function OrderListPage() {
|
export default function OrderListPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const token = useAuthStore(s => s.token)
|
const token = useAuthStore(s => s.token)
|
||||||
|
|
||||||
/** 当前状态筛选(undefined = 全部) */
|
/** 当前状态筛选(undefined = 全部) */
|
||||||
const [status, setStatus] = useState<number | undefined>(undefined)
|
const [status, setStatus] = useState<OrderStatus | undefined>(undefined)
|
||||||
const [orders, setOrders] = useState<Order[]>([])
|
const [orders, setOrders] = useState<OrderListItem[]>([])
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [total, setTotal] = useState(0)
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [finished, setFinished] = useState(false)
|
const [finished, setFinished] = useState(false)
|
||||||
const loadingRef = useRef(false)
|
const loadingRef = useRef(false)
|
||||||
|
|
||||||
/** 订单详情弹层 */
|
/** 订单详情弹层(statusName 沿用列表行,详情接口不返回 status_name) */
|
||||||
const [showDetail, setShowDetail] = useState(false)
|
const [showDetail, setShowDetail] = useState(false)
|
||||||
const [orderDetail, setOrderDetail] = useState<Order | null>(null)
|
const [orderDetail, setOrderDetail] = useState<OrderDetail | null>(null)
|
||||||
|
const [detailStatusName, setDetailStatusName] = useState('')
|
||||||
|
|
||||||
const loggedIn = !!token
|
const loggedIn = !!token
|
||||||
|
|
||||||
/** 解析路由参数中的 status('all' / 数字 / 缺省 → undefined) */
|
/** 解析路由参数中的 status('all' / 数字 / 缺省 → undefined) */
|
||||||
const parseStatus = useCallback((raw?: string): number | undefined => {
|
const parseStatus = useCallback((raw?: string): OrderStatus | undefined => {
|
||||||
if (!raw || raw === 'all' || raw === '') return undefined
|
if (!raw || raw === 'all' || raw === '') return undefined
|
||||||
const num = Number(raw)
|
const num = Number(raw)
|
||||||
return Number.isNaN(num) ? undefined : num
|
return Number.isNaN(num) ? undefined : (num as OrderStatus)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
/** 拉取订单列表 */
|
/** 拉取订单列表 */
|
||||||
const loadOrders = useCallback(
|
const loadOrders = useCallback(
|
||||||
async (pageNum: number, reset: boolean, statusParam?: number) => {
|
async (pageNum: number, reset: boolean, statusParam?: OrderStatus) => {
|
||||||
if (!loggedIn || loadingRef.current) return
|
if (!loggedIn || loadingRef.current) return
|
||||||
loadingRef.current = true
|
loadingRef.current = true
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -53,11 +53,10 @@ export default function OrderListPage() {
|
|||||||
page: pageNum,
|
page: pageNum,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
const { data, total: totalCount } = res.data
|
const { data, total } = res.data
|
||||||
setOrders(prev => (reset ? data : [...prev, ...data]))
|
setOrders(prev => (reset ? data : [...prev, ...data]))
|
||||||
setTotal(totalCount)
|
|
||||||
setPage(pageNum)
|
setPage(pageNum)
|
||||||
setFinished(pageNum * PAGE_SIZE >= totalCount)
|
setFinished(pageNum * PAGE_SIZE >= total)
|
||||||
} catch {
|
} catch {
|
||||||
// 错误已由 request 层 toast
|
// 错误已由 request 层 toast
|
||||||
} finally {
|
} finally {
|
||||||
@@ -82,7 +81,7 @@ export default function OrderListPage() {
|
|||||||
|
|
||||||
/** 切换状态筛选 */
|
/** 切换状态筛选 */
|
||||||
const handleStatusTap = useCallback(
|
const handleStatusTap = useCallback(
|
||||||
(value?: number) => {
|
(value?: OrderStatus) => {
|
||||||
setStatus(value)
|
setStatus(value)
|
||||||
setFinished(false)
|
setFinished(false)
|
||||||
loadOrders(1, true, value)
|
loadOrders(1, true, value)
|
||||||
@@ -91,19 +90,20 @@ export default function OrderListPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** 查看订单详情 */
|
/** 查看订单详情 */
|
||||||
const handleOrderTap = useCallback(async (order: Order) => {
|
const handleOrderTap = useCallback(async (order: OrderListItem) => {
|
||||||
try {
|
try {
|
||||||
const res = await getOrderDetailApi(order.id)
|
const res = await getOrderDetailApi(order.id)
|
||||||
setOrderDetail(res.data)
|
setOrderDetail(res.data)
|
||||||
|
setDetailStatusName(order.status_name)
|
||||||
setShowDetail(true)
|
setShowDetail(true)
|
||||||
} catch {
|
} catch {
|
||||||
// 错误已由 request 层 toast
|
// 错误已由 request 层 toast
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
/** 取消订单(仅待汇总可取消) */
|
/** 取消订单(仅待接单可取消,以 can_cancel 为准) */
|
||||||
const handleCancelOrder = useCallback(
|
const handleCancelOrder = useCallback(
|
||||||
(order: Order) => {
|
(order: OrderListItem) => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '取消订单',
|
title: '取消订单',
|
||||||
content: `确定取消订单 ${order.order_no} 吗?`,
|
content: `确定取消订单 ${order.order_no} 吗?`,
|
||||||
@@ -123,6 +123,11 @@ export default function OrderListPage() {
|
|||||||
[loadOrders, status],
|
[loadOrders, status],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 跳转关联账单详情 */
|
||||||
|
const handleBillTap = useCallback((billId: number) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${billId}` })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const goLogin = useCallback(() => {
|
const goLogin = useCallback(() => {
|
||||||
Taro.navigateTo({ url: '/pages/login/index' })
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -131,17 +136,11 @@ export default function OrderListPage() {
|
|||||||
<View className='order-list-page'>
|
<View className='order-list-page'>
|
||||||
{/* ========== 状态筛选 ========== */}
|
{/* ========== 状态筛选 ========== */}
|
||||||
<ScrollView scrollX className='status-scroll'>
|
<ScrollView scrollX className='status-scroll'>
|
||||||
<View
|
{ORDER_STATUS_FILTERS.map(item => (
|
||||||
className={`status-chip ${status === undefined ? 'active' : ''}`}
|
|
||||||
onClick={() => handleStatusTap(undefined)}
|
|
||||||
>
|
|
||||||
<Text>全部</Text>
|
|
||||||
</View>
|
|
||||||
{ORDER_NAV_ITEMS.map(item => (
|
|
||||||
<View
|
<View
|
||||||
key={item.key}
|
key={item.label}
|
||||||
className={`status-chip ${status === item.status ? 'active' : ''}`}
|
className={`status-chip ${status === item.value ? 'active' : ''}`}
|
||||||
onClick={() => handleStatusTap(item.status)}
|
onClick={() => handleStatusTap(item.value)}
|
||||||
>
|
>
|
||||||
<Text>{item.label}</Text>
|
<Text>{item.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -164,25 +163,47 @@ export default function OrderListPage() {
|
|||||||
<View key={order.id} className='order-item' onClick={() => handleOrderTap(order)}>
|
<View key={order.id} className='order-item' onClick={() => handleOrderTap(order)}>
|
||||||
<View className='order-item__header'>
|
<View className='order-item__header'>
|
||||||
<Text className='order-item__no'>{order.order_no}</Text>
|
<Text className='order-item__no'>{order.order_no}</Text>
|
||||||
<Text className='order-item__status'>{ORDER_STATUS_MAP[order.status]}</Text>
|
<Text className={`order-item__status order-item__status--${order.status}`}>
|
||||||
|
{order.status_name}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{/* 商品预览(仅前 3 条,完整明细见详情) */}
|
||||||
|
<Text className='order-item__preview'>
|
||||||
|
{order.items.map(i => `${i.product_name}×${i.quantity}`).join('、')}
|
||||||
|
{order.item_count > 3 ? ` 等${order.item_count}种` : ''}
|
||||||
|
</Text>
|
||||||
<View className='order-item__body'>
|
<View className='order-item__body'>
|
||||||
<Text className='order-item__date'>{order.order_date}</Text>
|
<Text className='order-item__date'>订货日期 {order.order_date}</Text>
|
||||||
<View className='order-item__amounts'>
|
<View className='order-item__amounts'>
|
||||||
<Text className='order-item__qty'>共 {order.total_quantity} 件</Text>
|
<Text className='order-item__qty'>共 {order.total_quantity} 件</Text>
|
||||||
<Text className='order-item__amount'>¥{order.total_amount}</Text>
|
<Text className='order-item__amount'>¥{order.total_amount}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{order.remark && <Text className='order-item__remark'>备注:{order.remark}</Text>}
|
{order.remark && <Text className='order-item__remark'>备注:{order.remark}</Text>}
|
||||||
{order.status === 0 && (
|
{(order.can_cancel || order.bill_id > 0) && (
|
||||||
<View
|
<View className='order-item__footer'>
|
||||||
className='order-item__cancel'
|
{order.bill_id > 0 && (
|
||||||
onClick={e => {
|
<View
|
||||||
e.stopPropagation()
|
className='order-item__bill'
|
||||||
handleCancelOrder(order)
|
onClick={e => {
|
||||||
}}
|
e.stopPropagation()
|
||||||
>
|
handleBillTap(order.bill_id)
|
||||||
取消订单
|
}}
|
||||||
|
>
|
||||||
|
查看账单
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{order.can_cancel && (
|
||||||
|
<View
|
||||||
|
className='order-item__cancel'
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleCancelOrder(order)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消订单
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -207,8 +228,10 @@ export default function OrderListPage() {
|
|||||||
<View className='detail-popup'>
|
<View className='detail-popup'>
|
||||||
<Text className='detail-popup__title'>{orderDetail.order_no}</Text>
|
<Text className='detail-popup__title'>{orderDetail.order_no}</Text>
|
||||||
<View className='detail-popup__meta'>
|
<View className='detail-popup__meta'>
|
||||||
<Text>{orderDetail.order_date}</Text>
|
<Text>{orderDetail.order_date} 下单 {orderDetail.created_at}</Text>
|
||||||
<Text className='detail-popup__status'>{ORDER_STATUS_MAP[orderDetail.status]}</Text>
|
<Text className='detail-popup__status'>
|
||||||
|
{detailStatusName || ORDER_STATUS_TEXT[orderDetail.status] || ''}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView scrollY className='detail-popup__list'>
|
<ScrollView scrollY className='detail-popup__list'>
|
||||||
{(orderDetail.items ?? []).map(item => (
|
{(orderDetail.items ?? []).map(item => (
|
||||||
@@ -216,13 +239,16 @@ export default function OrderListPage() {
|
|||||||
<View className='detail-popup__item-info'>
|
<View className='detail-popup__item-info'>
|
||||||
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
||||||
<Text className='detail-popup__item-spec'>
|
<Text className='detail-popup__item-spec'>
|
||||||
{item.product_spec} ¥{item.price} × {item.quantity}
|
{item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
{orderDetail.remark && (
|
||||||
|
<Text className='detail-popup__remark'>备注:{orderDetail.remark}</Text>
|
||||||
|
)}
|
||||||
<View className='detail-popup__footer'>
|
<View className='detail-popup__footer'>
|
||||||
<Text className='detail-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
<Text className='detail-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -144,6 +144,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 账单入口 =====
|
||||||
|
.bill-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 84rpx;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fff0f0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ee0a24;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #323233;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
min-width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #ee0a24;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #c8c9cc;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 功能菜单 =====
|
// ===== 功能菜单 =====
|
||||||
.menu-cell {
|
.menu-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+46
-14
@@ -1,28 +1,18 @@
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text, Image } from '@tarojs/components'
|
import { View, Text, Image } from '@tarojs/components'
|
||||||
import { Icon } from '@antmjs/vantui'
|
import { Icon } from '@antmjs/vantui'
|
||||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||||
import { getUserInfoApi } from '@/services/auth'
|
import { getUserInfoApi } from '@/services/auth'
|
||||||
|
import { getBillListApi } from '@/services/bill'
|
||||||
|
import type { BillSummary } from '@/services/bill'
|
||||||
import { ORDER_NAV_ITEMS } from '@/types/order'
|
import { ORDER_NAV_ITEMS } from '@/types/order'
|
||||||
import { resolveAvatarUrl } from '@/utils/format'
|
import { resolveAvatarUrl } from '@/utils/format'
|
||||||
import type { UserType } from '@/types/user'
|
import type { UserType } from '@/types/user'
|
||||||
import './index.less'
|
import './index.less'
|
||||||
|
|
||||||
/** 菜单项(后续单独页面开发时在此追加) */
|
/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
{
|
|
||||||
key: 'orders',
|
|
||||||
label: '所有订单',
|
|
||||||
icon: 'orders-o',
|
|
||||||
onClick: () => Taro.navigateTo({ url: '/pages/order-list/index?status=all' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'statement',
|
|
||||||
label: '账单',
|
|
||||||
icon: 'balance-list-o',
|
|
||||||
onClick: () => Taro.navigateTo({ url: '/pages/statement/index' }),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'message',
|
key: 'message',
|
||||||
label: '消息中心',
|
label: '消息中心',
|
||||||
@@ -43,6 +33,9 @@ export default function ProfilePage() {
|
|||||||
const setUser = useAuthStore(s => s.setUser)
|
const setUser = useAuthStore(s => s.setUser)
|
||||||
const logout = useAuthStore(s => s.logout)
|
const logout = useAuthStore(s => s.logout)
|
||||||
|
|
||||||
|
/** 账单待支付汇总(门店口径,含审核中) */
|
||||||
|
const [billSummary, setBillSummary] = useState<BillSummary | null>(null)
|
||||||
|
|
||||||
const loggedIn = !!token && !!user
|
const loggedIn = !!token && !!user
|
||||||
|
|
||||||
/** 功能菜单(门店账号追加「门店信息」入口) */
|
/** 功能菜单(门店账号追加「门店信息」入口) */
|
||||||
@@ -65,6 +58,10 @@ export default function ProfilePage() {
|
|||||||
getUserInfoApi()
|
getUserInfoApi()
|
||||||
.then(res => setUser(res.data))
|
.then(res => setUser(res.data))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
// 刷新账单待支付汇总(pageSize=1 仅取 summary)
|
||||||
|
getBillListApi({ page: 1, pageSize: 1 })
|
||||||
|
.then(res => setBillSummary(res.data.summary))
|
||||||
|
.catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 身份标签 */
|
/** 身份标签 */
|
||||||
@@ -79,6 +76,11 @@ export default function ProfilePage() {
|
|||||||
Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` })
|
Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/** 账单入口 → 账单列表页 */
|
||||||
|
const goBill = useCallback(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/bill/index' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
/** 退出登录 */
|
/** 退出登录 */
|
||||||
const handleLogout = useCallback(() => {
|
const handleLogout = useCallback(() => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
@@ -169,6 +171,36 @@ export default function ProfilePage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* ========== 我的账单 ========== */}
|
||||||
|
<View className='profile-section'>
|
||||||
|
<View className='profile-section__header'>
|
||||||
|
<Text className='profile-section__title'>我的账单</Text>
|
||||||
|
<Text className='profile-section__more' onClick={goBill}>全部 ›</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-entry' onClick={goBill}>
|
||||||
|
<View className='bill-entry__icon'>
|
||||||
|
<Icon name='balance-list-o' size={28} color='#ee0a24' />
|
||||||
|
</View>
|
||||||
|
{loggedIn && billSummary && billSummary.unpaid_count > 0 ? (
|
||||||
|
<>
|
||||||
|
<View className='bill-entry__info'>
|
||||||
|
<Text className='bill-entry__amount'>¥{billSummary.unpaid_amount}</Text>
|
||||||
|
<Text className='bill-entry__desc'>待支付 {billSummary.unpaid_count} 笔(含审核中)</Text>
|
||||||
|
</View>
|
||||||
|
<View className='bill-entry__badge'>{billSummary.unpaid_count}</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<View className='bill-entry__info'>
|
||||||
|
<Text className='bill-entry__title'>
|
||||||
|
{loggedIn ? '暂无待支付账单' : '登录后查看账单'}
|
||||||
|
</Text>
|
||||||
|
<Text className='bill-entry__desc'>账单由后台按采购单自动生成</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text className='bill-entry__arrow'>›</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* ========== 功能菜单 ========== */}
|
{/* ========== 功能菜单 ========== */}
|
||||||
<View className='profile-section profile-section--menu'>
|
<View className='profile-section profile-section--menu'>
|
||||||
{menuItems.map(item => (
|
{menuItems.map(item => (
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import Taro, { useRouter } from '@tarojs/taro'
|
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import { Button, Empty } from '@antmjs/vantui'
|
|
||||||
import { getStatementDetailApi, payStatementApi } from '@/services/statement'
|
|
||||||
import { STATEMENT_STATUS_MAP } from '@/types/statement'
|
|
||||||
import type { StatementDetail } from '@/types/statement'
|
|
||||||
import './index.less'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单详情
|
|
||||||
* 商品汇总(金额直接合并,单价明细见订单记录)+ 回筐计费 + 关联订单 + 付款确认
|
|
||||||
*/
|
|
||||||
export default function StatementDetailPage() {
|
|
||||||
const router = useRouter()
|
|
||||||
const id = Number(router.params.id ?? 0)
|
|
||||||
|
|
||||||
const [detail, setDetail] = useState<StatementDetail | null>(null)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [paying, setPaying] = useState(false)
|
|
||||||
|
|
||||||
const loadDetail = useCallback(async () => {
|
|
||||||
if (!id) return
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const res = await getStatementDetailApi(id)
|
|
||||||
setDetail(res.data)
|
|
||||||
} catch {
|
|
||||||
// 错误已由 request 层 toast
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadDetail()
|
|
||||||
}, [loadDetail])
|
|
||||||
|
|
||||||
/** 付款确认 */
|
|
||||||
const handlePay = useCallback(async () => {
|
|
||||||
if (!detail || paying) return
|
|
||||||
const confirm = await Taro.showModal({
|
|
||||||
title: '付款确认',
|
|
||||||
content: `确认已支付账单 ${detail.statement_no} 共 ¥${detail.total_amount}?`,
|
|
||||||
confirmText: '确认付款',
|
|
||||||
})
|
|
||||||
if (!confirm.confirm) return
|
|
||||||
setPaying(true)
|
|
||||||
try {
|
|
||||||
await payStatementApi(detail.id)
|
|
||||||
Taro.showToast({ title: '付款确认成功', icon: 'success' })
|
|
||||||
loadDetail()
|
|
||||||
} catch {
|
|
||||||
// 错误(重复付款等)已由 request 层 toast
|
|
||||||
} finally {
|
|
||||||
setPaying(false)
|
|
||||||
}
|
|
||||||
}, [detail, paying, loadDetail])
|
|
||||||
|
|
||||||
if (loading && !detail) {
|
|
||||||
return <View className='bill-detail'><Empty description='加载中...' /></View>
|
|
||||||
}
|
|
||||||
if (!detail) {
|
|
||||||
return <View className='bill-detail'><Empty description='账单不存在' /></View>
|
|
||||||
}
|
|
||||||
|
|
||||||
const crateNegative = Number(detail.crate_amount) < 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='bill-detail'>
|
|
||||||
{/* ===== 账单信息 ===== */}
|
|
||||||
<View className='bill-card'>
|
|
||||||
<View className='bill-card__header'>
|
|
||||||
<Text className='bill-card__no'>{detail.statement_no}</Text>
|
|
||||||
<Text
|
|
||||||
className={detail.status === 0 ? 'bill-card__status bill-card__status--unpaid' : 'bill-card__status bill-card__status--paid'}
|
|
||||||
>
|
|
||||||
{STATEMENT_STATUS_MAP[detail.status]}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>账单周期</Text>
|
|
||||||
<Text className='bill-card__value'>{detail.period_start} ~ {detail.period_end}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>应结算日期</Text>
|
|
||||||
<Text className='bill-card__value'>{detail.settlement_date ?? '-'}</Text>
|
|
||||||
</View>
|
|
||||||
{detail.status === 1 && detail.settled_at && (
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>付款时间</Text>
|
|
||||||
<Text className='bill-card__value'>{detail.settled_at}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>商品金额</Text>
|
|
||||||
<Text className='bill-card__value'>¥{detail.goods_amount}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>回筐金额</Text>
|
|
||||||
<Text className={crateNegative ? 'bill-card__value bill-card__value--credit' : 'bill-card__value'}>
|
|
||||||
{crateNegative ? `-¥${Math.abs(Number(detail.crate_amount)).toFixed(2)}` : `¥${detail.crate_amount}`}
|
|
||||||
{crateNegative ? '(抵扣)' : ''}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-card__row bill-card__row--total'>
|
|
||||||
<Text className='bill-card__label'>账单总额</Text>
|
|
||||||
<Text className='bill-card__total'>¥{detail.total_amount}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ===== 商品汇总 ===== */}
|
|
||||||
<View className='bill-card'>
|
|
||||||
<Text className='bill-section__title'>商品汇总({detail.goods?.length ?? 0})</Text>
|
|
||||||
<Text className='bill-section__desc'>同一商品多次订单金额直接合并;单价明细请查看订单记录</Text>
|
|
||||||
{(detail.goods ?? []).map((item, index) => (
|
|
||||||
<View key={index} className='bill-goods'>
|
|
||||||
<View className='bill-goods__main'>
|
|
||||||
<Text className='bill-goods__name'>{item.product_name}</Text>
|
|
||||||
<Text className='bill-goods__spec'>{item.product_spec || ''}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-goods__side'>
|
|
||||||
<Text className='bill-goods__qty'>{item.quantity} {item.unit}</Text>
|
|
||||||
<Text className='bill-goods__amount'>¥{item.amount}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
{(detail.goods ?? []).length === 0 && <Empty description='暂无商品记录' />}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ===== 回筐计费 ===== */}
|
|
||||||
<View className='bill-card'>
|
|
||||||
<Text className='bill-section__title'>回筐计费</Text>
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>周转框</Text>
|
|
||||||
<Text className='bill-card__value'>{detail.box_num} 个{detail.box_num < 0 ? '(抵扣)' : ''}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bill-card__row'>
|
|
||||||
<Text className='bill-card__label'>托盘</Text>
|
|
||||||
<Text className='bill-card__value'>{detail.tray_num} 个{detail.tray_num < 0 ? '(抵扣)' : ''}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ===== 关联订单 ===== */}
|
|
||||||
<View className='bill-card'>
|
|
||||||
<Text className='bill-section__title'>关联订单({detail.orders?.length ?? 0})</Text>
|
|
||||||
{(detail.orders ?? []).map(order => (
|
|
||||||
<View
|
|
||||||
key={order.id}
|
|
||||||
className='bill-order'
|
|
||||||
onClick={() => Taro.navigateTo({ url: `/pages/order-list/index` })}
|
|
||||||
>
|
|
||||||
<Text className='bill-order__no'>{order.order_no}</Text>
|
|
||||||
<Text className='bill-order__date'>{order.order_date}</Text>
|
|
||||||
<Text className='bill-order__amount'>¥{order.total_amount}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
{(detail.orders ?? []).length === 0 && <Empty description='暂无订单记录' />}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ===== 付款按钮 ===== */}
|
|
||||||
{detail.status === 0 && (
|
|
||||||
<View className='bill-pay'>
|
|
||||||
<Button
|
|
||||||
type='danger'
|
|
||||||
block
|
|
||||||
round
|
|
||||||
loading={paying}
|
|
||||||
className='bill-pay__btn'
|
|
||||||
onClick={handlePay}
|
|
||||||
>
|
|
||||||
付款(¥{detail.total_amount})
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
.statement-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f7f8fa;
|
|
||||||
padding: 20rpx 24rpx 60rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
// ===== 头部 =====
|
|
||||||
.statement-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8rpx 8rpx 24rpx;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tip {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #969799;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.statement-empty {
|
|
||||||
padding-top: 120rpx;
|
|
||||||
|
|
||||||
&__btn {
|
|
||||||
margin-top: 24rpx;
|
|
||||||
padding: 14rpx 60rpx;
|
|
||||||
background: #ee0a24;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.statement-loading {
|
|
||||||
padding: 30rpx 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #c8c9cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 账单单项 =====
|
|
||||||
.statement-item {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 24rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__no {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #323233;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__status {
|
|
||||||
font-size: 24rpx;
|
|
||||||
|
|
||||||
&--unpaid {
|
|
||||||
color: #ee0a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--paid {
|
|
||||||
color: #07c160;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__body {
|
|
||||||
margin-top: 12rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__period {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #969799;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__amount {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #323233;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__settle {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #969799;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from 'react'
|
|
||||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import { Empty } from '@antmjs/vantui'
|
|
||||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
|
||||||
import { getStatementListApi } from '@/services/statement'
|
|
||||||
import { STATEMENT_STATUS_MAP } from '@/types/statement'
|
|
||||||
import type { Statement } from '@/types/statement'
|
|
||||||
import './index.less'
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 账单页
|
|
||||||
* 账单按门店回款周期自动生成:列表 + 查看详情 + 付款
|
|
||||||
*/
|
|
||||||
export default function StatementPage() {
|
|
||||||
const token = useAuthStore(s => s.token)
|
|
||||||
|
|
||||||
const [statements, setStatements] = useState<Statement[]>([])
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [finished, setFinished] = useState(false)
|
|
||||||
const loadingRef = useRef(false)
|
|
||||||
|
|
||||||
const loggedIn = !!token
|
|
||||||
|
|
||||||
/** 拉取账单列表 */
|
|
||||||
const loadList = useCallback(
|
|
||||||
async (pageNum: number, reset: boolean) => {
|
|
||||||
if (!loggedIn || loadingRef.current) return
|
|
||||||
loadingRef.current = true
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const res = await getStatementListApi({ page: pageNum, pageSize: PAGE_SIZE })
|
|
||||||
const { data, total } = res.data
|
|
||||||
setStatements(prev => (reset ? data : [...prev, ...data]))
|
|
||||||
setPage(pageNum)
|
|
||||||
setFinished(pageNum * PAGE_SIZE >= total)
|
|
||||||
} catch {
|
|
||||||
// 错误已由 request 层 toast
|
|
||||||
} finally {
|
|
||||||
loadingRef.current = false
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[loggedIn],
|
|
||||||
)
|
|
||||||
|
|
||||||
useDidShow(() => {
|
|
||||||
loadList(1, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
useReachBottom(() => {
|
|
||||||
if (!finished && !loadingRef.current && loggedIn) {
|
|
||||||
loadList(page + 1, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const goLogin = useCallback(() => {
|
|
||||||
Taro.navigateTo({ url: '/pages/login/index' })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const goDetail = useCallback((id: number) => {
|
|
||||||
Taro.navigateTo({ url: `/pages/statement-detail/index?id=${id}` })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='statement-page'>
|
|
||||||
{/* ========== 头部 ========== */}
|
|
||||||
<View className='statement-header'>
|
|
||||||
<Text className='statement-header__title'>账单</Text>
|
|
||||||
<Text className='statement-header__tip'>按回款周期自动生成</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ========== 列表 ========== */}
|
|
||||||
{!loggedIn ? (
|
|
||||||
<Empty description='登录后查看账单' className='statement-empty'>
|
|
||||||
<View className='statement-empty__btn' onClick={goLogin}>去登录</View>
|
|
||||||
</Empty>
|
|
||||||
) : statements.length === 0 ? (
|
|
||||||
loading ? (
|
|
||||||
<View className='statement-loading'><Text>加载中...</Text></View>
|
|
||||||
) : (
|
|
||||||
<Empty description='暂无账单' className='statement-empty' />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
statements.map(st => (
|
|
||||||
<View key={st.id} className='statement-item' onClick={() => goDetail(st.id)}>
|
|
||||||
<View className='statement-item__header'>
|
|
||||||
<Text className='statement-item__no'>{st.statement_no}</Text>
|
|
||||||
<Text
|
|
||||||
className={st.status === 0 ? 'statement-item__status statement-item__status--unpaid' : 'statement-item__status statement-item__status--paid'}
|
|
||||||
>
|
|
||||||
{STATEMENT_STATUS_MAP[st.status]}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className='statement-item__body'>
|
|
||||||
<Text className='statement-item__period'>
|
|
||||||
{st.period_start} ~ {st.period_end}
|
|
||||||
</Text>
|
|
||||||
<Text className='statement-item__amount'>¥{st.total_amount}</Text>
|
|
||||||
</View>
|
|
||||||
{st.settlement_date && (
|
|
||||||
<Text className='statement-item__settle'>应结算:{st.settlement_date}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loggedIn && finished && statements.length > 0 && (
|
|
||||||
<View className='statement-loading'><Text>没有更多了</Text></View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { BASE_URL, get } from '@/utils/request'
|
||||||
|
import type { PaginatedData } from '@/types/api'
|
||||||
|
|
||||||
|
/** 账单支付进度:0 待支付 / 1 审核中 / 2 已支付(由后端推导,展示以此为准) */
|
||||||
|
export type BillPayState = 0 | 1 | 2
|
||||||
|
|
||||||
|
/** 账单(列表行与详情的 bill 字段一致) */
|
||||||
|
export interface Bill {
|
||||||
|
id: number
|
||||||
|
/** 账单编号(ZD 前缀) */
|
||||||
|
bill_no: string
|
||||||
|
/** 账单日期(出账日,Y-m-d) */
|
||||||
|
bill_date: string
|
||||||
|
/** 关联采购单 ID */
|
||||||
|
purchase_id: number
|
||||||
|
/** 关联采购单 */
|
||||||
|
purchase: { id: number; purchase_no: string; purchase_date: string } | null
|
||||||
|
/** 商品金额(订单汇总快照) */
|
||||||
|
product_amount: string
|
||||||
|
/** 配送费 */
|
||||||
|
delivery_fee: string
|
||||||
|
/** 周转筐 / 周转托盘数量 */
|
||||||
|
box_num: number
|
||||||
|
tray_num: number
|
||||||
|
/** 筐 / 托盘单价(出账时快照) */
|
||||||
|
box_price: string
|
||||||
|
tray_price: string
|
||||||
|
/** 附加金额 = box_num×box_price + tray_num×tray_price */
|
||||||
|
added_amount: string
|
||||||
|
/** 账单总金额 = 商品金额 + 配送费 + 附加金额 */
|
||||||
|
total_amount: string
|
||||||
|
/** 原始支付状态:0 未支付 / 1 已支付(展示用 pay_state 系列字段) */
|
||||||
|
status: 0 | 1
|
||||||
|
status_name: string
|
||||||
|
/** 支付进度:0 待支付 / 1 审核中 / 2 已支付 */
|
||||||
|
pay_state: BillPayState
|
||||||
|
/** 支付进度中文名(列表状态标签直接用它) */
|
||||||
|
pay_state_name: string
|
||||||
|
/** 是否可发起合并付款(=待支付) */
|
||||||
|
can_pay: boolean
|
||||||
|
/** 关联支付记录 ID,0=未发起付款 */
|
||||||
|
payment_id: number
|
||||||
|
/** 应结算日期 = 账单日期 + 门店回款周期天数 */
|
||||||
|
settlement_date: string
|
||||||
|
/** 付款时间(已支付时非空) */
|
||||||
|
paid_at: string | null
|
||||||
|
/** 付款备注 */
|
||||||
|
pay_remark: string
|
||||||
|
/** 账单备注 */
|
||||||
|
remark: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待支付汇总(门店口径,含审核中,不受筛选参数影响) */
|
||||||
|
export interface BillSummary {
|
||||||
|
unpaid_count: number
|
||||||
|
unpaid_amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单合并商品明细(跨本账单全部订单按商品聚合) */
|
||||||
|
export interface BillItem {
|
||||||
|
product_id: number
|
||||||
|
product_name: string
|
||||||
|
product_spec: string
|
||||||
|
unit: string
|
||||||
|
/** 加权平均单价(Σ金额÷Σ数量) */
|
||||||
|
price: string
|
||||||
|
/** 合计数量 */
|
||||||
|
quantity: number
|
||||||
|
weight: string
|
||||||
|
/** 合计金额 */
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单关联订单 */
|
||||||
|
export interface BillOrder {
|
||||||
|
id: number
|
||||||
|
order_no: string
|
||||||
|
order_date: string
|
||||||
|
total_quantity: number
|
||||||
|
total_weight: string
|
||||||
|
total_amount: string
|
||||||
|
/** 订单状态枚举(0/1/2/3/4/9) */
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单详情 */
|
||||||
|
export interface BillDetail {
|
||||||
|
bill: Bill
|
||||||
|
items: BillItem[]
|
||||||
|
orders: BillOrder[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单列表查询参数 */
|
||||||
|
export interface BillListParams {
|
||||||
|
/** 原始支付状态:0 未支付(含审核中)/ 1 已支付,不传=全部 */
|
||||||
|
status?: 0 | 1
|
||||||
|
/** 传 1 = 仅可发起付款的账单(合并付款选择页专用) */
|
||||||
|
payable?: 1
|
||||||
|
/** 账单日期起(Y-m-d) */
|
||||||
|
start_date?: string
|
||||||
|
/** 账单日期止(Y-m-d),不得早于 start_date */
|
||||||
|
end_date?: string
|
||||||
|
page?: number
|
||||||
|
/** 每页数量,默认 10,最大 50 */
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单列表(附加 summary 待支付汇总):GET /mini/bill */
|
||||||
|
export function getBillListApi(params: BillListParams = {}) {
|
||||||
|
return get<PaginatedData<Bill> & { summary: BillSummary }>('/mini/bill', { data: params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账单详情(校验本店归属):GET /mini/bill/{id} */
|
||||||
|
export function getBillDetailApi(id: number) {
|
||||||
|
return get<BillDetail>(`/mini/bill/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单合并导出文件地址:GET /mini/bill/export
|
||||||
|
* 返回 xlsx 文件流(非 JSON,需带 token 下载);categoryId 仅过滤商品明细,费用仍全额汇总
|
||||||
|
*/
|
||||||
|
export function getBillExportUrl(ids: number[], categoryId = 0): string {
|
||||||
|
return `${BASE_URL}/mini/bill/export?ids=${ids.join(',')}&category_id=${categoryId}`
|
||||||
|
}
|
||||||
+20
-14
@@ -1,6 +1,6 @@
|
|||||||
import { get, post, put } from '@/utils/request'
|
import { get, post, put } from '@/utils/request'
|
||||||
import type { PaginatedData } from '@/types/api'
|
import type { PaginatedData } from '@/types/api'
|
||||||
import type { Order, OrderCreateResult, OrderSummary } from '@/types/order'
|
import type { OrderCreateResult, OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
|
||||||
|
|
||||||
/** 下单明细行(金额一律服务端重算,前端不传金额) */
|
/** 下单明细行(金额一律服务端重算,前端不传金额) */
|
||||||
export interface OrderItemParam {
|
export interface OrderItemParam {
|
||||||
@@ -15,29 +15,35 @@ export interface CreateOrderParams {
|
|||||||
remark?: string
|
remark?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 订单列表查询参数 */
|
||||||
|
export interface OrderListParams {
|
||||||
|
/** 订单状态,不传=全部 */
|
||||||
|
status?: OrderStatus
|
||||||
|
/** 订货日期起(Y-m-d) */
|
||||||
|
start_date?: string
|
||||||
|
/** 订货日期止(Y-m-d),不得早于 start_date */
|
||||||
|
end_date?: string
|
||||||
|
page?: number
|
||||||
|
/** 每页数量,默认 10,最大 50 */
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
/** 下单:POST /mini/order */
|
/** 下单:POST /mini/order */
|
||||||
export function createOrderApi(params: CreateOrderParams) {
|
export function createOrderApi(params: CreateOrderParams) {
|
||||||
return post<OrderCreateResult>('/mini/order', params)
|
return post<OrderCreateResult>('/mini/order', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 历史订单列表(强制本店隔离):GET /mini/order */
|
/** 历史订单列表(强制本店隔离,按订货日期倒序):GET /mini/order */
|
||||||
export function getOrderListApi(
|
export function getOrderListApi(params: OrderListParams = {}) {
|
||||||
params: { status?: number; page?: number; pageSize?: number } = {},
|
return get<PaginatedData<OrderListItem>>('/mini/order', { data: params })
|
||||||
) {
|
|
||||||
return get<PaginatedData<Order>>('/mini/order', { data: params })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 周期汇总:GET /mini/order/summary */
|
/** 订单详情(校验本店归属,含完整明细):GET /mini/order/{id} */
|
||||||
export function getOrderSummaryApi(period: 'day' | 'week' | 'month' = 'month') {
|
|
||||||
return get<OrderSummary>('/mini/order/summary', { data: { period } })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 订单详情(校验本店归属):GET /mini/order/{id} */
|
|
||||||
export function getOrderDetailApi(id: number) {
|
export function getOrderDetailApi(id: number) {
|
||||||
return get<Order>(`/mini/order/${id}`)
|
return get<OrderDetail>(`/mini/order/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消订单(仅待汇总可取消):PUT /mini/order/{id}/cancel */
|
/** 取消订单(仅待接单可取消):PUT /mini/order/{id}/cancel */
|
||||||
export function cancelOrderApi(id: number) {
|
export function cancelOrderApi(id: number) {
|
||||||
return put(`/mini/order/${id}/cancel`)
|
return put(`/mini/order/${id}/cancel`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { get, post } from '@/utils/request'
|
|
||||||
import type { PaginatedData } from '@/types/api'
|
|
||||||
import type { PendingContainers, Statement, StatementDetail } from '@/types/statement'
|
|
||||||
|
|
||||||
/** 账单列表(仅本店,按回款周期自动生成):GET /mini/statement */
|
|
||||||
export function getStatementListApi(params: { page?: number; pageSize?: number } = {}) {
|
|
||||||
return get<PaginatedData<Statement>>('/mini/statement', { data: params })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 账单详情(校验归属,含商品汇总/回筐计费/关联订单):GET /mini/statement/{id} */
|
|
||||||
export function getStatementDetailApi(id: number) {
|
|
||||||
return get<StatementDetail>(`/mini/statement/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 付款确认:POST /mini/statement/{id}/pay */
|
|
||||||
export function payStatementApi(id: number) {
|
|
||||||
return post<{ id: number; total_amount: string }>(`/mini/statement/${id}/pay`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 当前门店待回筐/托盘台账:GET /mini/statement/pending-containers */
|
|
||||||
export function getPendingContainersApi() {
|
|
||||||
return get<PendingContainers>('/mini/statement/pending-containers')
|
|
||||||
}
|
|
||||||
+79
-49
@@ -1,85 +1,115 @@
|
|||||||
/** 门店订单状态:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消 */
|
/**
|
||||||
export type OrderStatus = 0 | 1 | 2 | 3 | 9
|
* 门店订单状态(新枚举):
|
||||||
|
* 0 待接单 / 1 已接单 / 2 采购中 / 3 配送中 / 4 已完成 / 9 已取消
|
||||||
|
*/
|
||||||
|
export type OrderStatus = 0 | 1 | 2 | 3 | 4 | 9
|
||||||
|
|
||||||
export const ORDER_STATUS_MAP: Record<OrderStatus, string> = {
|
/**
|
||||||
0: '待汇总',
|
* 状态文案兜底映射(完整 6 态)
|
||||||
1: '已汇总',
|
* 仅用于接口未返回 status_name 的场景(订单详情、账单关联订单);
|
||||||
2: '配送中',
|
* 列表展示一律使用接口返回的 status_name,不要再硬编码
|
||||||
3: '已完成',
|
*/
|
||||||
|
export const ORDER_STATUS_TEXT: Record<number, string> = {
|
||||||
|
0: '待接单',
|
||||||
|
1: '已接单',
|
||||||
|
2: '采购中',
|
||||||
|
3: '配送中',
|
||||||
|
4: '已完成',
|
||||||
9: '已取消',
|
9: '已取消',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 状态筛选(value 为 null 表示全部) */
|
/** 状态筛选(订单列表页 chips,value 为 undefined 表示全部) */
|
||||||
export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | null; label: string }> = [
|
export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | undefined; label: string }> = [
|
||||||
{ value: null, label: '全部' },
|
{ value: undefined, label: '全部' },
|
||||||
{ value: 0, label: '待汇总' },
|
{ value: 0, label: '待接单' },
|
||||||
{ value: 1, label: '已汇总' },
|
{ value: 1, label: '已接单' },
|
||||||
{ value: 2, label: '配送中' },
|
{ value: 2, label: '采购中' },
|
||||||
{ value: 3, label: '已完成' },
|
{ value: 3, label: '配送中' },
|
||||||
|
{ value: 4, label: '已完成' },
|
||||||
{ value: 9, label: '已取消' },
|
{ value: 9, label: '已取消' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/** 我的页「订单总汇」导航(status 映射后端订单状态枚举) */
|
||||||
* 我的页「订单总汇」导航(业务语言 → 后端枚举)
|
|
||||||
* status 映射 StoreOrder.status:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消
|
|
||||||
* 注意:后端当前无「待付款」状态(status 缺省 = 不传参,显示全部),待后端补充后在此对齐
|
|
||||||
*/
|
|
||||||
export const ORDER_NAV_ITEMS: Array<{ key: string; label: string; status?: number; icon: string }> = [
|
export const ORDER_NAV_ITEMS: Array<{ key: string; label: string; status?: number; icon: string }> = [
|
||||||
{ key: 'pending_review', label: '待审核', status: 0, icon: 'clock-o' },
|
{ key: 'pending', label: '待接单', status: 0, icon: 'clock-o' },
|
||||||
{ key: 'pending_stock', label: '待配货', status: 1, icon: 'logistics' },
|
{ key: 'accepted', label: '已接单', status: 1, icon: 'passed' },
|
||||||
{ key: 'stocking', label: '配货中', status: 2, icon: 'van-o' },
|
{ key: 'purchasing', label: '采购中', status: 2, icon: 'shopping-cart-o' },
|
||||||
{ key: 'pending_pay', label: '待付款', icon: 'pending-payment' },
|
{ key: 'delivering', label: '配送中', status: 3, icon: 'logistics' },
|
||||||
{ key: 'completed', label: '已完成', status: 3, icon: 'completed' },
|
{ key: 'completed', label: '已完成', status: 4, icon: 'completed' },
|
||||||
{ key: 'all', label: '我的订单', icon: 'orders-o' },
|
{ key: 'all', label: '全部订单', icon: 'orders-o' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 门店订单 */
|
/** 订单列表行商品预览(仅前 3 条) */
|
||||||
export interface Order {
|
export interface OrderItemPreview {
|
||||||
|
product_name: string
|
||||||
|
quantity: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单列表行(状态名/可取消/商品预览均由后端给出,直接展示) */
|
||||||
|
export interface OrderListItem {
|
||||||
id: number
|
id: number
|
||||||
order_no: string
|
order_no: string
|
||||||
/** 订货日期(Y-m-d) */
|
/** 订货日期(Y-m-d) */
|
||||||
order_date: string
|
order_date: string
|
||||||
total_quantity: string
|
|
||||||
total_amount: string
|
|
||||||
status: OrderStatus
|
status: OrderStatus
|
||||||
|
/** 状态中文名(直接展示,前端不要再硬编码映射) */
|
||||||
|
status_name: string
|
||||||
|
/** 是否可取消(=待接单),取消按钮据此渲染 */
|
||||||
|
can_cancel: boolean
|
||||||
|
total_quantity: number
|
||||||
|
/** 总称重(斤,常为 0) */
|
||||||
|
total_weight: string
|
||||||
|
total_amount: string
|
||||||
remark: string
|
remark: string
|
||||||
/** 详情接口返回 */
|
/** 关联采购单 ID,0=未归集 */
|
||||||
items?: OrderItem[]
|
purchase_id: number
|
||||||
|
/** 关联账单 ID,0=未出账(>0 可跳账单详情) */
|
||||||
|
bill_id: number
|
||||||
|
created_at: string
|
||||||
|
/** 明细种数(如「共 4 种」) */
|
||||||
|
item_count: number
|
||||||
|
/** 商品预览,仅前 3 条;完整明细走详情接口 */
|
||||||
|
items: OrderItemPreview[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 订单明细 */
|
/** 订单明细(详情接口,下单时商品快照) */
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
id: number
|
id: number
|
||||||
order_id: number
|
order_id: number
|
||||||
product_id: number
|
product_id: number
|
||||||
product_name: string
|
product_name: string
|
||||||
product_spec: string
|
product_spec: string
|
||||||
/** 下单时等级实际价快照 */
|
unit: string
|
||||||
|
/** 下单时门店等级实际价快照 */
|
||||||
price: string
|
price: string
|
||||||
quantity: string
|
quantity: number
|
||||||
/** 称重(默认 0) */
|
/** 称重(斤,参考值) */
|
||||||
weight: string
|
weight: string
|
||||||
amount: string
|
amount: string
|
||||||
remark: string
|
remark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 订单详情(无 status_name/can_cancel,状态展示沿用列表行或 ORDER_STATUS_TEXT) */
|
||||||
|
export interface OrderDetail {
|
||||||
|
id: number
|
||||||
|
order_no: string
|
||||||
|
store_id: number
|
||||||
|
order_date: string
|
||||||
|
total_quantity: number
|
||||||
|
total_weight: string
|
||||||
|
total_amount: string
|
||||||
|
status: OrderStatus
|
||||||
|
remark: string
|
||||||
|
purchase_id: number
|
||||||
|
bill_id: number
|
||||||
|
created_at: string
|
||||||
|
items: OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
/** 下单返回 */
|
/** 下单返回 */
|
||||||
export interface OrderCreateResult {
|
export interface OrderCreateResult {
|
||||||
id: number
|
id: number
|
||||||
order_no: string
|
order_no: string
|
||||||
total_amount: string
|
total_amount: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 周期汇总分组 */
|
|
||||||
export interface SummaryGroup {
|
|
||||||
period_label: string
|
|
||||||
total_amount: string
|
|
||||||
total_quantity: string
|
|
||||||
order_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 周期汇总 */
|
|
||||||
export interface OrderSummary {
|
|
||||||
period: 'day' | 'week' | 'month'
|
|
||||||
groups: SummaryGroup[]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
/** 账单状态:0 待付款 / 1 已付款 */
|
|
||||||
export type StatementStatus = 0 | 1
|
|
||||||
|
|
||||||
export const STATEMENT_STATUS_MAP: Record<StatementStatus, string> = {
|
|
||||||
0: '待付款',
|
|
||||||
1: '已付款',
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 账单 */
|
|
||||||
export interface Statement {
|
|
||||||
id: number
|
|
||||||
statement_no: string
|
|
||||||
period_start: string
|
|
||||||
period_end: string
|
|
||||||
/** 商品金额合计 */
|
|
||||||
goods_amount: string
|
|
||||||
/** 账单计费周转框数(可为负=抵扣) */
|
|
||||||
box_num: number
|
|
||||||
/** 账单计费托盘数(可为负=抵扣) */
|
|
||||||
tray_num: number
|
|
||||||
/** 回筐金额合计(可为负=抵扣) */
|
|
||||||
crate_amount: string
|
|
||||||
/** 账单总金额 = 商品金额 + 回筐金额 */
|
|
||||||
total_amount: string
|
|
||||||
/** 生成时快照的回款周期 */
|
|
||||||
payment_cycle_days: number
|
|
||||||
/** 应结算日期 = 周期结束 + 回款周期天 */
|
|
||||||
settlement_date: string | null
|
|
||||||
status: StatementStatus
|
|
||||||
settled_at: string | null
|
|
||||||
remark: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 账单商品汇总记录(按商品聚合,不记单价) */
|
|
||||||
export interface StatementGoods {
|
|
||||||
product_name: string
|
|
||||||
product_spec: string
|
|
||||||
unit: string
|
|
||||||
quantity: string
|
|
||||||
weight: string
|
|
||||||
amount: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 账单关联订单(下单单价明细在订单记录中查看) */
|
|
||||||
export interface StatementOrder {
|
|
||||||
id: number
|
|
||||||
order_no: string
|
|
||||||
order_date: string
|
|
||||||
total_amount: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 账单详情 */
|
|
||||||
export interface StatementDetail extends Statement {
|
|
||||||
goods: StatementGoods[]
|
|
||||||
orders: StatementOrder[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 门店待回筐台账 */
|
|
||||||
export interface PendingContainers {
|
|
||||||
pending_box_num: number
|
|
||||||
pending_tray_num: number
|
|
||||||
box_price: string
|
|
||||||
tray_price: string
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { getToken } from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出文件下载(双端)
|
||||||
|
* 后端导出接口成功返回 xlsx 文件流,业务失败时仍以文件流形式返回 JSON
|
||||||
|
* (blob.type 为 application/json),两端都需探测并解析 msg 提示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 从 Content-Disposition 解析文件名(优先 RFC 5987 filename*=utf-8''...) */
|
||||||
|
export function parseExportFilename(disposition?: string | null): string {
|
||||||
|
if (!disposition) return ''
|
||||||
|
const star = disposition.match(/filename\*=utf-8''([^;]+)/i)
|
||||||
|
if (star?.[1]) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(star[1])
|
||||||
|
} catch {
|
||||||
|
// 编码异常时退化为普通 filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const plain = disposition.match(/filename="?([^";]+)"?/i)
|
||||||
|
return plain?.[1] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 JSON 文本中的业务错误信息 */
|
||||||
|
function parseJsonError(text: string): Error {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text)
|
||||||
|
return new Error(json.msg || '导出失败,请稍后重试')
|
||||||
|
} catch {
|
||||||
|
return new Error('导出失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** H5:fetch 带鉴权拉取 blob → <a download> 触发保存 */
|
||||||
|
async function downloadFileH5(url: string, fallbackName: string): Promise<void> {
|
||||||
|
const token = getToken()
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`下载失败(${res.status})`)
|
||||||
|
}
|
||||||
|
const blob = await res.blob()
|
||||||
|
|
||||||
|
// 业务失败:返回的是 JSON blob
|
||||||
|
if (blob.type.includes('application/json')) {
|
||||||
|
throw parseJsonError(await blob.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename =
|
||||||
|
parseExportFilename(res.headers.get('Content-Disposition')) || fallbackName
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = objectUrl
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取小程序本地文件内容(utf8) */
|
||||||
|
function readTempFile(filePath: string, length?: number): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Taro.getFileSystemManager().readFile({
|
||||||
|
filePath,
|
||||||
|
encoding: 'utf8',
|
||||||
|
...(length !== undefined ? { position: 0, length } : {}),
|
||||||
|
success: r => resolve(r.data as string),
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小程序:downloadFile 下载后 openDocument 打开;业务失败时下载"成功"但内容是 JSON */
|
||||||
|
async function downloadFileWeapp(url: string): Promise<void> {
|
||||||
|
const token = getToken()
|
||||||
|
const { tempFilePath } = await Taro.downloadFile({
|
||||||
|
url,
|
||||||
|
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 探测前 200 字节:以 '{' 开头即为 JSON 错误响应
|
||||||
|
const head = await readTempFile(tempFilePath, 200)
|
||||||
|
if (head.trimStart().startsWith('{')) {
|
||||||
|
throw parseJsonError(await readTempFile(tempFilePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
await Taro.openDocument({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
fileType: 'xlsx',
|
||||||
|
showMenu: true,
|
||||||
|
fail: () => {
|
||||||
|
Taro.showToast({ title: '文件已下载,打开失败', icon: 'none' })
|
||||||
|
},
|
||||||
|
} as Parameters<typeof Taro.openDocument>[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带鉴权下载导出文件
|
||||||
|
* - H5:保存到浏览器下载目录,返回保存的文件名
|
||||||
|
* - 小程序:直接调起系统文档预览(showMenu 支持转发/保存)
|
||||||
|
*/
|
||||||
|
export async function downloadExportFile(url: string, fallbackName: string): Promise<void> {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
return downloadFileH5(url, fallbackName)
|
||||||
|
}
|
||||||
|
return downloadFileWeapp(url)
|
||||||
|
}
|
||||||
@@ -90,6 +90,25 @@ function handleBusinessError(data: ApiResponse): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理请求参数
|
||||||
|
* data 中的 undefined 在 GET 查询串里会被序列化为字符串 'undefined'(null 同理),
|
||||||
|
* 导致后端收到字符串而非缺省;统一在此剔除。GET 额外剔除 null,
|
||||||
|
* POST/PUT 等请求体保留 null(JSON 序列化语义可能依赖它)
|
||||||
|
*/
|
||||||
|
function sanitizeData(data: any, method?: RequestConfig['method']): any {
|
||||||
|
if (!data || typeof data !== 'object' || Array.isArray(data)) return data
|
||||||
|
const isGet = !method || method === 'GET'
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
const value = data[key]
|
||||||
|
if (value === undefined) return
|
||||||
|
if (isGet && value === null) return
|
||||||
|
result[key] = value
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发起网络请求
|
* 发起网络请求
|
||||||
*
|
*
|
||||||
@@ -126,6 +145,7 @@ export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>>
|
|||||||
Taro.request({
|
Taro.request({
|
||||||
...restConfig,
|
...restConfig,
|
||||||
url: BASE_URL + restConfig.url,
|
url: BASE_URL + restConfig.url,
|
||||||
|
data: sanitizeData(restConfig.data, restConfig.method),
|
||||||
header,
|
header,
|
||||||
timeout: restConfig.timeout || DEFAULT_TIMEOUT,
|
timeout: restConfig.timeout || DEFAULT_TIMEOUT,
|
||||||
success(res) {
|
success(res) {
|
||||||
|
|||||||
Reference in New Issue
Block a user