Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

wwh 1 tydzień temu
rodzic
commit
1f9a9dd803

+ 239 - 0
doc/店铺后台/库存管理/商品入库/商品入库前端技术方案.md

@@ -0,0 +1,239 @@
1
+# 商品入库 — 前端技术方案
2
+
3
+> **依据:** 《商品入库功能需求.md》v1.0.1、《商品入库技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺入库单列表、高级检索、**手工入库**、详情查看;**不含** 出库/调整/库存查询/库存日志页面、平台端、C 端。  
6
+> **实现状态:** `index.vue`、`detail.vue`、`form.vue`、`api/agri/seller/stock/inbound.js` **已按需求与 UI 稿落地**;待菜单配置及 `/agri/seller/stock/inbound` 联调。
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-drawer`(`size="85%"`,底部固定「取消/确认」) |
20
+| 图片 | 全局 `image-preview`(列表缩略、明细主图) |
21
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
22
+
23
+---
24
+
25
+## 2. 业务要点(前端需体现)
26
+
27
+| 项 | 说明 |
28
+|----|------|
29
+| 入库类型 | `0` 新增商品规格(系统自动)、`1` 采购、`2` 退货、`3` 其他;**手工表单仅 1~3** |
30
+| 自动入库 | 商品保存触发,**仅留痕**;详情页对 type=`0` 展示提示文案 |
31
+| 手工入库 | 确认后 **增加可用库存**;单据 **不可编辑/删除** |
32
+| 明细粒度 | 首期 **单规格**(`skuId` 传 `null`);规格空展示 **—** |
33
+| 条码 | 按 **`goods_sn`** 精确匹配;0 条报错、1 条追加、多条弹窗选择 |
34
+| 重复商品 | 同 `goodsId`(+ `skuId`)再次添加 **合并数量 +1** 并提示 |
35
+| 导入 | UI 保留按钮;后端 **`POST /import/preview` 未实现**,点击提示改用选品/条码 |
36
+
37
+---
38
+
39
+## 3. 文件清单
40
+
41
+| 类型 | 路径 | 说明 |
42
+|------|------|------|
43
+| 列表页 | `ruoyi-ui/src/views/agri/seller/stock/inbound/index.vue` | 检索、表格、分页、打开入库/详情抽屉 |
44
+| 详情抽屉 | `ruoyi-ui/src/views/agri/seller/stock/inbound/detail.vue` | 基本信息 + 入库明细(只读) |
45
+| 入库抽屉 | `ruoyi-ui/src/views/agri/seller/stock/inbound/form.vue` | 手工入库表单、选品、条码、明细编辑 |
46
+| 入库 API | `ruoyi-ui/src/api/agri/seller/stock/inbound.js` | list、detail、create、operators、goods/options、goods/byBarcode |
47
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
48
+
49
+**组件 name(keep-alive):**
50
+
51
+| 组件 | name |
52
+|------|------|
53
+| 列表页 | `AgriSellerStockInbound` |
54
+| 详情抽屉 | `SellerStockInboundDetail` |
55
+| 入库抽屉 | `SellerStockInboundForm` |
56
+
57
+---
58
+
59
+## 4. 菜单与路由
60
+
61
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
62
+|----------|----------|-------------------|----------|
63
+| 商品入库 | `agri/seller/stock/inbound/index` | `seller/stock/inbound` | `agri:seller:stock:inbound:list` |
64
+
65
+**上级菜单:** 店铺经营管理端 → **库存管理**
66
+
67
+| 按钮权限 | 标识 | 页面落点 |
68
+|----------|------|----------|
69
+| 列表 | `agri:seller:stock:inbound:list` | 进入列表页、经办人下拉 |
70
+| 详情 | `agri:seller:stock:inbound:query` | 操作列「详情」 |
71
+| 手工入库 | `agri:seller:stock:inbound:add` | 「商品入库」、选品、条码 |
72
+
73
+---
74
+
75
+## 5. 页面结构(与代码一致)
76
+
77
+```text
78
+商品入库 index.vue
79
+├── 检索区 search-card(el-card,v-show 受 right-toolbar 控制)
80
+│   ├── 入库搜索 keyword(单号/商品名模糊)
81
+│   ├── 入库时间 dateRange(daterange → beginTime/endTime)
82
+│   ├── 入库类型 inboundType(0~3 下拉,可清空=全部)
83
+│   ├── 经办人 operatorId(GET /operators 动态下拉)
84
+│   └── 搜索 / 重置
85
+├── 列表区 table-card(el-card)
86
+│   ├── 工具栏:商品入库(add 权限)+ right-toolbar
87
+│   ├── el-table border(empty-text 动态)
88
+│   │   ├── 入库单号 inboundNo
89
+│   │   ├── 入库类型 inboundTypeText
90
+│   │   ├── 入库时间 inboundTime
91
+│   │   ├── 备注 remark(空则 —)
92
+│   │   ├── 经办人 operatorName
93
+│   │   └── 操作:详情
94
+│   └── pagination
95
+├── 详情抽屉 SellerStockInboundDetail(见 §6)
96
+└── 入库抽屉 SellerStockInboundForm(见 §7)
97
+```
98
+
99
+**不提供:** 页内店铺选择、入库单编辑/作废/反审、打印/导出、成本录入。
100
+
101
+---
102
+
103
+## 6. 详情抽屉 detail.vue
104
+
105
+### 6.1 对外接口
106
+
107
+| 类型 | 名称 | 说明 |
108
+|------|------|------|
109
+| props | `visible` | 抽屉显隐(`.sync`) |
110
+| props | `inboundId` | 入库单主键 |
111
+| emit | `update:visible` | 关闭抽屉 |
112
+| 方法 | `reload()` | 可选重载(预留) |
113
+
114
+### 6.2 分区结构
115
+
116
+| 区块 | 内容 |
117
+|------|------|
118
+| 基本信息 | 入库单号、类型、时间、经办人、备注 |
119
+| 系统提示 | `inboundType === '0'` 时说明「仅留痕」 |
120
+| 入库明细 | 表格:商品信息(图+名)、规格、入库数量 |
121
+
122
+**无底部操作栏**(确认后只读)。
123
+
124
+---
125
+
126
+## 7. 手工入库抽屉 form.vue
127
+
128
+### 7.1 基本信息
129
+
130
+| 字段 | 组件 | 校验 |
131
+|------|------|------|
132
+| 入库类型 | `el-radio-group`,默认 `1` 采购 | 必选(IN1) |
133
+| 备注 | `textarea`,maxlength=200 | ≤200 字(IN4) |
134
+
135
+### 7.2 入库商品区
136
+
137
+| 能力 | 实现 |
138
+|------|------|
139
+| 选择商品 | `el-dialog` + 分页表格多选 → `GET /goods/options` |
140
+| 导入商品 | 按钮占位,提示暂未开放 |
141
+| 条码录入 | 输入框 + Enter/确定 → `GET /goods/byBarcode` |
142
+| 明细表格 | 商品信息、规格、当前可用库存(只读)、入库数量 `el-input-number` min=1、删除 |
143
+
144
+### 7.3 提交 payload
145
+
146
+```json
147
+{
148
+  "inboundType": "1",
149
+  "remark": "optional",
150
+  "items": [{ "goodsId": 1001, "skuId": null, "quantity": 10 }]
151
+}
152
+```
153
+
154
+| 前端校验 | 对应规则 |
155
+|----------|----------|
156
+| 明细至少一行 | IN2 |
157
+| 每行 quantity 为正整数 | IN3 |
158
+| 入库类型已选 | IN1 |
159
+
160
+成功:`msgSuccess('入库成功')` → 关闭抽屉 → 父页刷新列表与经办人下拉。
161
+
162
+---
163
+
164
+## 8. 店铺上下文(X-Shop-Id)
165
+
166
+| 步骤 | 说明 |
167
+|------|------|
168
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
169
+| 全部 API | `sellerShopHeaders()` |
170
+| Navbar 切店 | `location.reload()` 整页刷新列表 |
171
+
172
+---
173
+
174
+## 9. API 封装
175
+
176
+**模块:** `@/api/agri/seller/stock/inbound.js`
177
+
178
+| 方法 | HTTP | 路径 | 权限 |
179
+|------|------|------|------|
180
+| `listSellerStockInbound` | GET | `/list` | list |
181
+| `getSellerStockInbound` | GET | `/{inboundId}` | query |
182
+| `createSellerStockInbound` | POST | `/` | add |
183
+| `listStockInboundOperators` | GET | `/operators` | list |
184
+| `listStockInboundGoodsOptions` | GET | `/goods/options` | add |
185
+| `getStockInboundGoodsByBarcode` | GET | `/goods/byBarcode?barcode=` | add |
186
+
187
+**列表 Query:** `pageNum`、`pageSize`、`keyword`、`inboundType`、`operatorId`、`beginTime`、`endTime`。
188
+
189
+**排序:** 后端固定 `inbound_time DESC`(最新在前)。
190
+
191
+---
192
+
193
+## 10. 空状态与错误提示
194
+
195
+| 场景 | 前端表现 |
196
+|------|----------|
197
+| 无数据 | 「暂无入库记录」 |
198
+| 有筛选无结果 | 「未找到符合条件的入库单」 |
199
+| 未选商品提交 | `请添加入库商品` |
200
+| 数量非法 | `请输入正确的入库数量` |
201
+| 条码未匹配 | 展示后端 `未找到该条码对应的商品` |
202
+| 条码多匹配 | 弹窗选择规格 |
203
+| 提交成功 | `入库成功` |
204
+
205
+---
206
+
207
+## 11. 与兄弟模块边界(前端)
208
+
209
+| 模块 | 关系 |
210
+|------|------|
211
+| **商品列表** | 新增规格自动入库单在本列表展示;不在商品页重复入库入口 |
212
+| **商品出库 / 库存调整** | 独立菜单,本方案不实现 |
213
+| **库存查询 / 库存日志** | 可读本模块写入记录;页面待建 |
214
+
215
+---
216
+
217
+## 12. 联调检查清单
218
+
219
+- [ ] 菜单挂载 `agri/seller/stock/inbound/index`,路由可访问
220
+- [ ] 列表默认按入库时间倒序
221
+- [ ] 检索:单号、商品名、类型、时间、经办人组合过滤
222
+- [ ] 手工入库:采购/退货/其他类型提交后库存增加、列表刷新
223
+- [ ] 商品保存触发的「新增商品规格」单可在列表查看,详情有留痕提示
224
+- [ ] 条码:无匹配/单匹配/多匹配三种路径
225
+- [ ] 同商品重复添加合并数量
226
+- [ ] Navbar 切店后列表仅当前店数据
227
+- [ ] 权限:无 `add` 不显示「商品入库」;无 `query` 不显示「详情」
228
+
229
+---
230
+
231
+## 13. 版本记录
232
+
233
+| 版本 | 说明 |
234
+|------|------|
235
+| **v1.0** | 首版:列表/检索/详情抽屉/手工入库抽屉/API 封装;对齐需求 v1.0.1 与 UI 稿;导入占位待后端 |
236
+
237
+---
238
+
239
+*文档版本:v1.0 · 依据《商品入库功能需求.md》v1.0.1、《商品入库技术方案.md》v1.0*

+ 60 - 0
ruoyi-ui/src/api/agri/seller/stock/inbound.js

@@ -0,0 +1,60 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 入库单列表
5
+export function listSellerStockInbound(query) {
6
+  return request({
7
+    url: '/agri/seller/stock/inbound/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 入库单详情
15
+export function getSellerStockInbound(inboundId) {
16
+  return request({
17
+    url: '/agri/seller/stock/inbound/' + inboundId,
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 手工创建入库单
24
+export function createSellerStockInbound(data) {
25
+  return request({
26
+    url: '/agri/seller/stock/inbound',
27
+    method: 'post',
28
+    data: data,
29
+    headers: sellerShopHeaders()
30
+  })
31
+}
32
+
33
+// 经办人下拉(历史入库操作人)
34
+export function listStockInboundOperators() {
35
+  return request({
36
+    url: '/agri/seller/stock/inbound/operators',
37
+    method: 'get',
38
+    headers: sellerShopHeaders()
39
+  })
40
+}
41
+
42
+// 商品选择器
43
+export function listStockInboundGoodsOptions(query) {
44
+  return request({
45
+    url: '/agri/seller/stock/inbound/goods/options',
46
+    method: 'get',
47
+    params: query,
48
+    headers: sellerShopHeaders()
49
+  })
50
+}
51
+
52
+// 条码/商品编号录入
53
+export function getStockInboundGoodsByBarcode(barcode) {
54
+  return request({
55
+    url: '/agri/seller/stock/inbound/goods/byBarcode',
56
+    method: 'get',
57
+    params: { barcode },
58
+    headers: sellerShopHeaders()
59
+  })
60
+}

+ 8 - 1
ruoyi-ui/src/views/agri/seller/goods/index.vue

@@ -190,7 +190,14 @@
190 190
           </el-col>
191 191
           <el-col :span="12">
192 192
             <el-form-item label="库存" prop="stock">
193
-              <el-input-number v-model="form.stock" :min="0" :precision="0" controls-position="right" style="width: 100%" />
193
+              <el-input-number
194
+                v-model="form.stock"
195
+                :min="0"
196
+                :precision="0"
197
+                :disabled="!!form.goodsId"
198
+                controls-position="right"
199
+                style="width: 100%"
200
+              />
194 201
             </el-form-item>
195 202
           </el-col>
196 203
         </el-row>

+ 4 - 2
ruoyi-ui/src/views/agri/seller/order/detail.vue

@@ -87,7 +87,7 @@
87 87
         </el-timeline-item>
88 88
       </el-timeline>
89 89
 
90
-      <div class="action-bar">
90
+      <div v-if="showActions" class="action-bar">
91 91
         <el-button
92 92
           v-if="canShip(detail)"
93 93
           type="primary"
@@ -127,7 +127,9 @@ export default {
127 127
   name: "SellerOrderDetail",
128 128
   props: {
129 129
     visible: { type: Boolean, default: false },
130
-    orderId: { type: [Number, String], default: null }
130
+    orderId: { type: [Number, String], default: null },
131
+    /** 是否展示底部履约/删除操作(嵌入只读场景设为 false) */
132
+    showActions: { type: Boolean, default: true }
131 133
   },
132 134
   data() {
133 135
     return {

+ 2 - 1
ruoyi-ui/src/views/agri/seller/review/detail.vue

@@ -53,7 +53,8 @@
53 53
       <h4 class="section-header">关联订单</h4>
54 54
       <el-descriptions :column="1" border size="small" class="mb16">
55 55
         <el-descriptions-item label="订单编号">
56
-          <el-button v-if="detail.orderNo" type="text" class="order-link" @click="handleViewOrder">{{ detail.orderNo }}</el-button>
56
+          <el-button v-if="detail.orderId && detail.orderNo" type="text" class="order-link" @click="handleViewOrder">{{ detail.orderNo }}</el-button>
57
+          <span v-else-if="detail.orderNo">{{ detail.orderNo }}</span>
57 58
           <span v-else>—</span>
58 59
         </el-descriptions-item>
59 60
       </el-descriptions>

+ 15 - 4
ruoyi-ui/src/views/agri/seller/review/index.vue

@@ -107,7 +107,7 @@
107 107
       <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
108 108
     </el-card>
109 109
 
110
-    <!-- 详情抽屉 -->
110
+    <!-- 评价详情抽屉 -->
111 111
     <seller-review-detail
112 112
       ref="reviewDetail"
113 113
       :visible.sync="detailOpen"
@@ -117,6 +117,13 @@
117 117
       @success="handleSuccess"
118 118
     />
119 119
 
120
+    <!-- 关联订单详情抽屉(只读) -->
121
+    <seller-order-detail
122
+      :visible.sync="orderDetailOpen"
123
+      :order-id="currentOrderId"
124
+      :show-actions="false"
125
+    />
126
+
120 127
     <!-- 回复弹窗 -->
121 128
     <el-dialog :title="replyDialogTitle" :visible.sync="replyOpen" width="560px" append-to-body @close="cancelReply">
122 129
       <el-form ref="replyFormRef" :model="replyForm" :rules="replyRules" label-width="90px">
@@ -148,10 +155,11 @@ import {
148 155
 import { getSellerContext } from "@/api/agri/seller/context"
149 156
 import { setSellerShopContext } from "@/utils/sellerShop"
150 157
 import SellerReviewDetail from "./detail"
158
+import SellerOrderDetail from "../order/detail"
151 159
 
152 160
 export default {
153 161
   name: "AgriSellerReview",
154
-  components: { SellerReviewDetail },
162
+  components: { SellerReviewDetail, SellerOrderDetail },
155 163
   data() {
156 164
     return {
157 165
       loading: true,
@@ -160,6 +168,8 @@ export default {
160 168
       reviewList: [],
161 169
       detailOpen: false,
162 170
       currentReviewId: null,
171
+      orderDetailOpen: false,
172
+      currentOrderId: null,
163 173
       replyOpen: false,
164 174
       replyDialogTitle: "回复评价",
165 175
       currentReplyReviewId: null,
@@ -281,10 +291,11 @@ export default {
281 291
       this.handleReply(detail)
282 292
     },
283 293
     handleViewOrder(detail) {
284
-      if (!detail.orderNo) {
294
+      if (!detail.orderId) {
285 295
         return
286 296
       }
287
-      this.$router.push({ path: "/seller/order", query: { orderNo: detail.orderNo } })
297
+      this.currentOrderId = detail.orderId
298
+      this.orderDetailOpen = true
288 299
     },
289 300
     cancelReply() {
290 301
       this.replyOpen = false

+ 1 - 1
ruoyi-ui/src/views/agri/seller/shop/index.vue

@@ -243,7 +243,7 @@ export default {
243 243
     },
244 244
     /** 跳转员工管理(同一当前店铺上下文) */
245 245
     goEmployeeManage() {
246
-      this.$router.push({ path: "/seller/employee" })
246
+      this.$router.push({ path: "/account/seller/employee" })
247 247
     }
248 248
   }
249 249
 }

+ 133 - 0
ruoyi-ui/src/views/agri/seller/stock/inbound/detail.vue

@@ -0,0 +1,133 @@
1
+<template>
2
+  <el-drawer
3
+    :title="'入库单详情 · ' + (detail.inboundNo || '')"
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.inboundNo || '—' }}</el-descriptions-item>
14
+        <el-descriptions-item label="入库类型">{{ detail.inboundTypeText || inboundTypeLabel(detail.inboundType) }}</el-descriptions-item>
15
+        <el-descriptions-item label="入库时间">{{ parseTime(detail.inboundTime) || '—' }}</el-descriptions-item>
16
+        <el-descriptions-item label="经办人">{{ detail.operatorName || '—' }}</el-descriptions-item>
17
+        <el-descriptions-item label="备注" :span="2">{{ detail.remark || '—' }}</el-descriptions-item>
18
+      </el-descriptions>
19
+      <p v-if="detail.inboundType === '0'" class="section-tip">系统自动入库:库存已在商品保存时写入,本单仅留痕。</p>
20
+
21
+      <h4 class="section-header">入库明细</h4>
22
+      <el-table border size="small" :data="detail.items || []" empty-text="暂无明细">
23
+        <el-table-column label="商品信息" min-width="220">
24
+          <template slot-scope="scope">
25
+            <div class="goods-cell">
26
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="44" :height="44" />
27
+              <span>{{ scope.row.goodsName || '—' }}</span>
28
+            </div>
29
+          </template>
30
+        </el-table-column>
31
+        <el-table-column label="商品规格" width="120" align="center">
32
+          <template slot-scope="scope">
33
+            <span>{{ specText(scope.row.specText) }}</span>
34
+          </template>
35
+        </el-table-column>
36
+        <el-table-column label="入库数量" prop="quantity" width="100" align="center" />
37
+      </el-table>
38
+    </div>
39
+  </el-drawer>
40
+</template>
41
+
42
+<script>
43
+import { getSellerStockInbound } from "@/api/agri/seller/stock/inbound"
44
+
45
+const INBOUND_TYPE_MAP = {
46
+  "0": "新增商品规格",
47
+  "1": "采购入库",
48
+  "2": "退货入库",
49
+  "3": "其他入库"
50
+}
51
+
52
+export default {
53
+  name: "SellerStockInboundDetail",
54
+  props: {
55
+    visible: { type: Boolean, default: false },
56
+    inboundId: { type: [Number, String], default: null }
57
+  },
58
+  data() {
59
+    return {
60
+      localVisible: false,
61
+      loading: false,
62
+      detail: {}
63
+    }
64
+  },
65
+  watch: {
66
+    visible(val) {
67
+      this.localVisible = val
68
+      if (val && this.inboundId) {
69
+        this.loadDetail()
70
+      }
71
+    },
72
+    localVisible(val) {
73
+      this.$emit("update:visible", val)
74
+    },
75
+    inboundId(val) {
76
+      if (val && this.localVisible) {
77
+        this.loadDetail()
78
+      }
79
+    }
80
+  },
81
+  methods: {
82
+    inboundTypeLabel(type) {
83
+      return INBOUND_TYPE_MAP[type] || "—"
84
+    },
85
+    specText(text) {
86
+      const val = text ? String(text).trim() : ""
87
+      return val || "—"
88
+    },
89
+    loadDetail() {
90
+      if (!this.inboundId) {
91
+        return
92
+      }
93
+      this.loading = true
94
+      getSellerStockInbound(this.inboundId).then(response => {
95
+        this.detail = response.data || {}
96
+        this.loading = false
97
+      }).catch(() => {
98
+        this.loading = false
99
+      })
100
+    },
101
+    reload() {
102
+      this.loadDetail()
103
+    }
104
+  }
105
+}
106
+</script>
107
+
108
+<style scoped lang="scss">
109
+.drawer-content {
110
+  padding: 0 20px 20px;
111
+}
112
+.section-header {
113
+  margin: 16px 0 10px;
114
+  font-size: 15px;
115
+  color: #303133;
116
+  border-left: 3px solid #409EFF;
117
+  padding-left: 8px;
118
+}
119
+.section-tip {
120
+  margin: -8px 0 12px;
121
+  font-size: 12px;
122
+  color: #909399;
123
+  line-height: 1.5;
124
+}
125
+.mb16 {
126
+  margin-bottom: 16px;
127
+}
128
+.goods-cell {
129
+  display: flex;
130
+  align-items: center;
131
+  gap: 8px;
132
+}
133
+</style>

+ 446 - 0
ruoyi-ui/src/views/agri/seller/stock/inbound/form.vue

@@ -0,0 +1,446 @@
1
+<template>
2
+  <el-drawer
3
+    title="商品入库"
4
+    :visible.sync="localVisible"
5
+    direction="rtl"
6
+    size="85%"
7
+    append-to-body
8
+    :wrapper-closable="false"
9
+    custom-class="inbound-form-drawer"
10
+    @close="handleClose"
11
+  >
12
+    <div class="drawer-body">
13
+      <h4 class="section-header">基本信息</h4>
14
+      <el-form ref="baseFormRef" :model="form" :rules="baseRules" label-width="90px" size="small" class="base-form">
15
+        <el-form-item label="入库类型" prop="inboundType">
16
+          <el-radio-group v-model="form.inboundType">
17
+            <el-radio label="1">采购入库</el-radio>
18
+            <el-radio label="2">退货入库</el-radio>
19
+            <el-radio label="3">其他入库</el-radio>
20
+          </el-radio-group>
21
+        </el-form-item>
22
+        <el-form-item label="备注" prop="remark">
23
+          <el-input
24
+            v-model="form.remark"
25
+            type="textarea"
26
+            :rows="3"
27
+            placeholder="请输入备注"
28
+            maxlength="200"
29
+            show-word-limit
30
+            style="max-width: 640px"
31
+          />
32
+        </el-form-item>
33
+      </el-form>
34
+
35
+      <h4 class="section-header">入库商品</h4>
36
+      <div class="goods-toolbar">
37
+        <el-button type="primary" size="mini" icon="el-icon-goods" @click="openGoodsPicker">选择商品</el-button>
38
+        <!-- <el-button size="mini" icon="el-icon-upload2" @click="handleImportTip">导入商品</el-button> -->
39
+        <!-- <div class="barcode-wrap">
40
+          <el-input
41
+            v-model="barcodeInput"
42
+            placeholder="请输入商品条码"
43
+            clearable
44
+            size="small"
45
+            style="width: 280px"
46
+            @keyup.enter.native="handleBarcodeConfirm"
47
+          >
48
+            <i slot="prefix" class="el-input__icon el-icon-full-screen" />
49
+            <el-button slot="append" type="primary" @click="handleBarcodeConfirm">确定(Enter)</el-button>
50
+          </el-input>
51
+        </div> -->
52
+      </div>
53
+
54
+      <el-table border size="small" :data="formItems" class="items-table" empty-text="请添加入库商品">
55
+        <el-table-column label="商品信息" min-width="220">
56
+          <template slot-scope="scope">
57
+            <div class="goods-cell">
58
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="44" :height="44" />
59
+              <span>{{ scope.row.goodsName || '—' }}</span>
60
+            </div>
61
+          </template>
62
+        </el-table-column>
63
+        <el-table-column label="商品规格" width="120" align="center">
64
+          <template slot-scope="scope">
65
+            <span>{{ specText(scope.row.specText) }}</span>
66
+          </template>
67
+        </el-table-column>
68
+        <el-table-column label="当前可用库存" width="120" align="center">
69
+          <template slot-scope="scope">
70
+            <span>{{ scope.row.stock != null ? scope.row.stock : '—' }}</span>
71
+          </template>
72
+        </el-table-column>
73
+        <el-table-column label="入库数量" width="160" align="center">
74
+          <template slot-scope="scope">
75
+            <el-input-number
76
+              v-model="scope.row.quantity"
77
+              :min="1"
78
+              :precision="0"
79
+              controls-position="right"
80
+              size="small"
81
+              style="width: 120px"
82
+            />
83
+          </template>
84
+        </el-table-column>
85
+        <el-table-column label="操作" width="80" align="center">
86
+          <template slot-scope="scope">
87
+            <el-button type="text" size="mini" @click="removeItem(scope.$index)">删除</el-button>
88
+          </template>
89
+        </el-table-column>
90
+      </el-table>
91
+    </div>
92
+
93
+    <div class="drawer-footer">
94
+      <el-button @click="handleCancel">取 消</el-button>
95
+      <el-button type="primary" :loading="submitting" @click="handleSubmit">确 认</el-button>
96
+    </div>
97
+
98
+    <!-- 商品选择器 -->
99
+    <el-dialog title="选择商品" :visible.sync="pickerOpen" width="820px" append-to-body @close="resetPicker">
100
+      <el-form :inline="true" size="small" @submit.native.prevent>
101
+        <el-form-item label="商品名称">
102
+          <el-input v-model="pickerQuery.keyword" placeholder="名称/编号" clearable style="width: 200px" @keyup.enter.native="searchGoodsOptions" />
103
+        </el-form-item>
104
+        <el-form-item>
105
+          <el-button type="primary" icon="el-icon-search" @click="searchGoodsOptions">搜索</el-button>
106
+        </el-form-item>
107
+      </el-form>
108
+      <el-table
109
+        ref="pickerTable"
110
+        border
111
+        v-loading="pickerLoading"
112
+        :data="pickerList"
113
+        max-height="360"
114
+        @selection-change="handlePickerSelection"
115
+      >
116
+        <el-table-column type="selection" width="50" align="center" />
117
+        <el-table-column label="商品信息" min-width="200">
118
+          <template slot-scope="scope">
119
+            <div class="goods-cell">
120
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="40" :height="40" />
121
+              <span>{{ scope.row.goodsName || '—' }}</span>
122
+            </div>
123
+          </template>
124
+        </el-table-column>
125
+        <el-table-column label="规格" width="100" align="center">
126
+          <template slot-scope="scope">
127
+            <span>{{ specText(scope.row.specText) }}</span>
128
+          </template>
129
+        </el-table-column>
130
+        <el-table-column label="编号" prop="goodsSn" width="120" :show-overflow-tooltip="true" />
131
+        <el-table-column label="可用库存" prop="stock" width="90" align="center" />
132
+      </el-table>
133
+      <pagination
134
+        v-show="pickerTotal > 0"
135
+        :total="pickerTotal"
136
+        :page.sync="pickerQuery.pageNum"
137
+        :limit.sync="pickerQuery.pageSize"
138
+        @pagination="loadGoodsOptions"
139
+      />
140
+      <div slot="footer" class="dialog-footer">
141
+        <el-button type="primary" @click="confirmPicker">确 定</el-button>
142
+        <el-button @click="pickerOpen = false">取 消</el-button>
143
+      </div>
144
+    </el-dialog>
145
+
146
+    <!-- 条码多匹配选择 -->
147
+    <el-dialog title="选择商品规格" :visible.sync="barcodePickOpen" width="640px" append-to-body>
148
+      <p class="pick-tip">该条码匹配到多个商品,请选择其一:</p>
149
+      <el-table border :data="barcodeCandidates" @row-click="selectBarcodeCandidate">
150
+        <el-table-column label="商品信息" min-width="180">
151
+          <template slot-scope="scope">
152
+            <div class="goods-cell">
153
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="40" :height="40" />
154
+              <span>{{ scope.row.goodsName || '—' }}</span>
155
+            </div>
156
+          </template>
157
+        </el-table-column>
158
+        <el-table-column label="规格" width="100" align="center">
159
+          <template slot-scope="scope">
160
+            <span>{{ specText(scope.row.specText) }}</span>
161
+          </template>
162
+        </el-table-column>
163
+        <el-table-column label="可用库存" prop="stock" width="90" align="center" />
164
+        <el-table-column label="操作" width="80" align="center">
165
+          <template slot-scope="scope">
166
+            <el-button type="text" size="mini" @click.stop="selectBarcodeCandidate(scope.row)">选择</el-button>
167
+          </template>
168
+        </el-table-column>
169
+      </el-table>
170
+    </el-dialog>
171
+  </el-drawer>
172
+</template>
173
+
174
+<script>
175
+import {
176
+  createSellerStockInbound,
177
+  listStockInboundGoodsOptions,
178
+  getStockInboundGoodsByBarcode
179
+} from "@/api/agri/seller/stock/inbound"
180
+
181
+export default {
182
+  name: "SellerStockInboundForm",
183
+  props: {
184
+    visible: { type: Boolean, default: false }
185
+  },
186
+  data() {
187
+    return {
188
+      localVisible: false,
189
+      submitting: false,
190
+      form: {
191
+        inboundType: "1",
192
+        remark: undefined
193
+      },
194
+      formItems: [],
195
+      barcodeInput: "",
196
+      baseRules: {
197
+        inboundType: [{ required: true, message: "请选择入库类型", trigger: "change" }]
198
+      },
199
+      pickerOpen: false,
200
+      pickerLoading: false,
201
+      pickerList: [],
202
+      pickerTotal: 0,
203
+      pickerSelection: [],
204
+      pickerQuery: {
205
+        pageNum: 1,
206
+        pageSize: 10,
207
+        keyword: undefined
208
+      },
209
+      barcodePickOpen: false,
210
+      barcodeCandidates: []
211
+    }
212
+  },
213
+  watch: {
214
+    visible(val) {
215
+      this.localVisible = val
216
+      if (val) {
217
+        this.resetForm()
218
+      }
219
+    },
220
+    localVisible(val) {
221
+      this.$emit("update:visible", val)
222
+    }
223
+  },
224
+  methods: {
225
+    specText(text) {
226
+      const val = text ? String(text).trim() : ""
227
+      return val || "—"
228
+    },
229
+    resetForm() {
230
+      this.form = {
231
+        inboundType: "1",
232
+        remark: undefined
233
+      }
234
+      this.formItems = []
235
+      this.barcodeInput = ""
236
+      this.submitting = false
237
+      this.$nextTick(() => {
238
+        if (this.$refs.baseFormRef) {
239
+          this.$refs.baseFormRef.clearValidate()
240
+        }
241
+      })
242
+    },
243
+    handleClose() {
244
+      this.resetForm()
245
+    },
246
+    handleCancel() {
247
+      this.localVisible = false
248
+    },
249
+    openGoodsPicker() {
250
+      this.pickerOpen = true
251
+      this.pickerQuery.pageNum = 1
252
+      this.loadGoodsOptions()
253
+    },
254
+    resetPicker() {
255
+      this.pickerSelection = []
256
+      this.pickerQuery.keyword = undefined
257
+      if (this.$refs.pickerTable) {
258
+        this.$refs.pickerTable.clearSelection()
259
+      }
260
+    },
261
+    searchGoodsOptions() {
262
+      this.pickerQuery.pageNum = 1
263
+      this.loadGoodsOptions()
264
+    },
265
+    loadGoodsOptions() {
266
+      this.pickerLoading = true
267
+      listStockInboundGoodsOptions(this.pickerQuery).then(response => {
268
+        this.pickerList = response.rows || []
269
+        this.pickerTotal = response.total || 0
270
+        this.pickerLoading = false
271
+      }).catch(() => {
272
+        this.pickerLoading = false
273
+      })
274
+    },
275
+    handlePickerSelection(rows) {
276
+      this.pickerSelection = rows || []
277
+    },
278
+    confirmPicker() {
279
+      if (!this.pickerSelection.length) {
280
+        this.$modal.msgWarning("请选择商品")
281
+        return
282
+      }
283
+      this.pickerSelection.forEach(row => {
284
+        this.appendGoodsRow(row)
285
+      })
286
+      this.pickerOpen = false
287
+    },
288
+    handleImportTip() {
289
+      this.$modal.msgWarning("批量导入功能暂未开放,请使用选择商品或条码录入")
290
+    },
291
+    handleBarcodeConfirm() {
292
+      const code = this.barcodeInput ? String(this.barcodeInput).trim() : ""
293
+      if (!code) {
294
+        this.$modal.msgWarning("请输入商品条码")
295
+        return
296
+      }
297
+      getStockInboundGoodsByBarcode(code).then(response => {
298
+        const rows = response.data || []
299
+        if (!rows.length) {
300
+          this.$modal.msgError("未找到该条码对应的商品")
301
+          return
302
+        }
303
+        if (rows.length === 1) {
304
+          this.appendGoodsRow(rows[0])
305
+          this.barcodeInput = ""
306
+          return
307
+        }
308
+        this.barcodeCandidates = rows
309
+        this.barcodePickOpen = true
310
+      })
311
+    },
312
+    selectBarcodeCandidate(row) {
313
+      this.appendGoodsRow(row)
314
+      this.barcodePickOpen = false
315
+      this.barcodeCandidates = []
316
+      this.barcodeInput = ""
317
+    },
318
+    itemKey(row) {
319
+      const skuId = row.skuId != null ? row.skuId : ""
320
+      return `${row.goodsId}_${skuId}`
321
+    },
322
+    appendGoodsRow(row) {
323
+      if (!row || row.goodsId == null) {
324
+        return
325
+      }
326
+      const key = this.itemKey(row)
327
+      const exist = this.formItems.find(item => this.itemKey(item) === key)
328
+      if (exist) {
329
+        exist.quantity = (exist.quantity || 0) + 1
330
+        this.$modal.msgSuccess("该商品已在明细中,已合并数量")
331
+        return
332
+      }
333
+      this.formItems.push({
334
+        goodsId: row.goodsId,
335
+        skuId: row.skuId || null,
336
+        goodsName: row.goodsName,
337
+        mainPic: row.mainPic,
338
+        specText: row.specText,
339
+        stock: row.stock,
340
+        quantity: 1
341
+      })
342
+    },
343
+    removeItem(index) {
344
+      this.formItems.splice(index, 1)
345
+    },
346
+    validateItems() {
347
+      if (!this.formItems.length) {
348
+        this.$modal.msgWarning("请添加入库商品")
349
+        return false
350
+      }
351
+      for (let i = 0; i < this.formItems.length; i++) {
352
+        const qty = this.formItems[i].quantity
353
+        if (qty == null || !Number.isInteger(Number(qty)) || Number(qty) < 1) {
354
+          this.$modal.msgWarning("请输入正确的入库数量")
355
+          return false
356
+        }
357
+      }
358
+      return true
359
+    },
360
+    handleSubmit() {
361
+      this.$refs.baseFormRef.validate(valid => {
362
+        if (!valid || !this.validateItems()) {
363
+          return
364
+        }
365
+        const payload = {
366
+          inboundType: this.form.inboundType,
367
+          remark: this.form.remark ? String(this.form.remark).trim() : undefined,
368
+          items: this.formItems.map(item => ({
369
+            goodsId: item.goodsId,
370
+            skuId: item.skuId || null,
371
+            quantity: Number(item.quantity)
372
+          }))
373
+        }
374
+        this.submitting = true
375
+        createSellerStockInbound(payload).then(response => {
376
+          const data = response.data || {}
377
+          this.$modal.msgSuccess("入库成功")
378
+          this.localVisible = false
379
+          this.$emit("success", data)
380
+          this.submitting = false
381
+        }).catch(() => {
382
+          this.submitting = false
383
+        })
384
+      })
385
+    }
386
+  }
387
+}
388
+</script>
389
+
390
+<style scoped lang="scss">
391
+.drawer-body {
392
+  padding: 0 20px 72px;
393
+}
394
+.section-header {
395
+  margin: 8px 0 12px;
396
+  font-size: 15px;
397
+  color: #303133;
398
+  border-left: 3px solid #409EFF;
399
+  padding-left: 8px;
400
+}
401
+.base-form {
402
+  margin-bottom: 8px;
403
+}
404
+.goods-toolbar {
405
+  display: flex;
406
+  flex-wrap: wrap;
407
+  align-items: center;
408
+  gap: 10px;
409
+  margin-bottom: 12px;
410
+}
411
+.barcode-wrap {
412
+  margin-left: auto;
413
+}
414
+.items-table {
415
+  margin-top: 4px;
416
+}
417
+.goods-cell {
418
+  display: flex;
419
+  align-items: center;
420
+  gap: 8px;
421
+}
422
+.drawer-footer {
423
+  position: absolute;
424
+  right: 0;
425
+  bottom: 0;
426
+  left: 0;
427
+  z-index: 10;
428
+  padding: 12px 20px;
429
+  text-align: right;
430
+  background: #fff;
431
+  border-top: 1px solid #EBEEF5;
432
+}
433
+.pick-tip {
434
+  margin: 0 0 12px;
435
+  font-size: 13px;
436
+  color: #606266;
437
+}
438
+</style>
439
+
440
+<style lang="scss">
441
+.inbound-form-drawer .el-drawer__body {
442
+  position: relative;
443
+  padding: 0;
444
+  overflow: auto;
445
+}
446
+</style>

+ 230 - 0
ruoyi-ui/src/views/agri/seller/stock/inbound/index.vue

@@ -0,0 +1,230 @@
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="keyword">
7
+          <el-input
8
+            v-model="queryParams.keyword"
9
+            placeholder="入库单号/商品名称"
10
+            clearable
11
+            style="width: 180px"
12
+            @keyup.enter.native="handleQuery"
13
+          />
14
+        </el-form-item>
15
+        <el-form-item label="入库时间">
16
+          <el-date-picker
17
+            v-model="dateRange"
18
+            type="daterange"
19
+            value-format="yyyy-MM-dd"
20
+            range-separator="至"
21
+            start-placeholder="开始日期"
22
+            end-placeholder="结束日期"
23
+            style="width: 240px"
24
+          />
25
+        </el-form-item>
26
+        <el-form-item label="入库类型" prop="inboundType">
27
+          <el-select v-model="queryParams.inboundType" placeholder="全部" clearable style="width: 150px">
28
+            <el-option v-for="item in inboundTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
29
+          </el-select>
30
+        </el-form-item>
31
+        <el-form-item label="经办人" prop="operatorId">
32
+          <el-select v-model="queryParams.operatorId" placeholder="全部" clearable filterable style="width: 140px">
33
+            <el-option
34
+              v-for="item in operatorOptions"
35
+              :key="item.operatorId"
36
+              :label="item.operatorName"
37
+              :value="item.operatorId"
38
+            />
39
+          </el-select>
40
+        </el-form-item>
41
+        <el-form-item>
42
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
43
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
44
+        </el-form-item>
45
+      </el-form>
46
+    </el-card>
47
+
48
+    <br/>
49
+
50
+    <!-- 列表区 -->
51
+    <el-card shadow="never" class="table-card">
52
+      <el-row :gutter="10" class="mb8">
53
+        <el-col :span="1.5">
54
+          <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleCreate" v-hasPermi="['agri:seller:stock:inbound:add']">商品入库</el-button>
55
+        </el-col>
56
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
57
+      </el-row>
58
+
59
+      <el-table border v-loading="loading" :data="inboundList" :empty-text="emptyTableText">
60
+        <el-table-column label="入库单号" align="center" prop="inboundNo" min-width="180" :show-overflow-tooltip="true" />
61
+        <el-table-column label="入库类型" align="center" width="130">
62
+          <template slot-scope="scope">
63
+            <span>{{ scope.row.inboundTypeText || inboundTypeLabel(scope.row.inboundType) }}</span>
64
+          </template>
65
+        </el-table-column>
66
+        <el-table-column label="入库时间" align="center" width="160">
67
+          <template slot-scope="scope">
68
+            <span>{{ parseTime(scope.row.inboundTime) || '—' }}</span>
69
+          </template>
70
+        </el-table-column>
71
+        <el-table-column label="备注" align="center" min-width="120" :show-overflow-tooltip="true">
72
+          <template slot-scope="scope">
73
+            <span>{{ scope.row.remark || '—' }}</span>
74
+          </template>
75
+        </el-table-column>
76
+        <el-table-column label="经办人" align="center" prop="operatorName" width="100" :show-overflow-tooltip="true">
77
+          <template slot-scope="scope">
78
+            <span>{{ scope.row.operatorName || '—' }}</span>
79
+          </template>
80
+        </el-table-column>
81
+        <el-table-column label="操作" align="center" width="90" fixed="right">
82
+          <template slot-scope="scope">
83
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['agri:seller:stock:inbound:query']">详情</el-button>
84
+          </template>
85
+        </el-table-column>
86
+      </el-table>
87
+
88
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
89
+    </el-card>
90
+
91
+    <!-- 详情抽屉 -->
92
+    <seller-stock-inbound-detail
93
+      ref="inboundDetail"
94
+      :visible.sync="detailOpen"
95
+      :inbound-id="currentInboundId"
96
+    />
97
+
98
+    <!-- 手工入库抽屉 -->
99
+    <seller-stock-inbound-form
100
+      :visible.sync="formOpen"
101
+      @success="handleFormSuccess"
102
+    />
103
+  </div>
104
+</template>
105
+
106
+<script>
107
+import {
108
+  listSellerStockInbound,
109
+  listStockInboundOperators
110
+} from "@/api/agri/seller/stock/inbound"
111
+import { getSellerContext } from "@/api/agri/seller/context"
112
+import { setSellerShopContext } from "@/utils/sellerShop"
113
+import SellerStockInboundDetail from "./detail"
114
+import SellerStockInboundForm from "./form"
115
+
116
+const INBOUND_TYPE_MAP = {
117
+  "0": "新增商品规格",
118
+  "1": "采购入库",
119
+  "2": "退货入库",
120
+  "3": "其他入库"
121
+}
122
+
123
+export default {
124
+  name: "AgriSellerStockInbound",
125
+  components: { SellerStockInboundDetail, SellerStockInboundForm },
126
+  data() {
127
+    return {
128
+      loading: false,
129
+      showSearch: true,
130
+      total: 0,
131
+      inboundList: [],
132
+      dateRange: [],
133
+      operatorOptions: [],
134
+      detailOpen: false,
135
+      formOpen: false,
136
+      currentInboundId: null,
137
+      queryParams: {
138
+        pageNum: 1,
139
+        pageSize: 10,
140
+        keyword: undefined,
141
+        inboundType: undefined,
142
+        operatorId: undefined
143
+      },
144
+      inboundTypeOptions: [
145
+        { value: "0", label: "新增商品规格" },
146
+        { value: "1", label: "采购入库" },
147
+        { value: "2", label: "退货入库" },
148
+        { value: "3", label: "其他入库" }
149
+      ]
150
+    }
151
+  },
152
+  computed: {
153
+    hasSearchFilter() {
154
+      return !!(this.queryParams.keyword || this.queryParams.inboundType || this.queryParams.operatorId || (this.dateRange && this.dateRange.length))
155
+    },
156
+    emptyTableText() {
157
+      return this.hasSearchFilter ? "未找到符合条件的入库单" : "暂无入库记录"
158
+    }
159
+  },
160
+  created() {
161
+    this.initPage()
162
+  },
163
+  methods: {
164
+    inboundTypeLabel(type) {
165
+      return INBOUND_TYPE_MAP[type] || "—"
166
+    },
167
+    initPage() {
168
+      this.loadShopContext().then(() => {
169
+        this.loadOperators()
170
+        this.getList()
171
+      }).catch(() => {
172
+        this.loadOperators()
173
+        this.getList()
174
+      })
175
+    },
176
+    loadShopContext() {
177
+      return getSellerContext().then(response => {
178
+        const data = response.data || {}
179
+        if (data.shopId != null) {
180
+          setSellerShopContext(data.shopId, data.shopName)
181
+        }
182
+      })
183
+    },
184
+    loadOperators() {
185
+      listStockInboundOperators().then(response => {
186
+        this.operatorOptions = response.data || []
187
+      }).catch(() => {
188
+        this.operatorOptions = []
189
+      })
190
+    },
191
+    getList() {
192
+      this.loading = true
193
+      listSellerStockInbound(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
194
+        this.inboundList = response.rows || []
195
+        this.total = response.total || 0
196
+        this.loading = false
197
+      }).catch(() => {
198
+        this.loading = false
199
+      })
200
+    },
201
+    handleQuery() {
202
+      this.queryParams.pageNum = 1
203
+      this.getList()
204
+    },
205
+    resetQuery() {
206
+      this.dateRange = []
207
+      this.resetForm("queryForm")
208
+      this.queryParams.pageNum = 1
209
+      this.getList()
210
+    },
211
+    handleCreate() {
212
+      this.formOpen = true
213
+    },
214
+    handleDetail(row) {
215
+      this.currentInboundId = row.inboundId
216
+      this.detailOpen = true
217
+    },
218
+    handleFormSuccess() {
219
+      this.loadOperators()
220
+      this.getList()
221
+    }
222
+  }
223
+}
224
+</script>
225
+
226
+<style scoped lang="scss">
227
+.mb8 {
228
+  margin-bottom: 8px;
229
+}
230
+</style>