xsh_1997 1 тиждень тому
батько
коміт
676da2b8b5

+ 159 - 117
doc/消费者APP/我的订单/我的订单前端技术方案.md

@@ -1,9 +1,9 @@
1 1
 # 我的订单 — 前端技术方案(C 端 · shop-app)
2 2
 
3
-> **依据:** 《我的订单功能需求.md》v1.1、《我的订单技术方案.md》v1.2  
4
-> **关联:** 《确认订单页(多商品)前端技术方案》、《确认订单页(单商品)前端技术方案》  
3
+> **依据:** 《我的订单功能需求.md》v1.2、《我的订单技术方案.md》v1.3  
4
+> **关联:** 《确认订单页(多商品)前端技术方案》、《确认订单页(单商品)前端技术方案》、《购物车前端技术方案》  
5 5
 > **范围:** 消费者 APP **`shop-app`** 个人中心订单入口、订单列表/详情、续付、确认收货、评价、售后。  
6
-> **实现状态:** v1.2 — 列表展示 **全部商品行**;评价在 **行内按钮**;底部 **无** 整单评价按钮
6
+> **实现状态:** v1.4 — 列表 **售后状态优先展示**(MO-L4)、待付款 **应付** 文案、全量商品行 + 行内评价已落地
7 7
 
8 8
 ---
9 9
 
@@ -16,6 +16,7 @@
16 16
 | 鉴权 | 进入页 **`ensureApiToken()`**;无 Token → 引导登录 |
17 17
 | 分页 | 列表 `pageNum` / `pageSize`;下拉刷新 + 上拉加载 |
18 18
 | 支付 v1 | **Mock 弹窗** + `payOrder`;与确认订单共用 |
19
+| 防抖 | 列表/详情操作 `useActionGuard` |
19 20
 | 再买一单 | 跳转 `subpackage/goods/detail?id={goodsId}` |
20 21
 
21 22
 ---
@@ -27,7 +28,8 @@
27 28
 | 个人中心订单区 | `pages/mine/index`(区块) | 角标 + 快捷入口 |
28 29
 | 订单列表 | `subpackage/order/list` | tab + 分页列表 |
29 30
 | 订单详情 | `subpackage/order/detail` | 分状态展示与操作 |
30
-| 评价列表 | `subpackage/order/review-list` | 待评价/已评价 |
31
+| 评价列表 | `subpackage/order/review-list` | 待评价/已评价(展平商品行) |
32
+| 评价查看 | `subpackage/order/review-view` | 全页查看评价 |
31 33
 | 评价编辑 | `subpackage/order/review-edit` | 打星+图文 |
32 34
 | 售后列表 | `subpackage/order/aftersale-list` | 进行中/已完结 |
33 35
 | 售后详情 | `subpackage/order/aftersale-detail` | 进度+处理结果 |
@@ -39,23 +41,13 @@
39 41
 |------|------|
40 42
 | 订单列表 | `tab=PENDING_PAY`(可选,默认 ALL) |
41 43
 | 订单详情 | `orderId=900` |
44
+| 评价列表 | `tab=PENDING` / `tab=DONE` |
42 45
 | 评价编辑 | `orderId=900&orderItemId=10` |
43
-| 提交售后 | `orderId=900&itemId=10` |
46
+| 提交售后 | `orderId=900`(整单,无需 itemId) |
44 47
 
45
-**路径常量:**
48
+**路径常量:** `utils/pageRoute.js`(`PAGE_ORDER_*`)
46 49
 
47
-```javascript
48
-// utils/pageRoute.js
49
-export const PAGE_ORDER_LIST = '/subpackage/order/list'
50
-export const PAGE_ORDER_DETAIL = '/subpackage/order/detail'
51
-export const PAGE_ORDER_REVIEW_LIST = '/subpackage/order/review-list'
52
-export const PAGE_ORDER_REVIEW_EDIT = '/subpackage/order/review-edit'
53
-export const PAGE_ORDER_AFTERSALE_LIST = '/subpackage/order/aftersale-list'
54
-export const PAGE_ORDER_AFTERSALE_DETAIL = '/subpackage/order/aftersale-detail'
55
-export const PAGE_ORDER_AFTERSALE_SUBMIT = '/subpackage/order/aftersale-submit'
56
-```
57
-
58
-**分包:** `pages.json` → `pkg-order` 注册上述页面;个人中心预加载 `pkg-order`(可选)。
50
+**分包:** `pages.json` → `pkg-order`;`pages/mine/index` 预加载 `pkg-order`。
59 51
 
60 52
 ---
61 53
 
@@ -63,17 +55,20 @@ export const PAGE_ORDER_AFTERSALE_SUBMIT = '/subpackage/order/aftersale-submit'
63 55
 
64 56
 | 类型 | 路径 | 说明 |
65 57
 |------|------|------|
66
-| API | `shop-app/api/order.js` | 扩展 badges/list/detail/pay/confirm |
58
+| API | `shop-app/api/order.js` | badges/list/detail/pay/confirm |
67 59
 | API | `shop-app/api/orderReview.js` | 评价 list/submit/get |
68 60
 | API | `shop-app/api/orderAftersale.js` | 售后 list/detail/submit |
69
-| 常量 | `shop-app/constants/order.js` | tab 枚举、状态文案 |
70
-| 映射 | `shop-app/utils/orderDisplay.js` | VO → 页面模型、actions 按钮 |
61
+| 常量 | `shop-app/constants/order.js` | tab、主状态、售后卡片文案 |
62
+| 映射 | `shop-app/utils/orderDisplay.js` | VO → 页面模型、actions、MO-L4 |
63
+| 操作 | `shop-app/utils/orderAction.js` | 支付弹窗、确认收货、售后入口 |
64
+| 导航 | `shop-app/utils/orderNav.js` | 列表/详情/评价/售后跳转 |
71 65
 | 列表页 | `subpackage/order/list.vue` | |
72 66
 | 详情页 | `subpackage/order/detail.vue` | |
73 67
 | 评价/售后页 | `subpackage/order/*.vue` | |
74
-| 组件 | `components/order/OrderGoodsRow.vue` | 商品行摘要 + **行内评价/查看评价** |
75
-| 组件 | `components/order/OrderStatusBar.vue` | 状态+倒计时 |
76
-| 组件 | `components/order/OrderActionBar.vue` | 底部操作按钮 |
68
+| 组件 | `components/order/OrderGoodsRow.vue` | 商品行 + **行内评价** |
69
+| 组件 | `components/order/OrderStatusBar.vue` | 详情状态+倒计时 |
70
+| 组件 | `components/order/OrderActionBar.vue` | 底部操作(无整单评价) |
71
+| 组件 | `components/order/ReviewDoneSummaryCard.vue` | 已评价紧凑卡 |
77 72
 
78 73
 ---
79 74
 
@@ -96,20 +91,13 @@ export const PAGE_ORDER_AFTERSALE_SUBMIT = '/subpackage/order/aftersale-submit'
96 91
 { tab: 'ALL', pageNum: 1, pageSize: 10 }
