账单与支付
This commit is contained in:
@@ -1,427 +0,0 @@
|
||||
# 小程序订单与账单接口文档
|
||||
|
||||
> 适用端:微信小程序(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),无需特殊分支处理。
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"appid": "wx7ed74d60503b5ee3",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": false,
|
||||
"es6": true,
|
||||
"postcss": false,
|
||||
"minified": true,
|
||||
"enhance": false
|
||||
|
||||
@@ -2,12 +2,16 @@ export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/product/index',
|
||||
'pages/product-detail/index',
|
||||
'pages/cart/index',
|
||||
'pages/message/index',
|
||||
'pages/profile/index',
|
||||
'pages/order-list/index',
|
||||
'pages/bill/index',
|
||||
'pages/bill-detail/index',
|
||||
'pages/payment/index',
|
||||
'pages/payment-records/index',
|
||||
'pages/payment-detail/index',
|
||||
'pages/settings/index',
|
||||
'pages/login/index',
|
||||
'pages/register/index',
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 可付款时为底部操作栏留出空间
|
||||
&--pay {
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.bill-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
@@ -67,6 +72,11 @@
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
flex-shrink: 0;
|
||||
|
||||
// 回筐抵扣(负附加金额)
|
||||
&--return {
|
||||
color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
&__total {
|
||||
@@ -104,6 +114,15 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f2f3f5;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -261,4 +280,46 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 去付款操作栏 =====
|
||||
.bill-pay-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);
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 26rpx;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
margin-left: 16rpx;
|
||||
font-size: 36rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
padding: 14rpx 48rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image, 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 { resolveFileUrl } from '@/utils/format'
|
||||
import type { OrderDetail } from '@/types/order'
|
||||
import type { BillDetail } from '@/services/bill'
|
||||
import './index.less'
|
||||
@@ -44,6 +45,11 @@ export default function BillDetailPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 去付款 → 发起付款页(预选本账单) */
|
||||
const goPay = () => {
|
||||
Taro.navigateTo({ url: `/pages/payment/index` })
|
||||
}
|
||||
|
||||
if (loading && !detail) {
|
||||
return <View className='bill-detail'><Empty description='加载中...' /></View>
|
||||
}
|
||||
@@ -53,8 +59,21 @@ export default function BillDetailPage() {
|
||||
|
||||
const { bill, items, orders } = detail
|
||||
|
||||
/** 附加金额:正=压筐附加,负=回筐抵扣(0 不带头符号) */
|
||||
const addedNum = Number(bill.added_amount)
|
||||
const addedLabel = addedNum < 0 ? '回筐抵扣' : '压筐附加'
|
||||
const addedText =
|
||||
addedNum < 0
|
||||
? `-¥${Math.abs(addedNum).toFixed(2)}`
|
||||
: addedNum > 0
|
||||
? `+¥${addedNum.toFixed(2)}`
|
||||
: '¥0.00'
|
||||
/** 筐/托盘明细:正压负回,数量取绝对值(单价为出账时快照) */
|
||||
const boxPart = `${bill.box_num < 0 ? '回筐' : '压筐'} ${Math.abs(bill.box_num)}×¥${bill.box_price}`
|
||||
const trayPart = `${bill.tray_num < 0 ? '回托盘' : '压托盘'} ${Math.abs(bill.tray_num)}×¥${bill.tray_price}`
|
||||
|
||||
return (
|
||||
<View className='bill-detail'>
|
||||
<View className={`bill-detail ${bill.can_pay ? 'bill-detail--pay' : ''}`}>
|
||||
{/* ===== 账单信息 ===== */}
|
||||
<View className='bill-card'>
|
||||
<View className='bill-card__header'>
|
||||
@@ -114,10 +133,10 @@ export default function BillDetailPage() {
|
||||
<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 className='bill-card__label'>{addedLabel}({boxPart},{trayPart})</Text>
|
||||
<Text className={`bill-card__value ${addedNum < 0 ? 'bill-card__value--return' : ''}`}>
|
||||
{addedText}
|
||||
</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>
|
||||
@@ -131,6 +150,14 @@ export default function BillDetailPage() {
|
||||
<Text className='bill-section__desc'>同一商品跨订单合并,单价为加权平均价</Text>
|
||||
{(items ?? []).map(item => (
|
||||
<View key={item.product_id} className='bill-goods'>
|
||||
{!!item.image && (
|
||||
<Image
|
||||
className='bill-goods__img'
|
||||
src={resolveFileUrl(item.image)}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
)}
|
||||
<View className='bill-goods__main'>
|
||||
<Text className='bill-goods__name'>{item.product_name}</Text>
|
||||
<Text className='bill-goods__spec'>
|
||||
@@ -200,6 +227,17 @@ export default function BillDetailPage() {
|
||||
</View>
|
||||
)}
|
||||
</Popup>
|
||||
|
||||
{/* ===== 去付款操作栏(可付款账单) ===== */}
|
||||
{bill.can_pay && (
|
||||
<View className='bill-pay-bar'>
|
||||
<View className='bill-pay-bar__info'>
|
||||
<Text className='bill-pay-bar__label'>账单总额</Text>
|
||||
<Text className='bill-pay-bar__amount'>¥{bill.total_amount}</Text>
|
||||
</View>
|
||||
<View className='bill-pay-bar__btn' onClick={goPay}>去付款</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,22 @@
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__pay {
|
||||
margin-top: 12rpx;
|
||||
padding: 8rpx 32rpx;
|
||||
background: #fff;
|
||||
color: #ee0a24;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 状态筛选 + 导出入口 =====
|
||||
|
||||
@@ -102,6 +102,11 @@ export default function BillListPage() {
|
||||
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${id}` })
|
||||
}, [])
|
||||
|
||||
/** 合并付款 → 发起付款页 */
|
||||
const goPay = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/payment/index' })
|
||||
}, [])
|
||||
|
||||
/** 进入/退出多选导出模式 */
|
||||
const toggleSelectMode = useCallback(() => {
|
||||
setSelectMode(prev => !prev)
|
||||
@@ -189,7 +194,10 @@ export default function BillListPage() {
|
||||
<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 className='bill-summary__right'>
|
||||
<Text className='bill-summary__amount'>¥{summary.unpaid_amount}</Text>
|
||||
<View className='bill-summary__pay' onClick={goPay}>去付款</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -73,12 +73,12 @@
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(25, 137, 250, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(238, 10, 36, 0.3);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
@@ -123,7 +123,7 @@
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
line-height: 96px;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 100%);
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
font-weight: 500;
|
||||
@@ -131,7 +131,7 @@
|
||||
border-radius: 48px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
box-shadow: 0 6px 24px rgba(25, 137, 250, 0.35);
|
||||
box-shadow: 0 6px 24px rgba(238, 10, 36, 0.35);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
/* 重置微信 Button 默认样式 */
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
.switch-link {
|
||||
font-size: 28px;
|
||||
color: #1989fa;
|
||||
color: #ee0a24;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,6 @@
|
||||
|
||||
.agree-link {
|
||||
font-size: 24px;
|
||||
color: #1989fa;
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,15 +77,66 @@
|
||||
}
|
||||
|
||||
&__preview {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #646566;
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
&__goods {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
&__goods-img {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f2f3f5;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--empty {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
&__goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__goods-name {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__goods-spec {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__goods-qty {
|
||||
margin-left: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #646566;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__more {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom, useRouter } from '@tarojs/taro'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { View, Text, Image, 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_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
|
||||
import './index.less'
|
||||
|
||||
@@ -167,11 +168,33 @@ export default function OrderListPage() {
|
||||
{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__preview'>
|
||||
{order.items.map((i, idx) => (
|
||||
<View key={idx} className='order-item__goods'>
|
||||
{i.image ? (
|
||||
<Image
|
||||
className='order-item__goods-img'
|
||||
src={resolveFileUrl(i.image)}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
) : (
|
||||
<View className='order-item__goods-img order-item__goods-img--empty' />
|
||||
)}
|
||||
<View className='order-item__goods-info'>
|
||||
<Text className='order-item__goods-name'>{i.product_name}</Text>
|
||||
{!!i.product_spec && (
|
||||
<Text className='order-item__goods-spec'>{i.product_spec} {i.unit}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='order-item__goods-qty'>×{i.quantity} </Text>
|
||||
</View>
|
||||
))}
|
||||
{order.item_count > 3 && (
|
||||
<Text className='order-item__more'>等 {order.item_count} 种商品</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='order-item__body'>
|
||||
<Text className='order-item__date'>订货日期 {order.order_date}</Text>
|
||||
<View className='order-item__amounts'>
|
||||
@@ -239,7 +262,7 @@ 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.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity}
|
||||
{item.product_spec ? `${item.product_spec}${item.unit} ` : ''}¥{item.price} × {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '支付详情',
|
||||
})
|
||||
@@ -0,0 +1,176 @@
|
||||
.pay-detail {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 已拒绝时为底部操作栏留出空间
|
||||
&--reject {
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.pay-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&__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: #ff976a; }
|
||||
&--1 { color: #07c160; }
|
||||
&--2 { color: #ee0a24; }
|
||||
}
|
||||
|
||||
&__amount {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__tip {
|
||||
display: block;
|
||||
margin: 16rpx 0 8rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #fff8ec;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ff976a;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
|
||||
&--reject {
|
||||
background: #fff5f5;
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-section__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
// ===== 汇款凭证 =====
|
||||
.pay-vouchers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
|
||||
&__img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
border-radius: 12rpx;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 合并账单 =====
|
||||
.pay-bill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__date {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 24rpx;
|
||||
|
||||
&--0 { color: #ee0a24; }
|
||||
&--1 { color: #07c160; }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 底部操作栏 =====
|
||||
.pay-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&__btn {
|
||||
padding: 14rpx 48rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Empty } from '@antmjs/vantui'
|
||||
import { getPaymentDetailApi, PAY_METHOD_NAMES, PAY_STATUS_NAMES } from '@/services/payment'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import type { PaymentDetail } from '@/services/payment'
|
||||
import './index.less'
|
||||
|
||||
/**
|
||||
* 支付详情页
|
||||
* 支付信息 + 汇款凭证(可预览)+ 合并付款的账单(可下钻账单详情)
|
||||
* 已拒绝时底部提供「重新发起付款」(账单已由后台释放,可重新合并提交)
|
||||
*/
|
||||
export default function PaymentDetailPage() {
|
||||
const router = useRouter()
|
||||
const id = Number(router.params.id ?? 0)
|
||||
|
||||
const [detail, setDetail] = useState<PaymentDetail | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getPaymentDetailApi(id)
|
||||
.then(res => setDetail(res.data))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
|
||||
/** 预览凭证图片 */
|
||||
const previewVoucher = useCallback((current: string) => {
|
||||
const urls = (detail?.payment.voucher_urls ?? []).map(resolveFileUrl)
|
||||
Taro.previewImage({ urls, current })
|
||||
}, [detail])
|
||||
|
||||
/** 下钻账单详情 */
|
||||
const goBill = useCallback((billId: number) => {
|
||||
Taro.navigateTo({ url: `/pages/bill-detail/index?id=${billId}` })
|
||||
}, [])
|
||||
|
||||
/** 已拒绝 → 携带本组账单重新发起付款 */
|
||||
const handleRepay = useCallback(() => {
|
||||
if (!detail) return
|
||||
const ids = detail.bills.map(b => b.id).join(',')
|
||||
Taro.redirectTo({ url: `/pages/payment/index?ids=${ids}` })
|
||||
}, [detail])
|
||||
|
||||
if (loading && !detail) {
|
||||
return <View className='pay-detail'><Empty description='加载中...' /></View>
|
||||
}
|
||||
if (!detail) {
|
||||
return <View className='pay-detail'><Empty description='支付记录不存在' /></View>
|
||||
}
|
||||
|
||||
const { payment, bills } = detail
|
||||
const vouchers = payment.voucher_urls.map(resolveFileUrl)
|
||||
|
||||
return (
|
||||
<View className={`pay-detail ${payment.status === 2 ? 'pay-detail--reject' : ''}`}>
|
||||
{/* ===== 支付信息 ===== */}
|
||||
<View className='pay-card'>
|
||||
<View className='pay-card__header'>
|
||||
<Text className='pay-card__no'>{payment.payment_no}</Text>
|
||||
<Text className={`pay-card__status pay-card__status--${payment.status}`}>
|
||||
{PAY_STATUS_NAMES[payment.status]}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='pay-card__amount'>¥{payment.amount}</Text>
|
||||
{payment.status === 0 && (
|
||||
<Text className='pay-card__tip'>付款申请已提交,商家审核通过后账单将置为已支付</Text>
|
||||
)}
|
||||
{payment.status === 2 && (
|
||||
<Text className='pay-card__tip pay-card__tip--reject'>
|
||||
审核未通过{payment.audit_remark ? `:${payment.audit_remark}` : ''},账单已释放,可重新发起付款
|
||||
</Text>
|
||||
)}
|
||||
<View className='pay-card__row'>
|
||||
<Text className='pay-card__label'>支付方式</Text>
|
||||
<Text className='pay-card__value'>{PAY_METHOD_NAMES[payment.pay_method]}</Text>
|
||||
</View>
|
||||
<View className='pay-card__row'>
|
||||
<Text className='pay-card__label'>提交时间</Text>
|
||||
<Text className='pay-card__value'>{payment.created_at}</Text>
|
||||
</View>
|
||||
{payment.audited_at && (
|
||||
<View className='pay-card__row'>
|
||||
<Text className='pay-card__label'>审核时间</Text>
|
||||
<Text className='pay-card__value'>{payment.audited_at}</Text>
|
||||
</View>
|
||||
)}
|
||||
{payment.remark && (
|
||||
<View className='pay-card__row'>
|
||||
<Text className='pay-card__label'>付款备注</Text>
|
||||
<Text className='pay-card__value'>{payment.remark}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ===== 汇款凭证 ===== */}
|
||||
<View className='pay-card'>
|
||||
<Text className='pay-section__title'>汇款凭证({vouchers.length})</Text>
|
||||
<View className='pay-vouchers'>
|
||||
{vouchers.map((url, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
className='pay-vouchers__img'
|
||||
src={url}
|
||||
mode='aspectFill'
|
||||
onClick={() => previewVoucher(url)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{vouchers.length === 0 && <Empty description='暂无凭证图片' />}
|
||||
</View>
|
||||
|
||||
{/* ===== 合并账单 ===== */}
|
||||
<View className='pay-card'>
|
||||
<Text className='pay-section__title'>合并账单({bills.length})</Text>
|
||||
{bills.map(bill => (
|
||||
<View key={bill.id} className='pay-bill' onClick={() => goBill(bill.id)}>
|
||||
<View className='pay-bill__main'>
|
||||
<Text className='pay-bill__no'>{bill.bill_no}</Text>
|
||||
<Text className='pay-bill__date'>{bill.bill_date}</Text>
|
||||
</View>
|
||||
<Text className='pay-bill__amount'>¥{bill.total_amount}</Text>
|
||||
<Text className={`pay-bill__status pay-bill__status--${bill.status}`}>
|
||||
{bill.status === 1 ? '已支付' : '未支付'}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{bills.length === 0 && <Empty description='暂无关联账单' />}
|
||||
</View>
|
||||
|
||||
{/* ===== 已拒绝 → 重新付款 ===== */}
|
||||
{payment.status === 2 && (
|
||||
<View className='pay-bar'>
|
||||
<View className='pay-bar__btn' onClick={handleRepay}>重新发起付款</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '支付记录',
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
.payment-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-empty {
|
||||
padding-top: 120rpx;
|
||||
|
||||
&__btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-loading {
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 支付记录单项 =====
|
||||
.payment-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: #ff976a; }
|
||||
&--1 { color: #07c160; }
|
||||
&--2 { color: #ee0a24; }
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__method {
|
||||
font-size: 24rpx;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&__date {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
&__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 32rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__bills {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__reject {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #fff5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ee0a24;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
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 { getPaymentListApi, PAY_METHOD_NAMES, PAY_STATUS_NAMES } from '@/services/payment'
|
||||
import type { Payment, PayStatus } from '@/services/payment'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
/** 状态筛选(undefined = 全部) */
|
||||
const STATUS_FILTERS: Array<{ value: PayStatus | undefined; label: string }> = [
|
||||
{ value: undefined, label: '全部' },
|
||||
{ value: 0, label: '待审核' },
|
||||
{ value: 1, label: '已通过' },
|
||||
{ value: 2, label: '已拒绝' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 支付记录列表页
|
||||
* 门店口径支付记录(合并付款申请),支持状态筛选;点击进支付详情
|
||||
*/
|
||||
export default function PaymentRecordsPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
const [status, setStatus] = useState<PayStatus | undefined>(undefined)
|
||||
const [records, setRecords] = useState<Payment[]>([])
|
||||
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, statusParam?: PayStatus) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPaymentListApi({ status: statusParam, page: pageNum, pageSize: PAGE_SIZE })
|
||||
const { data, total } = res.data
|
||||
setRecords(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, status)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !loadingRef.current && loggedIn) {
|
||||
loadList(page + 1, false, status)
|
||||
}
|
||||
})
|
||||
|
||||
/** 切换状态筛选 */
|
||||
const handleStatusTap = useCallback(
|
||||
(value?: PayStatus) => {
|
||||
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/payment-detail/index?id=${id}` })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='payment-page'>
|
||||
{/* ========== 状态筛选 ========== */}
|
||||
<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='payment-empty'>
|
||||
<View className='payment-empty__btn' onClick={goLogin}>去登录</View>
|
||||
</Empty>
|
||||
) : records.length === 0 ? (
|
||||
loading ? (
|
||||
<View className='payment-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无支付记录' className='payment-empty' />
|
||||
)
|
||||
) : (
|
||||
records.map(record => (
|
||||
<View key={record.id} className='payment-item' onClick={() => goDetail(record.id)}>
|
||||
<View className='payment-item__header'>
|
||||
<Text className='payment-item__no'>{record.payment_no}</Text>
|
||||
<Text className={`payment-item__status payment-item__status--${record.status}`}>
|
||||
{PAY_STATUS_NAMES[record.status]}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='payment-item__body'>
|
||||
<View className='payment-item__meta'>
|
||||
<Text className='payment-item__method'>{PAY_METHOD_NAMES[record.pay_method]}</Text>
|
||||
<Text className='payment-item__date'>{record.created_at}</Text>
|
||||
</View>
|
||||
<View className='payment-item__side'>
|
||||
<Text className='payment-item__amount'>¥{record.amount}</Text>
|
||||
<Text className='payment-item__bills'>合并 {record.bills_count ?? 0} 张账单</Text>
|
||||
</View>
|
||||
</View>
|
||||
{record.status === 2 && !!record.audit_remark && (
|
||||
<Text className='payment-item__reject'>拒绝原因:{record.audit_remark}</Text>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{loggedIn && finished && records.length > 0 && (
|
||||
<View className='payment-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '账单付款',
|
||||
})
|
||||
@@ -0,0 +1,323 @@
|
||||
.pay-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 160rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.pay-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__extra {
|
||||
font-size: 26rpx;
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-empty {
|
||||
padding: 40rpx 0;
|
||||
|
||||
&__btn {
|
||||
margin-top: 24rpx;
|
||||
padding: 14rpx 60rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-loading {
|
||||
padding: 24rpx 0 8rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 账单选择行 =====
|
||||
.pay-bill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__no {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 支付方式 =====
|
||||
.pay-methods {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.pay-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
|
||||
&.active {
|
||||
.pay-method__label {
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__radio {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #c8c9cc;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.on {
|
||||
background: #ee0a24;
|
||||
border-color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 0 8rpx;
|
||||
}
|
||||
|
||||
&__qrcode {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__qrcode-tip {
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__bank {
|
||||
width: 100%;
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__copy {
|
||||
margin-top: 16rpx;
|
||||
padding: 8rpx 40rpx;
|
||||
border: 1rpx solid #ee0a24;
|
||||
border-radius: 999rpx;
|
||||
color: #ee0a24;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: block;
|
||||
padding: 24rpx 0 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 汇款凭证 =====
|
||||
.pay-vouchers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.pay-voucher {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
&__del {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 0 0 0 12rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--add {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed #dcdee0;
|
||||
background: #fafafa;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__add-text {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 备注 =====
|
||||
.pay-remark {
|
||||
width: 100%;
|
||||
height: 140rpx;
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// ===== 提交栏 =====
|
||||
.pay-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);
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 26rpx;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
margin-left: 16rpx;
|
||||
font-size: 36rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
padding: 14rpx 48rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom, useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image, Textarea } from '@tarojs/components'
|
||||
import { Empty, Icon } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getBillListApi } from '@/services/bill'
|
||||
import { createPaymentApi, getPaymentConfigApi } from '@/services/payment'
|
||||
import { chooseAndUploadImages } from '@/utils/upload'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import type { Bill } from '@/services/bill'
|
||||
import type { PayMethod, PaymentConfig } from '@/services/payment'
|
||||
import type { UploadedFile } from '@/utils/upload'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
/** 凭证最多上传张数 */
|
||||
const MAX_VOUCHERS = 3
|
||||
|
||||
/** 支付方式选项 */
|
||||
const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: string }> = [
|
||||
{ value: 1, label: '微信支付', icon: 'wechat', desc: '扫码完成转账' },
|
||||
{ value: 2, label: '支付宝', icon: 'alipay', desc: '扫码完成转账' },
|
||||
{ value: 3, label: '对公汇款', icon: 'credit-pay', desc: '银行转账至对公账户' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 发起付款页(合并付款)
|
||||
* 选择本店可付款账单(?payable=1)→ 选择支付方式(展示收款码 / 对公账户)→ 上传汇款凭证 → 提交,后台审核
|
||||
* 支持 ?ids=1,2 预选账单(账单详情页"去付款"跳转)
|
||||
*/
|
||||
export default function PaymentPage() {
|
||||
const router = useRouter()
|
||||
const token = useAuthStore(s => s.token)
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 预选账单(路由参数,仅在首次加载时消费一次) */
|
||||
const presetRef = useRef<number[] | null>(
|
||||
(router.params.ids || '')
|
||||
.split(',')
|
||||
.map(Number)
|
||||
.filter(n => n > 0),
|
||||
)
|
||||
|
||||
const [bills, setBills] = useState<Bill[]>([])
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
const [config, setConfig] = useState<PaymentConfig | null>(null)
|
||||
const [payMethod, setPayMethod] = useState<PayMethod>(1)
|
||||
const [vouchers, setVouchers] = useState<UploadedFile[]>([])
|
||||
const [remark, setRemark] = useState('')
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
/** 拉取可付款账单(首次加载应用路由预选) */
|
||||
const loadBills = useCallback(
|
||||
async (pageNum: number, reset: boolean) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getBillListApi({ payable: 1, page: pageNum, pageSize: PAGE_SIZE })
|
||||
const { data, total } = res.data
|
||||
setBills(prev => (reset ? data : [...prev, ...data]))
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= total)
|
||||
if (presetRef.current) {
|
||||
const preset = presetRef.current
|
||||
presetRef.current = null
|
||||
setSelectedIds(prev => Array.from(new Set([...prev, ...preset])))
|
||||
}
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn],
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
loadBills(1, true)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !loadingRef.current && loggedIn) {
|
||||
loadBills(page + 1, false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 收款配置(收款码 / 对公账户信息,挂载时加载一次) */
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return
|
||||
getPaymentConfigApi()
|
||||
.then(res => setConfig(res.data))
|
||||
.catch(() => {})
|
||||
}, [loggedIn])
|
||||
|
||||
/** 勾选账单 */
|
||||
const toggleBill = useCallback((id: number) => {
|
||||
setSelectedIds(prev => (prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]))
|
||||
}, [])
|
||||
|
||||
/** 全选已加载账单 */
|
||||
const allChecked = bills.length > 0 && selectedIds.length >= bills.length
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
setSelectedIds(prev => (prev.length >= bills.length ? [] : bills.map(b => b.id)))
|
||||
}, [bills])
|
||||
|
||||
/** 已选账单合计金额(展示口径,实际以后端计算为准) */
|
||||
const totalAmount = bills
|
||||
.filter(b => selectedIds.includes(b.id))
|
||||
.reduce((sum, b) => sum + Number(b.total_amount), 0)
|
||||
.toFixed(2)
|
||||
|
||||
/** 上传凭证 */
|
||||
const handleAddVoucher = useCallback(async () => {
|
||||
if (uploading) return
|
||||
const remain = MAX_VOUCHERS - vouchers.length
|
||||
if (remain <= 0) {
|
||||
Taro.showToast({ title: `最多上传 ${MAX_VOUCHERS} 张`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
setUploading(true)
|
||||
try {
|
||||
const files = await chooseAndUploadImages(remain)
|
||||
setVouchers(prev => [...prev, ...files])
|
||||
} catch {
|
||||
// 用户取消或上传失败(upload 内已 toast)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}, [uploading, vouchers.length])
|
||||
|
||||
const handleRemoveVoucher = useCallback((index: number) => {
|
||||
setVouchers(prev => prev.filter((_, i) => i !== index))
|
||||
}, [])
|
||||
|
||||
/** 预览凭证 / 收款码 */
|
||||
const previewImage = useCallback((urls: string[], current: string) => {
|
||||
Taro.previewImage({ urls, current })
|
||||
}, [])
|
||||
|
||||
/** 复制对公账户信息 */
|
||||
const copyBankInfo = useCallback(() => {
|
||||
if (!config?.bank_info) return
|
||||
Taro.setClipboardData({ data: config.bank_info })
|
||||
}, [config])
|
||||
|
||||
/** 提交付款申请 */
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (submitting) return
|
||||
if (selectedIds.length === 0) {
|
||||
Taro.showToast({ title: '请选择要付款的账单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (vouchers.length === 0) {
|
||||
Taro.showToast({ title: '请上传汇款凭证', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await createPaymentApi({
|
||||
bill_ids: selectedIds,
|
||||
pay_method: payMethod,
|
||||
voucher_ids: vouchers.map(v => v.id),
|
||||
remark: remark.trim() || undefined,
|
||||
})
|
||||
Taro.showToast({ title: res.msg || '付款申请已提交', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({ url: `/pages/payment-detail/index?id=${res.data.id}` })
|
||||
}, 800)
|
||||
} catch {
|
||||
// 账单状态可能已变化(如已被其他端付款),刷新列表
|
||||
loadBills(1, true)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [submitting, selectedIds, vouchers, payMethod, remark, loadBills])
|
||||
|
||||
/** 当前支付方式的收款展示 */
|
||||
const renderMethodContent = () => {
|
||||
if (payMethod === 3) {
|
||||
return config?.bank_info ? (
|
||||
<View className='pay-method__content'>
|
||||
<Text className='pay-method__bank'>{config.bank_info}</Text>
|
||||
<View className='pay-method__copy' onClick={copyBankInfo}>复制</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text className='pay-method__empty'>暂未配置对公账户,请选择其他方式或联系客服</Text>
|
||||
)
|
||||
}
|
||||
const qrcode = resolveFileUrl(payMethod === 1 ? config?.wechat_qrcode : config?.alipay_qrcode)
|
||||
return qrcode ? (
|
||||
<View className='pay-method__content'>
|
||||
<Image
|
||||
className='pay-method__qrcode'
|
||||
src={qrcode}
|
||||
mode='aspectFit'
|
||||
onClick={() => previewImage([qrcode], qrcode)}
|
||||
/>
|
||||
<Text className='pay-method__qrcode-tip'>长按二维码保存,或点击大图查看</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text className='pay-method__empty'>暂未配置收款码,请选择其他方式或联系客服</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='pay-page'>
|
||||
{/* ========== 选择账单 ========== */}
|
||||
<View className='pay-section'>
|
||||
<View className='pay-section__header'>
|
||||
<Text className='pay-section__title'>选择账单</Text>
|
||||
{bills.length > 0 && (
|
||||
<Text className='pay-section__extra' onClick={toggleSelectAll}>
|
||||
{allChecked ? '取消全选' : '全选'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{!loggedIn ? (
|
||||
<Empty description='登录后发起付款' className='pay-empty'>
|
||||
<View
|
||||
className='pay-empty__btn'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
|
||||
>
|
||||
去登录
|
||||
</View>
|
||||
</Empty>
|
||||
) : bills.length === 0 ? (
|
||||
loading ? (
|
||||
<View className='pay-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='暂无可付款账单' className='pay-empty' />
|
||||
)
|
||||
) : (
|
||||
bills.map(bill => {
|
||||
const checked = selectedIds.includes(bill.id)
|
||||
return (
|
||||
<View key={bill.id} className='pay-bill' onClick={() => toggleBill(bill.id)}>
|
||||
<View className={`pay-bill__check ${checked ? 'on' : ''}`}>
|
||||
{checked && <Icon name='success' size={14} color='#fff' />}
|
||||
</View>
|
||||
<View className='pay-bill__main'>
|
||||
<Text className='pay-bill__no'>{bill.bill_no}</Text>
|
||||
<Text className='pay-bill__meta'>账单日期 {bill.bill_date} · 应结算 {bill.settlement_date}</Text>
|
||||
</View>
|
||||
<Text className='pay-bill__amount'>¥{bill.total_amount}</Text>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
)}
|
||||
{loggedIn && !finished && bills.length > 0 && (
|
||||
<View className='pay-loading'><Text>{loading ? '加载中...' : '上拉加载更多'}</Text></View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 支付方式 ========== */}
|
||||
<View className='pay-section'>
|
||||
<Text className='pay-section__title'>支付方式</Text>
|
||||
<View className='pay-methods'>
|
||||
{PAY_METHODS.map(m => (
|
||||
<View
|
||||
key={m.value}
|
||||
className={`pay-method ${payMethod === m.value ? 'active' : ''}`}
|
||||
onClick={() => setPayMethod(m.value)}
|
||||
>
|
||||
<Icon name={m.icon} size={22} color={payMethod === m.value ? '#ee0a24' : '#969799'} />
|
||||
<View className='pay-method__info'>
|
||||
<Text className='pay-method__label'>{m.label}</Text>
|
||||
<Text className='pay-method__desc'>{m.desc}</Text>
|
||||
</View>
|
||||
<View className={`pay-method__radio ${payMethod === m.value ? 'on' : ''}`}>
|
||||
{payMethod === m.value && <Icon name='success' size={12} color='#fff' />}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{renderMethodContent()}
|
||||
</View>
|
||||
|
||||
{/* ========== 汇款凭证 ========== */}
|
||||
<View className='pay-section'>
|
||||
<View className='pay-section__header'>
|
||||
<Text className='pay-section__title'>汇款凭证</Text>
|
||||
<Text className='pay-section__hint'>转账截图或回单,最多 {MAX_VOUCHERS} 张</Text>
|
||||
</View>
|
||||
<View className='pay-vouchers'>
|
||||
{vouchers.map((v, i) => {
|
||||
const url = resolveFileUrl(v.url)
|
||||
return (
|
||||
<View key={v.id} className='pay-voucher'>
|
||||
<Image
|
||||
className='pay-voucher__img'
|
||||
src={url}
|
||||
mode='aspectFill'
|
||||
onClick={() => previewImage(vouchers.map(x => resolveFileUrl(x.url)), url)}
|
||||
/>
|
||||
<View className='pay-voucher__del' onClick={() => handleRemoveVoucher(i)}>
|
||||
<Icon name='cross' size={12} color='#fff' />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
{vouchers.length < MAX_VOUCHERS && (
|
||||
<View className='pay-voucher pay-voucher--add' onClick={handleAddVoucher}>
|
||||
<Icon name={uploading ? 'more' : 'plus'} size={24} color='#969799' />
|
||||
<Text className='pay-voucher__add-text'>{uploading ? '上传中' : '上传凭证'}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 备注 ========== */}
|
||||
<View className='pay-section'>
|
||||
<Text className='pay-section__title'>备注(选填)</Text>
|
||||
<Textarea
|
||||
className='pay-remark'
|
||||
value={remark}
|
||||
maxlength={255}
|
||||
placeholder='如:汇款人姓名、转账时间等'
|
||||
onInput={e => setRemark(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* ========== 提交栏 ========== */}
|
||||
{loggedIn && bills.length > 0 && (
|
||||
<View className='pay-bar'>
|
||||
<View className='pay-bar__info'>
|
||||
<Text className='pay-bar__count'>已选 {selectedIds.length} 张</Text>
|
||||
<Text className='pay-bar__amount'>¥{totalAmount}</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`pay-bar__btn ${selectedIds.length === 0 || submitting ? 'disabled' : ''}`}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? '提交中...' : '提交付款'}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
.goods-detail {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: 160rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__empty {
|
||||
padding-top: 160rpx;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
padding-top: 160rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
// ===== 商品图轮播 =====
|
||||
.goods-swiper {
|
||||
width: 100%;
|
||||
height: 750rpx;
|
||||
background: #f2f3f5;
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 信息卡 =====
|
||||
.goods-card {
|
||||
background: #fff;
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&__price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
&__price {
|
||||
font-size: 44rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
|
||||
&--none {
|
||||
font-size: 28rpx;
|
||||
color: #c8c9cc;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__unit {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 34rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__spec {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
margin: 0 12rpx 12rpx 0;
|
||||
padding: 6rpx 16rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 底部加购栏 =====
|
||||
.goods-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&__hint {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-left: 24rpx;
|
||||
padding: 16rpx 48rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image, RichText } from '@tarojs/components'
|
||||
import { Empty, Stepper, Swiper, SwiperItem } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import useCartStore from '@/stores/cart/useCartStore'
|
||||
import { getProductDetailApi } from '@/services/product'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import type { Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
/** 图文详情图片自适应(rich-text 内部节点不吃页面样式,预处理内联样式) */
|
||||
function normalizeContent(html: string): string {
|
||||
return html.replace(/<img\b/gi, '<img style="max-width:100%;height:auto;display:block;"')
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品详情页(免登录浏览)
|
||||
* 未登录/未绑店/未设等级 price=null → 不展示价格、加购引导登录;
|
||||
* 已登录门店展示该店等级换算价,可直接加购
|
||||
*/
|
||||
export default function ProductDetailPage() {
|
||||
const router = useRouter()
|
||||
const id = Number(router.params.id ?? 0)
|
||||
|
||||
const token = useAuthStore(s => s.token)
|
||||
const addItem = useCartStore(s => s.addItem)
|
||||
|
||||
const [product, setProduct] = useState<Product | null>(null)
|
||||
const [failed, setFailed] = useState(false)
|
||||
const [qty, setQty] = useState(1)
|
||||
const [adding, setAdding] = useState(false)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
useDidShow(() => {
|
||||
if (!id) {
|
||||
setFailed(true)
|
||||
return
|
||||
}
|
||||
setFailed(false)
|
||||
getProductDetailApi(id)
|
||||
.then(res => setProduct(res.data))
|
||||
.catch(() => setFailed(true)) // 下架/不存在:request 层已 toast
|
||||
})
|
||||
|
||||
/** 图片地址列表(preview_url 优先) */
|
||||
const images = (product?.images_arr ?? [])
|
||||
.map(img => resolveFileUrl(img.preview_url || img.file_url))
|
||||
.filter(Boolean)
|
||||
|
||||
const previewImage = useCallback(
|
||||
(current: string) => {
|
||||
Taro.previewImage({ urls: images, current })
|
||||
},
|
||||
[images],
|
||||
)
|
||||
|
||||
/** 加入购物车(服务端校验上架与等级价) */
|
||||
const handleAdd = useCallback(async () => {
|
||||
if (!product || adding) return
|
||||
setAdding(true)
|
||||
try {
|
||||
await addItem(product.id, qty)
|
||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
setAdding(false)
|
||||
}
|
||||
}, [product, adding, qty, addItem])
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
if (failed) {
|
||||
return (
|
||||
<View className='goods-detail'>
|
||||
<Empty description='商品不存在或已下架' className='goods-detail__empty' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
if (!product) {
|
||||
return (
|
||||
<View className='goods-detail'>
|
||||
<View className='goods-detail__loading'><Text>加载中...</Text></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='goods-detail'>
|
||||
{/* ========== 商品图轮播 ========== */}
|
||||
{images.length > 0 ? (
|
||||
<Swiper className='goods-swiper' height={375} loop={images.length > 1} autoPlay={0} paginationColor='#ee0a24'>
|
||||
{images.map(url => (
|
||||
<SwiperItem key={url}>
|
||||
<Image
|
||||
className='goods-swiper__img'
|
||||
src={url}
|
||||
mode='aspectFill'
|
||||
onClick={() => previewImage(url)}
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
<View className='goods-swiper goods-swiper--empty'>
|
||||
<Text className='goods-swiper__empty-text'>暂无图片</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 基本信息 ========== */}
|
||||
<View className='goods-card'>
|
||||
<View className='goods-card__price-row'>
|
||||
{product.price !== null ? (
|
||||
<Text className='goods-card__price'>¥{product.price}</Text>
|
||||
) : (
|
||||
<Text className='goods-card__price goods-card__price--none'>
|
||||
{loggedIn ? '价格待定' : '登录后查看价格'}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='goods-card__unit'>/{product.unit}</Text>
|
||||
</View>
|
||||
<Text className='goods-card__name'>{product.name}</Text>
|
||||
<Text className='goods-card__spec'>{product.spec}</Text>
|
||||
<View className='goods-card__meta'>
|
||||
{!!product.shelf_life && product.shelf_life > 0 && (
|
||||
<Text className='goods-card__tag'>保质期 {product.shelf_life} 天</Text>
|
||||
)}
|
||||
{product.stock !== null && product.stock !== undefined && (
|
||||
<Text className='goods-card__tag'>库存 {product.stock}</Text>
|
||||
)}
|
||||
{product.category?.name && (
|
||||
<Text className='goods-card__tag'>{product.category.name}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 图文详情 ========== */}
|
||||
{!!product.content && (
|
||||
<View className='goods-card'>
|
||||
<Text className='goods-card__section'>商品详情</Text>
|
||||
<RichText className='goods-card__content' nodes={normalizeContent(product.content)} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 底部加购栏 ========== */}
|
||||
<View className='goods-bar'>
|
||||
{!loggedIn ? (
|
||||
<>
|
||||
<Text className='goods-bar__hint'>登录后即可下单采购</Text>
|
||||
<View className='goods-bar__btn' onClick={goLogin}>去登录</View>
|
||||
</>
|
||||
) : product.price === null ? (
|
||||
<Text className='goods-bar__hint'>门店未设等级价,暂不可下单,请联系客服</Text>
|
||||
) : (
|
||||
<>
|
||||
<Stepper
|
||||
value={qty}
|
||||
min={1}
|
||||
max={99999999.99}
|
||||
onChange={e => setQty(Number(e.detail))}
|
||||
/>
|
||||
<View
|
||||
className={`goods-bar__btn ${adding ? 'disabled' : ''}`}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{adding ? '加入中...' : '加入购物车'}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -190,6 +190,11 @@ export default function ProductPage() {
|
||||
setShowPopup(true)
|
||||
}, [])
|
||||
|
||||
/** 跳转商品详情 */
|
||||
const goDetail = useCallback((id: number) => {
|
||||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` })
|
||||
}, [])
|
||||
|
||||
/** 确认加购 */
|
||||
const handleConfirmAdd = useCallback(async () => {
|
||||
if (!current || addingRef.current) return
|
||||
@@ -274,7 +279,7 @@ export default function ProductPage() {
|
||||
<Empty description='暂无商品' className='product-empty' />
|
||||
) : (
|
||||
products.map(product => (
|
||||
<View key={product.id} className='product-item'>
|
||||
<View key={product.id} className='product-item' onClick={() => goDetail(product.id)}>
|
||||
<Image
|
||||
className='product-item__image'
|
||||
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
|
||||
@@ -290,7 +295,13 @@ export default function ProductPage() {
|
||||
) : (
|
||||
<Text className='product-item__price product-item__price--none'>价格待定</Text>
|
||||
)}
|
||||
<View className='product-item__add' onClick={() => handleAddTap(product)}>
|
||||
<View
|
||||
className='product-item__add'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleAddTap(product)
|
||||
}}
|
||||
>
|
||||
<Text className='product-item__add-icon'>+</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -315,6 +326,7 @@ export default function ProductPage() {
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
style={{ paddingBottom: '110px' }}
|
||||
onClose={() => setShowPopup(false)}
|
||||
>
|
||||
{current && (
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function ProfilePage() {
|
||||
|
||||
const loggedIn = !!token && !!user
|
||||
|
||||
/** 功能菜单(门店账号追加「门店信息」入口) */
|
||||
/** 功能菜单(门店账号追加「门店信息」「支付记录」入口) */
|
||||
const menuItems = useMemo(() => {
|
||||
if (!user?.store) return MENU_ITEMS
|
||||
return [
|
||||
@@ -48,6 +48,12 @@ export default function ProfilePage() {
|
||||
icon: 'shop-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/store-info/index' }),
|
||||
},
|
||||
{
|
||||
key: 'payment-records',
|
||||
label: '支付记录',
|
||||
icon: 'balance-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/payment-records/index' }),
|
||||
},
|
||||
...MENU_ITEMS,
|
||||
]
|
||||
}, [user?.store])
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(25, 137, 250, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(238, 10, 36, 0.3);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
@@ -108,8 +108,8 @@
|
||||
flex: 1;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
background: #e8f7ef;
|
||||
color: #07c160;
|
||||
background: #fff0f0;
|
||||
color: #ee0a24;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
@@ -135,7 +135,7 @@
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
line-height: 96px;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 100%);
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
font-weight: 500;
|
||||
@@ -143,7 +143,7 @@
|
||||
border-radius: 48px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
box-shadow: 0 6px 24px rgba(25, 137, 250, 0.35);
|
||||
box-shadow: 0 6px 24px rgba(238, 10, 36, 0.35);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
/* 重置微信 Button 默认样式 */
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
.switch-link {
|
||||
font-size: 28px;
|
||||
color: #1989fa;
|
||||
color: #ee0a24;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,6 @@
|
||||
|
||||
.agree-link {
|
||||
font-size: 24px;
|
||||
color: #1989fa;
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ export interface Bill {
|
||||
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 */
|
||||
/** 附加金额 = box_num×box_price + tray_num×tray_price(可能为负数:回筐抵扣) */
|
||||
added_amount: string
|
||||
/** 账单总金额 = 商品金额 + 配送费 + 附加金额 */
|
||||
total_amount: string
|
||||
@@ -70,6 +70,8 @@ export interface BillItem {
|
||||
weight: string
|
||||
/** 合计金额 */
|
||||
amount: string
|
||||
/** 商品首图 URL(无图为空字符串) */
|
||||
image: string
|
||||
}
|
||||
|
||||
/** 账单关联订单 */
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
import type { PaginatedData } from '@/types/api'
|
||||
|
||||
/** 支付方式:1 微信 / 2 支付宝 / 3 对公汇款 */
|
||||
export type PayMethod = 1 | 2 | 3
|
||||
|
||||
export const PAY_METHOD_NAMES: Record<PayMethod, string> = {
|
||||
1: '微信支付',
|
||||
2: '支付宝',
|
||||
3: '对公汇款',
|
||||
}
|
||||
|
||||
/** 支付状态:0 待审核 / 1 已通过 / 2 已拒绝 */
|
||||
export type PayStatus = 0 | 1 | 2
|
||||
|
||||
export const PAY_STATUS_NAMES: Record<PayStatus, string> = {
|
||||
0: '待审核',
|
||||
1: '已通过',
|
||||
2: '已拒绝',
|
||||
}
|
||||
|
||||
/** 支付配置(付款页展示,图片为解析后的预览地址,可能为空串) */
|
||||
export interface PaymentConfig {
|
||||
wechat_qrcode: string
|
||||
alipay_qrcode: string
|
||||
bank_info: string
|
||||
}
|
||||
|
||||
/** 支付记录(列表行与详情的 payment 字段一致) */
|
||||
export interface Payment {
|
||||
id: number
|
||||
/** 支付编号(ZF 前缀) */
|
||||
payment_no: string
|
||||
store_id: number
|
||||
user_id: number
|
||||
/** 合并付款总金额 */
|
||||
amount: string
|
||||
pay_method: PayMethod
|
||||
/** 凭证图片 ID 数组(模型 casts 为 array) */
|
||||
voucher_ids: number[]
|
||||
status: PayStatus
|
||||
/** 提交备注 */
|
||||
remark: string
|
||||
/** 审核时间 */
|
||||
audited_at: string | null
|
||||
auditor_id: number | null
|
||||
/** 审核备注(拒绝原因) */
|
||||
audit_remark: string | null
|
||||
created_at: string
|
||||
/** 列表返回:合并账单数 */
|
||||
bills_count?: number
|
||||
}
|
||||
|
||||
/** 支付记录详情(payment 附加凭证图片 URL 列表) */
|
||||
export interface PaymentDetail {
|
||||
payment: Payment & { voucher_urls: string[] }
|
||||
/** 合并付款的账单 */
|
||||
bills: Array<{
|
||||
id: number
|
||||
bill_no: string
|
||||
bill_date: string
|
||||
product_amount: string
|
||||
delivery_fee: string
|
||||
added_amount: string
|
||||
total_amount: string
|
||||
status: 0 | 1
|
||||
}>
|
||||
}
|
||||
|
||||
/** 发起付款返回 */
|
||||
export interface PaymentCreateResult {
|
||||
id: number
|
||||
payment_no: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
/** 支付配置:GET /mini/payment/config */
|
||||
export function getPaymentConfigApi() {
|
||||
return get<PaymentConfig>('/mini/payment/config')
|
||||
}
|
||||
|
||||
/** 支付记录列表:GET /mini/payment?status=&page=&pageSize= */
|
||||
export function getPaymentListApi(params: { status?: PayStatus; page?: number; pageSize?: number } = {}) {
|
||||
return get<PaginatedData<Payment>>('/mini/payment', { data: params })
|
||||
}
|
||||
|
||||
/** 发起合并付款:POST /mini/payment */
|
||||
export function createPaymentApi(data: {
|
||||
bill_ids: number[]
|
||||
pay_method: PayMethod
|
||||
voucher_ids: number[]
|
||||
remark?: string
|
||||
}) {
|
||||
return post<PaymentCreateResult>('/mini/payment', data)
|
||||
}
|
||||
|
||||
/** 支付记录详情:GET /mini/payment/{id} */
|
||||
export function getPaymentDetailApi(id: number) {
|
||||
return get<PaymentDetail>(`/mini/payment/${id}`)
|
||||
}
|
||||
@@ -21,3 +21,8 @@ export interface ProductListParams {
|
||||
export function getProductListApi(params: ProductListParams = {}) {
|
||||
return get<PaginatedData<Product>>('/mini/product/list', { data: params })
|
||||
}
|
||||
|
||||
/** 商品详情(免登录;未登录/未绑店/未设等级 price=null;下架或不存在业务报错):GET /mini/product/{id} */
|
||||
export function getProductDetailApi(id: number) {
|
||||
return get<Product>(`/mini/product/${id}`)
|
||||
}
|
||||
|
||||
@@ -42,8 +42,12 @@ export const ORDER_NAV_ITEMS: Array<{ key: string; label: string; status?: numbe
|
||||
/** 订单列表行商品预览(仅前 3 条) */
|
||||
export interface OrderItemPreview {
|
||||
product_name: string
|
||||
/** 规格包规 */
|
||||
product_spec: string
|
||||
quantity: number
|
||||
unit: string
|
||||
/** 首图 URL(无图为空字符串) */
|
||||
image: string
|
||||
}
|
||||
|
||||
/** 订单列表行(状态名/可取消/商品预览均由后端给出,直接展示) */
|
||||
|
||||
+12
-7
@@ -1,3 +1,5 @@
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
|
||||
/** 商品分类节点(children 递归;叶子分类无 children 字段) */
|
||||
export interface Category {
|
||||
id: number
|
||||
@@ -6,10 +8,11 @@ export interface Category {
|
||||
children?: Category[]
|
||||
}
|
||||
|
||||
/** 商品图片 */
|
||||
/** 商品图片(SysFile 序列化,含预览地址与文件地址) */
|
||||
export interface ProductImage {
|
||||
id: number
|
||||
file_url: string
|
||||
preview_url: string
|
||||
}
|
||||
|
||||
/** 商品 */
|
||||
@@ -22,12 +25,15 @@ export interface Product {
|
||||
unit: string
|
||||
/** 商品图文详情(HTML) */
|
||||
content: string
|
||||
/** 当前门店等级的实际销售价(未设等级价为 null) */
|
||||
/** 当前门店等级的实际销售价(未登录/未绑店/未设等级为 null) */
|
||||
price: string | null
|
||||
images_arr: ProductImage[]
|
||||
/** 排序 / 保质期 / 库存 / 状态(仅返回上架商品) */
|
||||
/** 所属分类(详情接口 with 返回) */
|
||||
category?: { id: number; name: string } | null
|
||||
/** 排序 / 库存 / 状态(仅返回上架商品) */
|
||||
sort?: number
|
||||
shelf_life?: string | null
|
||||
/** 保质期(天,0=未设置) */
|
||||
shelf_life?: number | null
|
||||
stock?: number | null
|
||||
status?: number
|
||||
}
|
||||
@@ -35,7 +41,6 @@ export interface Product {
|
||||
/** 商品首图地址 */
|
||||
export function getProductCover(product: Product): string {
|
||||
const first = product.images_arr?.[0]
|
||||
if (!first || !first.file_url) return ''
|
||||
if (/^https?:\/\//i.test(first.file_url)) return first.file_url
|
||||
return first.file_url
|
||||
if (!first) return ''
|
||||
return resolveFileUrl(first.preview_url || first.file_url)
|
||||
}
|
||||
|
||||
+10
-3
@@ -40,11 +40,18 @@ export function formatTime(value?: string): string {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务器文件地址(收款码、汇款凭证等):绝对地址直接用,相对路径拼接服务器域名
|
||||
*/
|
||||
export function resolveFileUrl(url?: string): string {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return `${SERVER_ORIGIN}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析头像地址:绝对地址直接用,相对路径拼接服务器域名
|
||||
*/
|
||||
export function resolveAvatarUrl(avatar?: string): string {
|
||||
if (!avatar) return ''
|
||||
if (/^https?:\/\//i.test(avatar)) return avatar
|
||||
return `${SERVER_ORIGIN}${avatar.startsWith('/') ? '' : '/'}${avatar}`
|
||||
return resolveFileUrl(avatar)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { BASE_URL, getToken } from '@/utils/request'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
/** 上传结果(/mini/upload 返回) */
|
||||
export interface UploadedFile {
|
||||
/** 文件 ID(提交业务接口时使用的 voucher_ids 元素) */
|
||||
id: number
|
||||
/** 预览地址 */
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单张图片到 /mini/upload(凭证等场景,≤5MB)
|
||||
* uploadFile 不受 request 层封装(multipart),此处自行解析统一响应结构并 toast
|
||||
*/
|
||||
export function uploadImage(filePath: string): Promise<UploadedFile> {
|
||||
const token = getToken()
|
||||
return new Promise((resolve, reject) => {
|
||||
Taro.uploadFile({
|
||||
url: `${BASE_URL}/mini/upload`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
success(res) {
|
||||
let body: ApiResponse<UploadedFile> | null = null
|
||||
try {
|
||||
body = JSON.parse(res.data)
|
||||
} catch {
|
||||
// 非 JSON 响应(网关错误页等)
|
||||
}
|
||||
if (res.statusCode >= 200 && res.statusCode < 300 && body?.success) {
|
||||
resolve(body.data)
|
||||
return
|
||||
}
|
||||
const msg = body?.msg || `上传失败(${res.statusCode})`
|
||||
Taro.showToast({ title: msg, icon: 'none' })
|
||||
reject(new Error(msg))
|
||||
},
|
||||
fail(err) {
|
||||
Taro.showToast({ title: '上传失败,请检查网络', icon: 'none' })
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择并上传图片:一次选择 count 张,逐张上传,全部成功才返回
|
||||
*/
|
||||
export async function chooseAndUploadImages(count: number): Promise<UploadedFile[]> {
|
||||
const res = await Taro.chooseImage({ count, sizeType: ['compressed'] })
|
||||
const files: UploadedFile[] = []
|
||||
for (const path of res.tempFilePaths) {
|
||||
files.push(await uploadImage(path))
|
||||
}
|
||||
return files
|
||||
}
|
||||
Reference in New Issue
Block a user