This commit is contained in:
liu
2026-08-14 20:21:50 +08:00
parent 4a5b7003ca
commit 03434feb35
21 changed files with 1513 additions and 629 deletions
+427
View File
@@ -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 | 关联采购单 ID0=未归集 |
| bill_id | number | 关联账单 ID0=未出账(>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/billpayable=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
View File
@@ -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;
}
}
}
+205
View File
@@ -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>
)
}
+158
View File
@@ -0,0 +1,158 @@
.bill-page {
min-height: 100vh;
background: #f7f8fa;
padding: 20rpx 24rpx 60rpx;
box-sizing: border-box;
// ===== 待支付汇总 =====
.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;
}
}
// ===== 状态筛选 =====
.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;
&__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;
}
}
}
+157
View File
@@ -0,0 +1,157 @@
import { useCallback, useRef, useState } from 'react'
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { Empty } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import { getBillListApi } from '@/services/bill'
import type { Bill, BillSummary } from '@/services/bill'
import './index.less'
const PAGE_SIZE = 10
/** 状态筛选(接口 status 为原始支付状态:0 未支付含审核中 / 1 已支付) */
const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [
{ value: undefined, label: '全部' },
{ value: 0, label: '未支付' },
{ value: 1, label: '已支付' },
]
/**
* 账单列表页
* 采购单完成后由后台按门店生成(只读);头部 summary 为门店口径待支付汇总(含审核中,不受筛选影响)
*/
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 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}` })
}, [])
return (
<View className='bill-page'>
{/* ========== 待支付汇总(门店口径,含审核中) ========== */}
{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>
)}
{/* ========== 状态筛选 ========== */}
<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 ? (
<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 => (
<View key={bill.id} className='bill-item' onClick={() => goDetail(bill.id)}>
<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>
))
)}
{loggedIn && finished && bills.length > 0 && (
<View className='bill-loading'><Text></Text></View>
)}
</View>
)
}
+41 -2
View File
@@ -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;
+70 -44
View File
@@ -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>
+64
View File
@@ -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
View File
@@ -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 => (
-178
View File
@@ -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>
)
}
-101
View File
@@ -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;
}
}
}
-116
View File
@@ -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>
)
}
+117
View File
@@ -0,0 +1,117 @@
import { 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}`)
}
+20 -14
View File
@@ -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`)
}
-23
View File
@@ -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
View File
@@ -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: '已完成' },
/** 状态筛选(订单列表页 chipsvalue 为 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.status0 待汇总 / 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[]
/** 关联采购单 ID0=未归集 */
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[]
}
-64
View File
@@ -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
}
+20
View File
@@ -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) {