97 92
 ```
98 93
 
99
-**tab 枚举(与后端 `OrderAppConstants` 一致):**
94
+**列表行新增字段(v1.2):**
100 95
 
101
-```javascript
102
-export const ORDER_TAB = {
103
-  ALL: 'ALL',
104
-  PENDING_PAY: 'PENDING_PAY',
105
-  PENDING_SHIP: 'PENDING_SHIP',
106
-  PENDING_RECEIVE: 'PENDING_RECEIVE',
107
-  COMPLETED: 'COMPLETED',
108
-  CLOSED: 'CLOSED',
109
-  REVIEW_PENDING: 'REVIEW_PENDING',
110
-  REVIEW_DONE: 'REVIEW_DONE'
111
-}
112
-```
96
+| 字段 | 说明 |
97
+|------|------|
98
+| `aftersaleStatus` | `null` / `1` 进行中 / `2` 已完结 |
99
+| `orderStatusText` | 订单主状态文案(待付款/待发货…) |
100
+| `items[]` | 全部商品行 |
113 101
 
114 102
 ### 4.2 评价 `api/orderReview.js`
115 103
 
@@ -119,11 +107,6 @@ export const ORDER_TAB = {
119 107
 | `getOrderReview(orderId, orderItemId)` | GET | `/api/order/{orderId}/review?orderItemId=` |
120 108
 | `submitReview(orderId, data)` | POST | `/api/order/{orderId}/review` |
121 109
 
122
-```javascript
123
-// submitReview body
124
-{ orderItemId: 10, score: 5, content: '很好', pics: ['https://.../1.jpg'] }
125
-```
126
-
127 110
 ### 4.3 售后 `api/orderAftersale.js`
128 111
 
129 112
 | 方法 | HTTP | 路径 |
@@ -134,125 +117,184 @@ export const ORDER_TAB = {
134 117
 
135 118
 ---
136 119
 
137
-## 5. 页面逻辑
120
+## 5. 订单列表(`list.vue`)
138 121
 
139
-### 5.1 个人中心 · 订单入口
122
+### 5.1 页签与入口
140 123
 
141
-```text
142
-onShow → getOrderBadges()
143
-    → 展示 pendingPayCount / pendingShipCount / pendingReceiveCount / aftersaleInProgressCount
144
-    → 点击「全部」→ list?tab=ALL
145
-    → 点击「待付款」→ list?tab=PENDING_PAY
146
-    …
147
-```
124
+| 页签 | `ORDER_TAB` | 说明 |
125
+|------|-------------|------|
126
+| 全部 | `ALL` | |
127
+| 待付款 | `PENDING_PAY` | 续付入口 |
128
+| 待发货 | `PENDING_SHIP` | |
129
+| 待收货 | `PENDING_RECEIVE` | 平台已发货 |
130
+| 已完成 | `COMPLETED` | 交易成功 |
131
+| 已关闭 | `CLOSED` | |
132
+
133
+顶栏链接:**评价** → `review-list`;**退款/售后** → `aftersale-list`。
148 134
 
149
-### 5.2 订单列表
135
+### 5.2 卡片结构
150 136
 
151 137
 ```text
152
-onLoad(query.tab)
153
-    → listOrders({ tab, pageNum, pageSize })
154
-    → mapOrderListRow(row)
155
-        → card.statusText:有 aftersaleStatus 时展示「售后处理中/售后已完成」,否则 orderStatusText
156
-        → card.items[] 逐行渲染 OrderGoodsRow
157
-        → 已完成:行内 reviewStatus=PENDING →「评价」;DONE →「查看评价」
158
-    → 底部 OrderActionBar:过滤 REVIEW / VIEW_REVIEW,仅 PAY / 确认收货 / 售后 / 再买一单
159
-    → 行内 @review → goReviewEdit(orderId, orderItemId) 或 showItemReview(...)
138
+order-card
139
+├── 店头:头像 + 店名 + statusText(MO-L4)
140
+├── 商品区:OrderGoodsRow × items.length(全量)
141
+│   └── 已完成:行内「评价」/「查看评价」
142
+├── 下单时间 + amountLabel + payAmountText
143
+├── 待付款:支付倒计时 payRemainSeconds
144
+└── OrderActionBar(无 REVIEW / VIEW_REVIEW)
160 145
 ```
161 146
 
162
-### 5.3 订单详情
147
+### 5.3 列表状态文案(MO-L4)
163 148
 
164
-```text
165
-onLoad(orderId) → getOrderDetail(orderId)
166
-    → items[] 全量展示;行内评价按钮(同列表)
167
-    → 底部 actions 不含整单评价
168
-```
149
+`mapOrderCardStatusText(orderStatusText, aftersaleStatus)`:
150
+
151
+| aftersaleStatus | 卡片 statusText |
152
+|-----------------|-----------------|
153
+| `1` | **售后处理中** |
154
+| `2` | **售后已完成** |
155
+| `null` / 无 | **orderStatusText**(待付款/待发货/交易成功…) |
156
+
157
+- 保留字段 `orderStatusText`、`aftersaleStatus` 供调试;**展示用 `statusText`**
158
+- 有售后态时 `statusIsAftersale=true`,样式蓝色区分
159
+- **详情页**仍展示订单主状态(不套用 MO-L4)
160
+
161
+### 5.4 金额文案
162
+
163
+| orderStatus | amountLabel |
164
+|-------------|-------------|
165
+| `0` 待支付 | **应付** |
166
+| 其它 | **实付** |
167
+
168
+---
169
+
170
+## 6. 订单详情(`detail.vue`)
169 171
 
170
-### 5.4 续付(待付款)
172
+- `OrderStatusBar`:主状态 + 待付款倒计时 / 关闭原因 / 最新物流摘要
173
+- 收货快照、全部 `items[]`(含服务保障、行小计、行备注)
174
+- 行内评价按钮(同列表)
175
+- 底部 `OrderActionBar`:过滤整单评价按钮
176
+- 分状态字段:支付时间、成交时间、关闭原因、物流节点(无地图)
177
+
178
+---
179
+
180
+## 7. 评价模块
181
+
182
+### 7.1 评价列表 `review-list.vue`
183
+
184
+| Tab | 展示 | 点击 |
185
+|-----|------|------|
186
+| 待评价 | 展平 `PENDING` 商品行 +「评价」 | 商品区→订单详情;评价→`review-edit` |
187
+| 已评价 | `ReviewDoneSummaryCard` 紧凑卡 | → `review-view` |
188
+
189
+### 7.2 评价编辑 `review-edit.vue`
190
+
191
+`orderId` + `orderItemId` → `submitReview`;顶部展示评价商品。
192
+
193
+### 7.3 一行一评
194
+
195
+- `reviewStatus` per `orderItemId`:`PENDING` / `DONE` / `NONE`
196
+- 底部 actions **不含** `REVIEW`、`VIEW_REVIEW`
197
+
198
+---
199
+
200
+## 8. 售后模块
201
+
202
+- 列表:`aftersale-list` 进行中/已完结(文案:商家处理中/售后完结)
203
+- 详情:`aftersale-detail` 进度 + 凭证图(`normalizePicList`)
204
+- 提交:`aftersale-submit` 类型/原因/凭证
205
+
206
+**与订单列表文案区分:** 订单卡片用「售后处理中/售后已完成」;售后专区用「商家处理中/售后完结」。
207
+
208
+---
209
+
210
+## 9. 页面流程
211
+
212
+### 9.1 个人中心角标
171 213
 
172 214
 ```text
173
-点击「继续支付」
174
-    → payOrder(orderId)
175
-        ├── 成功 → toast + 刷新详情 / 跳转待发货 tab
176
-        └── 失败(超时等)→ 提示 + 刷新
215
+onShow → getOrderBadges()
216
+    → pendingPayCount / pendingShipCount / pendingReceiveCount / aftersaleInProgressCount
217
+    → 点击快捷入口带 tab 进 list 或 aftersale-list
177 218
 ```
178 219
 
179
-### 5.5 确认收货
220
+### 9.2 续付(待付款)
180 221
 
181 222
 ```text
