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/profile/index',
|
||||
'pages/order-list/index',
|
||||
'pages/statement/index',
|
||||
'pages/statement-detail/index',
|
||||
'pages/bill/index',
|
||||
'pages/bill-detail/index',
|
||||
'pages/settings/index',
|
||||
'pages/login/index',
|
||||
'pages/register/index',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.bill-detail {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 120rpx;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.bill-card {
|
||||
@@ -26,13 +26,20 @@
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
|
||||
&--unpaid {
|
||||
color: #ee0a24;
|
||||
}
|
||||
// 0 待支付 / 1 审核中 / 2 已支付
|
||||
&--0 { color: #ee0a24; }
|
||||
&--1 { color: #ff976a; }
|
||||
&--2 { color: #07c160; }
|
||||
}
|
||||
|
||||
&--paid {
|
||||
color: #07c160;
|
||||
}
|
||||
&__tip {
|
||||
display: block;
|
||||
margin: -4rpx 0 16rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #fffbe8;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ed6a0c;
|
||||
}
|
||||
|
||||
&__row {
|
||||
@@ -51,15 +58,15 @@
|
||||
&__label {
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
|
||||
&--credit {
|
||||
color: #07c160;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__total {
|
||||
@@ -118,6 +125,7 @@
|
||||
&__side {
|
||||
text-align: right;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__qty {
|
||||
@@ -145,35 +153,112 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
margin: 0 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-pay {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
// ===== 订单明细弹层 =====
|
||||
.order-popup {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
|
||||
&__btn {
|
||||
border: none;
|
||||
&__title {
|
||||
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 {
|
||||
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 {
|
||||
@@ -105,8 +122,23 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
&__footer {
|
||||
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;
|
||||
padding: 8rpx 24rpx;
|
||||
border: 1rpx solid #ee0a24;
|
||||
@@ -180,6 +212,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__remark {
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -4,46 +4,46 @@ import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { Empty, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
||||
import { ORDER_NAV_ITEMS, ORDER_STATUS_MAP } from '@/types/order'
|
||||
import type { Order, OrderStatus } from '@/types/order'
|
||||
import { ORDER_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order'
|
||||
import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/**
|
||||
* 订单列表页(框架)
|
||||
* 入口:/pages/order-list/index?status=0|1|2|3|9|all
|
||||
* status 与 ORDER_NAV_ITEMS 映射(业务语言 → 后端枚举),缺省为全部
|
||||
* 订单列表页
|
||||
* 入口:/pages/order-list/index?status=0|1|2|3|4|9|all
|
||||
* 状态文案一律使用接口返回的 status_name(详情接口无该字段时沿用列表行)
|
||||
*/
|
||||
export default function OrderListPage() {
|
||||
const router = useRouter()
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
/** 当前状态筛选(undefined = 全部) */
|
||||
const [status, setStatus] = useState<number | undefined>(undefined)
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [status, setStatus] = useState<OrderStatus | undefined>(undefined)
|
||||
const [orders, setOrders] = useState<OrderListItem[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
/** 订单详情弹层 */
|
||||
/** 订单详情弹层(statusName 沿用列表行,详情接口不返回 status_name) */
|
||||
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
|
||||
|
||||
/** 解析路由参数中的 status('all' / 数字 / 缺省 → undefined) */
|
||||
const parseStatus = useCallback((raw?: string): number | undefined => {
|
||||
const parseStatus = useCallback((raw?: string): OrderStatus | undefined => {
|
||||
if (!raw || raw === 'all' || raw === '') return undefined
|
||||
const num = Number(raw)
|
||||
return Number.isNaN(num) ? undefined : num
|
||||
return Number.isNaN(num) ? undefined : (num as OrderStatus)
|
||||
}, [])
|
||||
|
||||
/** 拉取订单列表 */
|
||||
const loadOrders = useCallback(
|
||||
async (pageNum: number, reset: boolean, statusParam?: number) => {
|
||||
async (pageNum: number, reset: boolean, statusParam?: OrderStatus) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
@@ -53,11 +53,10 @@ export default function OrderListPage() {
|
||||
page: pageNum,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { data, total: totalCount } = res.data
|
||||
const { data, total } = res.data
|
||||
setOrders(prev => (reset ? data : [...prev, ...data]))
|
||||
setTotal(totalCount)
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= totalCount)
|
||||
setFinished(pageNum * PAGE_SIZE >= total)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
@@ -82,7 +81,7 @@ export default function OrderListPage() {
|
||||
|
||||
/** 切换状态筛选 */
|
||||
const handleStatusTap = useCallback(
|
||||
(value?: number) => {
|
||||
(value?: OrderStatus) => {
|
||||
setStatus(value)
|
||||
setFinished(false)
|
||||
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 {
|
||||
const res = await getOrderDetailApi(order.id)
|
||||
setOrderDetail(res.data)
|
||||
setDetailStatusName(order.status_name)
|
||||
setShowDetail(true)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 取消订单(仅待汇总可取消) */
|
||||
/** 取消订单(仅待接单可取消,以 can_cancel 为准) */
|
||||
const handleCancelOrder = useCallback(
|
||||
(order: Order) => {
|
||||
(order: OrderListItem) => {
|
||||
Taro.showModal({
|
||||
title: '取消订单',
|
||||
content: `确定取消订单 ${order.order_no} 吗?`,
|
||||
@@ -123,6 +123,11 @@ export default function OrderListPage() {
|
||||
[loadOrders, status],
|
||||
)
|
||||
|
||||
/** 跳转关联账单详情 */
|
||||
const handleBillTap = useCallback((billId: number) => {
|
||||
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${billId}` })
|
||||
}, [])
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
@@ -131,17 +136,11 @@ export default function OrderListPage() {
|
||||
<View className='order-list-page'>
|
||||
{/* ========== 状态筛选 ========== */}
|
||||
<ScrollView scrollX className='status-scroll'>
|
||||
<View
|
||||
className={`status-chip ${status === undefined ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(undefined)}
|
||||
>
|
||||
<Text>全部</Text>
|
||||
</View>
|
||||
{ORDER_NAV_ITEMS.map(item => (
|
||||
{ORDER_STATUS_FILTERS.map(item => (
|
||||
<View
|
||||
key={item.key}
|
||||
className={`status-chip ${status === item.status ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(item.status)}
|
||||
key={item.label}
|
||||
className={`status-chip ${status === item.value ? 'active' : ''}`}
|
||||
onClick={() => handleStatusTap(item.value)}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</View>
|
||||
@@ -164,25 +163,47 @@ export default function OrderListPage() {
|
||||
<View key={order.id} className='order-item' onClick={() => handleOrderTap(order)}>
|
||||
<View className='order-item__header'>
|
||||
<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>
|
||||
{/* 商品预览(仅前 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'>
|
||||
<Text className='order-item__date'>{order.order_date}</Text>
|
||||
<Text className='order-item__date'>订货日期 {order.order_date}</Text>
|
||||
<View className='order-item__amounts'>
|
||||
<Text className='order-item__qty'>共 {order.total_quantity} 件</Text>
|
||||
<Text className='order-item__amount'>¥{order.total_amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{order.remark && <Text className='order-item__remark'>备注:{order.remark}</Text>}
|
||||
{order.status === 0 && (
|
||||
<View
|
||||
className='order-item__cancel'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleCancelOrder(order)
|
||||
}}
|
||||
>
|
||||
取消订单
|
||||
{(order.can_cancel || order.bill_id > 0) && (
|
||||
<View className='order-item__footer'>
|
||||
{order.bill_id > 0 && (
|
||||
<View
|
||||
className='order-item__bill'
|
||||
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>
|
||||
@@ -207,8 +228,10 @@ export default function OrderListPage() {
|
||||
<View className='detail-popup'>
|
||||
<Text className='detail-popup__title'>{orderDetail.order_no}</Text>
|
||||
<View className='detail-popup__meta'>
|
||||
<Text>{orderDetail.order_date}</Text>
|
||||
<Text className='detail-popup__status'>{ORDER_STATUS_MAP[orderDetail.status]}</Text>
|
||||
<Text>{orderDetail.order_date} 下单 {orderDetail.created_at}</Text>
|
||||
<Text className='detail-popup__status'>
|
||||
{detailStatusName || ORDER_STATUS_TEXT[orderDetail.status] || ''}
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView scrollY className='detail-popup__list'>
|
||||
{(orderDetail.items ?? []).map(item => (
|
||||
@@ -216,13 +239,16 @@ export default function OrderListPage() {
|
||||
<View className='detail-popup__item-info'>
|
||||
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
||||
<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>
|
||||
</View>
|
||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
{orderDetail.remark && (
|
||||
<Text className='detail-popup__remark'>备注:{orderDetail.remark}</Text>
|
||||
)}
|
||||
<View className='detail-popup__footer'>
|
||||
<Text className='detail-popup__total'>合计 ¥{orderDetail.total_amount}</Text>
|
||||
</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 {
|
||||
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 { View, Text, Image } from '@tarojs/components'
|
||||
import { Icon } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getUserInfoApi } from '@/services/auth'
|
||||
import { getBillListApi } from '@/services/bill'
|
||||
import type { BillSummary } from '@/services/bill'
|
||||
import { ORDER_NAV_ITEMS } from '@/types/order'
|
||||
import { resolveAvatarUrl } from '@/utils/format'
|
||||
import type { UserType } from '@/types/user'
|
||||
import './index.less'
|
||||
|
||||
/** 菜单项(后续单独页面开发时在此追加) */
|
||||
/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */
|
||||
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',
|
||||
label: '消息中心',
|
||||
@@ -43,6 +33,9 @@ export default function ProfilePage() {
|
||||
const setUser = useAuthStore(s => s.setUser)
|
||||
const logout = useAuthStore(s => s.logout)
|
||||
|
||||
/** 账单待支付汇总(门店口径,含审核中) */
|
||||
const [billSummary, setBillSummary] = useState<BillSummary | null>(null)
|
||||
|
||||
const loggedIn = !!token && !!user
|
||||
|
||||
/** 功能菜单(门店账号追加「门店信息」入口) */
|
||||
@@ -65,6 +58,10 @@ export default function ProfilePage() {
|
||||
getUserInfoApi()
|
||||
.then(res => setUser(res.data))
|
||||
.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'}` })
|
||||
}, [])
|
||||
|
||||
/** 账单入口 → 账单列表页 */
|
||||
const goBill = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/bill/index' })
|
||||
}, [])
|
||||
|
||||
/** 退出登录 */
|
||||
const handleLogout = useCallback(() => {
|
||||
Taro.showModal({
|
||||
@@ -169,6 +171,36 @@ export default function ProfilePage() {
|
||||
</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'>
|
||||
{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 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 {
|
||||
@@ -15,29 +15,35 @@ export interface CreateOrderParams {
|
||||
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 */
|
||||
export function createOrderApi(params: CreateOrderParams) {
|
||||
return post<OrderCreateResult>('/mini/order', params)
|
||||
}
|
||||
|
||||
/** 历史订单列表(强制本店隔离):GET /mini/order */
|
||||
export function getOrderListApi(
|
||||
params: { status?: number; page?: number; pageSize?: number } = {},
|
||||
) {
|
||||
return get<PaginatedData<Order>>('/mini/order', { data: params })
|
||||
/** 历史订单列表(强制本店隔离,按订货日期倒序):GET /mini/order */
|
||||
export function getOrderListApi(params: OrderListParams = {}) {
|
||||
return get<PaginatedData<OrderListItem>>('/mini/order', { data: params })
|
||||
}
|
||||
|
||||
/** 周期汇总:GET /mini/order/summary */
|
||||
export function getOrderSummaryApi(period: 'day' | 'week' | 'month' = 'month') {
|
||||
return get<OrderSummary>('/mini/order/summary', { data: { period } })
|
||||
}
|
||||
|
||||
/** 订单详情(校验本店归属):GET /mini/order/{id} */
|
||||
/** 订单详情(校验本店归属,含完整明细):GET /mini/order/{id} */
|
||||
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) {
|
||||
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: '待汇总',
|
||||
1: '已汇总',
|
||||
2: '配送中',
|
||||
3: '已完成',
|
||||
/**
|
||||
* 状态文案兜底映射(完整 6 态)
|
||||
* 仅用于接口未返回 status_name 的场景(订单详情、账单关联订单);
|
||||
* 列表展示一律使用接口返回的 status_name,不要再硬编码
|
||||
*/
|
||||
export const ORDER_STATUS_TEXT: Record<number, string> = {
|
||||
0: '待接单',
|
||||
1: '已接单',
|
||||
2: '采购中',
|
||||
3: '配送中',
|
||||
4: '已完成',
|
||||
9: '已取消',
|
||||
}
|
||||
|
||||
/** 状态筛选(value 为 null 表示全部) */
|
||||
export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | null; label: string }> = [
|
||||
{ value: null, label: '全部' },
|
||||
{ value: 0, label: '待汇总' },
|
||||
{ value: 1, label: '已汇总' },
|
||||
{ value: 2, label: '配送中' },
|
||||
{ value: 3, label: '已完成' },
|
||||
/** 状态筛选(订单列表页 chips,value 为 undefined 表示全部) */
|
||||
export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | undefined; label: string }> = [
|
||||
{ value: undefined, label: '全部' },
|
||||
{ value: 0, label: '待接单' },
|
||||
{ value: 1, label: '已接单' },
|
||||
{ value: 2, label: '采购中' },
|
||||
{ value: 3, label: '配送中' },
|
||||
{ value: 4, label: '已完成' },
|
||||
{ value: 9, label: '已取消' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 我的页「订单总汇」导航(业务语言 → 后端枚举)
|
||||
* status 映射 StoreOrder.status:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消
|
||||
* 注意:后端当前无「待付款」状态(status 缺省 = 不传参,显示全部),待后端补充后在此对齐
|
||||
*/
|
||||
/** 我的页「订单总汇」导航(status 映射后端订单状态枚举) */
|
||||
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_stock', label: '待配货', status: 1, icon: 'logistics' },
|
||||
{ key: 'stocking', label: '配货中', status: 2, icon: 'van-o' },
|
||||
{ key: 'pending_pay', label: '待付款', icon: 'pending-payment' },
|
||||
{ key: 'completed', label: '已完成', status: 3, icon: 'completed' },
|
||||
{ key: 'all', label: '我的订单', icon: 'orders-o' },
|
||||
{ key: 'pending', label: '待接单', status: 0, icon: 'clock-o' },
|
||||
{ key: 'accepted', label: '已接单', status: 1, icon: 'passed' },
|
||||
{ key: 'purchasing', label: '采购中', status: 2, icon: 'shopping-cart-o' },
|
||||
{ key: 'delivering', label: '配送中', status: 3, icon: 'logistics' },
|
||||
{ key: 'completed', label: '已完成', status: 4, icon: 'completed' },
|
||||
{ key: 'all', label: '全部订单', icon: 'orders-o' },
|
||||
]
|
||||
|
||||
/** 门店订单 */
|
||||
export interface Order {
|
||||
/** 订单列表行商品预览(仅前 3 条) */
|
||||
export interface OrderItemPreview {
|
||||
product_name: string
|
||||
quantity: number
|
||||
unit: string
|
||||
}
|
||||
|
||||
/** 订单列表行(状态名/可取消/商品预览均由后端给出,直接展示) */
|
||||
export interface OrderListItem {
|
||||
id: number
|
||||
order_no: string
|
||||
/** 订货日期(Y-m-d) */
|
||||
order_date: string
|
||||
total_quantity: string
|
||||
total_amount: string
|
||||
status: OrderStatus
|
||||
/** 状态中文名(直接展示,前端不要再硬编码映射) */
|
||||
status_name: string
|
||||
/** 是否可取消(=待接单),取消按钮据此渲染 */
|
||||
can_cancel: boolean
|
||||
total_quantity: number
|
||||
/** 总称重(斤,常为 0) */
|
||||
total_weight: string
|
||||
total_amount: string
|
||||
remark: string
|
||||
/** 详情接口返回 */
|
||||
items?: OrderItem[]
|
||||
/** 关联采购单 ID,0=未归集 */
|
||||
purchase_id: number
|
||||
/** 关联账单 ID,0=未出账(>0 可跳账单详情) */
|
||||
bill_id: number
|
||||
created_at: string
|
||||
/** 明细种数(如「共 4 种」) */
|
||||
item_count: number
|
||||
/** 商品预览,仅前 3 条;完整明细走详情接口 */
|
||||
items: OrderItemPreview[]
|
||||
}
|
||||
|
||||
/** 订单明细 */
|
||||
/** 订单明细(详情接口,下单时商品快照) */
|
||||
export interface OrderItem {
|
||||
id: number
|
||||
order_id: number
|
||||
product_id: number
|
||||
product_name: string
|
||||
product_spec: string
|
||||
/** 下单时等级实际价快照 */
|
||||
unit: string
|
||||
/** 下单时门店等级实际价快照 */
|
||||
price: string
|
||||
quantity: string
|
||||
/** 称重(默认 0) */
|
||||
quantity: number
|
||||
/** 称重(斤,参考值) */
|
||||
weight: string
|
||||
amount: 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 {
|
||||
id: number
|
||||
order_no: 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({
|
||||
...restConfig,
|
||||
url: BASE_URL + restConfig.url,
|
||||
data: sanitizeData(restConfig.data, restConfig.method),
|
||||
header,
|
||||
timeout: restConfig.timeout || DEFAULT_TIMEOUT,
|
||||
success(res) {
|
||||
|
||||
Reference in New Issue
Block a user