xsh_1997 il y a 2 semaines
Parent
commit
c859f4bfc7

+ 203 - 0
doc/店铺后台/商品管理/商品列表/商品列表前端技术方案.md

@@ -0,0 +1,203 @@
1
+# 商品列表 — 前端技术方案
2
+
3
+> **依据:** 《商品列表功能需求.md》v1.1、《店铺商品列表技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端(店铺经营管理端)** 当前店铺商品列表、检索、发品(v1.0 单规格)、编辑、提交上架、下架、删除;平台审核 **不在本页**。  
6
+> **实现状态:** 页面与 API 封装已落地,待菜单配置及 `/agri/seller/goods` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI(与 RuoYi-Vue 一致) |
15
+| 请求 | `@/utils/request`;商家端接口携带 **`X-Shop-Id`** |
16
+| 参考页面 | `ruoyi-ui/src/views/agri/goods/audit/index.vue`(列表 Tab、批量操作) |
17
+| 商家端参考 | `ruoyi-ui/src/views/agri/seller/employee/index.vue`(`loadShopContext`) |
18
+| 布局 | 检索区 `el-card` + `<br/>` + 列表区 `el-card` + 表格 `border` |
19
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器(§5 商家端店铺上下文) |
20
+
21
+---
22
+
23
+## 2. 文件清单
24
+
25
+| 类型 | 路径 | 说明 |
26
+|------|------|------|
27
+| 列表页 | `ruoyi-ui/src/views/agri/seller/goods/index.vue` | 检索、Tab、列表、批量操作、发品/编辑弹窗 |
28
+| 详情抽屉 | `ruoyi-ui/src/views/agri/seller/goods/detail.vue` | 详情展示、行内操作 |
29
+| 商品 API | `ruoyi-ui/src/api/agri/seller/goods.js` | 列表、下拉、CRUD、submit、offShelf、delete |
30
+| 店铺上下文 | `ruoyi-ui/src/api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
31
+
32
+**组件 name(keep-alive):** `AgriSellerGoods`
33
+
34
+---
35
+
36
+## 3. 菜单与路由
37
+
38
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
39
+|----------|----------|-------------------|----------|
40
+| 商品列表 | `agri/seller/goods/index` | `seller/goods` | `agri:seller:goods:list` |
41
+
42
+**上级菜单:** 店铺经营管理端 → **商品管理**
43
+
44
+| 按钮权限 | 标识 |
45
+|----------|------|
46
+| 详情 | `agri:seller:goods:query` |
47
+| 添加 / 编辑 / 提交上架 | `agri:seller:goods:edit` |
48
+| 添加 | `agri:seller:goods:add` |
49
+| 下架 | `agri:seller:goods:offshelf` |
50
+| 删除 | `agri:seller:goods:remove` |
51
+
52
+---
53
+
54
+## 4. 页面结构
55
+
56
+```text
57
+商品列表
58
+├── 检索区(el-card)
59
+│   ├── 商品编号 goodsSn(模糊)
60
+│   ├── 商品名称 goodsName(模糊)
61
+│   └── 商品分类 categoryId(平台二级下拉)
62
+├── 列表区(el-card + border 表格)
63
+│   ├── 状态 Tab:全部 / 未上架 / 待审核 / 出售中 / 审核失败 / 已下架
64
+│   ├── 工具栏:添加商品、批量提交上架、批量下架、批量删除
65
+│   ├── 列:编号、主图、名称、售价、库存、销量、商品分类、店铺分类、状态
66
+│   └── 行操作(按状态):详情、编辑、提交上架、下架、删除
67
+├── 详情抽屉 detail.vue(72% 宽)
68
+│   ├── 基础 / 分类 / 状态 / 服务快照 / 详情 HTML
69
+│   └── 操作:编辑、提交上架、下架、删除(can* 或状态驱动)
70
+└── 添加/编辑弹窗(720px · v1.0 单规格)
71
+    ├── 商品分类 categoryId(必填,平台二级)
72
+    ├── 店铺商品分类 shopCategoryId(选填,本店二级)
73
+    ├── 商品名称、主图、销售价、库存
74
+    ├── 商品详情 detailContent(Editor 富文本)
75
+    └── 服务说明 serviceIds(多选;新建默认勾选 defaultShow)
76
+```
77
+
78
+**v1.0 未实现(后续扩展):** 多图 gallery、多规格 SKU、运费模版、属性模版、独立 form 路由页。
79
+
80
+---
81
+
82
+## 5. 店铺上下文(X-Shop-Id)
83
+
84
+| 步骤 | 说明 |
85
+|------|------|
86
+| 页面 `created` | `getSellerContext()` → `setSellerShopContext` |
87
+| 每次 API | `sellerShopHeaders()` 注入 `X-Shop-Id` |
88
+| 切换店铺 | **仅 Navbar** → `location.reload()` |
89
+
90
+---
91
+
92
+## 6. 接口封装
93
+
94
+**Base URL:** `/agri/seller/goods`
95
+
96
+| 前端方法 | HTTP | 路径 | 用途 |
97
+|----------|------|------|------|
98
+| `listSellerGoods` | GET | `/list` | 分页列表 |
99
+| `sellerGoodsCategoryOptions` | GET | `/categoryOptions` | 平台二级分类 |
100
+| `sellerGoodsShopCategoryOptions` | GET | `/shopCategoryOptions` | 本店二级店铺分类 |
101
+| `sellerGoodsServiceOptions` | GET | `/serviceOptions` | `{ all, defaultShow }` |
102
+| `getSellerGoods` | GET | `/{goodsId}` | 详情 |
103
+| `addSellerGoods` | POST | `/` | 新增 → status=0 |
104
+| `updateSellerGoods` | PUT | `/` | 编辑(不改状态) |
105
+| `submitSellerGoods` | PUT | `/{goodsId}/submit` | 单条提交上架 |
106
+| `submitSellerGoodsBatch` | PUT | `/submit` | 批量提交 |
107
+| `offShelfSellerGoods` | PUT | `/{goodsId}/offShelf` | 单条下架 |
108
+| `offShelfSellerGoodsBatch` | PUT | `/offShelf` | 批量下架 |
109
+| `delSellerGoods` | DELETE | `/{goodsIds}` | 删除(逗号分隔批量) |
110
+
111
+### 6.1 列表 Query
112
+
113
+| 字段 | 说明 |
114
+|------|------|
115
+| pageNum, pageSize | 分页 |
116
+| goodsSn, goodsName | 模糊 |
117
+| categoryId | 平台二级分类 ID |
118
+| goodsStatusQuery | `0`~`4`;Tab 切换写入;不传查全部 |
119
+
120
+> 注意:商家列表用 **`goodsStatusQuery`**,与平台 `/agri/goods/list` 的 `goodsStatus` 字段名不同。
121
+
122
+### 6.2 商品状态(goods_status)
123
+
124
+| 值 | 标签 | 行操作 |
125
+|----|------|--------|
126
+| 0 | 未上架 | 详情、编辑、提交上架、删除 |
127
+| 1 | 待审核 | 详情、编辑 |
128
+| 2 | 出售中 | 详情、编辑、下架 |
129
+| 3 | 审核失败 | 详情、编辑、提交上架、删除 |
130
+| 4 | 已下架 | 详情、编辑、提交上架、删除 |
131
+
132
+### 6.3 保存体(GoodsSaveDTO · v1.0)
133
+
134
+| 字段 | 新增 | 编辑 | 说明 |
135
+|------|------|------|------|
136
+| categoryId | 必填 | 必填 | 平台二级 |
137
+| shopCategoryId | 选填 | 选填 | 本店二级 |
138
+| goodsName | 必填 | 必填 | max 200 |
139
+| mainPic | 必填 | 必填 | image-upload |
140
+| detailContent | 必填 | 必填 | 富文本 |
141
+| salePrice | 必填 | 必填 | ≥0 |
142
+| stock | 必填 | 必填 | 整数 ≥0 |
143
+| serviceIds | 选填 | 选填 | 数组 |
144
+| goodsId | — | 必填 | 编辑 |
145
+| goodsStatus | **禁止** | **禁止** | 后端拒收 |
146
+
147
+保存后状态 **不变**(新建为 0);须单独调用 submit 接口上架。
148
+
149
+### 6.4 批量操作规则
150
+
151
+| 操作 | 允许状态 | 失败策略 |
152
+|------|----------|----------|
153
+| 批量提交 | 0、3、4 | 含其他状态 → 整批失败,`data.reasons` |
154
+| 批量下架 | 2 | 同上 |
155
+| 批量删除 | 0、3、4 | 同上 |
156
+
157
+前端通过 `selection.every(...)` 预禁用按钮;最终以服务端校验为准。
158
+
159
+### 6.5 详情 VO 扩展字段
160
+
161
+| 字段 | 用途 |
162
+|------|------|
163
+| canSubmit / canOffShelf / canDelete / canEdit | 按钮显隐 |
164
+| rejectReason | 审核失败展示 |
165
+| services[] | 服务快照;编辑时回显 `serviceIds` |
166
+| shopCategoryPath | 店铺分类路径 |
167
+
168
+---
169
+
170
+## 7. 交互要点
171
+
172
+| 场景 | 行为 |
173
+|------|------|
174
+| 添加成功 | 提示「未上架」;不自动提交 |
175
+| 提交成功 | 展示返回的 `goodsStatusLabel`(待审核 / 出售中) |
176
+| 下架 | 二次确认「C 端不可购买」 |
177
+| 删除 | 二次确认;待审核/出售中不可删 |
178
+| 审核失败 | 详情抽屉红色展示 `rejectReason` |
179
+| 服务默认勾选 | `serviceOptions.defaultShow` → 新建表单 `serviceIds` |
180
+
181
+---
182
+
183
+## 8. 联调检查清单
184
+
185
+- [ ] 菜单组件路径 `agri/seller/goods/index` 及按钮权限
186
+- [ ] Navbar 切换店铺后列表数据隔离
187
+- [ ] 五态 Tab 筛选正确
188
+- [ ] 添加 → 未上架 → 提交上架(免审开/关两种策略)
189
+- [ ] 出售中编辑保存后状态不变
190
+- [ ] 批量操作含非法状态时后端报错
191
+- [ ] 分类/店铺分类/服务下拉数据正确
192
+
193
+---
194
+
195
+## 9. 修订记录
196
+
197
+| 版本 | 说明 |
198
+|------|------|
199
+| v1.0 | 首版:列表 + v1.0 单规格发品弹窗 + 详情抽屉 + 状态流转操作 |
200
+
201
+---
202
+
203
+*文档版本:v1.0 · 关联《商品列表功能需求.md》v1.1、《店铺商品列表技术方案.md》v1.0*

+ 116 - 0
ruoyi-ui/src/api/agri/seller/goods.js

@@ -0,0 +1,116 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 查询当前店铺商品列表
5
+export function listSellerGoods(query) {
6
+  return request({
7
+    url: '/agri/seller/goods/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 平台二级「商品分类」下拉
15
+export function sellerGoodsCategoryOptions() {
16
+  return request({
17
+    url: '/agri/seller/goods/categoryOptions',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 本店二级「店铺商品分类」下拉
24
+export function sellerGoodsShopCategoryOptions(visibleOnly) {
25
+  return request({
26
+    url: '/agri/seller/goods/shopCategoryOptions',
27
+    method: 'get',
28
+    params: { visibleOnly: visibleOnly },
29
+    headers: sellerShopHeaders()
30
+  })
31
+}
32
+
33
+// 商品服务选项(all + defaultShow)
34
+export function sellerGoodsServiceOptions() {
35
+  return request({
36
+    url: '/agri/seller/goods/serviceOptions',
37
+    method: 'get',
38
+    headers: sellerShopHeaders()
39
+  })
40
+}
41
+
42
+// 查询商品详情
43
+export function getSellerGoods(goodsId) {
44
+  return request({
45
+    url: '/agri/seller/goods/' + goodsId,
46
+    method: 'get',
47
+    headers: sellerShopHeaders()
48
+  })
49
+}
50
+
51
+// 新增商品
52
+export function addSellerGoods(data) {
53
+  return request({
54
+    url: '/agri/seller/goods',
55
+    method: 'post',
56
+    data: data,
57
+    headers: sellerShopHeaders()
58
+  })
59
+}
60
+
61
+// 修改商品
62
+export function updateSellerGoods(data) {
63
+  return request({
64
+    url: '/agri/seller/goods',
65
+    method: 'put',
66
+    data: data,
67
+    headers: sellerShopHeaders()
68
+  })
69
+}
70
+
71
+// 提交上架(单条)
72
+export function submitSellerGoods(goodsId) {
73
+  return request({
74
+    url: '/agri/seller/goods/' + goodsId + '/submit',
75
+    method: 'put',
76
+    headers: sellerShopHeaders()
77
+  })
78
+}
79
+
80
+// 批量提交上架
81
+export function submitSellerGoodsBatch(data) {
82
+  return request({
83
+    url: '/agri/seller/goods/submit',
84
+    method: 'put',
85
+    data: data,
86
+    headers: sellerShopHeaders()
87
+  })
88
+}
89
+
90
+// 下架(单条)
91
+export function offShelfSellerGoods(goodsId) {
92
+  return request({
93
+    url: '/agri/seller/goods/' + goodsId + '/offShelf',
94
+    method: 'put',
95
+    headers: sellerShopHeaders()
96
+  })
97
+}
98
+
99
+// 批量下架
100
+export function offShelfSellerGoodsBatch(data) {
101
+  return request({
102
+    url: '/agri/seller/goods/offShelf',
103
+    method: 'put',
104
+    data: data,
105
+    headers: sellerShopHeaders()
106
+  })
107
+}
108
+
109
+// 删除商品(支持批量,逗号分隔 ID)
110
+export function delSellerGoods(goodsIds) {
111
+  return request({
112
+    url: '/agri/seller/goods/' + goodsIds,
113
+    method: 'delete',
114
+    headers: sellerShopHeaders()
115
+  })
116
+}

+ 192 - 0
ruoyi-ui/src/views/agri/seller/goods/detail.vue

@@ -0,0 +1,192 @@
1
+<template>
2
+  <el-drawer
3
+    :title="'商品详情 · ' + (detail.goodsName || detail.goodsSn || '')"
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-descriptions :column="2" border size="small" class="mb16">
13
+        <el-descriptions-item label="商品编号">{{ detail.goodsSn || '—' }}</el-descriptions-item>
14
+        <el-descriptions-item label="商品状态">
15
+          <el-tag size="small" :type="goodsStatusTag(detail.goodsStatus)">{{ goodsStatusLabel(detail.goodsStatus) }}</el-tag>
16
+        </el-descriptions-item>
17
+        <el-descriptions-item label="商品名称" :span="2">{{ detail.goodsName || '—' }}</el-descriptions-item>
18
+        <el-descriptions-item label="售价">{{ detail.salePrice != null ? detail.salePrice : '—' }}</el-descriptions-item>
19
+        <el-descriptions-item label="库存">{{ detail.stock != null ? detail.stock : '—' }}</el-descriptions-item>
20
+        <el-descriptions-item label="销量">{{ detail.salesCount != null ? detail.salesCount : '—' }}</el-descriptions-item>
21
+        <el-descriptions-item label="主图">
22
+          <image-preview v-if="detail.mainPic" :src="detail.mainPic" :width="80" :height="80" />
23
+          <span v-else>—</span>
24
+        </el-descriptions-item>
25
+      </el-descriptions>
26
+
27
+      <h4 class="section-header">分类信息</h4>
28
+      <el-descriptions :column="1" border size="small" class="mb16">
29
+        <el-descriptions-item label="商品分类">{{ detail.categoryPath || '—' }}</el-descriptions-item>
30
+        <el-descriptions-item label="店铺商品分类">{{ detail.shopCategoryPath || detail.shopCategoryName || '—' }}</el-descriptions-item>
31
+      </el-descriptions>
32
+
33
+      <h4 class="section-header">状态信息</h4>
34
+      <el-descriptions :column="2" border size="small" class="mb16">
35
+        <el-descriptions-item label="提交时间">{{ parseTime(detail.submitTime) || '—' }}</el-descriptions-item>
36
+        <el-descriptions-item label="审核时间">{{ parseTime(detail.auditTime) || '—' }}</el-descriptions-item>
37
+        <el-descriptions-item v-if="detail.goodsStatus === '3'" label="驳回原因" :span="2">
38
+          <span class="reject-reason">{{ detail.rejectReason || '—' }}</span>
39
+        </el-descriptions-item>
40
+        <el-descriptions-item v-if="detail.goodsStatus === '4'" label="下架时间">{{ parseTime(detail.offShelfTime) || '—' }}</el-descriptions-item>
41
+      </el-descriptions>
42
+
43
+      <h4 class="section-header">商品服务快照</h4>
44
+      <el-table v-if="detail.services && detail.services.length" border size="small" :data="detail.services" class="mb16">
45
+        <el-table-column label="服务名称" prop="serviceName" />
46
+        <el-table-column label="简介" prop="serviceIntro" :show-overflow-tooltip="true" />
47
+        <el-table-column label="图标" width="80">
48
+          <template slot-scope="scope">
49
+            <image-preview :src="scope.row.serviceIcon" :width="40" :height="40" />
50
+          </template>
51
+        </el-table-column>
52
+      </el-table>
53
+      <div v-else class="empty-tip mb16">暂无勾选服务项</div>
54
+
55
+      <h4 v-if="detail.detailContent" class="section-header">商品详情</h4>
56
+      <div v-if="detail.detailContent" class="detail-content mb16" v-html="detail.detailContent"></div>
57
+
58
+      <div class="action-bar">
59
+        <el-button v-if="detail.canEdit !== false" type="primary" plain @click="handleEdit" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
60
+        <el-button v-if="detail.canSubmit" type="success" @click="handleSubmit" v-hasPermi="['agri:seller:goods:edit']">提交上架</el-button>
61
+        <el-button v-if="detail.canOffShelf" type="warning" plain @click="handleOffShelf" v-hasPermi="['agri:seller:goods:offshelf']">下架</el-button>
62
+        <el-button v-if="detail.canDelete" type="danger" plain @click="handleDelete" v-hasPermi="['agri:seller:goods:remove']">删除</el-button>
63
+      </div>
64
+    </div>
65
+  </el-drawer>
66
+</template>
67
+
68
+<script>
69
+import {
70
+  getSellerGoods,
71
+  submitSellerGoods,
72
+  offShelfSellerGoods,
73
+  delSellerGoods
74
+} from "@/api/agri/seller/goods"
75
+
76
+export default {
77
+  name: "SellerGoodsDetail",
78
+  props: {
79
+    visible: { type: Boolean, default: false },
80
+    goodsId: { type: [Number, String], default: null }
81
+  },
82
+  data() {
83
+    return {
84
+      localVisible: false,
85
+      loading: false,
86
+      detail: {}
87
+    }
88
+  },
89
+  watch: {
90
+    visible(val) {
91
+      this.localVisible = val
92
+      if (val && this.goodsId) {
93
+        this.loadDetail()
94
+      }
95
+    },
96
+    localVisible(val) {
97
+      this.$emit("update:visible", val)
98
+    }
99
+  },
100
+  methods: {
101
+    /** 加载商品详情 */
102
+    loadDetail() {
103
+      this.loading = true
104
+      getSellerGoods(this.goodsId).then(response => {
105
+        this.detail = response.data || {}
106
+        this.loading = false
107
+      }).catch(() => {
108
+        this.loading = false
109
+      })
110
+    },
111
+    goodsStatusLabel(status) {
112
+      const map = { "0": "未上架", "1": "待审核", "2": "出售中", "3": "审核失败", "4": "已下架" }
113
+      return map[status] || this.detail.goodsStatusLabel || "—"
114
+    },
115
+    goodsStatusTag(status) {
116
+      const map = { "0": "", "1": "warning", "2": "success", "3": "danger", "4": "info" }
117
+      return map[status] || "info"
118
+    },
119
+    /** 跳转编辑 */
120
+    handleEdit() {
121
+      this.$emit("edit", this.detail)
122
+      this.localVisible = false
123
+    },
124
+    /** 提交上架 */
125
+    handleSubmit() {
126
+      this.$modal.confirm("确认提交上架该商品?").then(() => {
127
+        return submitSellerGoods(this.goodsId)
128
+      }).then(response => {
129
+        const data = response.data || {}
130
+        const label = data.goodsStatusLabel || "操作成功"
131
+        this.$modal.msgSuccess("提交成功,当前状态:" + label)
132
+        this.localVisible = false
133
+        this.$emit("success")
134
+      }).catch(() => {})
135
+    },
136
+    /** 下架 */
137
+    handleOffShelf() {
138
+      this.$modal.confirm("下架后 C 端不可购买,是否继续?").then(() => {
139
+        return offShelfSellerGoods(this.goodsId)
140
+      }).then(() => {
141
+        this.$modal.msgSuccess("下架成功")
142
+        this.localVisible = false
143
+        this.$emit("success")
144
+      }).catch(() => {})
145
+    },
146
+    /** 删除 */
147
+    handleDelete() {
148
+      this.$modal.confirm("确认删除该商品?删除后不可恢复。").then(() => {
149
+        return delSellerGoods(this.goodsId)
150
+      }).then(() => {
151
+        this.$modal.msgSuccess("删除成功")
152
+        this.localVisible = false
153
+        this.$emit("success")
154
+      }).catch(() => {})
155
+    }
156
+  }
157
+}
158
+</script>
159
+
160
+<style scoped>
161
+.drawer-content {
162
+  padding: 0 20px 20px;
163
+}
164
+.section-header {
165
+  margin: 16px 0 10px;
166
+  font-size: 15px;
167
+  color: #303133;
168
+  border-left: 3px solid #409EFF;
169
+  padding-left: 8px;
170
+}
171
+.mb16 {
172
+  margin-bottom: 16px;
173
+}
174
+.reject-reason {
175
+  color: #F56C6C;
176
+}
177
+.empty-tip {
178
+  color: #909399;
179
+  font-size: 13px;
180
+}
181
+.detail-content {
182
+  padding: 12px;
183
+  background: #fafafa;
184
+  border-radius: 4px;
185
+  line-height: 1.6;
186
+}
187
+.action-bar {
188
+  margin-top: 24px;
189
+  padding-top: 16px;
190
+  border-top: 1px solid #EBEEF5;
191
+}
192
+</style>

+ 449 - 0
ruoyi-ui/src/views/agri/seller/goods/index.vue

@@ -0,0 +1,449 @@
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="80px">
6
+        <el-form-item label="商品编号" prop="goodsSn">
7
+          <el-input v-model="queryParams.goodsSn" placeholder="商品编号" clearable style="width: 160px" @keyup.enter.native="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item label="商品名称" prop="goodsName">
10
+          <el-input v-model="queryParams.goodsName" placeholder="商品名称" clearable style="width: 160px" @keyup.enter.native="handleQuery" />
11
+        </el-form-item>
12
+        <el-form-item label="商品分类" prop="categoryId">
13
+          <el-select v-model="queryParams.categoryId" placeholder="全部" clearable filterable style="width: 200px">
14
+            <el-option v-for="item in categoryOptions" :key="item.categoryId" :label="item.label" :value="item.categoryId" />
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="0" />
31
+        <el-tab-pane label="待审核" name="1" />
32
+        <el-tab-pane label="出售中" name="2" />
33
+        <el-tab-pane label="审核失败" name="3" />
34
+        <el-tab-pane label="已下架" name="4" />
35
+      </el-tabs>
36
+
37
+      <el-row :gutter="10" class="mb8">
38
+        <el-col :span="1.5">
39
+          <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['agri:seller:goods:add']">添加商品</el-button>
40
+        </el-col>
41
+        <el-col :span="1.5">
42
+          <el-button type="success" plain icon="el-icon-upload2" size="mini" :disabled="!canBatchSubmit" @click="handleBatchSubmit" v-hasPermi="['agri:seller:goods:edit']">批量提交上架</el-button>
43
+        </el-col>
44
+        <el-col :span="1.5">
45
+          <el-button type="warning" plain icon="el-icon-bottom" size="mini" :disabled="!canBatchOffShelf" @click="handleBatchOffShelf" v-hasPermi="['agri:seller:goods:offshelf']">批量下架</el-button>
46
+        </el-col>
47
+        <el-col :span="1.5">
48
+          <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="!canBatchDelete" @click="handleBatchDelete" v-hasPermi="['agri:seller:goods:remove']">批量删除</el-button>
49
+        </el-col>
50
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
51
+      </el-row>
52
+
53
+      <el-table border v-loading="loading" :data="goodsList" @selection-change="handleSelectionChange">
54
+        <el-table-column type="selection" width="55" align="center" />
55
+        <el-table-column label="商品编号" align="center" prop="goodsSn" width="140" />
56
+        <el-table-column label="主图" align="center" width="80">
57
+          <template slot-scope="scope">
58
+            <image-preview :src="scope.row.mainPic" :width="50" :height="50" />
59
+          </template>
60
+        </el-table-column>
61
+        <el-table-column label="商品名称" align="center" prop="goodsName" min-width="140" :show-overflow-tooltip="true" />
62
+        <el-table-column label="售价" align="center" prop="salePrice" width="90" />
63
+        <el-table-column label="库存" align="center" prop="stock" width="70" />
64
+        <el-table-column label="销量" align="center" prop="salesCount" width="70" />
65
+        <el-table-column label="商品分类" align="center" prop="categoryPath" min-width="120" :show-overflow-tooltip="true" />
66
+        <el-table-column label="店铺分类" align="center" prop="shopCategoryName" min-width="110" :show-overflow-tooltip="true">
67
+          <template slot-scope="scope">
68
+            <span>{{ scope.row.shopCategoryName || '—' }}</span>
69
+          </template>
70
+        </el-table-column>
71
+        <el-table-column label="状态" align="center" prop="goodsStatus" width="90">
72
+          <template slot-scope="scope">
73
+            <el-tag size="small" :type="goodsStatusTag(scope.row.goodsStatus)">{{ goodsStatusLabel(scope.row) }}</el-tag>
74
+          </template>
75
+        </el-table-column>
76
+        <el-table-column label="操作" align="center" width="240" fixed="right">
77
+          <template slot-scope="scope">
78
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['agri:seller:goods:query']">详情</el-button>
79
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:goods:edit']">编辑</el-button>
80
+            <el-button v-if="canRowSubmit(scope.row)" size="mini" type="text" icon="el-icon-upload2" @click="handleRowSubmit(scope.row)" v-hasPermi="['agri:seller:goods:edit']">提交上架</el-button>
81
+            <el-button v-if="scope.row.goodsStatus === '2'" size="mini" type="text" icon="el-icon-bottom" @click="handleRowOffShelf(scope.row)" v-hasPermi="['agri:seller:goods:offshelf']">下架</el-button>
82
+            <el-button v-if="canRowDelete(scope.row)" size="mini" type="text" icon="el-icon-delete" @click="handleRowDelete(scope.row)" v-hasPermi="['agri:seller:goods:remove']">删除</el-button>
83
+          </template>
84
+        </el-table-column>
85
+      </el-table>
86
+
87
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
88
+    </el-card>
89
+
90
+    <!-- 详情抽屉 -->
91
+    <seller-goods-detail
92
+      :visible.sync="detailOpen"
93
+      :goods-id="currentGoodsId"
94
+      @edit="handleUpdateFromDetail"
95
+      @success="handleSuccess"
96
+    />
97
+
98
+    <!-- 添加/编辑弹窗 -->
99
+    <el-dialog :title="formTitle" :visible.sync="formOpen" width="720px" append-to-body @close="cancelForm">
100
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
101
+        <el-form-item label="商品分类" prop="categoryId">
102
+          <el-select v-model="form.categoryId" placeholder="请选择平台二级分类" filterable style="width: 100%">
103
+            <el-option v-for="item in categoryOptions" :key="item.categoryId" :label="item.label" :value="item.categoryId" />
104
+          </el-select>
105
+        </el-form-item>
106
+        <el-form-item label="店铺商品分类" prop="shopCategoryId">
107
+          <el-select v-model="form.shopCategoryId" placeholder="选填,本店二级分类" clearable filterable style="width: 100%">
108
+            <el-option v-for="item in shopCategoryOptions" :key="item.shopCategoryId" :label="item.label" :value="item.shopCategoryId" />
109
+          </el-select>
110
+        </el-form-item>
111
+        <el-form-item label="商品名称" prop="goodsName">
112
+          <el-input v-model="form.goodsName" placeholder="请输入商品名称" maxlength="200" />
113
+        </el-form-item>
114
+        <el-form-item label="商品主图" prop="mainPic">
115
+          <image-upload v-model="form.mainPic" :limit="1" :file-size="5" :file-type="['png', 'jpg', 'jpeg']" />
116
+        </el-form-item>
117
+        <el-row :gutter="16">
118
+          <el-col :span="12">
119
+            <el-form-item label="销售价" prop="salePrice">
120
+              <el-input-number v-model="form.salePrice" :min="0" :precision="2" controls-position="right" style="width: 100%" />
121
+            </el-form-item>
122
+          </el-col>
123
+          <el-col :span="12">
124
+            <el-form-item label="库存" prop="stock">
125
+              <el-input-number v-model="form.stock" :min="0" :precision="0" controls-position="right" style="width: 100%" />
126
+            </el-form-item>
127
+          </el-col>
128
+        </el-row>
129
+        <el-form-item label="商品详情" prop="detailContent">
130
+          <editor v-model="form.detailContent" :min-height="200" />
131
+        </el-form-item>
132
+        <el-form-item label="服务说明">
133
+          <el-checkbox-group v-model="form.serviceIds">
134
+            <el-checkbox v-for="item in serviceOptions" :key="item.serviceId" :label="item.serviceId">{{ item.serviceName }}</el-checkbox>
135
+          </el-checkbox-group>
136
+          <div v-if="!serviceOptions.length" class="form-tip">暂无可用服务项</div>
137
+        </el-form-item>
138
+      </el-form>
139
+      <div slot="footer" class="dialog-footer">
140
+        <el-button type="primary" @click="submitForm">保 存</el-button>
141
+        <el-button @click="cancelForm">取 消</el-button>
142
+      </div>
143
+    </el-dialog>
144
+  </div>
145
+</template>
146
+
147
+<script>
148
+import {
149
+  listSellerGoods,
150
+  sellerGoodsCategoryOptions,
151
+  sellerGoodsShopCategoryOptions,
152
+  sellerGoodsServiceOptions,
153
+  getSellerGoods,
154
+  addSellerGoods,
155
+  updateSellerGoods,
156
+  submitSellerGoods,
157
+  submitSellerGoodsBatch,
158
+  offShelfSellerGoods,
159
+  offShelfSellerGoodsBatch,
160
+  delSellerGoods
161
+} from "@/api/agri/seller/goods"
162
+import { getSellerContext } from "@/api/agri/seller/context"
163
+import { setSellerShopContext } from "@/utils/sellerShop"
164
+import SellerGoodsDetail from "./detail"
165
+
166
+export default {
167
+  name: "AgriSellerGoods",
168
+  components: { SellerGoodsDetail },
169
+  data() {
170
+    return {
171
+      loading: true,
172
+      showSearch: true,
173
+      total: 0,
174
+      goodsList: [],
175
+      selection: [],
176
+      detailOpen: false,
177
+      currentGoodsId: null,
178
+      formOpen: false,
179
+      formTitle: "",
180
+      statusTab: "all",
181
+      categoryOptions: [],
182
+      shopCategoryOptions: [],
183
+      serviceOptions: [],
184
+      defaultServiceIds: [],
185
+      queryParams: {
186
+        pageNum: 1,
187
+        pageSize: 10,
188
+        goodsSn: undefined,
189
+        goodsName: undefined,
190
+        categoryId: undefined,
191
+        goodsStatusQuery: undefined
192
+      },
193
+      form: {},
194
+      rules: {
195
+        categoryId: [{ required: true, message: "请选择商品分类", trigger: "change" }],
196
+        goodsName: [{ required: true, message: "请输入商品名称", trigger: "blur" }],
197
+        mainPic: [{ required: true, message: "请上传商品主图", trigger: "change" }],
198
+        salePrice: [{ required: true, message: "请输入销售价", trigger: "blur" }],
199
+        stock: [{ required: true, message: "请输入库存", trigger: "blur" }],
200
+        detailContent: [{ required: true, message: "请填写商品详情", trigger: "blur" }]
201
+      }
202
+    }
203
+  },
204
+  computed: {
205
+    /** 所选是否均可提交上架 */
206
+    canBatchSubmit() {
207
+      return this.selection.length > 0 && this.selection.every(item => this.canRowSubmit(item))
208
+    },
209
+    /** 所选是否均可下架 */
210
+    canBatchOffShelf() {
211
+      return this.selection.length > 0 && this.selection.every(item => item.goodsStatus === "2")
212
+    },
213
+    /** 所选是否均可删除 */
214
+    canBatchDelete() {
215
+      return this.selection.length > 0 && this.selection.every(item => this.canRowDelete(item))
216
+    }
217
+  },
218
+  created() {
219
+    this.initPage()
220
+  },
221
+  methods: {
222
+    /** 初始化:店铺上下文 + 下拉 + 列表 */
223
+    initPage() {
224
+      this.loadShopContext().then(() => {
225
+        this.loadFormOptions()
226
+        this.getList()
227
+      }).catch(() => {
228
+        this.loadFormOptions()
229
+        this.getList()
230
+      })
231
+    },
232
+    /** 写入 X-Shop-Id 缓存 */
233
+    loadShopContext() {
234
+      return getSellerContext().then(response => {
235
+        const data = response.data || {}
236
+        if (data.shopId != null) {
237
+          setSellerShopContext(data.shopId, data.shopName)
238
+        }
239
+      })
240
+    },
241
+    /** 加载发品/检索用下拉 */
242
+    loadFormOptions() {
243
+      sellerGoodsCategoryOptions().then(response => {
244
+        this.categoryOptions = response.data || []
245
+      }).catch(() => {})
246
+      sellerGoodsShopCategoryOptions(false).then(response => {
247
+        this.shopCategoryOptions = response.data || []
248
+      }).catch(() => {})
249
+      sellerGoodsServiceOptions().then(response => {
250
+        const data = response.data || {}
251
+        this.serviceOptions = data.all || []
252
+        const defaults = data.defaultShow || []
253
+        this.defaultServiceIds = defaults.map(item => item.serviceId)
254
+      }).catch(() => {})
255
+    },
256
+    /** 查询商品列表 */
257
+    getList() {
258
+      this.loading = true
259
+      listSellerGoods(this.queryParams).then(response => {
260
+        this.goodsList = response.rows || []
261
+        this.total = response.total || 0
262
+        this.loading = false
263
+      }).catch(() => {
264
+        this.loading = false
265
+      })
266
+    },
267
+    goodsStatusLabel(row) {
268
+      if (row.goodsStatusLabel) {
269
+        return row.goodsStatusLabel
270
+      }
271
+      const map = { "0": "未上架", "1": "待审核", "2": "出售中", "3": "审核失败", "4": "已下架" }
272
+      return map[row.goodsStatus] || "—"
273
+    },
274
+    goodsStatusTag(status) {
275
+      const map = { "0": "", "1": "warning", "2": "success", "3": "danger", "4": "info" }
276
+      return map[status] || "info"
277
+    },
278
+    /** 是否可提交上架 */
279
+    canRowSubmit(row) {
280
+      return row.goodsStatus === "0" || row.goodsStatus === "3" || row.goodsStatus === "4"
281
+    },
282
+    /** 是否可删除 */
283
+    canRowDelete(row) {
284
+      return this.canRowSubmit(row)
285
+    },
286
+    handleTabClick() {
287
+      this.queryParams.goodsStatusQuery = this.statusTab === "all" ? undefined : this.statusTab
288
+      this.queryParams.pageNum = 1
289
+      this.getList()
290
+    },
291
+    handleQuery() {
292
+      this.queryParams.pageNum = 1
293
+      this.getList()
294
+    },
295
+    resetQuery() {
296
+      this.resetForm("queryForm")
297
+      this.statusTab = "all"
298
+      this.queryParams.goodsStatusQuery = undefined
299
+      this.handleQuery()
300
+    },
301
+    handleSelectionChange(selection) {
302
+      this.selection = selection
303
+    },
304
+    handleDetail(row) {
305
+      this.currentGoodsId = row.goodsId
306
+      this.detailOpen = true
307
+    },
308
+    /** 重置表单 */
309
+    resetFormData() {
310
+      this.form = {
311
+        goodsId: undefined,
312
+        categoryId: undefined,
313
+        shopCategoryId: undefined,
314
+        goodsName: undefined,
315
+        mainPic: undefined,
316
+        detailContent: undefined,
317
+        salePrice: undefined,
318
+        stock: undefined,
319
+        serviceIds: [...this.defaultServiceIds]
320
+      }
321
+      this.resetForm("form")
322
+    },
323
+    /** 打开添加商品 */
324
+    handleAdd() {
325
+      this.resetFormData()
326
+      this.formOpen = true
327
+      this.formTitle = "添加商品"
328
+    },
329
+    /** 打开编辑商品 */
330
+    handleUpdate(row) {
331
+      this.resetFormData()
332
+      getSellerGoods(row.goodsId).then(response => {
333
+        const data = response.data || {}
334
+        this.form = {
335
+          goodsId: data.goodsId,
336
+          categoryId: data.categoryId,
337
+          shopCategoryId: data.shopCategoryId,
338
+          goodsName: data.goodsName,
339
+          mainPic: data.mainPic,
340
+          detailContent: data.detailContent,
341
+          salePrice: data.salePrice,
342
+          stock: data.stock,
343
+          serviceIds: (data.services || []).map(item => item.serviceId)
344
+        }
345
+        this.formOpen = true
346
+        this.formTitle = "编辑商品"
347
+      })
348
+    },
349
+    /** 详情抽屉跳转编辑 */
350
+    handleUpdateFromDetail(detail) {
351
+      this.handleUpdate({ goodsId: detail.goodsId })
352
+    },
353
+    cancelForm() {
354
+      this.formOpen = false
355
+      this.resetFormData()
356
+    },
357
+    /** 保存商品 */
358
+    submitForm() {
359
+      this.$refs["form"].validate(valid => {
360
+        if (!valid) {
361
+          return
362
+        }
363
+        const payload = { ...this.form }
364
+        if (!payload.shopCategoryId) {
365
+          payload.shopCategoryId = null
366
+        }
367
+        const submitFn = payload.goodsId != null ? updateSellerGoods : addSellerGoods
368
+        submitFn(payload).then(() => {
369
+          this.$modal.msgSuccess(payload.goodsId != null ? "修改成功" : "添加成功,商品状态为未上架")
370
+          this.formOpen = false
371
+          this.getList()
372
+        })
373
+      })
374
+    },
375
+    /** 单条提交上架 */
376
+    handleRowSubmit(row) {
377
+      this.$modal.confirm("确认提交上架商品「" + row.goodsName + "」?").then(() => {
378
+        return submitSellerGoods(row.goodsId)
379
+      }).then(response => {
380
+        const data = response.data || {}
381
+        const label = data.goodsStatusLabel || "操作成功"
382
+        this.$modal.msgSuccess("提交成功,当前状态:" + label)
383
+        this.handleSuccess()
384
+      }).catch(() => {})
385
+    },
386
+    /** 批量提交上架 */
387
+    handleBatchSubmit() {
388
+      const ids = this.selection.map(item => item.goodsId)
389
+      this.$modal.confirm("确认批量提交上架所选 " + ids.length + " 件商品?").then(() => {
390
+        return submitSellerGoodsBatch({ goodsIds: ids })
391
+      }).then(() => {
392
+        this.$modal.msgSuccess("批量提交成功")
393
+        this.handleSuccess()
394
+      }).catch(() => {})
395
+    },
396
+    /** 单条下架 */
397
+    handleRowOffShelf(row) {
398
+      this.$modal.confirm("下架后 C 端不可购买,是否继续?").then(() => {
399
+        return offShelfSellerGoods(row.goodsId)
400
+      }).then(() => {
401
+        this.$modal.msgSuccess("下架成功")
402
+        this.handleSuccess()
403
+      }).catch(() => {})
404
+    },
405
+    /** 批量下架 */
406
+    handleBatchOffShelf() {
407
+      const ids = this.selection.map(item => item.goodsId)
408
+      this.$modal.confirm("确认批量下架所选 " + ids.length + " 件商品?").then(() => {
409
+        return offShelfSellerGoodsBatch({ goodsIds: ids })
410
+      }).then(() => {
411
+        this.$modal.msgSuccess("批量下架成功")
412
+        this.handleSuccess()
413
+      }).catch(() => {})
414
+    },
415
+    /** 单条删除 */
416
+    handleRowDelete(row) {
417
+      this.$modal.confirm("确认删除商品「" + row.goodsName + "」?").then(() => {
418
+        return delSellerGoods(row.goodsId)
419
+      }).then(() => {
420
+        this.$modal.msgSuccess("删除成功")
421
+        this.handleSuccess()
422
+      }).catch(() => {})
423
+    },
424
+    /** 批量删除 */
425
+    handleBatchDelete() {
426
+      const ids = this.selection.map(item => item.goodsId)
427
+      this.$modal.confirm("确认批量删除所选 " + ids.length + " 件商品?").then(() => {
428
+        return delSellerGoods(ids.join(","))
429
+      }).then(() => {
430
+        this.$modal.msgSuccess("批量删除成功")
431
+        this.handleSuccess()
432
+      }).catch(() => {})
433
+    },
434
+    handleSuccess() {
435
+      this.getList()
436
+    }
437
+  }
438
+}
439
+</script>
440
+
441
+<style scoped>
442
+.search-card {
443
+  margin-bottom: 0;
444
+}
445
+.form-tip {
446
+  color: #909399;
447
+  font-size: 13px;
448
+}
449
+</style>