182
-uni.showModal 二次确认
183
-    → confirmReceive(orderId)
184
-    → 可选 navigateTo review-edit?orderId=
223
+「去支付」→ openPayModal → payOrder
224
+    ├── 成功 → 刷新列表/详情
225
+    └── 失败/超时 → Toast + 刷新
185 226
 ```
186 227
 
187
-### 5.6 评价编辑
228
+### 9.3 确认收货
188 229
 
189 230
 ```text
190
-review-edit?orderId=&orderItemId=
191
-    → submitReview(orderId, { orderItemId, score, content, pics })
192
-    → 返回列表/详情并刷新
231
+showModal → confirmReceive → 可选 goReviewEdit
193 232
 ```
194 233
 
195
-### 5.7 提交售后
234
+### 9.4 提交售后(整单)
196 235
 
197 236
 ```text
198
-从详情带 orderId + itemId 进入
199
-    → 选 applyType → 动态展示原因选项、是否填 returnQuantity
200
-    → 凭证图 upload
201
-    → submitAftersale({ orderId, orderItemId, applyType, ... })
202
-    → 跳转 aftersale-detail 或 aftersale-list?tab=IN_PROGRESS
237
+详情/列表「申请售后」
238
+    → goAftersaleSubmit(orderId)(不选商品、不弹 ActionSheet)
239
+    → aftersale-submit 展示订单全部 items[]
240
+    → 申请金额默认订单实付;退货数量上限为整单件数之和
241
+    → submitAftersale({ orderId, orderItemId: 首行ID, ... })
242
+         ※ orderItemId 为接口兼容字段,业务口径为整单售后
243
+    → aftersale-detail 或 aftersale-list
203 244
 ```
204 245
 
205 246
 ---
206 247
 
207
-## 6. 展示映射 `orderDisplay.js`
248
+## 10. 展示映射 `orderDisplay.js`
208 249
 
209
-- `mapOrderListRow`:`items[]` 全量映射;`firstItem = items[0]` 兼容
210
-- **列表状态文案:**
211
-  - 透传 `orderStatusText`(订单主状态)
212
-  - 透传 `aftersaleStatus`(`null` / `1` / `2`)
213
-  - `statusText`(卡片展示):`aftersaleStatus === '1'` → **售后处理中**;`'2'` → **售后已完成**;否则 **orderStatusText**
214
-- 单行含 `reviewStatus`(`PENDING` / `DONE` / `NONE`)、`orderItemId`
215
-- `footerActions(actions)`:过滤 `REVIEW`、`VIEW_REVIEW`
216
-- `showItemReview(orderId, orderItemId)`:弹窗展示已评内容(`orderAction.js`)
217
-- 售后专区列表/详情仍用 `mapAftersaleStatusText`(商家处理中 / 售后完结),与 **订单列表** 文案区分
250
+| 函数 | 用途 |
251
+|------|------|
252
+| `mapOrderListRow` | 列表卡片;`items[]` 全量;MO-L4 `statusText`;`amountLabel` |
253
+| `mapOrderCardStatusText` | 列表售后态优先 |
254
+| `mapOrderDetail` | 详情;主状态 `orderStatusText` |
255
+| `getItemReviewAction` | 行内评价按钮 |
256
+| `mapActions` | 过滤整单评价 |
257
+| `flattenPendingReviewItems` / `mapReviewDoneRow` | 评价列表 |
258
+| `mapAftersaleListRow` / `mapAftersaleDetail` | 售后专区 |
218 259
 
219 260
 ---
220 261
 
221
-## 7. 与确认订单衔接
262
+## 11. 与确认订单衔接
222 263
 
223 264
 | 场景 | 行为 |
224 265
 |------|------|
225
-| 支付成功页 | 可 `redirectTo` list?tab=PENDING_SHIP 或 detail |
226
-| 取消支付 | list?tab=CLOSED 或 detail 已关闭 |
227
-| pay/cancel API | **共用** `api/order.js` |
266
+| 支付成功 | `pay-result` 或进 `list?tab=PENDING_SHIP` |
267
+| 取消支付 | `list?tab=CLOSED` 或详情已关闭 |
268
+| 购物车结算 | `checkout-cart` → 支付后进订单列表 |
228 269
 
229 270
 ---
230 271
 
231
-## 8. 测试要点(前端)
272
+## 12. 联调检查清单
232 273
 
233
-| 编号 | 场景 |
234
-|------|------|
235
-| F1 | 未登录进入列表 → 引导登录 |
236
-| F2 | 角标与列表 tab 数量一致 |
237
-| F3 | 待付款倒计时展示与续付 |
238
-| F4 | 确认收货后状态变交易成功 |
239
-| F5 | 多商品订单列表展示全部 items;行内评价 |
240
-| F6 | 底部无整单「评价」按钮 |
241
-| F7 | 评价提交后该行变「查看评价」 |
242
-| F8 | 售后提交后进进行中列表 |
243
-| F9 | 列表存在 aftersaleStatus=1 → 卡片状态「售后处理中」;=2 →「售后已完成」;null → orderStatusText |
274
+- [ ] 未登录引导登录
275
+- [ ] 角标与列表 tab 一致
276
+- [ ] 多商品订单列表展示全部 items
277
+- [ ] 行内评价;底部无整单评价
278
+- [ ] `aftersaleStatus=1` → 列表「售后处理中」;`=2` →「售后已完成」
279
+- [ ] 无售后 → 显示 orderStatusText
280
+- [ ] 待付款显示「应付」+ 倒计时 + 去支付
281
+- [ ] 确认收货 → 交易成功 + 可评价
282
+- [ ] 评价列表待评价/已评价交互
283
+- [ ] 售后提交后进进行中列表
244 284
 
245 285
 ---
246 286
 
247
-## 9. 修订记录
287
+## 13. 修订记录
248 288
 
249 289
 | 版本 | 说明 |
250 290
 |------|------|
251
-| **v1.3** | 订单列表 `aftersaleStatus` 前端映射 statusText;与后端 orderStatusText 分离 |
252
-| **v1.2** | 列表/详情/评价列表:`items[]` 全量;行内评价;底部 actions 去评价 |
253
-| **v1.0** | 首版:路由、API、页面流程、与后端接口对齐 |
254
-| **v1.1** | 落地 list/detail/review/aftersale 分包页、个人中心订单入口与角标、公共组件 |
291
+| **v1.5** | 售后改为 **整单申请**:去掉多商品选择;`aftersale-submit` 展示全量商品;路由仅 `orderId` |
292
+| **v1.4** | 对齐需求 **v1.2**:`mapOrderCardStatusText`(MO-L4)、待付款「应付」、常量 `ORDER_AFTERSALE_CARD_*`;文档补全评价/售后/确认订单边界 |
293
+| **v1.3** | 文档规划 aftersaleStatus 映射(代码待落地) |
294
+| **v1.2** | 列表/详情全量 items;行内评价;底部去整单评价 |
295
+| **v1.1** | 落地分包页、个人中心角标、公共组件 |
296
+| **v1.0** | 首版路由与 API |
255 297
 
256 298
 ---
257 299
 
258
-*文档版本:v1.3 · 关联《我的订单功能需求.md》v1.2、《我的订单技术方案.md》v1.3*
300
+*文档版本:v1.5 · 关联《我的订单功能需求.md》v1.2、《我的订单技术方案.md》v1.3*

+ 143 - 55
doc/消费者APP/购物车/购物车前端技术方案.md

@@ -1,9 +1,9 @@
1 1
 # 购物车 — 前端技术方案(C 端 · shop-app)
2 2
 
3 3
 > **依据:** 《购物车功能需求.md》v1.0、《购物车技术方案.md》v1.0(仅作接口对照,**本文档独立维护**)  
4
-> **关联:** 《商品详情内页前端技术方案》、《店铺主页前端技术方案》、《我的服务前端技术方案》  
5
-> **范围:** C 端 **购物车 Tab 列表**、详情 **加购**、勾选/改量/删除/清理失效、**同店去结算预校验**;**不** 改后端、**不** 实现确认订单/支付完整页
6
-> **实现状态:** Tab 页与 API 已落地;确认订单页待建,prepare 结果已写入本地缓存
4
+> **关联:** 《商品详情内页前端技术方案》、《确认订单页(多商品)前端技术方案》v1.0、《店铺主页前端技术方案》  
5
+> **范围:** C 端 **购物车 Tab 列表**、详情 **加购**、勾选/改量/删除/清理失效、**同店去结算**;确认订单 **多商品页** 见另册,本模块负责跳转与 prepare 门禁
6
+> **实现状态:** Tab 页、组件、API、`checkout-cart` 跳转链路 **已落地**
7 7
 
8 8
 ---
9 9
 
@@ -12,11 +12,12 @@
12 12
 | 项 | 说明 |
13 13
 |----|------|
14 14
 | 框架 | uni-app **Vue 3** + **uview-plus** |
15
-| 请求 | `@/utils/request`;列表为 `AjaxResult.data`(`items` + `checkedSummary`) |
15
+| 请求 | `@/utils/request`;列表 `AjaxResult.data` 为 **`items[]` + `checkedSummary`** |
16 16
 | 鉴权 | **全部** `/api/cart/**` 须会员 Token |
17 17
 | 页面位置 | **主包 Tab** `pages/cart/index`(与 `pages.json` tabBar 一致) |
18
-| 规格 v1 | 统一规格;加购 `specKey` 不传, `specText` 由详情拼接或「默认」 |
19
-| 参考 | `components/search/ShopList` 卡片、`pages/mine` 未登录态 |
18
+| 规格 v1 | 统一规格;加购 `specKey` 不传,`specText` 由详情拼接或「默认」 |
19
+| 防抖 | 改量/勾选/去结算使用 `useActionGuard` |
20
+| 参考 | `subpackage/order/checkout-cart.vue`(去结算下游)、`pages/mine` 未登录态 |
20 21
 
21 22
 ---
22 23
 
@@ -25,10 +26,18 @@
25 26
 | 页面 | 需求代号 | 路径 | 入口 |
26 27
 |------|----------|------|------|
27 28
 | 购物车列表 | **A** | `pages/cart/index` | 底部 Tab「购物车」;详情加购后可自行切 Tab |
29
+| 确认订单(多商品) | — | `subpackage/order/checkout-cart` | 本页 **去结算**(另册维护) |
28 30
 
29 31
 **未登录:** 空态 +「去登录」→ `PAGE_LOGIN`(CT0)
30 32
 
31
-**路径常量:** `PAGE_CART`(`utils/pageRoute.js`)
33
+**路径常量:**
34
+
35
+| 常量 | 路径 |
36
+|------|------|
37
+| `PAGE_CART` | `/pages/cart/index` |
38
+| `PAGE_CHECKOUT_CART` | `/subpackage/order/checkout-cart` |
39
+
40
+**预加载:** `pages.json` → `preloadRule` 为 `pages/cart/index` 预加载 `pkg-order`。
32 41
 
33 42
 ---
34 43
 
@@ -36,13 +45,15 @@
36 45
 
37 46
 | 类型 | 路径 | 说明 |
38 47
 |------|------|------|
39
-| Tab 页 | `shop-app/pages/cart/index.vue` | 分组列表、底栏全选/合计/去结算 |
40
-| 店组 | `shop-app/components/cart/CartShopGroup.vue` | 店头全选、进店 |
41
-| 商品行 | `shop-app/components/cart/CartItemRow.vue` | 勾选、数量、删除、进详情 |
48
+| Tab 页 | `shop-app/pages/cart/index.vue` | 分组列表、工具栏、底栏全选/合计/去结算 |
49
+| 店组 | `shop-app/components/cart/CartShopGroup.vue` | 店头全选、进店、休息中标签 |
50
+| 商品行 | `shop-app/components/cart/CartItemRow.vue` | 勾选、数量、小计、失效标签、删除、进详情 |
42 51
 | 购物车 API | `shop-app/api/cart.js` | `/api/cart/**` |
43
-| 展示 | `shop-app/utils/cartDisplay.js` | 分组映射、失效文案、勾选统计辅助 |
44
-| 结算缓存 | `shop-app/utils/cartCheckout.js` | prepare 结果 `setStorageSync` |
45
-| 常量 | `shop-app/constants/cart.js` | 失效类型、空态、跨店提示、缓存键 |
52
+| 展示 | `shop-app/utils/cartDisplay.js` | 扁平 `items` 按店分组、失效文案、勾选统计 |
53
+| 结算导航 | `shop-app/utils/checkoutNav.js` | `goCartCheckout(cartItemIds)` |
54
+| 规格展示 | `shop-app/utils/cartSpec.js` | `parseCartSpecText`(`§` 分隔) |
55
+| 常量 | `shop-app/constants/cart.js` | 失效类型、空态、跨店提示 |
56
+| 常量 | `shop-app/constants/checkout.js` | `CART_CHECKOUT_IDS_KEY` |
46 57
 
47 58
 **已改上游:**
48 59
 
@@ -65,17 +76,26 @@
65 76
 | `removeCartItem` | DELETE | `/api/cart/items/{id}` | 单行删除 |
66 77
 | `removeCartItems` | DELETE | `/api/cart/items` | 删除选中 |
67 78
 | `cleanInvalidCart` | DELETE | `/api/cart/invalid` | 清理失效 |
68
-| `prepareCartCheckout` | POST | `/api/cart/checkout/prepare` | 去结算 |
79
+| `prepareCartCheckout` | POST | `/api/cart/checkout/prepare` | 去结算门禁 |
80
+
81
+### 4.1 列表响应 `CartListVO`
82
+
83
+| 字段 | 说明 |
84
+|------|------|
85
+| `items[]` | 扁平购物车行(含 `shopId/shopName/shopStatus`);排序 `cart_item_id DESC` |
86
+| `checkedSummary` | 已勾选且 `purchasable` 行合计 |
87
+
88
+前端 **`mapCartList`** 将 `items[]` **按 `shopId` 分组** 为 UI 用 `groups[]`(后端不分组)。
69 89
 
70
-### 4.1 加购 Body(详情)
90
+### 4.2 加购 Body(详情)
71 91
 
72 92
 | 字段 | 说明 |
73 93
 |------|------|
74 94
 | goodsId | 必填 |
75
-| quantity | 必填,与详情数量弹窗一致 |
76
-| specText | 规格展示快照;无则「默认」 |
95
+| quantity | 必填 |
96
+| specText | 规格快照;无则「默认」 |
77 97
 
78
-### 4.2 批量勾选 Body
98
+### 4.3 批量勾选 Body
79 99
 
80 100
 ```json
81 101
 {
@@ -85,114 +105,182 @@
85 105
 }
86 106
 ```
87 107
 
88
-### 4.3 去结算 Body
108
+### 4.4 去结算 Body
89 109
 
90 110
 ```json
91 111
 { "cartItemIds": [1, 2] }
92 112
 ```
93 113
 
94
-成功 `data` 经 `saveCheckoutPrepare` 写入 `cart_checkout_prepare`,供 **确认订单页(待建)** 读取
114
+成功后再跳转确认订单页;**不在购物车页缓存 prepare 结果**(确认页走 `POST /api/checkout/preview`)
95 115
 
96 116
 ---
97 117
 
98
-## 5. 列表页逻辑(`pages/cart/index.vue`)
118
+## 5. 列表页结构(`pages/cart/index.vue`)
119
+
120
+```text
121
+购物车 Tab
122
+├── 未登录:空态 + 去登录
123
+├── 加载中 / 加载失败(重试)
124
+├── 空购物车:「购物车是空的」+ 去逛逛(首页 Tab)
125
+└── 有数据
126
+    ├── 顶栏工具条
127
+    │   ├── 清理失效商品(有失效行时显示)
128
+    │   └── 删除选中(有勾选时显示)
129
+    ├── scroll-view · CartShopGroup × N
130
+    └── 底栏
131
+        ├── 全选(仅可购行)
132
+        ├── 合计 ¥checkedSummary.checkedAmountText
133
+        └── 去结算(N)
134
+```
135
+
136
+---
137
+
138
+## 6. 列表页逻辑
99 139
 
100 140
 ```text
101 141
 onShow
102 142
   → 无 Token:访客空态
103
-  → 有 Token:GET /api/cart → mapCartList
143
+  → 有 Token:GET /api/cart → mapCartList(items 按店分组)
104 144
 
105 145
 店组头勾选
106
-  → 该店全部可购行 checked 同步
146
+  → 该店全部 purchasable 行 checked 同步 → PUT checked → 刷新
107 147
 
108 148
 行勾选 / 底栏全选
109
-  → PUT checked → 刷新列表(含 checkedSummary)
149
+  → PUT checked → 刷新(含 checkedSummary)
110 150
 
111 151
 数量 +/-
112
-  → PUT quantity → 刷新(库存不足由后端 400)
152
+  → PUT quantity → 刷新(库存不足后端 400)
153
+
154
+删除单行
155
+  → 二次确认 → DELETE 单行 → 刷新
113 156
 
114
-删除 / 删除选中 / 清理失效
115
-  → DELETE 对应接口 → 刷新
157
+删除选中
158
+  → 二次确认(展示件数)→ DELETE 批量 → 刷新
159
+
160
+清理失效
161
+  → countInvalidCartItems → 二次确认(展示件数)→ DELETE /invalid → Toast removedCount
116 162
 
117 163
 去结算
118
-  → 无勾选:提示
164
+  → 无勾选可购行:提示
119 165
   → 跨店勾选:Toast「请选择同一店铺的商品结算」
120
-  → POST prepare → 缓存 data → 暂 Toast「确认订单功能开发中」
166
+  → POST prepare 成功 → goCartCheckout(ids)
167
+  → prepare 失败:request 已 Toast,不跳转
121 168
 ```
122 169
 
123 170
 | 需求规则 | 实现 |
124 171
 |----------|------|
125
-| CT2 按店分组 | 渲染 `CartShopGroup` |
172
+| CT2 按店分组 | `groupCartItemsByShop` |
126 173
 | 失效不可勾选 | `purchasable=false` 置灰勾选 |
127 174
 | CT-S1 不跨店结算 | `getCheckedShopIds.size > 1` 拦截 |
128
-| CT7 清理失效 | 顶栏「清理失效商品」 |
129
-| 缺货/下架进详情 | 点击主图/名称区域 `goGoodsDetail` |
175
+| CT7 清理失效 | 顶栏按钮 + 件数确认 |
176
+| 缺货/下架进详情 | 点击主图/名称 `goGoodsDetail` |
130 177
 | 进店 | 点击店名 `goShopHome` |
178
+| 行小计 | `subtotalText` 展示 |
179
+
180
+### 6.1 底栏合计
181
+
182
+使用接口 **`checkedSummary`**(仅 **已勾选且可购** 行),与后端口径一致。
183
+
184
+### 6.2 失效标签
185
+
186
+`invalidType` → `getCartInvalidLabel`(`cartDisplay.js`):
187
+
188
+| invalidType | 默认文案 |
189
+|-------------|----------|
190
+| OUT_OF_STOCK | 缺货 |
191
+| OFF_SHELF | 已下架 |
192
+| SHOP_CLOSED | 休息中 |
193
+| CATEGORY_HIDDEN | 不可购买 |
194
+| GOODS_DELETED | 已失效 |
195
+| SHOP_DELETED | 店铺失效 |
196
+
197
+优先使用后端 `invalidMsg`。
131 198
 
132
-### 5.1 底栏合计
199
+---
200
+
201
+## 7. 组件说明
133 202
 
134
-使用接口返回的 **`checkedSummary`**(仅 **已勾选且可购** 行),与后端口径一致。
203
+### 7.1 `CartShopGroup.vue`
135 204
 
136
-### 5.2 失效标签
205
+- 店头:勾选(仅当有可购行)、店名、停业「休息中」、进店箭头
206
+- 子组件 `CartItemRow` 列表
137 207
 
138
-`invalidType` → `getCartInvalidLabel`(`cartDisplay.js`),与后端 `CartConstants` 枚举对齐。
208
+### 7.2 `CartItemRow.vue`
209
+
210
+- 可购:勾选、+/- 改量、删除
211
+- 失效:勾选禁用、数量只读、失效标签、仍可删、可点进详情
212
+- 展示:主图、名称、规格列表、单价、**小计**
139 213
 
140 214
 ---
141 215
 
142
-## 6. 数据映射(`cartDisplay.js`)
216
+## 8. 数据映射(`cartDisplay.js`)
143 217
 
144 218
 | 函数 | 用途 |
145 219
 |------|------|
146
-| `mapCartList` | `data.items` → 页面 `items`;`groupCartItemsByShop` 生成 UI 用 `groups` + `checkedSummary` |
220
+| `mapCartList` | `items[]` → `groups[]` + `checkedSummary` |
221
+| `groupCartItemsByShop` | 内部:按 `shopId` 分组,保持行序 |
147 222
 | `hasInvalidCartItems` | 是否显示「清理失效」 |
148
-| `getCheckedPurchasableIds` | 去结算入参 |
223
+| `countInvalidCartItems` | 清理前展示件数 |
224
+| `getCheckedPurchasableIds` | 去结算 / prepare 入参 |
149 225
 | `getCheckedShopIds` | 跨店校验 |
150 226
 | `getAllPurchasableItems` | 全选逻辑 |
227
+| `getCartInvalidLabel` | 失效标签文案 |
151 228
 
152
-**行模型要点:** `displayPic`、`priceText`、`subtotalText`、`invalidLabel`、`purchasable`
229
+**行模型要点:** `displayPic`、`priceText`、`subtotalText`、`specList`、`invalidLabel`、`purchasable`
153 230
 
154 231
 ---
155 232
 
156
-## 7. 与确认订单边界
233
+## 9. 与确认订单(多商品)边界
157 234
 
158
-| 项 | 当前前端 |
235
+| 项 | 购物车侧 |
159 236
 |----|----------|
160
-| prepare 成功 | `saveCheckoutPrepare(res.data)` |
161
-| 跳转确认订单 | **待建**;`PAGE_ORDER_CONFIRM` 未注册 |
162
-| 下单成功删行 | 订单模块负责,购物车 **不处理** |
237
+| prepare | `POST /api/cart/checkout/prepare` 门禁(四条件、同店) |
238
+| 跳转 | `goCartCheckout` → `checkout-cart?cartItemIds=` + storage 兜底 |
239
+| 预览/提交 | **不在本页**;见《确认订单页(多商品)前端技术方案》 |
240
+| 支付成功删行 | **后端** 处理;返回 Tab 刷新可见 |
241
+
242
+```text
243
+去结算
244
+  → prepareCartCheckout(ids)
245
+  → goCartCheckout(ids)
246
+       → setStorage CART_CHECKOUT_IDS_KEY
247
+       → navigateTo PAGE_CHECKOUT_CART
248
+```
163 249
 
164 250
 ---
165 251
 
166
-## 8. 联调检查清单
252
+## 10. 联调检查清单
167 253
 
168 254
 - [ ] 未登录 Tab 仅引导登录
169 255
 - [ ] 详情加购后 Tab 刷新可见,同规格数量累加
170
-- [ ] 列表按店分组、失效标签与不可勾选
256
+- [ ] 列表 `items` 正确按店分组展示
257
+- [ ] 失效行不可勾选、标签与进店/进详情可用
171 258
 - [ ] 改量超过库存 → 400「库存不足」
172
-- [ ] 跨店勾选去结算 → 400/前端拦截文案一致
173
-- [ ] 同店 prepare 返回 `shopId`、`items`、`goodsAmount`
174
-- [ ] 清理失效、删除选中后列表刷新
259
+- [ ] 跨店勾选去结算 → 前端拦截文案
260
+- [ ] 同店 prepare 成功 → 跳转 `checkout-cart`
261
+- [ ] 清理失效展示件数;删除选中二次确认
175 262
 - [ ] 停业店可展示,prepare 拦截停业
176 263
 
177 264
 ---
178 265
 
179
-## 9. 非本期(前端不实现)
266
+## 11. 非本期(前端不实现)
180 267
 
181 268
 | 项 | 说明 |
182 269
 |------|------|
183
-| 确认订单、支付 UI | 另册;仅缓存 prepare |
184 270
 | 列表/搜索快捷加购 | 仅详情加购 |
185 271
 | 跨店一键多单 | CT-S1 不支持 |
186 272
 | 优惠券、凑单 | 未要求 |
273
+| 失效自动删除 | 用户触发「清理失效」 |
187 274
 
188 275
 ---
189 276
 
190
-## 10. 修订记录
277
+## 12. 修订记录
191 278
 
192 279
 | 版本 | 说明 |
193 280
 |------|------|
194
-| **v1.0** | 首版:Tab 购物车、详情加购、同店 prepare 与本地缓存 |
281
+| **v1.1** | 对齐需求 v1.0:**items 前端按店分组**、清理失效件数确认、行小计、去结算跳转 `checkout-cart`;更新与多商品确认页边界 |
282
+| **v1.0** | 首版:Tab 购物车、详情加购、同店 prepare |
195 283
 
196 284
 ---
197 285
 
198
-*文档版本:v1.0 · 不修改《购物车技术方案.md》(后端方案)。*
286
+*文档版本:v1.1 · 不修改《购物车技术方案.md》(后端方案)。*

+ 5 - 0
shop-app/components/cart/CartItemRow.vue

@@ -22,6 +22,7 @@
22 22
 				</view>
23 23
 				<view class="cart-item__price-row">
24 24
 					<text class="cart-item__price">¥{{ item.priceText }}</text>
25
+					<text v-if="item.subtotalText" class="cart-item__subtotal">小计 ¥{{ item.subtotalText }}</text>
25 26
 					<text v-if="item.invalidLabel" class="cart-item__tag">{{ item.invalidLabel }}</text>
26 27
 				</view>
27 28
 			</view>
@@ -146,6 +147,10 @@ function onPlus() {
146 147
 	color: #e53935;
147 148
 	font-weight: 600;
148 149
 }
150
+.cart-item__subtotal {
151
+	font-size: 24rpx;
152
+	color: #666;
153
+}
149 154
 .cart-item__tag {
150 155
 	font-size: 22rpx;
151 156
 	color: #e65100;

+ 23 - 0
shop-app/constants/order.js

@@ -1,3 +1,26 @@
1
+/** 订单主状态(与后端 OrderConstants 一致) */
2
+export const ORDER_STATUS = {
3
+  PENDING_PAY: '0',
4
+  PENDING_SHIP: '1',
5
+  SHIPPED: '2',
6
+  COMPLETED: '3',
7
+  CLOSED: '4'
8
+}
9
+
10
+/**
11
+ * 列表卡片售后态(与后端 OrderAppConstants 一致)
12
+ * MO-L4:有值时卡片状态区优先展示下列文案
13
+ */
14
+export const ORDER_AFTERSALE_CARD_STATUS = {
15
+  IN_PROGRESS: '1',
16
+  FINISHED: '2'
17
+}
18
+
19
+export const ORDER_AFTERSALE_CARD_LABEL = {
20
+  [ORDER_AFTERSALE_CARD_STATUS.IN_PROGRESS]: '售后处理中',
21
+  [ORDER_AFTERSALE_CARD_STATUS.FINISHED]: '售后已完成'
22
+}
23
+
1 24
 /** 订单列表 tab(与后端 OrderAppConstants 一致) */
