xsh_1997 1 неделя назад
Родитель
Сommit
b83dc8a8c9

+ 183 - 0
doc/店铺后台/库存管理/库存查询/库存查询前端技术方案.md

@@ -0,0 +1,183 @@
1
+# 库存查询 — 前端技术方案
2
+
3
+> **依据:** 《库存查询功能需求.md》v1.0、《库存查询技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺 **实时可用库存** 列表、关键词检索、**按商品查看出入库明细**(只读);**不含** 改库存、创建入/出库单、成本维护、平台端。  
6
+> **实现状态:** `index.vue`、`api/agri/seller/stock/query.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/stock/query` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/stock/log/index.vue`(只读列表)、`agri/seller/stock/inbound/index.vue`(布局) |
17
+| 布局 | 检索 `el-card` + `<br/>` + 列表 `el-card` + `border` 表格 |
18
+| 明细 | `el-dialog`(宽 900px,`append-to-body`) |
19
+| 图片 | 全局 `image-preview` |
20
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
21
+
22
+---
23
+
24
+## 2. 业务要点(前端需体现)
25
+
26
+| 项 | 说明 |
27
+|----|------|
28
+| 只读 | **无** 编辑库存、创建入/出库/调整单 |
29
+| 列表数据 | **未删除** 商品/规格的 **当前可用库存**(`biz_goods.stock`) |
30
+| 库存为 0 | **仍展示**;可用库存列可标红提示 |
31
+| 成本 | 后端 `cost` 为 null 时展示 **—**(本期无成本数据源) |
32
+| 明细同源 | `GET /{goodsId}/logs` 与 **库存日志** 同一套 `biz_stock_log` |
33
+| 明细数量 | **带符号**:入库 **+qty**、出库 **−qty**、0 仍为 0 |
34
+| 明细类型 | 展示 `ioTypeText`(变化原因文案),**不用** 笼统「入库增加库存」 |
35
+
36
+---
37
+
38
+## 3. 文件清单
39
+
40
+| 类型 | 路径 | 说明 |
41
+|------|------|------|
42
+| 列表页 | `ruoyi-ui/src/views/agri/seller/stock/query/index.vue` | 库存列表 + 出入库明细弹窗 |
43
+| 查询 API | `ruoyi-ui/src/api/agri/seller/stock/query.js` | list、logs |
44
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
45
+
46
+**组件 name(keep-alive):** `AgriSellerStockQuery`
47
+
48
+**不提供:** 独立明细页、分页明细(后端一次返回全量)、导出、跳转商品编辑。
49
+
50
+---
51
+
52
+## 4. 菜单与路由
53
+
54
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
55
+|----------|----------|-------------------|----------|
56
+| 库存查询 | `agri/seller/stock/query/index` | `seller/stock/query` | `agri:seller:stock:query:list` |
57
+
58
+**上级菜单:** 店铺经营管理端 → **库存管理**
59
+
60
+| 按钮权限 | 标识 | 页面落点 |
61
+|----------|------|----------|
62
+| 列表 + 明细 | `agri:seller:stock:query:list` | 进入页面、「出入库明细」 |
63
+
64
+---
65
+
66
+## 5. 页面结构(与代码一致)
67
+
68
+```text
69
+库存查询 index.vue
70
+├── 检索区 search-card
71
+│   ├── 库存搜索 keyword(商品名称/规格模糊,Enter 或搜索图标)
72
+│   └── 重置
73
+├── 列表区 table-card
74
+│   ├── right-toolbar
75
+│   ├── el-table border(empty-text 动态)
76
+│   │   ├── 商品信息(主图 + 名称)
77
+│   │   ├── 商品规格 specText(空则 —)
78
+│   │   ├── 可用库存 stock(0 可标红)
79
+│   │   ├── 成本 cost(null → —)
80
+│   │   └── 操作:出入库明细
81
+│   └── pagination
82
+└── 出入库明细弹窗 el-dialog
83
+    ├── 商品摘要(主图 + 名称 + 规格 + 当前库存)
84
+    └── el-table border
85
+        ├── 商品名称
86
+        ├── 出入库类型 ioTypeText
87
+        ├── 商品规格
88
+        ├── 出入库数量 ioQty(+绿 / −红)
89
+        └── 出入库时间 ioTime
90
+```
91
+
92
+---
93
+
94
+## 6. 出入库明细交互
95
+
96
+```text
97
+列表行 → 出入库明细
98
+    → 记录 currentRow(展示摘要)
99
+    → GET /{goodsId}/logs
100
+    → 表格展示(时间倒序由后端保证)
101
+    → 关闭弹窗清空状态
102
+```
103
+
104
+| 场景 | 文案 |
105
+|------|------|
106
+| 列表无数据 | 「暂无库存数据」 |
107
+| 检索无结果 | 「未找到符合条件的库存记录」 |
108
+| 明细无流水 | 「暂无出入库记录」 |
109
+
110
+---
111
+
112
+## 7. 数量展示规则
113
+
114
+| ioQty | 展示 | 样式 |
115
+|-------|------|------|
116
+| > 0 | `+N` | 绿色 `.qty-in` |
117
+| < 0 | `N`(自带负号) | 红色 `.qty-out` |
118
+| 0 | `0` | 默认色 |
119
+
120
+与 **库存日志** 模块差异:日志列表为 **非负变化量**;本模块明细为 **带符号出入库数量**(需求 §6.3)。
121
+
122
+---
123
+
124
+## 8. 店铺上下文(X-Shop-Id)
125
+
126
+| 步骤 | 说明 |
127
+|------|------|
128
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
129
+| 全部 API | `sellerShopHeaders()` |
130
+| Navbar 切店 | `location.reload()` 整页刷新 |
131
+
132
+---
133
+
134
+## 9. API 封装
135
+
136
+**模块:** `@/api/agri/seller/stock/query.js`
137
+
138
+| 方法 | HTTP | 路径 | 权限 |
139
+|------|------|------|------|
140
+| `listSellerStockQuery` | GET | `/list` | query:list |
141
+| `listSellerStockQueryLogs` | GET | `/{goodsId}/logs` | query:list |
142
+
143
+**列表 Query:** `pageNum`、`pageSize`、`keyword`。
144
+
145
+**列表 Row:** `goodsId`、`goodsName`、`mainPic`、`specText`、`stock`、`cost`。
146
+
147
+**明细 Row:** `goodsName`、`specText`、`ioTypeText`、`ioQty`、`ioTime`。
148
+
149
+---
150
+
151
+## 10. 与兄弟模块边界(前端)
152
+
153
+| 模块 | 关系 |
154
+|------|------|
155
+| **商品入库 / 出库** | 改库存后列表 `stock` 即时变化;明细含对应流水 |
156
+| **库存调整** | 调整后明细含「库存编辑」 |
157
+| **库存日志** | 全店时间序;本模块为 **单商品** 过滤同源数据 |
158
+| **商品列表** | 库存字段与列表 **同一口径**;本页不提供编辑入口 |
159
+
160
+---
161
+
162
+## 11. 联调检查清单
163
+
164
+- [ ] 菜单挂载 `agri/seller/stock/query/index`
165
+- [ ] 列表展示当前店铺未删除商品,含 stock=0 行
166
+- [ ] 关键词匹配商品名称、规格
167
+- [ ] 成本列无数据时为 —
168
+- [ ] 出入库明细:类型文案与库存日志一致
169
+- [ ] 明细数量正负号与方向正确
170
+- [ ] 支付扣减、手工入出库、删规格留痕等流水可见
171
+- [ ] Navbar 切店后列表刷新
172
+
173
+---
174
+
175
+## 12. 版本记录
176
+
177
+| 版本 | 说明 |
178
+|------|------|
179
+| **v1.0** | 首版:实时库存列表、关键词检索、出入库明细弹窗、API 封装;对齐需求 v1.0 与 UI 稿 |
180
+
181
+---
182
+
183
+*文档版本:v1.0 · 依据《库存查询功能需求.md》v1.0、《库存查询技术方案.md》v1.0*

+ 210 - 0
doc/店铺后台/库存管理/库存调整/库存调整前端技术方案.md

@@ -0,0 +1,210 @@
1
+# 库存调整 — 前端技术方案
2
+
3
+> **依据:** 《库存调整功能需求.md》v1.0、《库存调整技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺商品列表、关键词检索、**单规格库存调整**(增量/覆盖)、**调整记录**(只读);**不含** 入/出库单、批量调整、平台端。  
6
+> **实现状态:** `index.vue`、`api/agri/seller/stock/adjust.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/stock/adjust` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/stock/query/index.vue`(列表检索)、弹窗交互 |
17
+| 布局 | 检索 `el-card` + `<br/>` + 列表 `el-card` + `border` 表格 |
18
+| 调整/记录 | `el-dialog`(`append-to-body`) |
19
+| 图片 | 全局 `image-preview` |
20
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
21
+
22
+---
23
+
24
+## 2. 业务要点(前端需体现)
25
+
26
+| 项 | 说明 |
27
+|----|------|
28
+| 写库存入口 | **库存编辑** 场景唯一入口;**不产生** 入/出库单 |
29
+| 列表 | **未删除** 商品/规格;**不展示** 可用库存列(UI 稿) |
30
+| 调整方式 | `1` 增量(默认)/ `2` 覆盖 |
31
+| 调整数量 | 增量:**非零整数、可负**;覆盖:**≥0 整数** |
32
+| 明细类型 | 后端判定:增加(1)/减少(2)/覆盖(3),记录在「记录」弹窗 |
33
+| 不可撤销 | 确认后不可改;再次纠错须 **新一次调整** |
34
+| 与商品列表 | **已有规格改库存** 须走本模块(商品编辑页库存已 disabled) |
35
+
36
+---
37
+
38
+## 3. 文件清单
39
+
40
+| 类型 | 路径 | 说明 |
41
+|------|------|------|
42
+| 列表页 | `ruoyi-ui/src/views/agri/seller/stock/adjust/index.vue` | 商品列表 + 调整弹窗 + 记录弹窗 |
43
+| 调整 API | `ruoyi-ui/src/api/agri/seller/stock/adjust.js` | list、confirm、records |
44
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
45
+
46
+**组件 name(keep-alive):** `AgriSellerStockAdjust`
47
+
48
+**不提供:** 批量调整、调整单号列表、备注字段、导入、撤销、列表内改库存。
49
+
50
+---
51
+
52
+## 4. 菜单与路由
53
+
54
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
55
+|----------|----------|-------------------|----------|
56
+| 库存调整 | `agri/seller/stock/adjust/index` | `seller/stock/adjust` | `agri:seller:stock:adjust:list` |
57
+
58
+**上级菜单:** 店铺经营管理端 → **库存管理**
59
+
60
+| 按钮权限 | 标识 | 页面落点 |
61
+|----------|------|----------|
62
+| 列表 + 记录 | `agri:seller:stock:adjust:list` | 进入页面、「记录」 |
63
+| 确认调整 | `agri:seller:stock:adjust:edit` | 「调整」、POST 提交 |
64
+
65
+---
66
+
67
+## 5. 页面结构(与代码一致)
68
+
69
+```text
70
+库存调整 index.vue
71
+├── 检索区 search-card
72
+│   ├── 库存搜索 keyword(商品名称/规格模糊)
73
+│   └── 重置
74
+├── 列表区 table-card
75
+│   ├── right-toolbar
76
+│   ├── el-table border
77
+│   │   ├── 商品名称(主图 + 名称)
78
+│   │   ├── 商品规格
79
+│   │   └── 操作:调整 | 记录
80
+│   └── pagination
81
+├── 库存调整弹窗 el-dialog width=560px
82
+│   ├── 商品摘要(主图 + 名称 + 规格)
83
+│   ├── 调整方式 adjustMode(增量/覆盖 + 说明文案)
84
+│   ├── 调整数量 adjustQty(默认 1;覆盖 min=0)
85
+│   └── 取消 / 确认
86
+└── 记录弹窗 el-dialog width=760px
87
+    ├── 调整类型筛选 adjustType(全部/增加/减少/覆盖)
88
+    └── el-table:类型、调整前、调整后、操作账号、创建时间
89
+```
90
+
91
+---
92
+
93
+## 6. 库存调整弹窗
94
+
95
+### 6.1 提交 payload
96
+
97
+```json
98
+{
99
+  "adjustMode": "1",
100
+  "adjustQty": 5
101
+}
102
+```
103
+
104
+| adjustMode | 含义 | adjustQty 规则 |
105
+|------------|------|----------------|
106
+| `1` | 增量更新 | 非零整数,可为负 |
107
+| `2` | 覆盖更新 | ≥0 整数 |
108
+
109
+### 6.2 前端校验
110
+
111
+| 规则 | 说明 |
112
+|------|------|
113
+| ADJ1 | 调整方式必选 |
114
+| ADJ2 | 调整数量必填 |
115
+| ADJ3 | 增量时 qty ≠ 0 |
116
+| ADJ4 | 覆盖时 qty ≥ 0 |
117
+| 切换覆盖 | 若当前 qty 为负,自动置 0 |
118
+
119
+后端另校验 **调整后库存 ≥ 0**(ADJ5)。
120
+
121
+成功:`msgSuccess('调整成功')` → 关闭弹窗。
122
+
123
+---
124
+
125
+## 7. 调整记录弹窗
126
+
127
+**接口:** `GET /{goodsId}/records?adjustType=`
128
+
129
+| adjustType | 筛选 |
130
+|------------|------|
131
+| 空 | 全部 |
132
+| `1` | 增加库存 |
133
+| `2` | 减少库存 |
134
+| `3` | 覆盖库存 |
135
+
136
+**空态:** 「暂无数据」/ 有筛选无结果时友好提示。
137
+
138
+**只读:** 无编辑、删除。
139
+
140
+---
141
+
142
+## 8. 店铺上下文(X-Shop-Id)
143
+
144
+| 步骤 | 说明 |
145
+|------|------|
146
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
147
+| 全部 API | `sellerShopHeaders()` |
148
+| Navbar 切店 | `location.reload()` 整页刷新 |
149
+
150
+---
151
+
152
+## 9. API 封装
153
+
154
+**模块:** `@/api/agri/seller/stock/adjust.js`
155
+
156
+| 方法 | HTTP | 路径 | 权限 |
157
+|------|------|------|------|
158
+| `listSellerStockAdjust` | GET | `/list` | list |
159
+| `confirmSellerStockAdjust` | POST | `/{goodsId}` | edit |
160
+| `listSellerStockAdjustRecords` | GET | `/{goodsId}/records` | list |
161
+
162
+---
163
+
164
+## 10. 空状态与错误提示
165
+
166
+| 场景 | 文案 |
167
+|------|------|
168
+| 列表无数据 | 「暂无可调整商品」 |
169
+| 检索无结果 | 「未找到符合条件的商品」 |
170
+| 记录无数据 | 「暂无数据」 |
171
+| qty 为空 | 「请输入调整数量」 |
172
+| 增量 qty=0 | 「调整数量不能为 0」 |
173
+| 覆盖 qty 为负 | 「请输入正确的库存数量」 |
174
+| 提交成功 | 「调整成功」 |
175
+| 后端负库存 | 「调整后库存不能小于 0」等 |
176
+
177
+---
178
+
179
+## 11. 与兄弟模块边界(前端)
180
+
181
+| 模块 | 关系 |
182
+|------|------|
183
+| **商品列表** | 编辑页库存 disabled;改数走本模块 |
184
+| **商品入库/出库** | 业务入出库 **不走** 本模块 |
185
+| **库存查询** | 调整后可用库存即时更新;查询页 **无** 调整按钮 |
186
+| **库存日志** | 每次确认写入「库存编辑」流水 |
187
+
188
+---
189
+
190
+## 12. 联调检查清单
191
+
192
+- [ ] 菜单挂载 `agri/seller/stock/adjust/index`
193
+- [ ] 列表仅当前店铺未删除商品
194
+- [ ] 增量 +N / −N、覆盖 M 三种场景
195
+- [ ] 增量调减导致负库存时后端拒绝
196
+- [ ] 记录弹窗类型筛选与明细字段正确
197
+- [ ] 权限:无 `edit` 不显示「调整」
198
+- [ ] Navbar 切店后列表刷新
199
+
200
+---
201
+
202
+## 13. 版本记录
203
+
204
+| 版本 | 说明 |
205
+|------|------|
206
+| **v1.0** | 首版:列表/检索/调整弹窗/记录弹窗/API 封装;对齐需求 v1.0 与 UI 稿 |
207
+
208
+---
209
+
210
+*文档版本:v1.0 · 依据《库存调整功能需求.md》v1.0、《库存调整技术方案.md》v1.0*

