diff --git a/mini-order-bill-api.md b/mini-order-bill-api.md deleted file mode 100644 index 55ac4db..0000000 --- a/mini-order-bill-api.md +++ /dev/null @@ -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 & { summary: BillSummary }>('/mini/bill', { data: params }) -} - -/** 账单详情:GET /mini/bill/{id} */ -export function getBillDetailApi(id: number) { - return get(`/mini/bill/${id}`) -} -``` - -订单列表行商品预览渲染建议: - -```tsx - - {order.items.map(i => `${i.product_name}×${i.quantity}`).join('、')} - {order.item_count > 3 ? ` 等${order.item_count}种` : ''} - -``` - -## 注意事项 - -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),无需特殊分支处理。 diff --git a/project.config.json b/project.config.json index 03f465e..adba2c3 100644 --- a/project.config.json +++ b/project.config.json @@ -5,7 +5,7 @@ "appid": "wx7ed74d60503b5ee3", "setting": { "urlCheck": false, - "es6": false, + "es6": true, "postcss": false, "minified": true, "enhance": false diff --git a/src/app.config.ts b/src/app.config.ts index c3ce53a..10b4f41 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -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', diff --git a/src/pages/bill-detail/index.less b/src/pages/bill-detail/index.less index 3c50cd5..04c67e8 100644 --- a/src/pages/bill-detail/index.less +++ b/src/pages/bill-detail/index.less @@ -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; + } + } } diff --git a/src/pages/bill-detail/index.tsx b/src/pages/bill-detail/index.tsx index 567823b..ea91a4a 100644 --- a/src/pages/bill-detail/index.tsx +++ b/src/pages/bill-detail/index.tsx @@ -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 } @@ -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 ( - + {/* ===== 账单信息 ===== */} @@ -114,10 +133,10 @@ export default function BillDetailPage() { ¥{bill.delivery_fee} - - 附加金额(筐 {bill.box_num}×¥{bill.box_price},托盘 {bill.tray_num}×¥{bill.tray_price}) + {addedLabel}({boxPart},{trayPart}) + + {addedText} - ¥{bill.added_amount} 账单总额 @@ -131,6 +150,14 @@ export default function BillDetailPage() { 同一商品跨订单合并,单价为加权平均价 {(items ?? []).map(item => ( + {!!item.image && ( + + )} {item.product_name} @@ -200,6 +227,17 @@ export default function BillDetailPage() { )} + + {/* ===== 去付款操作栏(可付款账单) ===== */} + {bill.can_pay && ( + + + 账单总额 + ¥{bill.total_amount} + + 去付款 + + )} ) } diff --git a/src/pages/bill/index.less b/src/pages/bill/index.less index 4d6f54c..b04cfba 100644 --- a/src/pages/bill/index.less +++ b/src/pages/bill/index.less @@ -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; + } } // ===== 状态筛选 + 导出入口 ===== diff --git a/src/pages/bill/index.tsx b/src/pages/bill/index.tsx index 4a7bcb9..19ccc55 100644 --- a/src/pages/bill/index.tsx +++ b/src/pages/bill/index.tsx @@ -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() { 待支付账单(含审核中) {summary.unpaid_count} 笔 - ¥{summary.unpaid_amount} + + ¥{summary.unpaid_amount} + 去付款 + )} diff --git a/src/pages/login/index.less b/src/pages/login/index.less index d040dd0..50befb1 100644 --- a/src/pages/login/index.less +++ b/src/pages/login/index.less @@ -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; } } diff --git a/src/pages/order-list/index.less b/src/pages/order-list/index.less index 671a766..345e645 100644 --- a/src/pages/order-list/index.less +++ b/src/pages/order-list/index.less @@ -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; diff --git a/src/pages/order-list/index.tsx b/src/pages/order-list/index.tsx index 3f88179..6c2623d 100644 --- a/src/pages/order-list/index.tsx +++ b/src/pages/order-list/index.tsx @@ -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} - {/* 商品预览(仅前 3 条,完整明细见详情) */} - - {order.items.map(i => `${i.product_name}×${i.quantity}`).join('、')} - {order.item_count > 3 ? ` 等${order.item_count}种` : ''} - + {/* 商品预览 */} + + {order.items.map((i, idx) => ( + + {i.image ? ( + + ) : ( + + )} + + {i.product_name} + {!!i.product_spec && ( + {i.product_spec} {i.unit} + )} + + ×{i.quantity} + + ))} + {order.item_count > 3 && ( + 等 {order.item_count} 种商品 + )} + 订货日期 {order.order_date} @@ -239,7 +262,7 @@ export default function OrderListPage() { {item.product_name} - {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity} + {item.product_spec ? `${item.product_spec}${item.unit} ` : ''}¥{item.price} × {item.quantity} ¥{item.amount} diff --git a/src/pages/payment-detail/index.config.ts b/src/pages/payment-detail/index.config.ts new file mode 100644 index 0000000..c92f687 --- /dev/null +++ b/src/pages/payment-detail/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '支付详情', +}) diff --git a/src/pages/payment-detail/index.less b/src/pages/payment-detail/index.less new file mode 100644 index 0000000..21a5959 --- /dev/null +++ b/src/pages/payment-detail/index.less @@ -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; + } + } +} diff --git a/src/pages/payment-detail/index.tsx b/src/pages/payment-detail/index.tsx new file mode 100644 index 0000000..22878ee --- /dev/null +++ b/src/pages/payment-detail/index.tsx @@ -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(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 + } + if (!detail) { + return + } + + const { payment, bills } = detail + const vouchers = payment.voucher_urls.map(resolveFileUrl) + + return ( + + {/* ===== 支付信息 ===== */} + + + {payment.payment_no} + + {PAY_STATUS_NAMES[payment.status]} + + + ¥{payment.amount} + {payment.status === 0 && ( + 付款申请已提交,商家审核通过后账单将置为已支付 + )} + {payment.status === 2 && ( + + 审核未通过{payment.audit_remark ? `:${payment.audit_remark}` : ''},账单已释放,可重新发起付款 + + )} + + 支付方式 + {PAY_METHOD_NAMES[payment.pay_method]} + + + 提交时间 + {payment.created_at} + + {payment.audited_at && ( + + 审核时间 + {payment.audited_at} + + )} + {payment.remark && ( + + 付款备注 + {payment.remark} + + )} + + + {/* ===== 汇款凭证 ===== */} + + 汇款凭证({vouchers.length}) + + {vouchers.map((url, i) => ( + previewVoucher(url)} + /> + ))} + + {vouchers.length === 0 && } + + + {/* ===== 合并账单 ===== */} + + 合并账单({bills.length}) + {bills.map(bill => ( + goBill(bill.id)}> + + {bill.bill_no} + {bill.bill_date} + + ¥{bill.total_amount} + + {bill.status === 1 ? '已支付' : '未支付'} + + + ))} + {bills.length === 0 && } + + + {/* ===== 已拒绝 → 重新付款 ===== */} + {payment.status === 2 && ( + + 重新发起付款 + + )} + + ) +} diff --git a/src/pages/payment-records/index.config.ts b/src/pages/payment-records/index.config.ts new file mode 100644 index 0000000..952ff14 --- /dev/null +++ b/src/pages/payment-records/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '支付记录', +}) diff --git a/src/pages/payment-records/index.less b/src/pages/payment-records/index.less new file mode 100644 index 0000000..81e5ba6 --- /dev/null +++ b/src/pages/payment-records/index.less @@ -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; + } + } +} diff --git a/src/pages/payment-records/index.tsx b/src/pages/payment-records/index.tsx new file mode 100644 index 0000000..54441a5 --- /dev/null +++ b/src/pages/payment-records/index.tsx @@ -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(undefined) + const [records, setRecords] = useState([]) + 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 ( + + {/* ========== 状态筛选 ========== */} + + {STATUS_FILTERS.map(item => ( + handleStatusTap(item.value)} + > + {item.label} + + ))} + + + {/* ========== 支付记录列表 ========== */} + {!loggedIn ? ( + + 去登录 + + ) : records.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + records.map(record => ( + goDetail(record.id)}> + + {record.payment_no} + + {PAY_STATUS_NAMES[record.status]} + + + + + {PAY_METHOD_NAMES[record.pay_method]} + {record.created_at} + + + ¥{record.amount} + 合并 {record.bills_count ?? 0} 张账单 + + + {record.status === 2 && !!record.audit_remark && ( + 拒绝原因:{record.audit_remark} + )} + + )) + )} + + {loggedIn && finished && records.length > 0 && ( + 没有更多了 + )} + + ) +} diff --git a/src/pages/payment/index.config.ts b/src/pages/payment/index.config.ts new file mode 100644 index 0000000..f43505d --- /dev/null +++ b/src/pages/payment/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '账单付款', +}) diff --git a/src/pages/payment/index.less b/src/pages/payment/index.less new file mode 100644 index 0000000..f541482 --- /dev/null +++ b/src/pages/payment/index.less @@ -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; + } + } + } +} diff --git a/src/pages/payment/index.tsx b/src/pages/payment/index.tsx new file mode 100644 index 0000000..4b23750 --- /dev/null +++ b/src/pages/payment/index.tsx @@ -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( + (router.params.ids || '') + .split(',') + .map(Number) + .filter(n => n > 0), + ) + + const [bills, setBills] = useState([]) + const [selectedIds, setSelectedIds] = useState([]) + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(false) + const [finished, setFinished] = useState(false) + const loadingRef = useRef(false) + + const [config, setConfig] = useState(null) + const [payMethod, setPayMethod] = useState(1) + const [vouchers, setVouchers] = useState([]) + 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 ? ( + + {config.bank_info} + 复制 + + ) : ( + 暂未配置对公账户,请选择其他方式或联系客服 + ) + } + const qrcode = resolveFileUrl(payMethod === 1 ? config?.wechat_qrcode : config?.alipay_qrcode) + return qrcode ? ( + + previewImage([qrcode], qrcode)} + /> + 长按二维码保存,或点击大图查看 + + ) : ( + 暂未配置收款码,请选择其他方式或联系客服 + ) + } + + return ( + + {/* ========== 选择账单 ========== */} + + + 选择账单 + {bills.length > 0 && ( + + {allChecked ? '取消全选' : '全选'} + + )} + + {!loggedIn ? ( + + Taro.navigateTo({ url: '/pages/login/index' })} + > + 去登录 + + + ) : bills.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + bills.map(bill => { + const checked = selectedIds.includes(bill.id) + return ( + toggleBill(bill.id)}> + + {checked && } + + + {bill.bill_no} + 账单日期 {bill.bill_date} · 应结算 {bill.settlement_date} + + ¥{bill.total_amount} + + ) + }) + )} + {loggedIn && !finished && bills.length > 0 && ( + {loading ? '加载中...' : '上拉加载更多'} + )} + + + {/* ========== 支付方式 ========== */} + + 支付方式 + + {PAY_METHODS.map(m => ( + setPayMethod(m.value)} + > + + + {m.label} + {m.desc} + + + {payMethod === m.value && } + + + ))} + + {renderMethodContent()} + + + {/* ========== 汇款凭证 ========== */} + + + 汇款凭证 + 转账截图或回单,最多 {MAX_VOUCHERS} 张 + + + {vouchers.map((v, i) => { + const url = resolveFileUrl(v.url) + return ( + + previewImage(vouchers.map(x => resolveFileUrl(x.url)), url)} + /> + handleRemoveVoucher(i)}> + + + + ) + })} + {vouchers.length < MAX_VOUCHERS && ( + + + {uploading ? '上传中' : '上传凭证'} + + )} + + + + {/* ========== 备注 ========== */} + + 备注(选填) +