Преглед изворни кода

Merge remote-tracking branch 'origin/master'

wwh пре 1 недеља
родитељ
комит
48e849314b

+ 231 - 0
doc/店铺后台/售后管理/售后管理前端技术方案.md

@@ -0,0 +1,231 @@
1
+# 售后管理 — 前端技术方案
2
+
3
+> **依据:** 《售后管理功能需求.md》v1.0、《售后管理技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺售后列表、检索、详情查看、**处理完结**;**不含** C 端提交售后、退款原路退回、平台仲裁。  
6
+> **实现状态:** `index.vue`、`detail.vue`、`api/agri/seller/aftersale.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/aftersale` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/review/index.vue`(检索+页签+抽屉+弹窗)、`agri/seller/ship/index.vue`(组合订单信息列) |
17
+| 布局 | 检索 `el-card` + `<br/>` + 列表 `el-card` + `border` 表格 |
18
+| 详情 | 右侧 `el-drawer`(`size="72%"`,`append-to-body`) |
19
+| 处理完结 | `el-dialog` + **二次确认**(`$modal.confirm`) |
20
+| 图片 | 全局 `image-preview`(凭证图可预览大图) |
21
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
22
+
23
+---
24
+
25
+## 2. 业务要点(前端需体现)
26
+
27
+| 项 | 说明 |
28
+|----|------|
29
+| 数据来源 | C 端买家 **已提交** 的售后;商家 **不可代客发起** |
30
+| 售后状态 | `1` 进行中 / `2` 已完结;**单向** 完结,不可回退 |
31
+| 申请类型 | `1` 仅退款-未发货 / `2` 仅退款-已发货 / `3` 退货退款;**只读展示** |
32
+| 退货数量 | 类型 `1` 列表/详情展示 **—**;其他类型展示数值 |
33
+| 快照 | 商品/订单/会员/收货信息均为提交快照;列表手机 **后端脱敏** |
34
+| 处理完结 | 填写 **处理结果**(≤500 字);**不** 自动退款/关单/回库存 |
35
+| 关联订单 | 详情订单编号点击 → **订单详情抽屉**(只读,复用 `SellerOrderDetail`) |
36
+
37
+---
38
+
39
+## 3. 文件清单
40
+
41
+| 类型 | 路径 | 说明 |
42
+|------|------|------|
43
+| 列表页 | `ruoyi-ui/src/views/agri/seller/aftersale/index.vue` | 检索、状态页签、表格、处理弹窗、详情/订单抽屉宿主 |
44
+| 详情抽屉 | `ruoyi-ui/src/views/agri/seller/aftersale/detail.vue` | 进度条、分区展示、处理按钮(事件上抛) |
45
+| 售后 API | `ruoyi-ui/src/api/agri/seller/aftersale.js` | list、detail、finish |
46
+| 订单详情 | `agri/seller/order/detail.vue` | 嵌套只读抽屉(`:show-actions="false"`) |
47
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
48
+
49
+**组件 name(keep-alive):**
50
+
51
+| 组件 | name |
52
+|------|------|
53
+| 列表页 | `AgriSellerAftersale` |
54
+| 详情抽屉 | `SellerAftersaleDetail` |
55
+
56
+---
57
+
58
+## 4. 菜单与路由
59
+
60
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
61
+|----------|----------|-------------------|----------|
62
+| 售后管理 | `agri/seller/aftersale/index` | `seller/aftersale` | `agri:seller:aftersale:list` |
63
+
64
+**上级菜单:** 店铺经营管理端 → **订单管理**
65
+
66
+| 按钮权限 | 标识 | 页面落点 |
67
+|----------|------|----------|
68
+| 列表 | `agri:seller:aftersale:list` | 进入列表页 |
69
+| 详情 | `agri:seller:aftersale:query` | 操作列「查看详情」 |
70
+| 处理完结 | `agri:seller:aftersale:finish` | 操作列/详情「处理售后」、确认完结 |
71
+
72
+---
73
+
74
+## 5. 页面结构(与代码一致)
75
+
76
+```text
77
+售后管理 index.vue
78
+├── 检索区 search-card
79
+│   ├── 订单编号 orderNo(模糊)
80
+│   ├── 售后编号 aftersaleNo(模糊)
81
+│   ├── 申请类型 applyType(1/2/3,可清空)
82
+│   └── 搜索 / 重置
83
+├── 列表区 table-card
84
+│   ├── 售后状态页签 statusTab:全部(all) | 进行中(1) | 已完结(2)
85
+│   ├── right-toolbar
86
+│   ├── el-table border(empty-text 动态)
87
+│   │   ├── 售后编号 aftersaleNo
88
+│   │   ├── 申请类型 applyTypeText
89
+│   │   ├── 售后订单信息 min-width=300(订单号+商品快照+会员/收货/地址)
90
+│   │   ├── 售后原因 applyReason
91
+│   │   ├── 退货数量(类型1为 —)
92
+│   │   ├── 申请金额 applyAmount
93
+│   │   ├── 申请时间 createTime
94
+│   │   ├── 售后状态 Tag
95
+│   │   ├── 完结时间 finishTime
96
+│   │   └── 操作:查看详情 / 处理售后(仅 status=1)
97
+│   └── pagination
98
+├── 详情抽屉 SellerAftersaleDetail(见 §6)
99
+├── 订单详情抽屉 SellerOrderDetail(只读嵌套)
100
+└── 处理售后弹窗 el-dialog width=560px
101
+    ├── 警告提示(不可回退、不自动退款)
102
+    └── processResult textarea maxlength=500
103
+```
104
+
105
+**不提供:** 页内店铺选择、修改买家申请内容、代客发起、批量处理、导出。
106
+
107
+---
108
+
109
+## 6. 详情抽屉 detail.vue
110
+
111
+### 6.1 对外接口
112
+
113
+| 类型 | 名称 | 说明 |
114
+|------|------|------|
115
+| props | `visible` | 抽屉显隐(`.sync`) |
116
+| props | `aftersaleId` | 当前售后主键 |
117
+| emit | `update:visible` | 关闭抽屉 |
118
+| emit | `finish` | 点击「处理售后」,payload 为详情对象 |
119
+| emit | `view-order` | 点击订单编号,payload 含 `orderId` |
120
+| 方法 | `reload()` | 完结成功后父页调用重载详情 |
121
+
122
+### 6.2 分区结构
123
+
124
+| 区块 | 内容 |
125
+|------|------|
126
+| 流程进度 | `el-steps`:商家处理 → 售后完结(`active=0` 进行中 / `2` 已完结) |
127
+| 售后信息 | 编号、状态、类型、原因、退货数量、申请金额、申请/完结时间 |
128
+| 商品信息 | 主图+名称+规格+购买数量(快照) |
129
+| 订单信息 | 订单编号(可点)、订单总金额 |
130
+| 买家/收货 | 会员名称、收货人、手机、地址 |
131
+| 补充说明 | 买家 `description`(有则展示) |
132
+| 凭证图片 | `evidencePics[]` 网格预览 |
133
+| 处理结果 | 已完结展示 `processResult`;进行中提示待处理 |
134
+| 操作栏 | 进行中显示「处理售后」(finish 权限) |
135
+
136
+---
137
+
138
+## 7. 处理完结交互
139
+
140
+```text
141
+列表/详情 → 处理售后
142
+    → 打开 finish 弹窗,填写 processResult
143
+    → $modal.confirm 二次确认
144
+    → PUT /{aftersaleId}/finish
145
+    → msgSuccess → 刷新列表 → 若详情打开则 reload()
146
+```
147
+
148
+| 校验 | 说明 |
149
+|------|------|
150
+| processResult 必填 | 对应 ASM7 / 后端 MSG |
151
+| maxlength=500 | 对齐需求 §8.2(后端上限 1000,前端按需求 500) |
152
+| 仅 status=`1` | 已完结隐藏处理按钮 |
153
+
154
+---
155
+
156
+## 8. 店铺上下文(X-Shop-Id)
157
+
158
+| 步骤 | 说明 |
159
+|------|------|
160
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
161
+| 全部 API | `sellerShopHeaders()` |
162
+| Navbar 切店 | `location.reload()` 整页刷新 |
163
+
164
+---
165
+
166
+## 9. API 封装
167
+
168
+**模块:** `@/api/agri/seller/aftersale.js`
169
+
170
+| 方法 | HTTP | 路径 | 权限 |
171
+|------|------|------|------|
172
+| `listSellerAftersale` | GET | `/list` | list |
173
+| `getSellerAftersale` | GET | `/{aftersaleId}` | query |
174
+| `finishSellerAftersale` | PUT | `/{aftersaleId}/finish` | finish |
175
+
176
+**列表 Query:** `pageNum`、`pageSize`、`aftersaleNo`、`orderNo`、`applyType`、`aftersaleStatus`。
177
+
178
+**完结 Body:** `{ "processResult": "..." }`
179
+
180
+**排序:** 后端固定 `create_time DESC`(申请时间最新在前)。
181
+
182
+---
183
+
184
+## 10. 空状态与错误提示
185
+
186
+| 场景 | 前端表现 |
187
+|------|----------|
188
+| 无数据 | 「暂无售后申请」 |
189
+| 有筛选无结果 | 「未找到符合条件的售后申请」 |
190
+| 处理结果为空 | 表单校验「请填写处理结果」 |
191
+| 二次确认取消 | 不提交 |
192
+| 提交成功 | 「售后已完结」 |
193
+| 已完结重复处理 | 展示后端「售后已完结,不可重复处理」 |
194
+
195
+---
196
+
197
+## 11. 与兄弟模块边界(前端)
198
+
199
+| 模块 | 关系 |
200
+|------|------|
201
+| **全部订单** | 详情内嵌订单抽屉只读查看;**不在** 订单页完结售后 |
202
+| **发货管理** | 独立菜单;履约与售后并行 |
203
+| **评价管理** | 独立;售后不影响评价入口 |
204
+| **商品入库** | 退货加库存须商家手工「退货入库」,**不** 随售后自动 |
205
+
206
+---
207
+
208
+## 12. 联调检查清单
209
+
210
+- [ ] 菜单挂载 `agri/seller/aftersale/index`,路由可访问
211
+- [ ] 列表默认按申请时间倒序,仅当前店铺数据
212
+- [ ] 页签:全部 / 进行中 / 已完结 与 `aftersaleStatus` 联动
213
+- [ ] 检索:订单编号、售后编号、申请类型组合过滤
214
+- [ ] 进行中显示「处理售后」;已完结隐藏
215
+- [ ] 完结后 C 端详情可读 `processResult` 与进度「售后完结」
216
+- [ ] 凭证图点击预览;列表手机为脱敏值
217
+- [ ] 订单编号打开嵌套订单详情抽屉
218
+- [ ] Navbar 切店后列表刷新
219
+- [ ] 权限:无 `finish` 不显示处理按钮
220
+
221
+---
222
+
223
+## 13. 版本记录
224
+
225
+| 版本 | 说明 |
226
+|------|------|
227
+| **v1.0** | 首版:列表/检索/详情抽屉/处理完结弹窗/API 封装;对齐需求 v1.0 与 C 端售后协作口径 |
228
+
229
+---
230
+
231
+*文档版本:v1.0 · 依据《售后管理功能需求.md》v1.0、《售后管理技术方案.md》v1.0*

+ 31 - 0
ruoyi-ui/src/api/agri/seller/aftersale.js

@@ -0,0 +1,31 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 售后列表
5
+export function listSellerAftersale(query) {
6
+  return request({
7
+    url: '/agri/seller/aftersale/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 售后详情
15
+export function getSellerAftersale(aftersaleId) {
16
+  return request({
17
+    url: '/agri/seller/aftersale/' + aftersaleId,
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 处理完结
24
+export function finishSellerAftersale(aftersaleId, data) {
25
+  return request({
26
+    url: '/agri/seller/aftersale/' + aftersaleId + '/finish',
27
+    method: 'put',
28
+    data: data,
29
+    headers: sellerShopHeaders()
30
+  })
31
+}

+ 244 - 0
ruoyi-ui/src/views/agri/seller/aftersale/detail.vue

@@ -0,0 +1,244 @@
1
+<template>
2
+  <el-drawer
3
+    :title="'售后详情 · ' + (detail.aftersaleNo || '')"
4
+    :visible.sync="localVisible"
5
+    direction="rtl"
6
+    size="72%"
7
+    append-to-body
8
+    custom-class="detail-drawer"
9
+  >
10
+    <div v-loading="loading" class="drawer-content">
11
+      <h4 class="section-header">流程进度</h4>
12
+      <el-steps :active="progressActive" finish-status="success" align-center class="progress-steps mb16">
13
+        <el-step title="商家处理" description="买家已提交,待商家处理" />
14
+        <el-step title="售后完结" description="处理结果同步至买家端" />
15
+      </el-steps>
16
+
17
+      <h4 class="section-header">售后信息</h4>
18
+      <el-descriptions :column="2" border size="small" class="mb16">
19
+        <el-descriptions-item label="售后编号">{{ detail.aftersaleNo || '—' }}</el-descriptions-item>
20
+        <el-descriptions-item label="售后状态">
21
+          <el-tag size="small" :type="aftersaleStatusTag(detail.aftersaleStatus)">
22
+            {{ detail.aftersaleStatusText || aftersaleStatusLabel(detail.aftersaleStatus) }}
23
+          </el-tag>
24
+        </el-descriptions-item>
25
+        <el-descriptions-item label="申请类型">{{ detail.applyTypeText || applyTypeLabel(detail.applyType) }}</el-descriptions-item>
26
+        <el-descriptions-item label="售后原因">{{ detail.applyReason || '—' }}</el-descriptions-item>
27
+        <el-descriptions-item label="退货数量">{{ returnQuantityText(detail) }}</el-descriptions-item>
28
+        <el-descriptions-item label="申请金额">¥{{ detail.applyAmount != null ? detail.applyAmount : '—' }}</el-descriptions-item>
29
+        <el-descriptions-item label="申请时间">{{ parseTime(detail.createTime) || '—' }}</el-descriptions-item>
30
+        <el-descriptions-item label="完结时间">{{ parseTime(detail.finishTime) || '—' }}</el-descriptions-item>
31
+      </el-descriptions>
32
+
33
+      <h4 class="section-header">商品信息</h4>
34
+      <div class="goods-target mb16">
35
+        <image-preview v-if="detail.goodsImage" :src="detail.goodsImage" :width="64" :height="64" />
36
+        <div class="goods-text">
37
+          <div class="goods-name">{{ detail.goodsName || '—' }}</div>
38
+          <div class="sub-text">{{ detail.goodsSpec || '默认' }}</div>
39
+          <div class="sub-text">购买数量 × {{ detail.quantity != null ? detail.quantity : '—' }}</div>
40
+        </div>
41
+      </div>
42
+
43
+      <h4 class="section-header">订单信息</h4>
44
+      <el-descriptions :column="2" border size="small" class="mb16">
45
+        <el-descriptions-item label="订单编号">
46
+          <el-button v-if="detail.orderId && detail.orderNo" type="text" class="order-link" @click="handleViewOrder">{{ detail.orderNo }}</el-button>
47
+          <span v-else-if="detail.orderNo">{{ detail.orderNo }}</span>
48
+          <span v-else>—</span>
49
+        </el-descriptions-item>
50
+        <el-descriptions-item label="订单总金额">¥{{ detail.payAmount != null ? detail.payAmount : '—' }}</el-descriptions-item>
51
+      </el-descriptions>
52
+
53
+      <h4 class="section-header">买家/收货信息</h4>
54
+      <el-descriptions :column="2" border size="small" class="mb16">
55
+        <el-descriptions-item label="会员名称">{{ detail.memberNickName || '—' }}</el-descriptions-item>
56
+        <el-descriptions-item label="收货人">{{ detail.consigneeName || '—' }}</el-descriptions-item>
57
+        <el-descriptions-item label="手机号">{{ detail.consigneeMobile || '—' }}</el-descriptions-item>
58
+        <el-descriptions-item label="收货地址" :span="2">{{ detail.consigneeAddress || '—' }}</el-descriptions-item>
59
+      </el-descriptions>
60
+
61
+      <h4 v-if="detail.description" class="section-header">补充说明</h4>
62
+      <div v-if="detail.description" class="text-block mb16">{{ detail.description }}</div>
63
+
64
+      <h4 v-if="detail.evidencePics && detail.evidencePics.length" class="section-header">凭证图片</h4>
65
+      <div v-if="detail.evidencePics && detail.evidencePics.length" class="pics-row mb16">
66
+        <image-preview
67
+          v-for="(pic, idx) in detail.evidencePics"
68
+          :key="idx"
69
+          :src="pic"
70
+          :width="72"
71
+          :height="72"
72
+          class="pic-item"
73
+        />
74
+      </div>
75
+
76
+      <h4 class="section-header">处理结果</h4>
77
+      <el-descriptions :column="1" border size="small" class="mb16">
78
+        <el-descriptions-item v-if="detail.processResult" label="商家处理">
79
+          <div class="text-block">{{ detail.processResult }}</div>
80
+        </el-descriptions-item>
81
+        <el-descriptions-item v-else label="提示">
82
+          <span class="pending-tip">待处理,请填写处理结果并完结售后</span>
83
+        </el-descriptions-item>
84
+      </el-descriptions>
85
+
86
+      <div v-if="canFinish" class="action-bar">
87
+        <el-button type="primary" plain @click="handleFinish" v-hasPermi="['agri:seller:aftersale:finish']">处理售后</el-button>
88
+      </div>
89
+    </div>
90
+  </el-drawer>
91
+</template>
92
+
93
+<script>
94
+import { getSellerAftersale } from "@/api/agri/seller/aftersale"
95
+
96
+const APPLY_TYPE_MAP = {
97
+  "1": "仅退款-未发货",
98
+  "2": "仅退款-已发货",
99
+  "3": "退货退款"
100
+}
101
+
102
+const AFTERSALE_STATUS_MAP = {
103
+  "1": "进行中",
104
+  "2": "已完结"
105
+}
106
+
107
+export default {
108
+  name: "SellerAftersaleDetail",
109
+  props: {
110
+    visible: { type: Boolean, default: false },
111
+    aftersaleId: { type: [Number, String], default: null }
112
+  },
113
+  data() {
114
+    return {
115
+      localVisible: false,
116
+      loading: false,
117
+      detail: {}
118
+    }
119
+  },
120
+  computed: {
121
+    progressActive() {
122
+      return this.detail.aftersaleStatus === "2" ? 2 : 0
123
+    },
124
+    canFinish() {
125
+      return this.detail.aftersaleStatus === "1"
126
+    }
127
+  },
128
+  watch: {
129
+    visible(val) {
130
+      this.localVisible = val
131
+      if (val && this.aftersaleId) {
132
+        this.loadDetail()
133
+      }
134
+    },
135
+    localVisible(val) {
136
+      this.$emit("update:visible", val)
137
+    },
138
+    aftersaleId(val) {
139
+      if (val && this.localVisible) {
140
+        this.loadDetail()
141
+      }
142
+    }
143
+  },
144
+  methods: {
145
+    applyTypeLabel(type) {
146
+      return APPLY_TYPE_MAP[type] || "—"
147
+    },
148
+    aftersaleStatusLabel(status) {
149
+      return AFTERSALE_STATUS_MAP[status] || "—"
150
+    },
151
+    aftersaleStatusTag(status) {
152
+      const map = { "1": "warning", "2": "success" }
153
+      return map[status] || "info"
154
+    },
155
+    returnQuantityText(detail) {
156
+      if (detail.applyType === "1") {
157
+        return "—"
158
+      }
159
+      return detail.returnQuantity != null ? detail.returnQuantity : "—"
160
+    },
161
+    loadDetail() {
162
+      if (!this.aftersaleId) {
163
+        return
164
+      }
165
+      this.loading = true
166
+      getSellerAftersale(this.aftersaleId).then(response => {
167
+        this.detail = response.data || {}
168
+        this.loading = false
169
+      }).catch(() => {
170
+        this.loading = false
171
+      })
172
+    },
173
+    reload() {
174
+      this.loadDetail()
175
+    },
176
+    handleViewOrder() {
177
+      this.$emit("view-order", this.detail)
178
+    },
179
+    handleFinish() {
180
+      this.$emit("finish", this.detail)
181
+    }
182
+  }
183
+}
184
+</script>
185
+
186
+<style scoped lang="scss">
187
+.drawer-content {
188
+  padding: 0 20px 20px;
189
+}
190
+.section-header {
191
+  margin: 16px 0 10px;
192
+  font-size: 15px;
193
+  color: #303133;
194
+  border-left: 3px solid #409EFF;
195
+  padding-left: 8px;
196
+}
197
+.progress-steps {
198
+  padding: 8px 0 4px;
199
+}
200
+.mb16 {
201
+  margin-bottom: 16px;
202
+}
203
+.goods-target {
204
+  display: flex;
205
+  align-items: flex-start;
206
+  gap: 12px;
207
+  padding: 12px;
208
+  background: #fafafa;
209
+  border-radius: 4px;
210
+}
211
+.goods-name {
212
+  font-weight: 500;
213
+  color: #303133;
214
+}
215
+.sub-text {
216
+  margin-top: 4px;
217
+  font-size: 12px;
218
+  color: #909399;
219
+}
220
+.text-block {
221
+  line-height: 1.6;
222
+  white-space: pre-wrap;
223
+  word-break: break-word;
224
+}
225
+.pics-row {
226
+  display: flex;
227
+  flex-wrap: wrap;
228
+  gap: 8px;
229
+}
230
+.pic-item {
231
+  flex-shrink: 0;
232
+}
233
+.pending-tip {
234
+  color: #E6A23C;
235
+}
236
+.order-link {
237
+  padding: 0;
238
+}
239
+.action-bar {
240
+  margin-top: 24px;
241
+  padding-top: 16px;
242
+  border-top: 1px solid #EBEEF5;
243
+}
244
+</style>

+ 387 - 0
ruoyi-ui/src/views/agri/seller/aftersale/index.vue

@@ -0,0 +1,387 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 检索区 -->
4
+    <el-card shadow="never" class="search-card">
5
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
6
+        <el-form-item label="订单编号" prop="orderNo">
7
+          <el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item label="售后编号" prop="aftersaleNo">
10
+          <el-input v-model="queryParams.aftersaleNo" placeholder="售后编号" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
11
+        </el-form-item>
12
+        <el-form-item label="申请类型" prop="applyType">
13
+          <el-select v-model="queryParams.applyType" placeholder="全部" clearable style="width: 150px">
14
+            <el-option v-for="item in applyTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
15
+          </el-select>
16
+        </el-form-item>
17
+        <el-form-item>
18
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
19
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
20
+        </el-form-item>
21
+      </el-form>
22
+    </el-card>
23
+
24
+    <br/>
25
+
26
+    <!-- 列表区 -->
27
+    <el-card shadow="never" class="table-card">
28
+      <el-tabs v-model="statusTab" @tab-click="handleTabClick">
29
+        <el-tab-pane label="全部" name="all" />
30
+        <el-tab-pane label="进行中" name="1" />
31
+        <el-tab-pane label="已完结" name="2" />
32
+      </el-tabs>
33
+
34
+      <el-row :gutter="10" class="mb8">
35
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
36
+      </el-row>
37
+
38
+      <el-table border v-loading="loading" :data="aftersaleList" :empty-text="emptyTableText">
39
+        <el-table-column label="售后编号" align="center" prop="aftersaleNo" min-width="160" :show-overflow-tooltip="true" />
40
+        <el-table-column label="申请类型" align="center" width="130">
41
+          <template slot-scope="scope">
42
+            <span>{{ scope.row.applyTypeText || applyTypeLabel(scope.row.applyType) }}</span>
43
+          </template>
44
+        </el-table-column>
45
+        <el-table-column label="售后订单信息" align="left" min-width="300">
46
+          <template slot-scope="scope">
47
+            <div class="order-info">
48
+              <div class="order-no">订单号:{{ scope.row.orderNo || '—' }}</div>
49
+              <div class="goods-info">
50
+                <image-preview v-if="scope.row.goodsImage" :src="scope.row.goodsImage" :width="44" :height="44" />
51
+                <div class="goods-text">
52
+                  <div>{{ scope.row.goodsName || '—' }}</div>
53
+                  <div class="sub-text">
54
+                    {{ scope.row.goodsSpec || '默认' }}
55
+                    × {{ scope.row.quantity != null ? scope.row.quantity : '—' }}
56
+                    · ¥{{ scope.row.payAmount != null ? scope.row.payAmount : '—' }}
57
+                  </div>
58
+                  <div class="sub-text">
59
+                    {{ scope.row.memberNickName || '—' }}
60
+                    · {{ scope.row.consigneeName || '—' }}
61
+                    · {{ scope.row.consigneeMobile || '—' }}
62
+                  </div>
63
+                  <div class="sub-text addr-text">{{ scope.row.consigneeAddress || '—' }}</div>
64
+                </div>
65
+              </div>
66
+            </div>
67
+          </template>
68
+        </el-table-column>
69
+        <el-table-column label="售后原因" align="center" prop="applyReason" width="120" :show-overflow-tooltip="true">
70
+          <template slot-scope="scope">
71
+            <span>{{ scope.row.applyReason || '—' }}</span>
72
+          </template>
73
+        </el-table-column>
74
+        <el-table-column label="退货数量" align="center" width="90">
75
+          <template slot-scope="scope">
76
+            <span>{{ returnQuantityText(scope.row) }}</span>
77
+          </template>
78
+        </el-table-column>
79
+        <el-table-column label="申请金额" align="center" width="100">
80
+          <template slot-scope="scope">
81
+            <span>¥{{ scope.row.applyAmount != null ? scope.row.applyAmount : '—' }}</span>
82
+          </template>
83
+        </el-table-column>
84
+        <el-table-column label="申请时间" align="center" width="160">
85
+          <template slot-scope="scope">
86
+            <span>{{ parseTime(scope.row.createTime) || '—' }}</span>
87
+          </template>
88
+        </el-table-column>
89
+        <el-table-column label="售后状态" align="center" width="90">
90
+          <template slot-scope="scope">
91
+            <el-tag size="small" :type="aftersaleStatusTag(scope.row.aftersaleStatus)">
92
+              {{ scope.row.aftersaleStatusText || aftersaleStatusLabel(scope.row.aftersaleStatus) }}
93
+            </el-tag>
94
+          </template>
95
+        </el-table-column>
96
+        <el-table-column label="完结时间" align="center" width="160">
97
+          <template slot-scope="scope">
98
+            <span>{{ parseTime(scope.row.finishTime) || '—' }}</span>
99
+          </template>
100
+        </el-table-column>
101
+        <el-table-column label="操作" align="center" width="180" fixed="right">
102
+          <template slot-scope="scope">
103
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['agri:seller:aftersale:query']">查看详情</el-button>
104
+            <el-button
105
+              v-if="scope.row.aftersaleStatus === '1'"
106
+              size="mini"
107
+              type="text"
108
+              icon="el-icon-edit-outline"
109
+              @click="handleFinish(scope.row)"
110
+              v-hasPermi="['agri:seller:aftersale:finish']"
111
+            >处理售后</el-button>
112
+          </template>
113
+        </el-table-column>
114
+      </el-table>
115
+
116
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
117
+    </el-card>
118
+
119
+    <!-- 详情抽屉 -->
120
+    <seller-aftersale-detail
121
+      ref="aftersaleDetail"
122
+      :visible.sync="detailOpen"
123
+      :aftersale-id="currentAftersaleId"
124
+      @finish="openFinishDialog"
125
+      @view-order="handleViewOrder"
126
+    />
127
+
128
+    <!-- 关联订单详情抽屉(只读) -->
129
+    <seller-order-detail
130
+      :visible.sync="orderDetailOpen"
131
+      :order-id="currentOrderId"
132
+      :show-actions="false"
133
+    />
134
+
135
+    <!-- 处理售后弹窗 -->
136
+    <el-dialog title="处理售后" :visible.sync="finishOpen" width="560px" append-to-body @close="cancelFinish">
137
+      <el-alert
138
+        title="完结后不可回退,处理结果将同步展示给买家;本操作不自动退款或变更订单状态。"
139
+        type="warning"
140
+        :closable="false"
141
+        show-icon
142
+        class="finish-alert"
143
+      />
144
+      <el-form ref="finishFormRef" :model="finishForm" :rules="finishRules" label-width="90px" class="finish-form">
145
+        <el-form-item label="处理结果" prop="processResult">
146
+          <el-input
147
+            v-model="finishForm.processResult"
148
+            type="textarea"
149
+            :rows="5"
150
+            placeholder="请输入处理结果,买家可在退款/售后详情查看"
151
+            maxlength="500"
152
+            show-word-limit
153
+          />
154
+        </el-form-item>
155
+      </el-form>
156
+      <div slot="footer" class="dialog-footer">
157
+        <el-button type="primary" @click="submitFinish">确认完结</el-button>
158
+        <el-button @click="cancelFinish">取 消</el-button>
159
+      </div>
160
+    </el-dialog>
161
+  </div>
162
+</template>
163
+
164
+<script>
165
+import {
166
+  listSellerAftersale,
167
+  finishSellerAftersale
168
+} from "@/api/agri/seller/aftersale"
169
+import { getSellerContext } from "@/api/agri/seller/context"
170
+import { setSellerShopContext } from "@/utils/sellerShop"
171
+import SellerAftersaleDetail from "./detail"
172
+import SellerOrderDetail from "../order/detail"
173
+
174
+const APPLY_TYPE_MAP = {
175
+  "1": "仅退款-未发货",
176
+  "2": "仅退款-已发货",
177
+  "3": "退货退款"
178
+}
179
+
180
+const AFTERSALE_STATUS_MAP = {
181
+  "1": "进行中",
182
+  "2": "已完结"
183
+}
184
+
185
+export default {
186
+  name: "AgriSellerAftersale",
187
+  components: { SellerAftersaleDetail, SellerOrderDetail },
188
+  data() {
189
+    return {
190
+      loading: false,
191
+      showSearch: true,
192
+      total: 0,
193
+      aftersaleList: [],
194
+      statusTab: "all",
195
+      detailOpen: false,
196
+      currentAftersaleId: null,
197
+      orderDetailOpen: false,
198
+      currentOrderId: null,
199
+      finishOpen: false,
200
+      finishSubmitting: false,
201
+      currentFinishAftersaleId: null,
202
+      queryParams: {
203
+        pageNum: 1,
204
+        pageSize: 10,
205
+        aftersaleNo: undefined,
206
+        orderNo: undefined,
207
+        applyType: undefined,
208
+        aftersaleStatus: undefined
209
+      },
210
+      applyTypeOptions: [
211
+        { value: "1", label: "仅退款-未发货" },
212
+        { value: "2", label: "仅退款-已发货" },
213
+        { value: "3", label: "退货退款" }
214
+      ],
215
+      finishForm: {
216
+        processResult: ""
217
+      },
218
+      finishRules: {
219
+        processResult: [{ required: true, message: "请填写处理结果", trigger: "blur" }]
220
+      }
221
+    }
222
+  },
223
+  computed: {
224
+    hasSearchFilter() {
225
+      const q = this.queryParams
226
+      return !!(q.aftersaleNo || q.orderNo || q.applyType || q.aftersaleStatus)
227
+    },
228
+    emptyTableText() {
229
+      return this.hasSearchFilter ? "未找到符合条件的售后申请" : "暂无售后申请"
230
+    }
231
+  },
232
+  created() {
233
+    this.initPage()
234
+  },
235
+  methods: {
236
+    applyTypeLabel(type) {
237
+      return APPLY_TYPE_MAP[type] || "—"
238
+    },
239
+    aftersaleStatusLabel(status) {
240
+      return AFTERSALE_STATUS_MAP[status] || "—"
241
+    },
242
+    aftersaleStatusTag(status) {
243
+      const map = { "1": "warning", "2": "success" }
244
+      return map[status] || "info"
245
+    },
246
+    returnQuantityText(row) {
247
+      if (row.applyType === "1") {
248
+        return "—"
249
+      }
250
+      return row.returnQuantity != null ? row.returnQuantity : "—"
251
+    },
252
+    initPage() {
253
+      this.loadShopContext().then(() => {
254
+        this.getList()
255
+      }).catch(() => {
256
+        this.getList()
257
+      })
258
+    },
259
+    loadShopContext() {
260
+      return getSellerContext().then(response => {
261
+        const data = response.data || {}
262
+        if (data.shopId != null) {
263
+          setSellerShopContext(data.shopId, data.shopName)
264
+        }
265
+      })
266
+    },
267
+    getList() {
268
+      this.loading = true
269
+      listSellerAftersale(this.queryParams).then(response => {
270
+        this.aftersaleList = response.rows || []
271
+        this.total = response.total || 0
272
+        this.loading = false
273
+      }).catch(() => {
274
+        this.loading = false
275
+      })
276
+    },
277
+    handleTabClick() {
278
+      this.queryParams.aftersaleStatus = this.statusTab === "all" ? undefined : this.statusTab
279
+      this.queryParams.pageNum = 1
280
+      this.getList()
281
+    },
282
+    handleQuery() {
283
+      this.queryParams.pageNum = 1
284
+      this.getList()
285
+    },
286
+    resetQuery() {
287
+      this.resetForm("queryForm")
288
+      this.statusTab = "all"
289
+      this.queryParams.aftersaleStatus = undefined
290
+      this.queryParams.pageNum = 1
291
+      this.getList()
292
+    },
293
+    handleDetail(row) {
294
+      this.currentAftersaleId = row.aftersaleId
295
+      this.detailOpen = true
296
+    },
297
+    handleViewOrder(detail) {
298
+      if (!detail.orderId) {
299
+        return
300
+      }
301
+      this.currentOrderId = detail.orderId
302
+      this.orderDetailOpen = true
303
+    },
304
+    openFinishDialog(detail) {
305
+      this.handleFinish(detail)
306
+    },
307
+    handleFinish(row) {
308
+      if (row.aftersaleStatus !== "1") {
309
+        return
310
+      }
311
+      this.currentFinishAftersaleId = row.aftersaleId
312
+      this.finishForm.processResult = ""
313
+      this.finishOpen = true
314
+      this.$nextTick(() => {
315
+        if (this.$refs.finishFormRef) {
316
+          this.$refs.finishFormRef.clearValidate()
317
+        }
318
+      })
319
+    },
320
+    cancelFinish() {
321
+      this.finishOpen = false
322
+      this.currentFinishAftersaleId = null
323
+      this.finishForm.processResult = ""
324
+      if (this.$refs.finishFormRef) {
325
+        this.$refs.finishFormRef.resetFields()
326
+      }
327
+    },
328
+    submitFinish() {
329
+      this.$refs.finishFormRef.validate(valid => {
330
+        if (!valid) {
331
+          return
332
+        }
333
+        this.$modal.confirm("确认完结该售后单?完结后不可回退,处理结果将同步至买家端。").then(() => {
334
+          finishSellerAftersale(this.currentFinishAftersaleId, {
335
+            processResult: this.finishForm.processResult.trim()
336
+          }).then(() => {
337
+            this.$modal.msgSuccess("售后已完结")
338
+            this.finishOpen = false
339
+            this.getList()
340
+            if (this.detailOpen && this.$refs.aftersaleDetail) {
341
+              this.$refs.aftersaleDetail.reload()
342
+            }
343
+          })
344
+        }).catch(() => {})
345
+      })
346
+    }
347
+  }
348
+}
349
+</script>
350
+
351
+<style scoped lang="scss">
352
+.mb8 {
353
+  margin-bottom: 8px;
354
+}
355
+.order-info {
356
+  .order-no {
357
+    font-weight: 600;
358
+    margin-bottom: 6px;
359
+  }
360
+}
361
+.goods-info {
362
+  display: flex;
363
+  align-items: flex-start;
364
+  gap: 8px;
365
+}
366
+.goods-text {
367
+  flex: 1;
368
+  min-width: 0;
369
+}
370
+.sub-text {
371
+  color: #909399;
372
+  font-size: 12px;
373
+  line-height: 1.4;
374
+}
375
+.addr-text {
376
+  display: -webkit-box;
377
+  -webkit-line-clamp: 2;
378
+  -webkit-box-orient: vertical;
379
+  overflow: hidden;
380
+}
381
+.finish-alert {
382
+  margin-bottom: 16px;
383
+}
384
+.finish-form {
385
+  margin-top: 4px;
386
+}
387
+</style>

+ 2 - 2
ruoyi-ui/src/views/agri/seller/ship/index.vue

@@ -54,7 +54,7 @@
54 54
 
55 55
       <el-row :gutter="10" class="mb8">
56 56
         <el-col :span="1.5">
57
-          <el-button type="primary" plain icon="el-icon-document" size="mini" @click="goAllOrders" v-hasPermi="['agri:seller:order:list']">查看全部订单</el-button>
57
+          <!-- <el-button type="primary" plain icon="el-icon-document" size="mini" @click="goAllOrders" v-hasPermi="['agri:seller:order:list']">查看全部订单</el-button> -->
58 58
         </el-col>
59 59
         <right-toolbar :showSearch.sync="showSearch" @queryTable="refreshData"></right-toolbar>
60 60
       </el-row>
@@ -466,7 +466,7 @@ export default {
466 466
       this.getList()
467 467
     },
468 468
     goAllOrders() {
469
-      this.$router.push({ path: "/seller/order" })
469
+      this.$router.push({ path: "/sellerOrder/seller/order" })
470 470
     },
471 471
     handleDetail(row) {
472 472
       this.currentOrderId = row.orderId