2 25
 export const ORDER_TAB = {
3 26
   ALL: 'ALL',

+ 7 - 1
shop-app/pages/cart/index.vue

@@ -80,6 +80,7 @@ import {
80 80
 import {
81 81
 	mapCartList,
82 82
 	hasInvalidCartItems,
83
+	countInvalidCartItems,
83 84
 	getCheckedShopIds,
84 85
 	getCheckedPurchasableIds,
85 86
 	getAllPurchasableItems
@@ -250,9 +251,14 @@ function onDeleteChecked() {
250 251
 }
251 252
 
252 253
 async function onCleanInvalid() {
254
+	const invalidCount = countInvalidCartItems(groups.value)
255
+	if (!invalidCount) {
256
+		uni.showToast({ title: '暂无失效商品', icon: 'none' })
257
+		return
258
+	}
253 259
 	uni.showModal({
254 260
 		title: '提示',
255
-		content: '确定清理全部失效商品吗?',
261
+		content: `确定清理 ${invalidCount} 件失效商品吗?`,
256 262
 		success: async (res) => {
257 263
 			if (!res.confirm) return
258 264
 			try {

+ 2 - 2
shop-app/subpackage/account/profile.vue

@@ -1,10 +1,10 @@
1 1
 <template>
2 2
 	<view class="form-page">
3 3
 		<view class="form-card">
4
-			<view class="form-row">
4
+			<!-- <view class="form-row">
5 5
 				<text class="form-row__label">用户 ID</text>
6 6
 				<text class="form-row__readonly">{{ profile.memberId || '—' }}</text>
7
-			</view>
7
+			</view> -->
8 8
 			<view class="form-row">
9 9
 				<text class="form-row__label">会员名称</text>
10 10
 				<text class="form-row__readonly">{{ profile.memberCode || '—' }}</text>

+ 43 - 18
shop-app/subpackage/order/aftersale-submit.vue

@@ -4,10 +4,18 @@
4 4
 			<u-loading-icon mode="circle" />
5 5
 		</view>
6 6
 
7
-		<template v-else-if="orderItem">
7
+		<template v-else-if="orderItems.length">
8 8
 			<view class="as-submit-card">
9
-				<text class="as-submit-card__title">商品信息</text>
10
-				<order-goods-row :item="orderItem" />
9
+				<text class="as-submit-card__title">订单商品</text>
10
+				<text class="as-submit-card__tip">本单共 {{ orderItems.length }} 件商品,售后按整单申请</text>
11
+				<view class="as-submit-goods">
12
+					<order-goods-row
13
+						v-for="item in orderItems"
14
+						:key="item.orderItemId"
15
+						:item="item"
16
+						show-subtotal
17
+					/>
18
+				</view>
11 19
 			</view>
12 20
 
13 21
 			<view class="as-submit-card">
@@ -43,6 +51,7 @@
43 51
 					<text class="as-qty-num">{{ returnQuantity }}</text>
44 52
 					<view class="as-qty-btn" @click="changeReturnQty(1)">+</view>
45 53
 				</view>
54
+				<text class="as-submit-card__tip">最多 {{ totalQuantity }} 件</text>
46 55
 			</view>
47 56
 
48 57
 			<view class="as-submit-card">
@@ -98,8 +107,8 @@ import ImageUploadGrid from '@/components/order/ImageUploadGrid.vue'
98 107
 const submitGuard = useActionGuard()
99 108
 
100 109
 const orderId = ref('')
101
-const orderItemId = ref('')
102
-const orderItem = ref(null)
110
+/** 整单商品行 */
111
+const orderItems = ref([])
103 112
 const payAmount = ref(0)
104 113
 const pageLoading = ref(true)
105 114
 const submitting = submitGuard.locked
@@ -117,6 +126,10 @@ const evidenceMax = AFTERSALE_EVIDENCE_MAX
117 126
 
118 127
 const reasonOptions = computed(() => AFTERSALE_REASON_MAP[applyType.value] || [])
119 128
 
129
+const totalQuantity = computed(() =>
130
+	orderItems.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
131
+)
132
+
120 133
 const needReturnQty = computed(
121 134
 	() =>
122 135
 		applyType.value === AFTERSALE_APPLY_TYPE.REFUND_SHIPPED ||
@@ -129,26 +142,24 @@ function onApplyTypeChange(value) {
129 142
 }
130 143
 
131 144
 function changeReturnQty(delta) {
132
-	const max = orderItem.value ? orderItem.value.quantity : 1
145
+	const max = totalQuantity.value || 1
133 146
 	const next = returnQuantity.value + delta
134 147
 	returnQuantity.value = Math.max(1, Math.min(max, next))
135 148
 }
136 149
 
137
-async function loadOrderItem() {
150
+async function loadOrder() {
138 151
 	pageLoading.value = true
139 152
 	try {
140 153
 		const res = await getOrderDetail(orderId.value)
141 154
 		const mapped = mapOrderDetail(res.data)
142
-		const row = (mapped.items || []).find(
143
-			(item) => String(item.orderItemId) === String(orderItemId.value)
144
-		)
145
-		if (!row) {
155
+		const items = mapped.items || []
156
+		if (!items.length) {
146 157
 			throw new Error('订单商品不存在')
147 158
 		}
148
-		orderItem.value = row
159
+		orderItems.value = items
149 160
 		payAmount.value = mapped.payAmount
150 161
 		applyAmountInput.value = String(mapped.payAmount || '')
151
-		returnQuantity.value = 1
162
+		returnQuantity.value = totalQuantity.value || 1
152 163
 	} catch (e) {
153 164
 		uni.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
154 165
 		setTimeout(() => uni.navigateBack(), 800)
@@ -168,9 +179,15 @@ function onSubmit() {
168 179
 			uni.showToast({ title: '请填写申请金额', icon: 'none' })
169 180
 			return
170 181
 		}
182
+		const firstItem = orderItems.value[0]
183
+		if (!firstItem?.orderItemId) {
184
+			uni.showToast({ title: '订单商品信息异常', icon: 'none' })
185
+			return
186
+		}
171 187
 		const body = {
172 188
 			orderId: orderId.value,
173
-			orderItemId: orderItemId.value,
189
+			// 接口仍要求 orderItemId;整单售后传首行 ID,金额/数量为整单口径
190
+			orderItemId: firstItem.orderItemId,
174 191
 			applyType: applyType.value,
175 192
 			applyReason: applyReason.value,
176 193
 			applyAmount: amount,
@@ -196,13 +213,12 @@ function onSubmit() {
196 213
 onLoad((options) => {
197 214
 	if (!ensureApiToken(true)) return
198 215
 	orderId.value = options.orderId || ''
199
-	orderItemId.value = options.itemId || options.orderItemId || ''
200
-	if (!orderId.value || !orderItemId.value) {
201
-		uni.showToast({ title: '参数缺失', icon: 'none' })
216
+	if (!orderId.value) {
217
+		uni.showToast({ title: '订单信息缺失', icon: 'none' })
202 218
 		setTimeout(() => uni.navigateBack(), 800)
203 219
 		return
204 220
 	}
205
-	loadOrderItem()
221
+	loadOrder()
206 222
 })
207 223
 </script>
208 224
 
@@ -232,6 +248,15 @@ onLoad((options) => {
232 248
 	font-weight: 600;
233 249
 	color: #333;
234 250
 }
251
+.as-submit-card__tip {
252
+	display: block;
253
+	margin-bottom: 12rpx;
254
+	font-size: 24rpx;
255
+	color: #999;
256
+}
257
+.as-submit-goods :deep(.order-goods-row + .order-goods-row) {
258
+	border-top: 1rpx solid #f5f5f5;
259
+}
235 260
 .as-type-opt,
236 261
 .as-reason-opt {
237 262
 	padding: 16rpx 20rpx;

+ 9 - 2
shop-app/subpackage/order/list.vue

@@ -51,7 +51,10 @@
51 51
 					<view class="order-card__head">
52 52
 						<image class="order-card__shop-avatar" :src="card.shopAvatar" mode="aspectFill" />
53 53
 						<text class="order-card__shop-name">{{ card.shopName }}</text>
54
-						<text class="order-card__status">{{ card.statusText }}</text>
54
+						<text
55
+							class="order-card__status"
56
+							:class="{ 'order-card__status--aftersale': card.statusIsAftersale }"
57
+						>{{ card.statusText }}</text>
55 58
 					</view>
56 59
 
57 60
 					<view v-if="card.items && card.items.length" class="order-card__goods">
@@ -67,7 +70,8 @@
67 70
 					<view class="order-card__sum">
68 71
 						<text class="order-card__time">{{ card.createTime }}</text>
69 72
 						<text class="order-card__amount">
70
-							实付 <text class="order-card__price">¥{{ card.payAmountText }}</text>
73
+							{{ card.amountLabel }}
74
+							<text class="order-card__price">¥{{ card.payAmountText }}</text>
71 75
 						</text>
72 76
 					</view>
73 77
 
@@ -330,6 +334,9 @@ onShow(() => {
330 334
 	font-size: 26rpx;
331 335
 	color: #e65100;
332 336
 }
337
+.order-card__status--aftersale {
338
+	color: #1976d2;
339
+}
333 340
 .order-card__goods :deep(.order-goods-row + .order-goods-row) {
334 341
 	border-top: 1rpx solid #f5f5f5;
335 342
 }

+ 44 - 3
shop-app/utils/cartDisplay.js

@@ -74,21 +74,62 @@ function mapCheckedSummary(summary) {
74 74
   }
75 75
 }
76 76
 
77
+/** 扁平 items[] 按店铺分组(保持接口 cart_item_id 倒序下的店组顺序) */
78
+function groupCartItemsByShop(flatItems) {
79
+  const groups = []
80
+  const groupIndex = new Map()
81
+  for (const row of flatItems || []) {
82
+    const item = mapCartItem(row)
83
+    if (!item) continue
84
+    const shopId = row.shopId
85
+    let group
86
+    if (groupIndex.has(shopId)) {
87
+      group = groups[groupIndex.get(shopId)]
88
+    } else {
89
+      group = {
90
+        shopId,
91
+        shopName: row.shopName || '',
92
+        shopStatus: row.shopStatus,
93
+        isClosed: String(row.shopStatus) === SHOP_STATUS_CLOSED,
94
+        items: []
95
+      }
96
+      groupIndex.set(shopId, groups.length)
97
+      groups.push(group)
98
+    }
99
+    group.items.push(item)
100
+  }
101
+  return groups
102
+}
103
+
77 104
 /** 列表接口 data → 页面模型 */
78 105
 export function mapCartList(data) {
79 106
   if (!data) {
80 107
     return { groups: [], checkedSummary: mapCheckedSummary(null) }
81 108
   }
109
+  const groups = Array.isArray(data.groups) && data.groups.length
110
+    ? data.groups.map(mapShopGroup).filter(Boolean)
111
+    : groupCartItemsByShop(data.items || [])
82 112
   return {
83
-    groups: (data.groups || []).map(mapShopGroup).filter(Boolean),
113
+    groups,
84 114
     checkedSummary: mapCheckedSummary(data.checkedSummary)
85 115
   }
86 116
 }
87 117
 
88 118
 /** 是否存在失效行 */
89 119
 export function hasInvalidCartItems(groups) {
90
-  if (!Array.isArray(groups)) return false
91
-  return groups.some((g) => (g.items || []).some((item) => !item.purchasable))
120
+  return countInvalidCartItems(groups) > 0
121
+}
122
+
123
+/** 失效商品件数(清理失效前展示) */
124
+export function countInvalidCartItems(groups) {
125
+  if (!Array.isArray(groups)) return 0
126
+  let count = 0
127
+  groups.forEach((g) => {
128
+    ;(g.items || []).forEach((item) => {
129
+      if (!item.purchasable) count += 1
130
+    })
131
+  })
132
+  return count
92 133
 }
93 134
 
94 135
 /** 当前勾选的可购行 shopId 集合 */

+ 3 - 17
shop-app/utils/orderAction.js

@@ -52,25 +52,11 @@ function doBuyAgain(ctx) {
52 52
 }
53 53
 
54 54
 function openAftersalePicker(ctx) {
55
-  const items = ctx.items || []
56
-  if (!items.length) {
57
-    uni.showToast({ title: '暂无可售后商品', icon: 'none' })
55
+  if (!ctx.orderId) {
56
+    uni.showToast({ title: '订单信息缺失', icon: 'none' })
58 57
     return Promise.resolve()
59 58
   }
60
-  if (items.length === 1) {
61
-    goAftersaleSubmit(ctx.orderId, items[0].orderItemId)
62
-    return Promise.resolve()
63
-  }
64
-  const names = items.map((row) => row.goodsName || '商品')
65
-  uni.showActionSheet({
66
-    itemList: names,
67
-    success: (res) => {
68
-      const row = items[res.tapIndex]
69
-      if (row && row.orderItemId) {
70
-        goAftersaleSubmit(ctx.orderId, row.orderItemId)
71
-      }
72
-    }
73
-  })
59
+  goAftersaleSubmit(ctx.orderId)
74 60
   return Promise.resolve()
75 61
 }
76 62
 

+ 21 - 2
shop-app/utils/orderDisplay.js

@@ -5,7 +5,9 @@ import {
5 5
   ORDER_ACTION,
6 6
   ORDER_ACTION_LABEL,
7 7
   REVIEW_ITEM_STATUS,
8
-  AFTERSALE_APPLY_TYPE_OPTIONS
8
+  AFTERSALE_APPLY_TYPE_OPTIONS,
9
+  ORDER_STATUS,
10
+  ORDER_AFTERSALE_CARD_LABEL
9 11
 } from '@/constants/order'
10 12
 
11 13
 /** 订单级不再展示的评价按钮(改在商品行) */
@@ -151,6 +153,17 @@ function trimText(text, maxLen, emptyFallback = '') {
151 153
   return `${s.slice(0, maxLen)}…`
152 154
 }
153 155
 
156
+/**
157
+ * 列表卡片状态文案(MO-L4)
158
+ * 有 aftersaleStatus 时优先展示售后态,否则用订单主状态文案
159
+ */
160
+export function mapOrderCardStatusText(orderStatusText, aftersaleStatus) {
161
+  if (aftersaleStatus && ORDER_AFTERSALE_CARD_LABEL[aftersaleStatus]) {
162
+    return ORDER_AFTERSALE_CARD_LABEL[aftersaleStatus]
163
+  }
164
+  return orderStatusText || ''
165
+}
166
+
154 167
 /** 列表行 VO → 卡片模型 */
155 168
 export function mapOrderListRow(row) {
156 169
   if (!row) return null
@@ -161,11 +174,17 @@ export function mapOrderListRow(row) {
161 174
       : []
162 175
   const items = rawItems.map(mapFirstItem).filter(Boolean)
163 176
   const firstItem = items[0] || mapFirstItem(row.firstItem)
177
+  const orderStatusText = row.orderStatusText || ''
178
+  const aftersaleStatus = row.aftersaleStatus || null
164 179
   return {
165 180
     orderId: row.orderId,
166 181
     orderNo: row.orderNo || '',
167 182
     orderStatus: row.orderStatus,
168
-    statusText: row.orderStatusText || '',
183
+    orderStatusText,
184
+    aftersaleStatus,
185
+    statusText: mapOrderCardStatusText(orderStatusText, aftersaleStatus),
186
+    statusIsAftersale: !!aftersaleStatus,
187
+    amountLabel: row.orderStatus === ORDER_STATUS.PENDING_PAY ? '应付' : '实付',
169 188
     shopId: row.shopId,
170 189
     shopName: row.shopName || '',
171 190
     shopAvatar: resolveFileUrl(row.shopAvatar) || SHOP_PLACEHOLDER,

+ 4 - 4
shop-app/utils/orderNav.js

@@ -72,10 +72,10 @@ export function goAftersaleDetail(aftersaleId) {
72 72
   })
73 73
 }
74 74
 
75
-/** 提交售后 */
76
-export function goAftersaleSubmit(orderId, orderItemId) {
77
-  if (!orderId || !orderItemId) return
75
+/** 提交售后(整单,仅需 orderId) */
76
+export function goAftersaleSubmit(orderId) {
77
+  if (!orderId) return
78 78
   uni.navigateTo({
79
-    url: `${PAGE_ORDER_AFTERSALE_SUBMIT}?orderId=${orderId}&itemId=${orderItemId}`
79
+    url: `${PAGE_ORDER_AFTERSALE_SUBMIT}?orderId=${orderId}`
80 80
   })
81 81
 }