From 03434feb350ea41004cd0cad2629744db26c684f Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Fri, 14 Aug 2026 20:21:50 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A6=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mini-order-bill-api.md | 427 ++++++++++++++++++ src/app.config.ts | 4 +- .../index.config.ts | 0 .../index.less | 129 +++++- src/pages/bill-detail/index.tsx | 205 +++++++++ src/pages/{statement => bill}/index.config.ts | 0 src/pages/bill/index.less | 158 +++++++ src/pages/bill/index.tsx | 157 +++++++ src/pages/order-list/index.less | 43 +- src/pages/order-list/index.tsx | 114 +++-- src/pages/profile/index.less | 64 +++ src/pages/profile/index.tsx | 60 ++- src/pages/statement-detail/index.tsx | 178 -------- src/pages/statement/index.less | 101 ----- src/pages/statement/index.tsx | 116 ----- src/services/bill.ts | 117 +++++ src/services/order.ts | 34 +- src/services/statement.ts | 23 - src/types/order.ts | 128 ++++-- src/types/statement.ts | 64 --- src/utils/request.ts | 20 + 21 files changed, 1513 insertions(+), 629 deletions(-) create mode 100644 mini-order-bill-api.md rename src/pages/{statement-detail => bill-detail}/index.config.ts (100%) rename src/pages/{statement-detail => bill-detail}/index.less (55%) create mode 100644 src/pages/bill-detail/index.tsx rename src/pages/{statement => bill}/index.config.ts (100%) create mode 100644 src/pages/bill/index.less create mode 100644 src/pages/bill/index.tsx delete mode 100644 src/pages/statement-detail/index.tsx delete mode 100644 src/pages/statement/index.less delete mode 100644 src/pages/statement/index.tsx create mode 100644 src/services/bill.ts delete mode 100644 src/services/statement.ts delete mode 100644 src/types/statement.ts diff --git a/mini-order-bill-api.md b/mini-order-bill-api.md new file mode 100644 index 0000000..55ac4db --- /dev/null +++ b/mini-order-bill-api.md @@ -0,0 +1,427 @@ +# 小程序订单与账单接口文档 + +> 适用端:微信小程序(h5 仓库,Taro) +> 覆盖接口:订单列表 / 订单详情、账单列表 / 账单详情 +> 更新日期:2026-08-14 + +## 通用约定 + +| 项 | 说明 | +|---|---| +| 根地址 | `BASE_URL`(见 `src/utils/request.ts`,如 `http://localhost:8000/index.php`) | +| 认证 | 请求头 `Authorization: Bearer {token}`,token 来自登录接口,本地存储 key `auth_token` | +| 响应包络 | `{ success: boolean, data: T, msg?: string, showType?: number }` | +| 分页结构 | `{ data: T[], total: number, pageSize: number, current: number }`(账单列表在此结构上附加 `summary`) | +| 失败处理 | `success=false` 时 `msg` 为中文错误信息(含参数校验失败);HTTP 401 表示登录过期,需重新登录 | +| 金额字段 | 一律为**字符串**(decimal 序列化,如 `"140.00"`),直接展示即可;参与计算需自行转 number | +| 门店隔离 | 两个列表均强制按当前用户绑定门店过滤,无需也不能传门店参数 | + +## 状态枚举 + +**订单状态(status)** + +| 值 | 名称 | 说明 | +|---|---|---| +| 0 | 待接单 | 下单成功待后台接单,**仅此状态可取消** | +| 1 | 已接单 | 后台已接单,等待归集生成采购单 | +| 2 | 采购中 | 已归集进采购单 | +| 3 | 配送中 | | +| 4 | 已完成 | | +| 9 | 已取消 | | + +**账单支付进度(pay_state)**——由后端推导,覆盖原始支付状态(status)的展示口径 + +| 值 | 名称 | 推导条件 | 说明 | +|---|---|---|---| +| 0 | 待支付 | status=0 且 payment_id=0 | `can_pay=true`,可发起合并付款 | +| 1 | 审核中 | status=0 且 payment_id>0 | 已提交合并付款凭证,待后台审核;**审核拒绝后自动回到待支付** | +| 2 | 已支付 | status=1 | 线上审核通过或后台线下收款登记 | + +--- + +## GET /mini/order + +门店历史订单列表,按订货日期倒序(同日期按 ID 倒序)分页。行数据附带**商品预览**(前 3 条明细),完整明细请调订单详情。 + +- **权限**:需登录且已绑定正常门店 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| status | number | 否 | 订单状态(见枚举表),不传=全部 | +| start_date | string | 否 | 订货日期起,`Y-m-d`;可配合 `/mini/order/summary` 返回的 `period_label` 下钻 | +| end_date | string | 否 | 订货日期止,`Y-m-d`,不得早于 start_date | +| page | number | 否 | 页码,默认 1 | +| pageSize | number | 否 | 每页数量,默认 10,**最大 50** | + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "data": [ + { + "id": 1024, + "order_no": "SO202608140001", + "order_date": "2026-08-14", + "status": 0, + "status_name": "待接单", + "can_cancel": true, + "total_quantity": 8, + "total_weight": "0.000", + "total_amount": "44.00", + "remark": "下午送到", + "purchase_id": 0, + "bill_id": 0, + "created_at": "2026-08-14 09:30:00", + "item_count": 4, + "items": [ + { "product_name": "白菜", "quantity": 2, "unit": "件" }, + { "product_name": "土豆", "quantity": 2, "unit": "件" }, + { "product_name": "番茄", "quantity": 2, "unit": "件" } + ] + } + ], + "total": 36, + "pageSize": 10, + "current": 1 + } +} +``` + +### 字段说明(列表行) + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 订单 ID | +| order_no | string | 订单编号(SO 前缀) | +| order_date | string | 订货日期 `Y-m-d` | +| status | number | 订单状态(见枚举表) | +| **status_name** | string | 状态中文名,**直接展示,前端不要再硬编码映射** | +| **can_cancel** | boolean | 是否可取消(=待接单);取消按钮据此渲染 | +| total_quantity | number | 订货总数量(件/包数) | +| total_weight | string | 总称重(斤,手动录入参考值,常为 0) | +| total_amount | string | 订单金额 | +| remark | string | 订单备注,可能为空字符串 | +| purchase_id | number | 关联采购单 ID,0=未归集 | +| bill_id | number | 关联账单 ID,0=未出账(>0 可跳账单详情) | +| created_at | string | 下单时间 `Y-m-d H:i:s` | +| **item_count** | number | 明细种数(如「共 4 种」) | +| **items** | array | 商品预览,**仅前 3 条**(`product_name` / `quantity` / `unit`),完整明细走详情接口 | + +--- + +## GET /mini/order/{id} + +订单详情(校验本店归属),返回订单全部字段 + 完整明细(`items` 数组,含下单时商品快照)。 + +- **权限**:需登录且已绑定正常门店;非本店订单返回 `success=false`「订单不存在」 + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "id": 1024, + "order_no": "SO202608140001", + "store_id": 3, + "order_date": "2026-08-14", + "total_quantity": 8, + "total_weight": "0.000", + "total_amount": "44.00", + "status": 0, + "remark": "下午送到", + "purchase_id": 0, + "bill_id": 0, + "created_at": "2026-08-14 09:30:00", + "items": [ + { + "id": 501, + "order_id": 1024, + "product_id": 11, + "product_name": "白菜", + "product_spec": "约30斤/件", + "unit": "件", + "price": "5.50", + "quantity": 2, + "weight": "0.000", + "amount": "11.00", + "remark": "" + } + ] + } +} +``` + +### 字段说明 + +订单本体字段同列表行(无 `status_name` / `can_cancel` / 预览字段,状态展示沿用列表行数据或自行映射);`items` 为完整明细: + +| 字段 | 类型 | 说明 | +|---|---|---| +| product_name / product_spec / unit | string | 下单时商品快照(品名/规格包规/单位),商品档案后续变更不影响 | +| price | string | 下单时门店等级实际价快照 | +| quantity | number | 订货量(件/包数) | +| weight | string | 称重(斤,参考值) | +| amount | string | 明细金额 = price × quantity | + +--- + +## GET /mini/bill + +门店账单列表(采购单完成后由后台按门店生成,门店端只读),按账单日期倒序分页。响应在分页结构上**附加 `summary` 待支付汇总**(仅按门店口径统计,不受筛选参数影响),供页面头部与合并付款入口展示。 + +- **权限**:需登录且已绑定正常门店 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| status | number | 否 | 原始支付状态:0 未支付(含审核中)/ 1 已支付,不传=全部 | +| payable | number | 否 | 传 `1` = 仅可发起付款的账单(未支付且未在审核中),**合并付款选择页专用** | +| start_date | string | 否 | 账单日期起,`Y-m-d` | +| end_date | string | 否 | 账单日期止,`Y-m-d`,不得早于 start_date | +| page | number | 否 | 页码,默认 1 | +| pageSize | number | 否 | 每页数量,默认 10,**最大 50** | + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "data": [ + { + "id": 88, + "bill_no": "ZD202608140001", + "bill_date": "2026-08-14", + "purchase_id": 31, + "purchase": { "id": 31, "purchase_no": "PO202608130001", "purchase_date": "2026-08-13" }, + "product_amount": "100.00", + "delivery_fee": "10.00", + "box_num": 2, + "tray_num": 1, + "box_price": "5.00", + "tray_price": "20.00", + "added_amount": "30.00", + "total_amount": "140.00", + "status": 0, + "status_name": "未支付", + "pay_state": 0, + "pay_state_name": "待支付", + "can_pay": true, + "payment_id": 0, + "settlement_date": "2026-08-17", + "paid_at": null, + "pay_remark": "", + "remark": "", + "created_at": "2026-08-14 03:17:01" + } + ], + "total": 12, + "pageSize": 10, + "current": 1, + "summary": { "unpaid_count": 3, "unpaid_amount": "420.50" } + } +} +``` + +### 字段说明(列表行) + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | number | 账单 ID | +| bill_no | string | 账单编号(ZD 前缀) | +| bill_date | string | 账单日期(出账日)`Y-m-d` | +| purchase_id | number | 关联采购单 ID | +| purchase | object \| null | 关联采购单 `{ id, purchase_no, purchase_date }` | +| product_amount | string | 商品金额(订单汇总快照,不可修改) | +| delivery_fee | string | 配送费 | +| box_num / tray_num | number | 周转筐 / 周转托盘数量 | +| box_price / tray_price | string | 筐 / 托盘单价(出账时快照) | +| added_amount | string | 附加金额 = box_num×box_price + tray_num×tray_price | +| total_amount | string | **账单总金额 = 商品金额 + 配送费 + 附加金额** | +| status | number | 原始支付状态:0 未支付 / 1 已支付(展示用 pay_state 系列字段) | +| status_name | string | 支付状态中文名 | +| **pay_state** | number | 支付进度:0 待支付 / 1 审核中 / 2 已支付(见枚举表) | +| **pay_state_name** | string | 支付进度中文名,**列表状态标签直接用它** | +| **can_pay** | boolean | 是否可勾选发起合并付款(=待支付) | +| payment_id | number | 关联支付记录 ID,0=未发起付款;审核中时可跳支付记录详情查看进度 | +| **settlement_date** | string | 应结算日期 = 账单日期 + 门店回款周期天数 | +| paid_at | string \| null | 付款时间(已支付时非空) | +| pay_remark | string | 付款备注(如「微信支付(支付单号 ZF…)」、线下收款说明) | +| remark | string | 账单备注 | +| created_at | string | 出账时间 | + +### summary 字段 + +| 字段 | 类型 | 说明 | +|---|---|---| +| unpaid_count | number | 待支付账单笔数(**含审核中**) | +| unpaid_amount | string | 待支付金额合计(含审核中) | + +--- + +## GET /mini/bill/{id} + +账单详情(校验本店归属):账单信息(字段同列表行)+ 合并后的商品明细 + 关联订单。 + +- **权限**:需登录且已绑定正常门店;非本店账单返回 `success=false`「账单不存在」 + +### 响应示例 + +```json +{ + "success": true, + "msg": "ok", + "data": { + "bill": { "id": 88, "bill_no": "ZD202608140001", "...": "(字段同列表行)" }, + "items": [ + { + "product_id": 11, + "product_name": "白菜", + "product_spec": "约30斤/件", + "unit": "件", + "price": "5.50", + "quantity": 20, + "weight": "0.000", + "amount": "110.00" + } + ], + "orders": [ + { + "id": 1024, + "order_no": "SO202608130001", + "order_date": "2026-08-13", + "total_quantity": 8, + "total_weight": "0.000", + "total_amount": "44.00", + "status": 4 + } + ] + } +} +``` + +### 字段说明 + +- **bill**:与列表行完全一致(含 `pay_state_name` / `can_pay` / `settlement_date` 等派生字段) +- **items**:按商品聚合的合并明细(跨本账单全部订单),`price` 为加权平均口径(Σ金额÷Σ数量,保证 单价×数量=金额);`quantity` 为合计数量(number)、`amount` 为合计金额 +- **orders**:本账单关联的门店订单(单价明细可继续下钻 `GET /mini/order/{id}`),`status` 为订单状态枚举 + +--- + +## 前端接入示例 + +`src/types/order.ts` 关键类型(列表行替换为接口返回,**删除本地 `ORDER_STATUS_MAP` 硬编码**): + +```ts +/** 订单列表行(状态名/可取消/商品预览均由后端给出) */ +export interface OrderListItem { + id: number + order_no: string + order_date: string + status: number + status_name: string + can_cancel: boolean + total_quantity: number + total_weight: string + total_amount: string + remark: string + purchase_id: number + bill_id: number + created_at: string + item_count: number + items: Array<{ product_name: string; quantity: number; unit: string }> +} +``` + +`src/services/bill.ts`(**替代旧 `statement.ts`——旧 `/mini/statement` 已下线**): + +```ts +import { get } from '@/utils/request' +import type { PaginatedData } from '@/types/api' + +/** 账单(列表行与详情的 bill 字段一致) */ +export interface Bill { + id: number + bill_no: string + bill_date: string + purchase_id: number + purchase: { id: number; purchase_no: string; purchase_date: string } | null + product_amount: string + delivery_fee: string + box_num: number + tray_num: number + box_price: string + tray_price: string + added_amount: string + total_amount: string + status: 0 | 1 + status_name: string + /** 支付进度:0 待支付 / 1 审核中 / 2 已支付 */ + pay_state: 0 | 1 | 2 + pay_state_name: string + can_pay: boolean + payment_id: number + settlement_date: string + paid_at: string | null + pay_remark: string + remark: string + created_at: string +} + +export interface BillSummary { + unpaid_count: number + unpaid_amount: string +} + +export interface BillDetail { + bill: Bill + items: Array<{ + product_id: number; product_name: string; product_spec: string + unit: string; price: string; quantity: number; weight: string; amount: string + }> + orders: Array<{ + id: number; order_no: string; order_date: string + total_quantity: number; total_weight: string; total_amount: string; status: number + }> +} + +/** 账单列表:GET /mini/bill(payable=1 为合并付款选择页口径) */ +export function getBillListApi( + params: { status?: number; payable?: 1; start_date?: string; end_date?: string; page?: number; pageSize?: number } = {}, +) { + return get & { 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/src/app.config.ts b/src/app.config.ts index b5e1730..c3ce53a 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -6,8 +6,8 @@ export default defineAppConfig({ 'pages/message/index', 'pages/profile/index', 'pages/order-list/index', - 'pages/statement/index', - 'pages/statement-detail/index', + 'pages/bill/index', + 'pages/bill-detail/index', 'pages/settings/index', 'pages/login/index', 'pages/register/index', diff --git a/src/pages/statement-detail/index.config.ts b/src/pages/bill-detail/index.config.ts similarity index 100% rename from src/pages/statement-detail/index.config.ts rename to src/pages/bill-detail/index.config.ts diff --git a/src/pages/statement-detail/index.less b/src/pages/bill-detail/index.less similarity index 55% rename from src/pages/statement-detail/index.less rename to src/pages/bill-detail/index.less index 0633f50..3c50cd5 100644 --- a/src/pages/statement-detail/index.less +++ b/src/pages/bill-detail/index.less @@ -1,7 +1,7 @@ .bill-detail { min-height: 100vh; background: #f7f8fa; - padding: 20rpx 24rpx 120rpx; + padding: 20rpx 24rpx 60rpx; box-sizing: border-box; .bill-card { @@ -26,13 +26,20 @@ &__status { font-size: 24rpx; - &--unpaid { - color: #ee0a24; - } + // 0 待支付 / 1 审核中 / 2 已支付 + &--0 { color: #ee0a24; } + &--1 { color: #ff976a; } + &--2 { color: #07c160; } + } - &--paid { - color: #07c160; - } + &__tip { + display: block; + margin: -4rpx 0 16rpx; + padding: 12rpx 16rpx; + background: #fffbe8; + border-radius: 8rpx; + font-size: 22rpx; + color: #ed6a0c; } &__row { @@ -51,15 +58,15 @@ &__label { font-size: 26rpx; color: #969799; + flex: 1; + min-width: 0; + padding-right: 20rpx; } &__value { font-size: 26rpx; color: #323233; - - &--credit { - color: #07c160; - } + flex-shrink: 0; } &__total { @@ -118,6 +125,7 @@ &__side { text-align: right; margin-left: 16rpx; + flex-shrink: 0; } &__qty { @@ -145,35 +153,112 @@ border-bottom: none; } + &__main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + } + &__no { font-size: 26rpx; color: #323233; - flex: 1; - min-width: 0; } &__date { + font-size: 22rpx; + color: #969799; + margin-top: 4rpx; + } + + &__status { font-size: 24rpx; color: #969799; margin: 0 16rpx; + flex-shrink: 0; } &__amount { font-size: 26rpx; color: #323233; + flex-shrink: 0; } } - .bill-pay { - position: fixed; - left: 0; - right: 0; - bottom: 0; - padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); - background: #fff; + // ===== 订单明细弹层 ===== + .order-popup { + padding: 32rpx 32rpx 24rpx; - &__btn { - border: none; + &__title { + font-size: 32rpx; + font-weight: 600; + display: block; + } + + &__meta { + margin-top: 12rpx; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 24rpx; + color: #969799; + } + + &__status { + color: #ee0a24; + } + + &__list { + max-height: 480rpx; + margin-top: 20rpx; + } + + &__item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20rpx 0; + border-bottom: 1rpx solid #f2f3f5; + + &-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + } + + &-name { + font-size: 28rpx; + color: #323233; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-spec { + margin-top: 6rpx; + font-size: 22rpx; + color: #969799; + } + + &-amount { + font-size: 28rpx; + color: #323233; + font-weight: 500; + margin-left: 20rpx; + } + } + + &__footer { + margin-top: 24rpx; + display: flex; + justify-content: flex-end; + } + + &__total { + font-size: 30rpx; + color: #ee0a24; + font-weight: 600; } } } diff --git a/src/pages/bill-detail/index.tsx b/src/pages/bill-detail/index.tsx new file mode 100644 index 0000000..567823b --- /dev/null +++ b/src/pages/bill-detail/index.tsx @@ -0,0 +1,205 @@ +import { useCallback, useEffect, useState } from 'react' +import { useRouter } from '@tarojs/taro' +import { View, Text, ScrollView } from '@tarojs/components' +import { Empty, Popup } from '@antmjs/vantui' +import { getBillDetailApi } from '@/services/bill' +import { getOrderDetailApi } from '@/services/order' +import { ORDER_STATUS_TEXT } from '@/types/order' +import type { OrderDetail } from '@/types/order' +import type { BillDetail } from '@/services/bill' +import './index.less' + +/** + * 账单详情 + * 账单信息 + 金额构成(商品/配送费/筐托盘附加)+ 合并商品明细 + 关联订单(可下钻订单明细) + */ +export default function BillDetailPage() { + const router = useRouter() + const id = Number(router.params.id ?? 0) + + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(false) + + /** 关联订单明细弹层 */ + const [showOrder, setShowOrder] = useState(false) + const [orderDetail, setOrderDetail] = useState(null) + + useEffect(() => { + if (!id) return + setLoading(true) + getBillDetailApi(id) + .then(res => setDetail(res.data)) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [id]) + + /** 下钻关联订单明细 */ + const handleOrderTap = useCallback(async (orderId: number) => { + try { + const res = await getOrderDetailApi(orderId) + setOrderDetail(res.data) + setShowOrder(true) + } catch { + // 错误已由 request 层 toast + } + }, []) + + if (loading && !detail) { + return + } + if (!detail) { + return + } + + const { bill, items, orders } = detail + + return ( + + {/* ===== 账单信息 ===== */} + + + {bill.bill_no} + + {bill.pay_state_name} + + + {bill.pay_state === 1 && ( + 已提交付款凭证,待后台审核;审核拒绝后将回到待支付 + )} + + 账单日期 + {bill.bill_date} + + {bill.purchase && ( + + 关联采购单 + + {bill.purchase.purchase_no}({bill.purchase.purchase_date}) + + + )} + + 应结算日期 + {bill.settlement_date} + + {bill.pay_state === 2 && bill.paid_at && ( + + 付款时间 + {bill.paid_at} + + )} + {bill.pay_remark && ( + + 付款备注 + {bill.pay_remark} + + )} + {bill.remark && ( + + 账单备注 + {bill.remark} + + )} + + + {/* ===== 金额构成 ===== */} + + 金额构成 + + 商品金额 + ¥{bill.product_amount} + + + 配送费 + ¥{bill.delivery_fee} + + + + 附加金额(筐 {bill.box_num}×¥{bill.box_price},托盘 {bill.tray_num}×¥{bill.tray_price}) + + ¥{bill.added_amount} + + + 账单总额 + ¥{bill.total_amount} + + + + {/* ===== 商品明细(跨订单按商品合并) ===== */} + + 商品明细({items?.length ?? 0}) + 同一商品跨订单合并,单价为加权平均价 + {(items ?? []).map(item => ( + + + {item.product_name} + + {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} + + + + ×{item.quantity} + ¥{item.amount} + + + ))} + {(items ?? []).length === 0 && } + + + {/* ===== 关联订单 ===== */} + + 关联订单({orders?.length ?? 0}) + {(orders ?? []).map(order => ( + handleOrderTap(order.id)}> + + {order.order_no} + {order.order_date} + + {ORDER_STATUS_TEXT[order.status] || ''} + ¥{order.total_amount} + + ))} + {(orders ?? []).length === 0 && } + + + {/* ===== 订单明细弹层 ===== */} + setShowOrder(false)} + > + {orderDetail && ( + + {orderDetail.order_no} + + {orderDetail.order_date} + + {ORDER_STATUS_TEXT[orderDetail.status] || ''} + + + + {(orderDetail.items ?? []).map(item => ( + + + {item.product_name} + + {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity} + + + ¥{item.amount} + + ))} + + + 合计 ¥{orderDetail.total_amount} + + + )} + + + ) +} diff --git a/src/pages/statement/index.config.ts b/src/pages/bill/index.config.ts similarity index 100% rename from src/pages/statement/index.config.ts rename to src/pages/bill/index.config.ts diff --git a/src/pages/bill/index.less b/src/pages/bill/index.less new file mode 100644 index 0000000..73b1a48 --- /dev/null +++ b/src/pages/bill/index.less @@ -0,0 +1,158 @@ +.bill-page { + min-height: 100vh; + background: #f7f8fa; + padding: 20rpx 24rpx 60rpx; + box-sizing: border-box; + + // ===== 待支付汇总 ===== + .bill-summary { + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(135deg, #ee0a24, #ff4d4f); + border-radius: 16rpx; + padding: 28rpx; + margin-bottom: 20rpx; + color: #fff; + + &__info { + display: flex; + flex-direction: column; + } + + &__label { + font-size: 24rpx; + opacity: 0.85; + } + + &__count { + margin-top: 8rpx; + font-size: 28rpx; + font-weight: 600; + } + + &__amount { + font-size: 40rpx; + font-weight: 600; + } + } + + // ===== 状态筛选 ===== + .status-scroll { + white-space: nowrap; + margin-bottom: 20rpx; + } + + .status-chip { + display: inline-flex; + padding: 12rpx 28rpx; + margin-right: 16rpx; + border-radius: 999rpx; + background: #fff; + font-size: 26rpx; + color: #646566; + + &.active { + background: #ee0a24; + color: #fff; + } + } + + .bill-empty { + padding-top: 120rpx; + + &__btn { + margin-top: 24rpx; + padding: 14rpx 60rpx; + background: #ee0a24; + color: #fff; + font-size: 28rpx; + border-radius: 999rpx; + } + } + + .bill-loading { + padding: 30rpx 0; + text-align: center; + font-size: 24rpx; + color: #c8c9cc; + } + + // ===== 账单单项 ===== + .bill-item { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 16rpx; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__no { + font-size: 28rpx; + color: #323233; + font-weight: 500; + } + + &__status { + font-size: 24rpx; + + // 0 待支付 / 1 审核中 / 2 已支付 + &--0 { color: #ee0a24; } + &--1 { color: #ff976a; } + &--2 { color: #07c160; } + } + + &__body { + margin-top: 12rpx; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__meta { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + } + + &__date { + font-size: 24rpx; + color: #969799; + } + + &__purchase { + margin-top: 6rpx; + font-size: 22rpx; + color: #c8c9cc; + } + + &__amount { + font-size: 32rpx; + color: #323233; + font-weight: 600; + margin-left: 20rpx; + } + + &__footer { + margin-top: 12rpx; + display: flex; + align-items: center; + justify-content: space-between; + } + + &__settle { + font-size: 22rpx; + color: #969799; + } + + &__paid { + font-size: 22rpx; + color: #07c160; + } + } +} diff --git a/src/pages/bill/index.tsx b/src/pages/bill/index.tsx new file mode 100644 index 0000000..efbac89 --- /dev/null +++ b/src/pages/bill/index.tsx @@ -0,0 +1,157 @@ +import { useCallback, useRef, useState } from 'react' +import Taro, { useDidShow, useReachBottom } from '@tarojs/taro' +import { View, Text, ScrollView } from '@tarojs/components' +import { Empty } from '@antmjs/vantui' +import useAuthStore from '@/stores/auth/useAuthStore' +import { getBillListApi } from '@/services/bill' +import type { Bill, BillSummary } from '@/services/bill' +import './index.less' + +const PAGE_SIZE = 10 + +/** 状态筛选(接口 status 为原始支付状态:0 未支付含审核中 / 1 已支付) */ +const STATUS_FILTERS: Array<{ value: 0 | 1 | undefined; label: string }> = [ + { value: undefined, label: '全部' }, + { value: 0, label: '未支付' }, + { value: 1, label: '已支付' }, +] + +/** + * 账单列表页 + * 采购单完成后由后台按门店生成(只读);头部 summary 为门店口径待支付汇总(含审核中,不受筛选影响) + */ +export default function BillListPage() { + const token = useAuthStore(s => s.token) + + const [status, setStatus] = useState<0 | 1 | undefined>(undefined) + const [bills, setBills] = useState([]) + const [summary, setSummary] = useState(null) + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(false) + const [finished, setFinished] = useState(false) + const loadingRef = useRef(false) + + const loggedIn = !!token + + /** 拉取账单列表(summary 每次随响应刷新) */ + const loadList = useCallback( + async (pageNum: number, reset: boolean, statusParam?: 0 | 1) => { + if (!loggedIn || loadingRef.current) return + loadingRef.current = true + setLoading(true) + try { + const res = await getBillListApi({ status: statusParam, page: pageNum, pageSize: PAGE_SIZE }) + const { data, total, summary: sum } = res.data + setBills(prev => (reset ? data : [...prev, ...data])) + setSummary(sum) + setPage(pageNum) + setFinished(pageNum * PAGE_SIZE >= total) + } catch { + // 错误已由 request 层 toast + } finally { + loadingRef.current = false + setLoading(false) + } + }, + [loggedIn], + ) + + useDidShow(() => { + loadList(1, true, status) + }) + + useReachBottom(() => { + if (!finished && !loadingRef.current && loggedIn) { + loadList(page + 1, false, status) + } + }) + + /** 切换状态筛选 */ + const handleStatusTap = useCallback( + (value?: 0 | 1) => { + setStatus(value) + setFinished(false) + loadList(1, true, value) + }, + [loadList], + ) + + const goLogin = useCallback(() => { + Taro.navigateTo({ url: '/pages/login/index' }) + }, []) + + const goDetail = useCallback((id: number) => { + Taro.navigateTo({ url: `/pages/bill-detail/index?id=${id}` }) + }, []) + + return ( + + {/* ========== 待支付汇总(门店口径,含审核中) ========== */} + {loggedIn && summary && summary.unpaid_count > 0 && ( + + + 待支付账单(含审核中) + {summary.unpaid_count} 笔 + + ¥{summary.unpaid_amount} + + )} + + {/* ========== 状态筛选 ========== */} + + {STATUS_FILTERS.map(item => ( + handleStatusTap(item.value)} + > + {item.label} + + ))} + + + {/* ========== 账单列表 ========== */} + {!loggedIn ? ( + + 去登录 + + ) : bills.length === 0 ? ( + loading ? ( + 加载中... + ) : ( + + ) + ) : ( + bills.map(bill => ( + goDetail(bill.id)}> + + {bill.bill_no} + + {bill.pay_state_name} + + + + + 账单日期 {bill.bill_date} + {bill.purchase && ( + 采购单 {bill.purchase.purchase_no} + )} + + ¥{bill.total_amount} + + + 应结算 {bill.settlement_date} + {bill.pay_state === 2 && bill.paid_at && ( + 已于 {bill.paid_at} 支付 + )} + + + )) + )} + + {loggedIn && finished && bills.length > 0 && ( + 没有更多了 + )} + + ) +} diff --git a/src/pages/order-list/index.less b/src/pages/order-list/index.less index a2ed2a4..671a766 100644 --- a/src/pages/order-list/index.less +++ b/src/pages/order-list/index.less @@ -66,7 +66,24 @@ &__status { font-size: 24rpx; - color: #ee0a24; + + // 0 待接单 / 1 已接单 / 2 采购中 / 3 配送中 / 4 已完成 / 9 已取消 + &--0 { color: #ee0a24; } + &--1 { color: #1989fa; } + &--2 { color: #1989fa; } + &--3 { color: #ff976a; } + &--4 { color: #07c160; } + &--9 { color: #969799; } + } + + &__preview { + margin-top: 12rpx; + font-size: 24rpx; + color: #646566; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } &__body { @@ -105,8 +122,23 @@ display: block; } - &__cancel { + &__footer { margin-top: 16rpx; + display: flex; + justify-content: flex-end; + gap: 16rpx; + } + + &__bill { + display: inline-flex; + padding: 8rpx 24rpx; + border: 1rpx solid #1989fa; + color: #1989fa; + border-radius: 999rpx; + font-size: 24rpx; + } + + &__cancel { display: inline-flex; padding: 8rpx 24rpx; border: 1rpx solid #ee0a24; @@ -180,6 +212,13 @@ } } + &__remark { + margin-top: 16rpx; + font-size: 22rpx; + color: #969799; + display: block; + } + &__footer { margin-top: 24rpx; display: flex; diff --git a/src/pages/order-list/index.tsx b/src/pages/order-list/index.tsx index 20ec81b..3f88179 100644 --- a/src/pages/order-list/index.tsx +++ b/src/pages/order-list/index.tsx @@ -4,46 +4,46 @@ import { View, Text, ScrollView } from '@tarojs/components' import { Empty, Popup } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order' -import { ORDER_NAV_ITEMS, ORDER_STATUS_MAP } from '@/types/order' -import type { Order, OrderStatus } from '@/types/order' +import { ORDER_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order' +import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order' import './index.less' const PAGE_SIZE = 10 /** - * 订单列表页(框架) - * 入口:/pages/order-list/index?status=0|1|2|3|9|all - * status 与 ORDER_NAV_ITEMS 映射(业务语言 → 后端枚举),缺省为全部 + * 订单列表页 + * 入口:/pages/order-list/index?status=0|1|2|3|4|9|all + * 状态文案一律使用接口返回的 status_name(详情接口无该字段时沿用列表行) */ export default function OrderListPage() { const router = useRouter() const token = useAuthStore(s => s.token) /** 当前状态筛选(undefined = 全部) */ - const [status, setStatus] = useState(undefined) - const [orders, setOrders] = useState([]) + const [status, setStatus] = useState(undefined) + const [orders, setOrders] = useState([]) const [page, setPage] = useState(1) - const [total, setTotal] = useState(0) const [loading, setLoading] = useState(false) const [finished, setFinished] = useState(false) const loadingRef = useRef(false) - /** 订单详情弹层 */ + /** 订单详情弹层(statusName 沿用列表行,详情接口不返回 status_name) */ const [showDetail, setShowDetail] = useState(false) - const [orderDetail, setOrderDetail] = useState(null) + const [orderDetail, setOrderDetail] = useState(null) + const [detailStatusName, setDetailStatusName] = useState('') const loggedIn = !!token /** 解析路由参数中的 status('all' / 数字 / 缺省 → undefined) */ - const parseStatus = useCallback((raw?: string): number | undefined => { + const parseStatus = useCallback((raw?: string): OrderStatus | undefined => { if (!raw || raw === 'all' || raw === '') return undefined const num = Number(raw) - return Number.isNaN(num) ? undefined : num + return Number.isNaN(num) ? undefined : (num as OrderStatus) }, []) /** 拉取订单列表 */ const loadOrders = useCallback( - async (pageNum: number, reset: boolean, statusParam?: number) => { + async (pageNum: number, reset: boolean, statusParam?: OrderStatus) => { if (!loggedIn || loadingRef.current) return loadingRef.current = true setLoading(true) @@ -53,11 +53,10 @@ export default function OrderListPage() { page: pageNum, pageSize: PAGE_SIZE, }) - const { data, total: totalCount } = res.data + const { data, total } = res.data setOrders(prev => (reset ? data : [...prev, ...data])) - setTotal(totalCount) setPage(pageNum) - setFinished(pageNum * PAGE_SIZE >= totalCount) + setFinished(pageNum * PAGE_SIZE >= total) } catch { // 错误已由 request 层 toast } finally { @@ -82,7 +81,7 @@ export default function OrderListPage() { /** 切换状态筛选 */ const handleStatusTap = useCallback( - (value?: number) => { + (value?: OrderStatus) => { setStatus(value) setFinished(false) loadOrders(1, true, value) @@ -91,19 +90,20 @@ export default function OrderListPage() { ) /** 查看订单详情 */ - const handleOrderTap = useCallback(async (order: Order) => { + const handleOrderTap = useCallback(async (order: OrderListItem) => { try { const res = await getOrderDetailApi(order.id) setOrderDetail(res.data) + setDetailStatusName(order.status_name) setShowDetail(true) } catch { // 错误已由 request 层 toast } }, []) - /** 取消订单(仅待汇总可取消) */ + /** 取消订单(仅待接单可取消,以 can_cancel 为准) */ const handleCancelOrder = useCallback( - (order: Order) => { + (order: OrderListItem) => { Taro.showModal({ title: '取消订单', content: `确定取消订单 ${order.order_no} 吗?`, @@ -123,6 +123,11 @@ export default function OrderListPage() { [loadOrders, status], ) + /** 跳转关联账单详情 */ + const handleBillTap = useCallback((billId: number) => { + Taro.navigateTo({ url: `/pages/bill-detail/index?id=${billId}` }) + }, []) + const goLogin = useCallback(() => { Taro.navigateTo({ url: '/pages/login/index' }) }, []) @@ -131,17 +136,11 @@ export default function OrderListPage() { {/* ========== 状态筛选 ========== */} - handleStatusTap(undefined)} - > - 全部 - - {ORDER_NAV_ITEMS.map(item => ( + {ORDER_STATUS_FILTERS.map(item => ( handleStatusTap(item.status)} + key={item.label} + className={`status-chip ${status === item.value ? 'active' : ''}`} + onClick={() => handleStatusTap(item.value)} > {item.label} @@ -164,25 +163,47 @@ export default function OrderListPage() { handleOrderTap(order)}> {order.order_no} - {ORDER_STATUS_MAP[order.status]} + + {order.status_name} + + {/* 商品预览(仅前 3 条,完整明细见详情) */} + + {order.items.map(i => `${i.product_name}×${i.quantity}`).join('、')} + {order.item_count > 3 ? ` 等${order.item_count}种` : ''} + - {order.order_date} + 订货日期 {order.order_date} 共 {order.total_quantity} 件 ¥{order.total_amount} {order.remark && 备注:{order.remark}} - {order.status === 0 && ( - { - e.stopPropagation() - handleCancelOrder(order) - }} - > - 取消订单 + {(order.can_cancel || order.bill_id > 0) && ( + + {order.bill_id > 0 && ( + { + e.stopPropagation() + handleBillTap(order.bill_id) + }} + > + 查看账单 + + )} + {order.can_cancel && ( + { + e.stopPropagation() + handleCancelOrder(order) + }} + > + 取消订单 + + )} )} @@ -207,8 +228,10 @@ export default function OrderListPage() { {orderDetail.order_no} - {orderDetail.order_date} - {ORDER_STATUS_MAP[orderDetail.status]} + {orderDetail.order_date} 下单 {orderDetail.created_at} + + {detailStatusName || ORDER_STATUS_TEXT[orderDetail.status] || ''} + {(orderDetail.items ?? []).map(item => ( @@ -216,13 +239,16 @@ export default function OrderListPage() { {item.product_name} - {item.product_spec} ¥{item.price} × {item.quantity} + {item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity} ¥{item.amount} ))} + {orderDetail.remark && ( + 备注:{orderDetail.remark} + )} 合计 ¥{orderDetail.total_amount} diff --git a/src/pages/profile/index.less b/src/pages/profile/index.less index 32ed7d8..18ddc89 100644 --- a/src/pages/profile/index.less +++ b/src/pages/profile/index.less @@ -144,6 +144,70 @@ } } + // ===== 账单入口 ===== + .bill-entry { + display: flex; + align-items: center; + + &__icon { + width: 84rpx; + height: 84rpx; + border-radius: 24rpx; + background: #fff0f0; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + &__info { + flex: 1; + min-width: 0; + margin-left: 20rpx; + display: flex; + flex-direction: column; + } + + &__amount { + font-size: 36rpx; + font-weight: 600; + color: #ee0a24; + } + + &__title { + font-size: 28rpx; + color: #323233; + } + + &__desc { + margin-top: 6rpx; + font-size: 22rpx; + color: #969799; + } + + &__badge { + min-width: 36rpx; + height: 36rpx; + padding: 0 10rpx; + border-radius: 999rpx; + background: #ee0a24; + color: #fff; + font-size: 22rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + &__arrow { + font-size: 32rpx; + color: #c8c9cc; + line-height: 1; + margin-left: 12rpx; + flex-shrink: 0; + } + } + // ===== 功能菜单 ===== .menu-cell { display: flex; diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 607af04..336cf52 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,28 +1,18 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { View, Text, Image } from '@tarojs/components' import { Icon } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' import { getUserInfoApi } from '@/services/auth' +import { getBillListApi } from '@/services/bill' +import type { BillSummary } from '@/services/bill' import { ORDER_NAV_ITEMS } from '@/types/order' import { resolveAvatarUrl } from '@/utils/format' import type { UserType } from '@/types/user' import './index.less' -/** 菜单项(后续单独页面开发时在此追加) */ +/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */ const MENU_ITEMS = [ - { - key: 'orders', - label: '所有订单', - icon: 'orders-o', - onClick: () => Taro.navigateTo({ url: '/pages/order-list/index?status=all' }), - }, - { - key: 'statement', - label: '账单', - icon: 'balance-list-o', - onClick: () => Taro.navigateTo({ url: '/pages/statement/index' }), - }, { key: 'message', label: '消息中心', @@ -43,6 +33,9 @@ export default function ProfilePage() { const setUser = useAuthStore(s => s.setUser) const logout = useAuthStore(s => s.logout) + /** 账单待支付汇总(门店口径,含审核中) */ + const [billSummary, setBillSummary] = useState(null) + const loggedIn = !!token && !!user /** 功能菜单(门店账号追加「门店信息」入口) */ @@ -65,6 +58,10 @@ export default function ProfilePage() { getUserInfoApi() .then(res => setUser(res.data)) .catch(() => {}) + // 刷新账单待支付汇总(pageSize=1 仅取 summary) + getBillListApi({ page: 1, pageSize: 1 }) + .then(res => setBillSummary(res.data.summary)) + .catch(() => {}) }) /** 身份标签 */ @@ -79,6 +76,11 @@ export default function ProfilePage() { Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` }) }, []) + /** 账单入口 → 账单列表页 */ + const goBill = useCallback(() => { + Taro.navigateTo({ url: '/pages/bill/index' }) + }, []) + /** 退出登录 */ const handleLogout = useCallback(() => { Taro.showModal({ @@ -169,6 +171,36 @@ export default function ProfilePage() { + {/* ========== 我的账单 ========== */} + + + 我的账单 + 全部 › + + + + + + {loggedIn && billSummary && billSummary.unpaid_count > 0 ? ( + <> + + ¥{billSummary.unpaid_amount} + 待支付 {billSummary.unpaid_count} 笔(含审核中) + + {billSummary.unpaid_count} + + ) : ( + + + {loggedIn ? '暂无待支付账单' : '登录后查看账单'} + + 账单由后台按采购单自动生成 + + )} + + + + {/* ========== 功能菜单 ========== */} {menuItems.map(item => ( diff --git a/src/pages/statement-detail/index.tsx b/src/pages/statement-detail/index.tsx deleted file mode 100644 index 265d277..0000000 --- a/src/pages/statement-detail/index.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import Taro, { useRouter } from '@tarojs/taro' -import { View, Text } from '@tarojs/components' -import { Button, Empty } from '@antmjs/vantui' -import { getStatementDetailApi, payStatementApi } from '@/services/statement' -import { STATEMENT_STATUS_MAP } from '@/types/statement' -import type { StatementDetail } from '@/types/statement' -import './index.less' - -/** - * 账单详情 - * 商品汇总(金额直接合并,单价明细见订单记录)+ 回筐计费 + 关联订单 + 付款确认 - */ -export default function StatementDetailPage() { - const router = useRouter() - const id = Number(router.params.id ?? 0) - - const [detail, setDetail] = useState(null) - const [loading, setLoading] = useState(false) - const [paying, setPaying] = useState(false) - - const loadDetail = useCallback(async () => { - if (!id) return - setLoading(true) - try { - const res = await getStatementDetailApi(id) - setDetail(res.data) - } catch { - // 错误已由 request 层 toast - } finally { - setLoading(false) - } - }, [id]) - - useEffect(() => { - loadDetail() - }, [loadDetail]) - - /** 付款确认 */ - const handlePay = useCallback(async () => { - if (!detail || paying) return - const confirm = await Taro.showModal({ - title: '付款确认', - content: `确认已支付账单 ${detail.statement_no} 共 ¥${detail.total_amount}?`, - confirmText: '确认付款', - }) - if (!confirm.confirm) return - setPaying(true) - try { - await payStatementApi(detail.id) - Taro.showToast({ title: '付款确认成功', icon: 'success' }) - loadDetail() - } catch { - // 错误(重复付款等)已由 request 层 toast - } finally { - setPaying(false) - } - }, [detail, paying, loadDetail]) - - if (loading && !detail) { - return - } - if (!detail) { - return - } - - const crateNegative = Number(detail.crate_amount) < 0 - - return ( - - {/* ===== 账单信息 ===== */} - - - {detail.statement_no} - - {STATEMENT_STATUS_MAP[detail.status]} - - - - 账单周期 - {detail.period_start} ~ {detail.period_end} - - - 应结算日期 - {detail.settlement_date ?? '-'} - - {detail.status === 1 && detail.settled_at && ( - - 付款时间 - {detail.settled_at} - - )} - - 商品金额 - ¥{detail.goods_amount} - - - 回筐金额 - - {crateNegative ? `-¥${Math.abs(Number(detail.crate_amount)).toFixed(2)}` : `¥${detail.crate_amount}`} - {crateNegative ? '(抵扣)' : ''} - - - - 账单总额 - ¥{detail.total_amount} - - - - {/* ===== 商品汇总 ===== */} - - 商品汇总({detail.goods?.length ?? 0}) - 同一商品多次订单金额直接合并;单价明细请查看订单记录 - {(detail.goods ?? []).map((item, index) => ( - - - {item.product_name} - {item.product_spec || ''} - - - {item.quantity} {item.unit} - ¥{item.amount} - - - ))} - {(detail.goods ?? []).length === 0 && } - - - {/* ===== 回筐计费 ===== */} - - 回筐计费 - - 周转框 - {detail.box_num} 个{detail.box_num < 0 ? '(抵扣)' : ''} - - - 托盘 - {detail.tray_num} 个{detail.tray_num < 0 ? '(抵扣)' : ''} - - - - {/* ===== 关联订单 ===== */} - - 关联订单({detail.orders?.length ?? 0}) - {(detail.orders ?? []).map(order => ( - Taro.navigateTo({ url: `/pages/order-list/index` })} - > - {order.order_no} - {order.order_date} - ¥{order.total_amount} - - ))} - {(detail.orders ?? []).length === 0 && } - - - {/* ===== 付款按钮 ===== */} - {detail.status === 0 && ( - - - - )} - - ) -} diff --git a/src/pages/statement/index.less b/src/pages/statement/index.less deleted file mode 100644 index c401c66..0000000 --- a/src/pages/statement/index.less +++ /dev/null @@ -1,101 +0,0 @@ -.statement-page { - min-height: 100vh; - background: #f7f8fa; - padding: 20rpx 24rpx 60rpx; - box-sizing: border-box; - - // ===== 头部 ===== - .statement-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8rpx 8rpx 24rpx; - - &__title { - font-size: 36rpx; - font-weight: 600; - } - - &__tip { - font-size: 24rpx; - color: #969799; - } - } - - .statement-empty { - padding-top: 120rpx; - - &__btn { - margin-top: 24rpx; - padding: 14rpx 60rpx; - background: #ee0a24; - color: #fff; - font-size: 28rpx; - border-radius: 999rpx; - } - } - - .statement-loading { - padding: 30rpx 0; - text-align: center; - font-size: 24rpx; - color: #c8c9cc; - } - - // ===== 账单单项 ===== - .statement-item { - background: #fff; - border-radius: 16rpx; - padding: 24rpx; - margin-bottom: 16rpx; - - &__header { - display: flex; - align-items: center; - justify-content: space-between; - } - - &__no { - font-size: 28rpx; - color: #323233; - font-weight: 500; - } - - &__status { - font-size: 24rpx; - - &--unpaid { - color: #ee0a24; - } - - &--paid { - color: #07c160; - } - } - - &__body { - margin-top: 12rpx; - display: flex; - align-items: center; - justify-content: space-between; - } - - &__period { - font-size: 24rpx; - color: #969799; - } - - &__amount { - font-size: 28rpx; - color: #323233; - font-weight: 600; - } - - &__settle { - margin-top: 8rpx; - font-size: 22rpx; - color: #969799; - display: block; - } - } -} diff --git a/src/pages/statement/index.tsx b/src/pages/statement/index.tsx deleted file mode 100644 index 6f45eec..0000000 --- a/src/pages/statement/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useCallback, useRef, useState } from 'react' -import Taro, { useDidShow, useReachBottom } from '@tarojs/taro' -import { View, Text } from '@tarojs/components' -import { Empty } from '@antmjs/vantui' -import useAuthStore from '@/stores/auth/useAuthStore' -import { getStatementListApi } from '@/services/statement' -import { STATEMENT_STATUS_MAP } from '@/types/statement' -import type { Statement } from '@/types/statement' -import './index.less' - -const PAGE_SIZE = 10 - -/** - * 账单页 - * 账单按门店回款周期自动生成:列表 + 查看详情 + 付款 - */ -export default function StatementPage() { - const token = useAuthStore(s => s.token) - - const [statements, setStatements] = useState([]) - const [page, setPage] = useState(1) - const [loading, setLoading] = useState(false) - const [finished, setFinished] = useState(false) - const loadingRef = useRef(false) - - const loggedIn = !!token - - /** 拉取账单列表 */ - const loadList = useCallback( - async (pageNum: number, reset: boolean) => { - if (!loggedIn || loadingRef.current) return - loadingRef.current = true - setLoading(true) - try { - const res = await getStatementListApi({ page: pageNum, pageSize: PAGE_SIZE }) - const { data, total } = res.data - setStatements(prev => (reset ? data : [...prev, ...data])) - setPage(pageNum) - setFinished(pageNum * PAGE_SIZE >= total) - } catch { - // 错误已由 request 层 toast - } finally { - loadingRef.current = false - setLoading(false) - } - }, - [loggedIn], - ) - - useDidShow(() => { - loadList(1, true) - }) - - useReachBottom(() => { - if (!finished && !loadingRef.current && loggedIn) { - loadList(page + 1, false) - } - }) - - const goLogin = useCallback(() => { - Taro.navigateTo({ url: '/pages/login/index' }) - }, []) - - const goDetail = useCallback((id: number) => { - Taro.navigateTo({ url: `/pages/statement-detail/index?id=${id}` }) - }, []) - - return ( - - {/* ========== 头部 ========== */} - - 账单 - 按回款周期自动生成 - - - {/* ========== 列表 ========== */} - {!loggedIn ? ( - - 去登录 - - ) : statements.length === 0 ? ( - loading ? ( - 加载中... - ) : ( - - ) - ) : ( - statements.map(st => ( - goDetail(st.id)}> - - {st.statement_no} - - {STATEMENT_STATUS_MAP[st.status]} - - - - - {st.period_start} ~ {st.period_end} - - ¥{st.total_amount} - - {st.settlement_date && ( - 应结算:{st.settlement_date} - )} - - )) - )} - - {loggedIn && finished && statements.length > 0 && ( - 没有更多了 - )} - - ) -} diff --git a/src/services/bill.ts b/src/services/bill.ts new file mode 100644 index 0000000..835b80a --- /dev/null +++ b/src/services/bill.ts @@ -0,0 +1,117 @@ +import { get } from '@/utils/request' +import type { PaginatedData } from '@/types/api' + +/** 账单支付进度:0 待支付 / 1 审核中 / 2 已支付(由后端推导,展示以此为准) */ +export type BillPayState = 0 | 1 | 2 + +/** 账单(列表行与详情的 bill 字段一致) */ +export interface Bill { + id: number + /** 账单编号(ZD 前缀) */ + bill_no: string + /** 账单日期(出账日,Y-m-d) */ + bill_date: string + /** 关联采购单 ID */ + purchase_id: number + /** 关联采购单 */ + purchase: { id: number; purchase_no: string; purchase_date: string } | null + /** 商品金额(订单汇总快照) */ + product_amount: string + /** 配送费 */ + delivery_fee: string + /** 周转筐 / 周转托盘数量 */ + box_num: number + tray_num: number + /** 筐 / 托盘单价(出账时快照) */ + box_price: string + tray_price: string + /** 附加金额 = box_num×box_price + tray_num×tray_price */ + added_amount: string + /** 账单总金额 = 商品金额 + 配送费 + 附加金额 */ + total_amount: string + /** 原始支付状态:0 未支付 / 1 已支付(展示用 pay_state 系列字段) */ + status: 0 | 1 + status_name: string + /** 支付进度:0 待支付 / 1 审核中 / 2 已支付 */ + pay_state: BillPayState + /** 支付进度中文名(列表状态标签直接用它) */ + pay_state_name: string + /** 是否可发起合并付款(=待支付) */ + can_pay: boolean + /** 关联支付记录 ID,0=未发起付款 */ + payment_id: number + /** 应结算日期 = 账单日期 + 门店回款周期天数 */ + settlement_date: string + /** 付款时间(已支付时非空) */ + paid_at: string | null + /** 付款备注 */ + pay_remark: string + /** 账单备注 */ + remark: string + created_at: string +} + +/** 待支付汇总(门店口径,含审核中,不受筛选参数影响) */ +export interface BillSummary { + unpaid_count: number + unpaid_amount: string +} + +/** 账单合并商品明细(跨本账单全部订单按商品聚合) */ +export interface BillItem { + product_id: number + product_name: string + product_spec: string + unit: string + /** 加权平均单价(Σ金额÷Σ数量) */ + price: string + /** 合计数量 */ + quantity: number + weight: string + /** 合计金额 */ + amount: string +} + +/** 账单关联订单 */ +export interface BillOrder { + id: number + order_no: string + order_date: string + total_quantity: number + total_weight: string + total_amount: string + /** 订单状态枚举(0/1/2/3/4/9) */ + status: number +} + +/** 账单详情 */ +export interface BillDetail { + bill: Bill + items: BillItem[] + orders: BillOrder[] +} + +/** 账单列表查询参数 */ +export interface BillListParams { + /** 原始支付状态:0 未支付(含审核中)/ 1 已支付,不传=全部 */ + status?: 0 | 1 + /** 传 1 = 仅可发起付款的账单(合并付款选择页专用) */ + payable?: 1 + /** 账单日期起(Y-m-d) */ + start_date?: string + /** 账单日期止(Y-m-d),不得早于 start_date */ + end_date?: string + page?: number + /** 每页数量,默认 10,最大 50 */ + pageSize?: number +} + +/** 账单列表(附加 summary 待支付汇总):GET /mini/bill */ +export function getBillListApi(params: BillListParams = {}) { + return get & { summary: BillSummary }>('/mini/bill', { data: params }) +} + +/** 账单详情(校验本店归属):GET /mini/bill/{id} */ +export function getBillDetailApi(id: number) { + return get(`/mini/bill/${id}`) +} diff --git a/src/services/order.ts b/src/services/order.ts index c6ac632..92c1c97 100644 --- a/src/services/order.ts +++ b/src/services/order.ts @@ -1,6 +1,6 @@ import { get, post, put } from '@/utils/request' import type { PaginatedData } from '@/types/api' -import type { Order, OrderCreateResult, OrderSummary } from '@/types/order' +import type { OrderCreateResult, OrderDetail, OrderListItem, OrderStatus } from '@/types/order' /** 下单明细行(金额一律服务端重算,前端不传金额) */ export interface OrderItemParam { @@ -15,29 +15,35 @@ export interface CreateOrderParams { remark?: string } +/** 订单列表查询参数 */ +export interface OrderListParams { + /** 订单状态,不传=全部 */ + status?: OrderStatus + /** 订货日期起(Y-m-d) */ + start_date?: string + /** 订货日期止(Y-m-d),不得早于 start_date */ + end_date?: string + page?: number + /** 每页数量,默认 10,最大 50 */ + pageSize?: number +} + /** 下单:POST /mini/order */ export function createOrderApi(params: CreateOrderParams) { return post('/mini/order', params) } -/** 历史订单列表(强制本店隔离):GET /mini/order */ -export function getOrderListApi( - params: { status?: number; page?: number; pageSize?: number } = {}, -) { - return get>('/mini/order', { data: params }) +/** 历史订单列表(强制本店隔离,按订货日期倒序):GET /mini/order */ +export function getOrderListApi(params: OrderListParams = {}) { + return get>('/mini/order', { data: params }) } -/** 周期汇总:GET /mini/order/summary */ -export function getOrderSummaryApi(period: 'day' | 'week' | 'month' = 'month') { - return get('/mini/order/summary', { data: { period } }) -} - -/** 订单详情(校验本店归属):GET /mini/order/{id} */ +/** 订单详情(校验本店归属,含完整明细):GET /mini/order/{id} */ export function getOrderDetailApi(id: number) { - return get(`/mini/order/${id}`) + return get(`/mini/order/${id}`) } -/** 取消订单(仅待汇总可取消):PUT /mini/order/{id}/cancel */ +/** 取消订单(仅待接单可取消):PUT /mini/order/{id}/cancel */ export function cancelOrderApi(id: number) { return put(`/mini/order/${id}/cancel`) } diff --git a/src/services/statement.ts b/src/services/statement.ts deleted file mode 100644 index 6f7aa7f..0000000 --- a/src/services/statement.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { get, post } from '@/utils/request' -import type { PaginatedData } from '@/types/api' -import type { PendingContainers, Statement, StatementDetail } from '@/types/statement' - -/** 账单列表(仅本店,按回款周期自动生成):GET /mini/statement */ -export function getStatementListApi(params: { page?: number; pageSize?: number } = {}) { - return get>('/mini/statement', { data: params }) -} - -/** 账单详情(校验归属,含商品汇总/回筐计费/关联订单):GET /mini/statement/{id} */ -export function getStatementDetailApi(id: number) { - return get(`/mini/statement/${id}`) -} - -/** 付款确认:POST /mini/statement/{id}/pay */ -export function payStatementApi(id: number) { - return post<{ id: number; total_amount: string }>(`/mini/statement/${id}/pay`) -} - -/** 当前门店待回筐/托盘台账:GET /mini/statement/pending-containers */ -export function getPendingContainersApi() { - return get('/mini/statement/pending-containers') -} diff --git a/src/types/order.ts b/src/types/order.ts index 28ff210..02ad07c 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -1,85 +1,115 @@ -/** 门店订单状态:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消 */ -export type OrderStatus = 0 | 1 | 2 | 3 | 9 +/** + * 门店订单状态(新枚举): + * 0 待接单 / 1 已接单 / 2 采购中 / 3 配送中 / 4 已完成 / 9 已取消 + */ +export type OrderStatus = 0 | 1 | 2 | 3 | 4 | 9 -export const ORDER_STATUS_MAP: Record = { - 0: '待汇总', - 1: '已汇总', - 2: '配送中', - 3: '已完成', +/** + * 状态文案兜底映射(完整 6 态) + * 仅用于接口未返回 status_name 的场景(订单详情、账单关联订单); + * 列表展示一律使用接口返回的 status_name,不要再硬编码 + */ +export const ORDER_STATUS_TEXT: Record = { + 0: '待接单', + 1: '已接单', + 2: '采购中', + 3: '配送中', + 4: '已完成', 9: '已取消', } -/** 状态筛选(value 为 null 表示全部) */ -export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | null; label: string }> = [ - { value: null, label: '全部' }, - { value: 0, label: '待汇总' }, - { value: 1, label: '已汇总' }, - { value: 2, label: '配送中' }, - { value: 3, label: '已完成' }, +/** 状态筛选(订单列表页 chips,value 为 undefined 表示全部) */ +export const ORDER_STATUS_FILTERS: Array<{ value: OrderStatus | undefined; label: string }> = [ + { value: undefined, label: '全部' }, + { value: 0, label: '待接单' }, + { value: 1, label: '已接单' }, + { value: 2, label: '采购中' }, + { value: 3, label: '配送中' }, + { value: 4, label: '已完成' }, { value: 9, label: '已取消' }, ] -/** - * 我的页「订单总汇」导航(业务语言 → 后端枚举) - * status 映射 StoreOrder.status:0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消 - * 注意:后端当前无「待付款」状态(status 缺省 = 不传参,显示全部),待后端补充后在此对齐 - */ +/** 我的页「订单总汇」导航(status 映射后端订单状态枚举) */ export const ORDER_NAV_ITEMS: Array<{ key: string; label: string; status?: number; icon: string }> = [ - { key: 'pending_review', label: '待审核', status: 0, icon: 'clock-o' }, - { key: 'pending_stock', label: '待配货', status: 1, icon: 'logistics' }, - { key: 'stocking', label: '配货中', status: 2, icon: 'van-o' }, - { key: 'pending_pay', label: '待付款', icon: 'pending-payment' }, - { key: 'completed', label: '已完成', status: 3, icon: 'completed' }, - { key: 'all', label: '我的订单', icon: 'orders-o' }, + { key: 'pending', label: '待接单', status: 0, icon: 'clock-o' }, + { key: 'accepted', label: '已接单', status: 1, icon: 'passed' }, + { key: 'purchasing', label: '采购中', status: 2, icon: 'shopping-cart-o' }, + { key: 'delivering', label: '配送中', status: 3, icon: 'logistics' }, + { key: 'completed', label: '已完成', status: 4, icon: 'completed' }, + { key: 'all', label: '全部订单', icon: 'orders-o' }, ] -/** 门店订单 */ -export interface Order { +/** 订单列表行商品预览(仅前 3 条) */ +export interface OrderItemPreview { + product_name: string + quantity: number + unit: string +} + +/** 订单列表行(状态名/可取消/商品预览均由后端给出,直接展示) */ +export interface OrderListItem { id: number order_no: string /** 订货日期(Y-m-d) */ order_date: string - total_quantity: string - total_amount: string status: OrderStatus + /** 状态中文名(直接展示,前端不要再硬编码映射) */ + status_name: string + /** 是否可取消(=待接单),取消按钮据此渲染 */ + can_cancel: boolean + total_quantity: number + /** 总称重(斤,常为 0) */ + total_weight: string + total_amount: string remark: string - /** 详情接口返回 */ - items?: OrderItem[] + /** 关联采购单 ID,0=未归集 */ + purchase_id: number + /** 关联账单 ID,0=未出账(>0 可跳账单详情) */ + bill_id: number + created_at: string + /** 明细种数(如「共 4 种」) */ + item_count: number + /** 商品预览,仅前 3 条;完整明细走详情接口 */ + items: OrderItemPreview[] } -/** 订单明细 */ +/** 订单明细(详情接口,下单时商品快照) */ export interface OrderItem { id: number order_id: number product_id: number product_name: string product_spec: string - /** 下单时等级实际价快照 */ + unit: string + /** 下单时门店等级实际价快照 */ price: string - quantity: string - /** 称重(默认 0) */ + quantity: number + /** 称重(斤,参考值) */ weight: string amount: string remark: string } +/** 订单详情(无 status_name/can_cancel,状态展示沿用列表行或 ORDER_STATUS_TEXT) */ +export interface OrderDetail { + id: number + order_no: string + store_id: number + order_date: string + total_quantity: number + total_weight: string + total_amount: string + status: OrderStatus + remark: string + purchase_id: number + bill_id: number + created_at: string + items: OrderItem[] +} + /** 下单返回 */ export interface OrderCreateResult { id: number order_no: string total_amount: string } - -/** 周期汇总分组 */ -export interface SummaryGroup { - period_label: string - total_amount: string - total_quantity: string - order_count: number -} - -/** 周期汇总 */ -export interface OrderSummary { - period: 'day' | 'week' | 'month' - groups: SummaryGroup[] -} diff --git a/src/types/statement.ts b/src/types/statement.ts deleted file mode 100644 index 27c3e83..0000000 --- a/src/types/statement.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** 账单状态:0 待付款 / 1 已付款 */ -export type StatementStatus = 0 | 1 - -export const STATEMENT_STATUS_MAP: Record = { - 0: '待付款', - 1: '已付款', -} - -/** 账单 */ -export interface Statement { - id: number - statement_no: string - period_start: string - period_end: string - /** 商品金额合计 */ - goods_amount: string - /** 账单计费周转框数(可为负=抵扣) */ - box_num: number - /** 账单计费托盘数(可为负=抵扣) */ - tray_num: number - /** 回筐金额合计(可为负=抵扣) */ - crate_amount: string - /** 账单总金额 = 商品金额 + 回筐金额 */ - total_amount: string - /** 生成时快照的回款周期 */ - payment_cycle_days: number - /** 应结算日期 = 周期结束 + 回款周期天 */ - settlement_date: string | null - status: StatementStatus - settled_at: string | null - remark: string -} - -/** 账单商品汇总记录(按商品聚合,不记单价) */ -export interface StatementGoods { - product_name: string - product_spec: string - unit: string - quantity: string - weight: string - amount: string -} - -/** 账单关联订单(下单单价明细在订单记录中查看) */ -export interface StatementOrder { - id: number - order_no: string - order_date: string - total_amount: string -} - -/** 账单详情 */ -export interface StatementDetail extends Statement { - goods: StatementGoods[] - orders: StatementOrder[] -} - -/** 门店待回筐台账 */ -export interface PendingContainers { - pending_box_num: number - pending_tray_num: number - box_price: string - tray_price: string -} diff --git a/src/utils/request.ts b/src/utils/request.ts index f391934..c115743 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -90,6 +90,25 @@ function handleBusinessError(data: ApiResponse): void { } } +/** + * 清理请求参数 + * data 中的 undefined 在 GET 查询串里会被序列化为字符串 'undefined'(null 同理), + * 导致后端收到字符串而非缺省;统一在此剔除。GET 额外剔除 null, + * POST/PUT 等请求体保留 null(JSON 序列化语义可能依赖它) + */ +function sanitizeData(data: any, method?: RequestConfig['method']): any { + if (!data || typeof data !== 'object' || Array.isArray(data)) return data + const isGet = !method || method === 'GET' + const result: Record = {} + Object.keys(data).forEach(key => { + const value = data[key] + if (value === undefined) return + if (isGet && value === null) return + result[key] = value + }) + return result +} + /** * 发起网络请求 * @@ -126,6 +145,7 @@ export function request(config: RequestConfig): Promise> Taro.request({ ...restConfig, url: BASE_URL + restConfig.url, + data: sanitizeData(restConfig.data, restConfig.method), header, timeout: restConfig.timeout || DEFAULT_TIMEOUT, success(res) {