+ 32 - 0
ruoyi-ui/src/api/agri/seller/stock/adjust.js

@@ -0,0 +1,32 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 可调整商品列表
5
+export function listSellerStockAdjust(query) {
6
+  return request({
7
+    url: '/agri/seller/stock/adjust/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 确认库存调整
15
+export function confirmSellerStockAdjust(goodsId, data) {
16
+  return request({
17
+    url: '/agri/seller/stock/adjust/' + goodsId,
18
+    method: 'post',
19
+    data: data,
20
+    headers: sellerShopHeaders()
21
+  })
22
+}
23
+
24
+// 调整记录
25
+export function listSellerStockAdjustRecords(goodsId, query) {
26
+  return request({
27
+    url: '/agri/seller/stock/adjust/' + goodsId + '/records',
28
+    method: 'get',
29
+    params: query,
30
+    headers: sellerShopHeaders()
31
+  })
32
+}

+ 21 - 0
ruoyi-ui/src/api/agri/seller/stock/query.js

@@ -0,0 +1,21 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 实时库存列表
5
+export function listSellerStockQuery(query) {
6
+  return request({
7
+    url: '/agri/seller/stock/query/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 出入库明细(按商品)
15
+export function listSellerStockQueryLogs(goodsId) {
16
+  return request({
17
+    url: '/agri/seller/stock/query/' + goodsId + '/logs',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}

+ 399 - 0
ruoyi-ui/src/views/agri/seller/stock/adjust/index.vue

@@ -0,0 +1,399 @@
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: 260px"
12
+            @keyup.enter.native="handleQuery"
13
+          >
14
+            <el-button slot="append" icon="el-icon-search" @click="handleQuery" />
15
+          </el-input>
16
+        </el-form-item>
17
+        <el-form-item>
18
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
19
+        </el-form-item>
20
+      </el-form>
21
+    </el-card>
22
+
23
+    <br/>
24
+
25
+    <!-- 列表区 -->
26
+    <el-card shadow="never" class="table-card">
27
+      <el-row :gutter="10" class="mb8">
28
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
29
+      </el-row>
30
+
31
+      <el-table border v-loading="loading" :data="goodsList" :empty-text="emptyTableText">
32
+        <el-table-column label="商品名称" align="left" min-width="260">
33
+          <template slot-scope="scope">
34
+            <div class="goods-cell">
35
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="44" :height="44" />
36
+              <span class="goods-name">{{ scope.row.goodsName || '—' }}</span>
37
+            </div>
38
+          </template>
39
+        </el-table-column>
40
+        <el-table-column label="商品规格" align="center" width="120" :show-overflow-tooltip="true">
41
+          <template slot-scope="scope">
42
+            <span>{{ specText(scope.row.specText) }}</span>
43
+          </template>
44
+        </el-table-column>
45
+        <el-table-column label="操作" align="center" width="140" fixed="right">
46
+          <template slot-scope="scope">
47
+            <el-button size="mini" type="text" @click="openAdjust(scope.row)" v-hasPermi="['agri:seller:stock:adjust:edit']">调整</el-button>
48
+            <el-button size="mini" type="text" @click="openRecords(scope.row)" v-hasPermi="['agri:seller:stock:adjust:list']">记录</el-button>
49
+          </template>
50
+        </el-table-column>
51
+      </el-table>
52
+
53
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
54
+    </el-card>
55
+
56
+    <!-- 库存调整弹窗 -->
57
+    <el-dialog title="库存调整" :visible.sync="adjustOpen" width="560px" append-to-body @close="resetAdjust">
58
+      <div v-if="currentRow" class="adjust-goods-brief">
59
+        <image-preview v-if="currentRow.mainPic" :src="currentRow.mainPic" :width="48" :height="48" />
60
+        <div class="brief-text">
61
+          <div class="brief-name">{{ currentRow.goodsName || '—' }}</div>
62
+          <div class="brief-spec">规格:{{ specText(currentRow.specText) }}</div>
63
+        </div>
64
+      </div>
65
+      <el-form ref="adjustFormRef" :model="adjustForm" :rules="adjustRules" label-width="90px" size="small">
66
+        <el-form-item label="调整方式" prop="adjustMode">
67
+          <el-radio-group v-model="adjustForm.adjustMode" @change="handleAdjustModeChange">
68
+            <div class="mode-option">
69
+              <el-radio label="1">增量更新</el-radio>
70
+              <p class="mode-tip">在现有库存上增加或减少配额数量,调整数量为负数表示减少</p>
71
+            </div>
72
+            <div class="mode-option">
73
+              <el-radio label="2">覆盖更新</el-radio>
74
+              <p class="mode-tip">将原有库存数量进行覆盖,覆盖后更新为调整数量填写的库存数量</p>
75
+            </div>
76
+          </el-radio-group>
77
+        </el-form-item>
78
+        <el-form-item label="调整数量" prop="adjustQty">
79
+          <el-input-number
80
+            v-model="adjustForm.adjustQty"
81
+            :min="adjustQtyMin"
82
+            :precision="0"
83
+            controls-position="right"
84
+            style="width: 200px"
85
+          />
86
+        </el-form-item>
87
+      </el-form>
88
+      <div slot="footer" class="dialog-footer">
89
+        <el-button @click="adjustOpen = false">取 消</el-button>
90
+        <el-button type="primary" :loading="adjustSubmitting" @click="submitAdjust">确 认</el-button>
91
+      </div>
92
+    </el-dialog>
93
+
94
+    <!-- 调整记录弹窗 -->
95
+    <el-dialog title="记录" :visible.sync="recordsOpen" width="760px" append-to-body @close="resetRecords">
96
+      <div v-if="currentRow" class="records-toolbar">
97
+        <el-select v-model="recordQuery.adjustType" placeholder="调整类型" clearable size="small" style="width: 160px" @change="loadRecords">
98
+          <el-option v-for="item in adjustTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
99
+        </el-select>
100
+      </div>
101
+      <el-table border v-loading="recordsLoading" :data="recordList" max-height="420" :empty-text="recordsEmptyText">
102
+        <el-table-column label="调整类型" align="center" prop="adjustTypeText" width="110">
103
+          <template slot-scope="scope">
104
+            <span>{{ scope.row.adjustTypeText || adjustTypeLabel(scope.row.adjustType) }}</span>
105
+          </template>
106
+        </el-table-column>
107
+        <el-table-column label="调整前数量" align="center" prop="stockBefore" width="110">
108
+          <template slot-scope="scope">
109
+            <span>{{ scope.row.stockBefore != null ? scope.row.stockBefore : '—' }}</span>
110
+          </template>
111
+        </el-table-column>
112
+        <el-table-column label="调整后数量" align="center" prop="stockAfter" width="110">
113
+          <template slot-scope="scope">
114
+            <span>{{ scope.row.stockAfter != null ? scope.row.stockAfter : '—' }}</span>
115
+          </template>
116
+        </el-table-column>
117
+        <el-table-column label="操作账号" align="center" prop="operatorName" width="100" :show-overflow-tooltip="true">
118
+          <template slot-scope="scope">
119
+            <span>{{ scope.row.operatorName || '—' }}</span>
120
+          </template>
121
+        </el-table-column>
122
+        <el-table-column label="创建时间" align="center" width="160">
123
+          <template slot-scope="scope">
124
+            <span>{{ parseTime(scope.row.createTime) || '—' }}</span>
125
+          </template>
126
+        </el-table-column>
127
+      </el-table>
128
+    </el-dialog>
129
+  </div>
130
+</template>
131
+
132
+<script>
133
+import {
134
+  listSellerStockAdjust,
135
+  confirmSellerStockAdjust,
136
+  listSellerStockAdjustRecords
137
+} from "@/api/agri/seller/stock/adjust"
138
+import { getSellerContext } from "@/api/agri/seller/context"
139
+import { setSellerShopContext } from "@/utils/sellerShop"
140
+
141
+const ADJUST_TYPE_MAP = {
142
+  "1": "增加库存",
143
+  "2": "减少库存",
144
+  "3": "覆盖库存"
145
+}
146
+
147
+export default {
148
+  name: "AgriSellerStockAdjust",
149
+  data() {
150
+    const validateAdjustQty = (rule, value, callback) => {
151
+      if (value == null || value === "") {
152
+        callback(new Error("请输入调整数量"))
153
+        return
154
+      }
155
+      if (!Number.isInteger(Number(value))) {
156
+        callback(new Error("请输入正确的库存数量"))
157
+        return
158
+      }
159
+      if (this.adjustForm.adjustMode === "1") {
160
+        if (Number(value) === 0) {
161
+          callback(new Error("调整数量不能为 0"))
162
+          return
163
+        }
164
+      } else if (Number(value) < 0) {
165
+        callback(new Error("请输入正确的库存数量"))
166
+        return
167
+      }
168
+      callback()
169
+    }
170
+    return {
171
+      loading: false,
172
+      showSearch: true,
173
+      total: 0,
174
+      goodsList: [],
175
+      queryParams: {
176
+        pageNum: 1,
177
+        pageSize: 10,
178
+        keyword: undefined
179
+      },
180
+      currentRow: null,
181
+      adjustOpen: false,
182
+      adjustSubmitting: false,
183
+      adjustForm: {
184
+        adjustMode: "1",
185
+        adjustQty: 1
186
+      },
187
+      adjustRules: {
188
+        adjustMode: [{ required: true, message: "请选择调整方式", trigger: "change" }],
189
+        adjustQty: [{ validator: validateAdjustQty, trigger: "blur" }]
190
+      },
191
+      recordsOpen: false,
192
+      recordsLoading: false,
193
+      recordList: [],
194
+      recordQuery: {
195
+        adjustType: undefined
196
+      },
197
+      adjustTypeOptions: [
198
+        { value: "1", label: "增加库存" },
199
+        { value: "2", label: "减少库存" },
200
+        { value: "3", label: "覆盖库存" }
201
+      ]
202
+    }
203
+  },
204
+  computed: {
205
+    hasSearchFilter() {
206
+      return !!this.queryParams.keyword
207
+    },
208
+    emptyTableText() {
209
+      return this.hasSearchFilter ? "未找到符合条件的商品" : "暂无可调整商品"
210
+    },
211
+    adjustQtyMin() {
212
+      return this.adjustForm.adjustMode === "2" ? 0 : undefined
213
+    },
214
+    recordsEmptyText() {
215
+      return this.recordQuery.adjustType ? "未找到符合条件的调整记录" : "暂无数据"
216
+    }
217
+  },
218
+  created() {
219
+    this.initPage()
220
+  },
221
+  methods: {
222
+    specText(text) {
223
+      const val = text ? String(text).trim() : ""
224
+      return val && val !== "—" ? val : "—"
225
+    },
226
+    adjustTypeLabel(type) {
227
+      return ADJUST_TYPE_MAP[type] || "—"
228
+    },
229
+    initPage() {
230
+      this.loadShopContext().then(() => {
231
+        this.getList()
232
+      }).catch(() => {
233
+        this.getList()
234
+      })
235
+    },
236
+    loadShopContext() {
237
+      return getSellerContext().then(response => {
238
+        const data = response.data || {}
239
+        if (data.shopId != null) {
240
+          setSellerShopContext(data.shopId, data.shopName)
241
+        }
242
+      })
243
+    },
244
+    getList() {
245
+      this.loading = true
246
+      listSellerStockAdjust(this.queryParams).then(response => {
247
+        this.goodsList = response.rows || []
248
+        this.total = response.total || 0
249
+        this.loading = false
250
+      }).catch(() => {
251
+        this.loading = false
252
+      })
253
+    },
254
+    handleQuery() {
255
+      this.queryParams.pageNum = 1
256
+      this.getList()
257
+    },
258
+    resetQuery() {
259
+      this.resetForm("queryForm")
260
+      this.queryParams.pageNum = 1
261
+      this.getList()
262
+    },
263
+    openAdjust(row) {
264
+      this.currentRow = row
265
+      this.adjustForm = {
266
+        adjustMode: "1",
267
+        adjustQty: 1
268
+      }
269
+      this.adjustOpen = true
270
+      this.$nextTick(() => {
271
+        if (this.$refs.adjustFormRef) {
272
+          this.$refs.adjustFormRef.clearValidate()
273
+        }
274
+      })
275
+    },
276
+    handleAdjustModeChange(mode) {
277
+      if (mode === "2" && this.adjustForm.adjustQty != null && this.adjustForm.adjustQty < 0) {
278
+        this.adjustForm.adjustQty = 0
279
+      }
280
+      this.$nextTick(() => {
281
+        if (this.$refs.adjustFormRef) {
282
+          this.$refs.adjustFormRef.clearValidate("adjustQty")
283
+        }
284
+      })
285
+    },
286
+    resetAdjust() {
287
+      this.currentRow = null
288
+      this.adjustSubmitting = false
289
+      this.adjustForm = {
290
+        adjustMode: "1",
291
+        adjustQty: 1
292
+      }
293
+      if (this.$refs.adjustFormRef) {
294
+        this.$refs.adjustFormRef.resetFields()
295
+      }
296
+    },
297
+    submitAdjust() {
298
+      if (!this.currentRow || !this.currentRow.goodsId) {
299
+        return
300
+      }
301
+      this.$refs.adjustFormRef.validate(valid => {
302
+        if (!valid) {
303
+          return
304
+        }
305
+        this.adjustSubmitting = true
306
+        confirmSellerStockAdjust(this.currentRow.goodsId, {
307
+          adjustMode: this.adjustForm.adjustMode,
308
+          adjustQty: Number(this.adjustForm.adjustQty)
309
+        }).then(() => {
310
+          this.$modal.msgSuccess("调整成功")
311
+          this.adjustOpen = false
312
+          this.adjustSubmitting = false
313
+        }).catch(() => {
314
+          this.adjustSubmitting = false
315
+        })
316
+      })
317
+    },
318
+    openRecords(row) {
319
+      this.currentRow = row
320
+      this.recordQuery.adjustType = undefined
321
+      this.recordsOpen = true
322
+      this.loadRecords()
323
+    },
324
+    loadRecords() {
325
+      if (!this.currentRow || !this.currentRow.goodsId) {
326
+        return
327
+      }
328
+      this.recordsLoading = true
329
+      listSellerStockAdjustRecords(this.currentRow.goodsId, this.recordQuery).then(response => {
330
+        this.recordList = response.data || []
331
+        this.recordsLoading = false
332
+      }).catch(() => {
333
+        this.recordList = []
334
+        this.recordsLoading = false
335
+      })
336
+    },
337
+    resetRecords() {
338
+      this.currentRow = null
339
+      this.recordList = []
340
+      this.recordQuery.adjustType = undefined
341
+      this.recordsLoading = false
342
+    }
343
+  }
344
+}
345
+</script>
346
+
347
+<style scoped lang="scss">
348
+.mb8 {
349
+  margin-bottom: 8px;
350
+}
351
+.goods-cell {
352
+  display: flex;
353
+  align-items: center;
354
+  gap: 8px;
355
+}
356
+.goods-name {
357
+  flex: 1;
358
+  min-width: 0;
359
+  line-height: 1.4;
360
+}
361
+.adjust-goods-brief {
362
+  display: flex;
363
+  align-items: flex-start;
364
+  gap: 12px;
365
+  margin-bottom: 16px;
366
+  padding: 12px;
367
+  background: #fafafa;
368
+  border-radius: 4px;
369
+}
370
+.brief-text {
371
+  flex: 1;
372
+  min-width: 0;
373
+}
374
+.brief-name {
375
+  font-weight: 500;
376
+  color: #303133;
377
+  line-height: 1.4;
378
+}
379
+.brief-spec {
380
+  margin-top: 4px;
381
+  font-size: 12px;
382
+  color: #909399;
383
+}
384
+.mode-option {
385
+  margin-bottom: 12px;
386
+  &:last-child {
387
+    margin-bottom: 0;
388
+  }
389
+}
390
+.mode-tip {
391
+  margin: 4px 0 0 24px;
392
+  font-size: 12px;
393
+  color: #909399;
394
+  line-height: 1.5;
395
+}
396
+.records-toolbar {
397
+  margin-bottom: 12px;
398
+}
399
+</style>

+ 286 - 0
ruoyi-ui/src/views/agri/seller/stock/query/index.vue

@@ -0,0 +1,286 @@
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: 260px"
12
+            @keyup.enter.native="handleQuery"
13
+          >
14
+            <el-button slot="append" icon="el-icon-search" @click="handleQuery" />
15
+          </el-input>
16
+        </el-form-item>
17
+        <el-form-item>
18
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
19
+        </el-form-item>
20
+      </el-form>
21
+    </el-card>
22
+
23
+    <br/>
24
+
25
+    <!-- 列表区 -->
26
+    <el-card shadow="never" class="table-card">
27
+      <el-row :gutter="10" class="mb8">
28
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
29
+      </el-row>
30
+
31
+      <el-table border v-loading="loading" :data="stockList" :empty-text="emptyTableText">
32
+        <el-table-column label="商品信息" align="left" min-width="260">
33
+          <template slot-scope="scope">
34
+            <div class="goods-cell">
35
+              <image-preview v-if="scope.row.mainPic" :src="scope.row.mainPic" :width="44" :height="44" />
36
+              <span class="goods-name">{{ scope.row.goodsName || '—' }}</span>
37
+            </div>
38
+          </template>
39
+        </el-table-column>
40
+        <el-table-column label="商品规格" align="center" width="120" :show-overflow-tooltip="true">
41
+          <template slot-scope="scope">
42
+            <span>{{ specText(scope.row.specText) }}</span>
43
+          </template>
44
+        </el-table-column>
45
+        <el-table-column label="可用库存" align="center" width="100">
46
+          <template slot-scope="scope">
47
+            <span :class="{ 'stock-zero': scope.row.stock === 0 }">{{ scope.row.stock != null ? scope.row.stock : '—' }}</span>
48
+          </template>
49
+        </el-table-column>
50
+        <el-table-column label="成本" align="center" width="100">
51
+          <template slot-scope="scope">
52
+            <span>{{ scope.row.cost != null ? scope.row.cost : '—' }}</span>
53
+          </template>
54
+        </el-table-column>
55
+        <el-table-column label="操作" align="center" width="120" fixed="right">
56
+          <template slot-scope="scope">
57
+            <el-button size="mini" type="text" @click="openLogs(scope.row)" v-hasPermi="['agri:seller:stock:query:list']">出入库明细</el-button>
58
+          </template>
59
+        </el-table-column>
60
+      </el-table>
61
+
62
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
63
+    </el-card>
64
+
65
+    <!-- 出入库明细弹窗 -->
66
+    <el-dialog
67
+      :title="logsDialogTitle"
68
+      :visible.sync="logsOpen"
69
+      width="900px"
70
+      append-to-body
71
+      @close="resetLogs"
72
+    >
73
+      <div v-if="currentRow" class="logs-goods-brief">
74
+        <image-preview v-if="currentRow.mainPic" :src="currentRow.mainPic" :width="48" :height="48" />
75
+        <div class="brief-text">
76
+          <div class="brief-name">{{ currentRow.goodsName || '—' }}</div>
77
+          <div class="brief-spec">规格:{{ specText(currentRow.specText) }}</div>
78
+          <div class="brief-stock">当前可用库存:{{ currentRow.stock != null ? currentRow.stock : '—' }}</div>
79
+        </div>
80
+      </div>
81
+      <el-table border v-loading="logsLoading" :data="logList" max-height="420" :empty-text="logsEmptyText">
82
+        <el-table-column label="商品名称" align="left" min-width="200" :show-overflow-tooltip="true">
83
+          <template slot-scope="scope">
84
+            <div class="goods-cell">
85
+              <image-preview v-if="currentRow && currentRow.mainPic" :src="currentRow.mainPic" :width="36" :height="36" />
86
+              <span>{{ scope.row.goodsName || '—' }}</span>
87
+            </div>
88
+          </template>
89
+        </el-table-column>
90
+        <el-table-column label="出入库类型" align="center" min-width="130" :show-overflow-tooltip="true">
91
+          <template slot-scope="scope">
92
+            <span>{{ scope.row.ioTypeText || '—' }}</span>
93
+          </template>
94
+        </el-table-column>
95
+        <el-table-column label="商品规格" align="center" width="110" :show-overflow-tooltip="true">
96
+          <template slot-scope="scope">
97
+            <span>{{ specText(scope.row.specText) }}</span>
98
+          </template>
99
+        </el-table-column>
100
+        <el-table-column label="出入库数量" align="center" width="110">
101
+          <template slot-scope="scope">
102
+            <span :class="ioQtyClass(scope.row.ioQty)">{{ formatIoQty(scope.row.ioQty) }}</span>
103
+          </template>
104
+        </el-table-column>
105
+        <el-table-column label="出入库时间" align="center" width="160">
106
+          <template slot-scope="scope">
107
+            <span>{{ parseTime(scope.row.ioTime) || '—' }}</span>
108
+          </template>
109
+        </el-table-column>
110
+      </el-table>
111
+    </el-dialog>
112
+  </div>
113
+</template>
114
+
115
+<script>
116
+import {
117
+  listSellerStockQuery,
118
+  listSellerStockQueryLogs
119
+} from "@/api/agri/seller/stock/query"
120
+import { getSellerContext } from "@/api/agri/seller/context"
121
+import { setSellerShopContext } from "@/utils/sellerShop"
122
+
123
+export default {
124
+  name: "AgriSellerStockQuery",
125
+  data() {
126
+    return {
127
+      loading: false,
128
+      showSearch: true,
129
+      total: 0,
130
+      stockList: [],
131
+      queryParams: {
132
+        pageNum: 1,
133
+        pageSize: 10,
134
+        keyword: undefined
135
+      },
136
+      logsOpen: false,
137
+      logsLoading: false,
138
+      logList: [],
139
+      currentRow: null
140
+    }
141
+  },
142
+  computed: {
143
+    hasSearchFilter() {
144
+      return !!this.queryParams.keyword
145
+    },
146
+    emptyTableText() {
147
+      return this.hasSearchFilter ? "未找到符合条件的库存记录" : "暂无库存数据"
148
+    },
149
+    logsDialogTitle() {
150
+      return "出入库明细"
151
+    },
152
+    logsEmptyText() {
153
+      return "暂无出入库记录"
154
+    }
155
+  },
156
+  created() {
157
+    this.initPage()
158
+  },
159
+  methods: {
160
+    specText(text) {
161
+      const val = text ? String(text).trim() : ""
162
+      return val && val !== "—" ? val : "—"
163
+    },
164
+    formatIoQty(qty) {
165
+      if (qty == null) {
166
+        return "—"
167
+      }
168
+      return qty > 0 ? `+${qty}` : String(qty)
169
+    },
170
+    ioQtyClass(qty) {
171
+      if (qty == null || qty === 0) {
172
+        return ""
173
+      }
174
+      return qty > 0 ? "qty-in" : "qty-out"
175
+    },
176
+    initPage() {
177
+      this.loadShopContext().then(() => {
178
+        this.getList()
179
+      }).catch(() => {
180
+        this.getList()
181
+      })
182
+    },
183
+    loadShopContext() {
184
+      return getSellerContext().then(response => {
185
+        const data = response.data || {}
186
+        if (data.shopId != null) {
187
+          setSellerShopContext(data.shopId, data.shopName)
188
+        }
189
+      })
190
+    },
191
+    getList() {
192
+      this.loading = true
193
+      listSellerStockQuery(this.queryParams).then(response => {
194
+        this.stockList = 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.resetForm("queryForm")
207
+      this.queryParams.pageNum = 1
208
+      this.getList()
209
+    },
210
+    openLogs(row) {
211
+      this.currentRow = row
212
+      this.logsOpen = true
213
+      this.loadLogs(row.goodsId)
214
+    },
215
+    loadLogs(goodsId) {
216
+      if (!goodsId) {
217
+        return
218
+      }
219
+      this.logsLoading = true
220
+      listSellerStockQueryLogs(goodsId).then(response => {
221
+        this.logList = response.data || []
222
+        this.logsLoading = false
223
+      }).catch(() => {
224
+        this.logList = []
225
+        this.logsLoading = false
226
+      })
227
+    },
228
+    resetLogs() {
229
+      this.currentRow = null
230
+      this.logList = []
231
+      this.logsLoading = false
232
+    }
233
+  }
234
+}
235
+</script>
236
+
237
+<style scoped lang="scss">
238
+.mb8 {
239
+  margin-bottom: 8px;
240
+}
241
+.goods-cell {
242
+  display: flex;
243
+  align-items: center;
244
+  gap: 8px;
245
+}
246
+.goods-name {
247
+  flex: 1;
248
+  min-width: 0;
249
+  line-height: 1.4;
250
+}
251
+.stock-zero {
252
+  color: #F56C6C;
253
+}
254
+.logs-goods-brief {
255
+  display: flex;
256
+  align-items: flex-start;
257
+  gap: 12px;
258
+  margin-bottom: 16px;
259
+  padding: 12px;
260
+  background: #fafafa;
261
+  border-radius: 4px;
262
+}
263
+.brief-text {
264
+  flex: 1;
265
+  min-width: 0;
266
+}
267
+.brief-name {
268
+  font-weight: 500;
269
+  color: #303133;
270
+  line-height: 1.4;
271
+}
272
+.brief-spec,
273
+.brief-stock {
274
+  margin-top: 4px;
275
+  font-size: 12px;
276
+  color: #909399;
277
+}
278
+.qty-in {
279
+  color: #67C23A;
280
+  font-weight: 500;
281
+}
282
+.qty-out {
283
+  color: #F56C6C;
284
+  font-weight: 500;
285
+}
286
+</style>