xsh_1997 недель назад: 2
Родитель
Сommit
a73ea5f6cd

+ 219 - 0
doc/店铺后台/账号管理/员工管理/员工管理前端技术方案.md

@@ -0,0 +1,219 @@
1
+# 员工管理 — 前端技术方案
2
+
3
+> **依据:** 《员工管理功能需求.md》v1.2、《员工管理技术方案.md》v1.1  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端(店铺经营管理端)** 当前店铺员工 CRUD、改资料、改密;经营账号 **不在列表**。  
6
+> **实现状态:** 页面与 API 封装已落地,待后端 `/agri/seller/employee` 及 `X-Shop-Id` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/role/index.vue`(店铺上下文)、`agri/accountManage/index.vue`(改密弹窗) |
17
+| 布局 | 检索区 `el-card` + `<br/>` + 列表区 `el-card` + 表格 `border` |
18
+| 状态字典 | `sys_normal_disable`(0 正常 / 1 停用) |
19
+| 密码规则 | `@/utils/passwordRule` 混入 |
20
+
21
+---
22
+
23
+## 2. 文件清单
24
+
25
+| 类型 | 路径 | 说明 |
26
+|------|------|------|
27
+| 列表页 | `ruoyi-ui/src/views/agri/seller/employee/index.vue` | 列表、创建/编辑、修改账号/密码 |
28
+| 员工 API | `ruoyi-ui/src/api/agri/seller/employee.js` | CRUD、配额、角色下拉 |
29
+| 店铺上下文 | `ruoyi-ui/src/api/agri/seller/context.js` | 已有,复用 |
30
+| 店铺工具 | `ruoyi-ui/src/utils/sellerShop.js` | 已有,复用 |
31
+
32
+**组件 name(keep-alive):** `AgriSellerEmployee`
33
+
34
+---
35
+
36
+## 3. 菜单与路由
37
+
38
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
39
+|----------|----------|-------------------|----------|
40
+| 员工管理 | `agri/seller/employee/index` | `seller/employee` | `agri:seller:employee:list` |
41
+
42
+**上级菜单:** 店铺经营管理端 → **账号管理**
43
+
44
+| 按钮权限 | 标识 |
45
+|----------|------|
46
+| 详情回显 | `agri:seller:employee:query` |
47
+| 创建员工 | `agri:seller:employee:add` |
48
+| 编辑 / 修改账号 | `agri:seller:employee:edit` |
49
+| 修改密码 | `agri:seller:employee:resetPwd` |
50
+| 删除员工 | `agri:seller:employee:remove` |
51
+
52
+---
53
+
54
+## 4. 页面结构
55
+
56
+```text
57
+员工管理
58
+├── 检索区(el-card)
59
+│   └── 员工名称 employeeName(模糊,仅此一项)
60
+├── 列表区(el-card + border 表格)
61
+│   ├── 卡片头:当前店铺、商户名称、员工配额 usedCount/maxCount、店铺切换
62
+│   ├── 配额已满 warning
63
+│   ├── 工具栏:创建员工(配额满 disabled)
64
+│   ├── 列:员工账号、员工名称、手机号(脱敏)、邮箱、状态
65
+│   └── 操作:编辑、修改账号、修改密码、删除
66
+├── 创建/编辑弹窗(600px)
67
+│   ├── 员工名称、手机号(编辑 disabled)、邮箱、头像、状态
68
+│   ├── 绑定角色(多选,至少 1 个)
69
+│   └── 创建 **不含密码**;提示创建后须改密
70
+├── 修改账号弹窗(520px)
71
+│   └── 员工名称、手机号、邮箱 → PUT /profile
72
+└── 修改密码弹窗(520px)
73
+    └── 新密码 + 确认新密码 → PUT /resetPwd
74
+```
75
+
76
+---
77
+
78
+## 5. 复用组件
79
+
80
+| 组件 | 用途 |
81
+|------|------|
82
+| `ImageUpload` | 头像上传 limit=1, 5MB |
83
+| `dict-tag` | 状态展示 |
84
+| `Pagination` / `RightToolbar` | 分页、检索显隐 |
85
+| `el-alert` | 配额已满提示 |
86
+
87
+---
88
+
89
+## 6. API 对接
90
+
91
+**基路径:** `/agri/seller/employee`
92
+
93
+| 前端方法 | HTTP | 路径 | 说明 |
94
+|----------|------|------|------|
95
+| `listSellerEmployee` | GET | `/list` | 分页列表 |
96
+| `sellerEmployeeQuota` | GET | `/quota` | 配额与店铺/商户展示 |
97
+| `sellerEmployeeRoleOptions` | GET | `/roleOptions` | 当前店铺可选角色 |
98
+| `getSellerEmployee` | GET | `/{userId}` | 详情(含 roleIds、完整手机号) |
99
+| `addSellerEmployee` | POST | `/` | 创建(**无 password**) |
100
+| `updateSellerEmployee` | PUT | `/` | 编辑(含头像、状态、角色) |
101
+| `updateSellerEmployeeProfile` | PUT | `/profile` | 修改账号 |
102
+| `resetSellerEmployeePassword` | PUT | `/resetPwd` | 修改密码 |
103
+| `delSellerEmployee` | DELETE | `/{userIds}` | 删除 |
104
+
105
+### 6.1 列表 Query
106
+
107
+| 字段 | 说明 |
108
+|------|------|
109
+| pageNum, pageSize | 分页 |
110
+| employeeName | 员工名称模糊 |
111
+
112
+### 6.2 配额 VO
113
+
114
+| 字段 | 展示 |
115
+|------|------|
116
+| shopName / merchantName | 卡片头只读 |
117
+| usedCount / maxCount | 「员工 3/5」;**商户级**计数 |
118
+| quotaFull | `usedCount >= maxCount` 时禁用创建、展示 warning |
119
+
120
+### 6.3 创建 Body
121
+
122
+| 字段 | 说明 |
123
+|------|------|
124
+| employeeName, mobile | 必填;mobile=登录账号 |
125
+| email, avatar | 选填 |
126
+| status | 默认 `"0"` |
127
+| roleIds | 至少 1 个;来自 roleOptions |
128
+| shopId | **不传**(后端取 X-Shop-Id) |
129
+| password | **不传**(EM10) |
130
+
131
+创建成功:`msgSuccess` + 可选 `$modal.confirm` 引导立即改密;响应 `userId` 用于打开改密弹窗。
132
+
133
+### 6.4 编辑 vs 修改账号
134
+
135
+| 入口 | 接口 | 字段 |
136
+|------|------|------|
137
+| **编辑** | PUT `/` | 名称、手机、邮箱、头像、状态、roleIds |
138
+| **修改账号** | PUT `/profile` | 仅名称、手机、邮箱 |
139
+
140
+编辑时手机号 **disabled**(与创建后账号稳定策略一致;改手机走修改账号入口)。
141
+
142
+### 6.5 修改密码
143
+
144
+| 字段 | 说明 |
145
+|------|------|
146
+| userId | 必填 |
147
+| password / confirmPassword | pwdValidator + 一致校验 |
148
+
149
+---
150
+
151
+## 7. 交互与校验
152
+
153
+| 场景 | 前端行为 |
154
+|------|----------|
155
+| 店铺上下文 | 与角色管理相同:context → X-Shop-Id → 切换店铺刷新 |
156
+| 创建 | 配额满拦截;角色至少 1 个 |
157
+| 创建成功 | 提示须改密;可选立即打开改密弹窗 |
158
+| 启用员工 | 编辑 status=0;超配额由后端 msg |
159
+| 删除 | 二次确认「无法登录商家端」 |
160
+| 角色下拉 | 仅当前店铺正常角色(后端过滤停用) |
161
+
162
+---
163
+
164
+## 8. 权限指令
165
+
166
+```html
167
+v-hasPermi="['agri:seller:employee:add']"
168
+v-hasPermi="['agri:seller:employee:edit']"
169
+v-hasPermi="['agri:seller:employee:resetPwd']"
170
+v-hasPermi="['agri:seller:employee:remove']"
171
+```
172
+
173
+---
174
+
175
+## 9. 与关联模块边界
176
+
177
+| 模块 | 关系 |
178
+|------|------|
179
+| 角色管理 | roleOptions 数据源;须先建角色再绑员工 |
180
+| 店铺设置 | maxCount 全局配额;前端只展示与禁用创建 |
181
+| 平台账号管理 | 员工作为 sys_user 可被平台检索;CRUD 在本模块 |
182
+| 经营账号 | **不出现在列表**;不可在本模块创建/删除 |
183
+
184
+---
185
+
186
+## 10. 联调检查清单
187
+
188
+- [ ] 菜单 `agri/seller/employee/index` 可打开  
189
+- [ ] X-Shop-Id 正确;列表仅当前店铺员工  
190
+- [ ] 配额展示 usedCount/maxCount  
191
+- [ ] employeeName 检索  
192
+- [ ] 创建不含密码;成功后引导改密  
193
+- [ ] 角色多选至少 1 个;仅当前店铺角色  
194
+- [ ] 编辑 / 修改账号 / 改密 / 删除各接口正常  
195
+- [ ] 超配额创建/启用后端 msg  
196
+- [ ] 手机号重复后端 msg  
197
+- [ ] 切换店铺后列表变化  
198
+
199
+---
200
+
201
+## 11. 非本期(前端)
202
+
203
+| 项 | 说明 |
204
+|----|------|
205
+| 批量删除 | 需求未列 |
206
+| 经营账号行 | 列表不含 |
207
+| 创建表单密码项 | EM10 禁止 |
208
+
209
+---
210
+
211
+## 12. 修订记录
212
+
213
+| 版本 | 日期 | 说明 |
214
+|------|------|------|
215
+| v1.0 | 2026-06 | 首版:员工 CRUD、配额、改密、X-Shop-Id、前端技术方案 |
216
+
217
+---
218
+
219
+*文档版本:v1.0 · ruoyi-ui · 关联《员工管理功能需求.md》v1.2 / 《员工管理技术方案.md》v1.1*

+ 220 - 0
doc/店铺后台/账号管理/角色管理/角色管理前端技术方案.md

@@ -0,0 +1,220 @@
1
+# 角色管理 — 前端技术方案
2
+
3
+> **依据:** 《角色管理功能需求.md》v1.1、《角色管理技术方案.md》v1.1  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端(店铺经营管理端)** 当前店铺员工角色 CRUD、商家端菜单权限树;平台 admin/merchant 角色 **不在本页维护**。  
6
+> **实现状态:** 页面与 API 封装已落地,待后端 `/agri/seller/role` 及 `X-Shop-Id` 联调。
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/system/role/index.vue`(权限树交互) |
17
+| 布局 | 检索区 `el-card` + `<br/>` + 列表区 `el-card` + 表格 `border` |
18
+| 状态字典 | `sys_normal_disable`(0 正常 / 1 停用) |
19
+
20
+---
21
+
22
+## 2. 文件清单
23
+
24
+| 类型 | 路径 | 说明 |
25
+|------|------|------|
26
+| 列表页 | `ruoyi-ui/src/views/agri/seller/role/index.vue` | 列表、创建/修改弹窗、权限树 |
27
+| 角色 API | `ruoyi-ui/src/api/agri/seller/role.js` | CRUD、menuTree |
28
+| 店铺上下文 API | `ruoyi-ui/src/api/agri/seller/context.js` | 登录后默认店铺、切换店铺 |
29
+| 店铺上下文工具 | `ruoyi-ui/src/utils/sellerShop.js` | 缓存 shopId、组装 `X-Shop-Id` 请求头 |
30
+
31
+**组件 name(keep-alive):** `AgriSellerRole`
32
+
33
+---
34
+
35
+## 3. 菜单与路由
36
+
37
+若依路由由 **后端菜单** 动态加载,前端需配置:
38
+
39
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
40
+|----------|----------|-------------------|----------|
41
+| 角色管理 | `agri/seller/role/index` | `seller/role` | `agri:seller:role:list` |
42
+
43
+**上级菜单:** 店铺经营管理端 → **账号管理**
44
+
45
+| 按钮权限 | 标识 |
46
+|----------|------|
47
+| 详情回显 | `agri:seller:role:query` |
48
+| 创建角色 | `agri:seller:role:add` |
49
+| 修改角色 | `agri:seller:role:edit` |
50
+| 删除角色 | `agri:seller:role:remove` |
51
+
52
+---
53
+
54
+## 4. 页面结构
55
+
56
+```text
57
+角色管理
58
+├── 检索区(el-card)
59
+│   └── 角色名称 roleName(模糊,仅此一项)
60
+├── 列表区(el-card + border 表格)
61
+│   ├── 卡片头:当前店铺名称(只读);经营账号可多店时下拉切换
62
+│   ├── 工具栏:创建角色
63
+│   ├── 列:角色名称、角色描述、状态、绑定员工数、创建时间
64
+│   └── 操作:修改角色、删除
65
+└── 创建/修改弹窗(680px)
66
+    ├── 角色名称 roleName(必填)
67
+    ├── 角色描述 roleDesc(选填)
68
+    ├── 状态 status(默认正常)
69
+    └── 功能权限 el-tree(商家端 menuTree;至少 1 项)
70
+```
71
+
72
+**不提供:** 权限字符 roleKey、角色顺序(后端自动生成 `seller_s{shopId}_*`)。
73
+
74
+---
75
+
76
+## 5. 店铺上下文(X-Shop-Id)
77
+
78
+| 步骤 | 说明 |
79
+|------|------|
80
+| 页面 `created` | `GET /agri/seller/context` → 缓存 `shopId` / `shopName` |
81
+| 后续请求 | `sellerShop.js` 为 `/agri/seller/role/*` 附加 `X-Shop-Id` |
82
+| 切换店铺 | 经营账号且 `switchable=true` 时展示下拉 → `PUT /agri/seller/context/shop` → 刷新列表 |
83
+
84
+未携带有效 `X-Shop-Id` 时后端可能返回「请先选择当前店铺」。
85
+
86
+---
87
+
88
+## 6. API 对接
89
+
90
+**基路径:** `/agri/seller/role`(见 `src/api/agri/seller/role.js`)
91
+
92
+| 前端方法 | HTTP | 路径 | 调用场景 |
93
+|----------|------|------|----------|
94
+| `listSellerRole` | GET | `/list` | 列表、检索、分页 |
95
+| `sellerRoleMenuTree` | GET | `/menuTree` | 创建/编辑权限树 |
96
+| `getSellerRole` | GET | `/{roleId}` | 编辑回显(含 menuIds、bindCount) |
97
+| `addSellerRole` | POST | `/` | 创建角色 |
98
+| `updateSellerRole` | PUT | `/` | 修改角色 |
99
+| `delSellerRole` | DELETE | `/{roleIds}` | 删除 |
100
+
101
+**店铺上下文:**
102
+
103
+| 前端方法 | HTTP | 路径 |
104
+|----------|------|------|
105
+| `getSellerContext` | GET | `/agri/seller/context` |
106
+| `switchSellerShop` | PUT | `/agri/seller/context/shop` |
107
+
108
+### 6.1 列表请求参数
109
+
110
+| 字段 | 说明 |
111
+|------|------|
112
+| pageNum, pageSize | 分页 |
113
+| roleName | 角色名称模糊(**仅此检索项**) |
114
+
115
+**数据范围:** 后端按 `X-Shop-Id` 过滤当前店铺角色;默认 `create_time DESC`。
116
+
117
+### 6.2 列表行字段
118
+
119
+| 字段 | 展示规则 |
120
+|------|----------|
121
+| roleName | 主展示列 |
122
+| roleDesc | 空显示「—」 |
123
+| status | `dict-tag` · sys_normal_disable |
124
+| bindCount | 已绑定员工数;辅助删除判断 |
125
+| createTime | `parseTime` |
126
+
127
+### 6.3 创建/修改请求体
128
+
129
+| 字段 | 表单 | 说明 |
130
+|------|:----:|------|
131
+| roleName | 必填 | 店铺内唯一 |
132
+| roleDesc | 选填 | → remark |
133
+| status | 必填 | 默认 `"0"` |
134
+| menuIds | 必填 | 权限树勾选;至少 1 项 |
135
+| roleId | 编辑 | 修改时必填 |
136
+
137
+**权限树交互(对齐 RuoYi system/role):**
138
+
139
+- 展开/折叠、全选/全不选、父子联动
140
+- 提交时合并 **选中 + 半选** 节点 id
141
+- 数据源仅 **商家端** 菜单(`agri:seller:*`)
142
+
143
+### 6.4 删除
144
+
145
+| 场景 | 前端行为 |
146
+|------|----------|
147
+| 单独删除 | 二次确认;若 `bindCount>0` 提示可能失败 |
148
+| 已绑定员工 | 后端阻断 msg「已分配给员工…」 |
149
+| 批量删除 | **非本期**(需求未要求) |
150
+
151
+---
152
+
153
+## 7. 交互与校验
154
+
155
+| 场景 | 前端行为 |
156
+|------|----------|
157
+| 检索 | 仅 roleName;重置恢复全量 |
158
+| 创建 | 打开前加载 menuTree |
159
+| 编辑 | 并行加载 menuTree + 详情;回显 `menuIds` |
160
+| 未选权限 | 前端 `msgWarning`「请至少选择一项功能权限」 |
161
+| 停用角色 | 表单可选停用;列表仍展示 |
162
+| 权限变更 | 保存后已绑定员工权限由后端即时生效(RM7) |
163
+
164
+---
165
+
166
+## 8. 权限指令
167
+
168
+```html
169
+v-hasPermi="['agri:seller:role:add']"
170
+v-hasPermi="['agri:seller:role:edit']"
171
+v-hasPermi="['agri:seller:role:remove']"
172
+```
173
+
174
+---
175
+
176
+## 9. 与关联模块边界
177
+
178
+| 模块 | 关系 |
179
+|------|------|
180
+| 员工管理 | 本模块定义角色;员工管理 `roleOptions` 选用;停用角色不出现在下拉 |
181
+| 平台角色管理 | **互斥**;不含 admin/merchant |
182
+| 管理员列表 | 平台用户角色维护;与本模块无关 |
183
+| 店铺设置 | 子管理员 **人数** 上限;**不限制** 角色个数 |
184
+
185
+---
186
+
187
+## 10. 联调检查清单
188
+
189
+- [ ] 菜单组件路径 `agri/seller/role/index` 可打开  
190
+- [ ] 登录后 context 写入 `X-Shop-Id`;列表仅当前店铺角色  
191
+- [ ] 卡片头展示当前店铺名称  
192
+- [ ] roleName 模糊检索  
193
+- [ ] 创建:名称+至少一项权限;店铺内重名后端 msg  
194
+- [ ] 权限树仅商家端菜单  
195
+- [ ] 编辑回显 menuIds;修改成功  
196
+- [ ] 删除已绑定角色后端阻断  
197
+- [ ] 切换店铺后列表范围变化  
198
+- [ ] 经营账号外访问后端 msg「仅商户经营账号可管理角色」  
199
+
200
+---
201
+
202
+## 11. 非本期(前端)
203
+
204
+| 项 | 说明 |
205
+|----|------|
206
+| 批量删除 | 需求未列 |
207
+| 列表内状态开关 | 走编辑弹窗改 status |
208
+| 平台端同名页面 | 平台用 RuoYi 系统管理 · 角色管理 |
209
+
210
+---
211
+
212
+## 12. 修订记录
213
+
214
+| 版本 | 日期 | 说明 |
215
+|------|------|------|
216
+| v1.0 | 2026-06 | 首版:商家端角色 CRUD、X-Shop-Id、权限树、前端技术方案 |
217
+
218
+---
219
+
220
+*文档版本:v1.0 · ruoyi-ui · 关联《角色管理功能需求.md》v1.1 / 《角色管理技术方案.md》v1.1*

+ 18 - 0
ruoyi-ui/src/api/agri/seller/context.js

@@ -0,0 +1,18 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取商家端当前店铺上下文
4
+export function getSellerContext() {
5
+  return request({
6
+    url: '/agri/seller/context',
7
+    method: 'get'
8
+  })
9
+}
10
+
11
+// 经营账号切换当前店铺
12
+export function switchSellerShop(data) {
13
+  return request({
14
+    url: '/agri/seller/context/shop',
15
+    method: 'put',
16
+    data: data
17
+  })
18
+}

+ 88 - 0
ruoyi-ui/src/api/agri/seller/employee.js

@@ -0,0 +1,88 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 查询当前店铺员工列表
5
+export function listSellerEmployee(query) {
6
+  return request({
7
+    url: '/agri/seller/employee/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 查询员工配额(商户级已用/全局上限)
15
+export function sellerEmployeeQuota() {
16
+  return request({
17
+    url: '/agri/seller/employee/quota',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 当前店铺可选角色
24
+export function sellerEmployeeRoleOptions() {
25
+  return request({
26
+    url: '/agri/seller/employee/roleOptions',
27
+    method: 'get',
28
+    headers: sellerShopHeaders()
29
+  })
30
+}
31
+
32
+// 查询员工详情
33
+export function getSellerEmployee(userId) {
34
+  return request({
35
+    url: '/agri/seller/employee/' + userId,
36
+    method: 'get',
37
+    headers: sellerShopHeaders()
38
+  })
39
+}
40
+
41
+// 创建员工(不含密码)
42
+export function addSellerEmployee(data) {
43
+  return request({
44
+    url: '/agri/seller/employee',
45
+    method: 'post',
46
+    data: data,
47
+    headers: sellerShopHeaders()
48
+  })
49
+}
50
+
51
+// 编辑员工(含头像、状态、角色)
52
+export function updateSellerEmployee(data) {
53
+  return request({
54
+    url: '/agri/seller/employee',
55
+    method: 'put',
56
+    data: data,
57
+    headers: sellerShopHeaders()
58
+  })
59
+}
60
+
61
+// 修改账号(名称/手机/邮箱)
62
+export function updateSellerEmployeeProfile(data) {
63
+  return request({
64
+    url: '/agri/seller/employee/profile',
65
+    method: 'put',
66
+    data: data,
67
+    headers: sellerShopHeaders()
68
+  })
69
+}
70
+
71
+// 修改登录密码
72
+export function resetSellerEmployeePassword(data) {
73
+  return request({
74
+    url: '/agri/seller/employee/resetPwd',
75
+    method: 'put',
76
+    data: data,
77
+    headers: sellerShopHeaders()
78
+  })
79
+}
80
+
81
+// 删除员工
82
+export function delSellerEmployee(userIds) {
83
+  return request({
84
+    url: '/agri/seller/employee/' + userIds,
85
+    method: 'delete',
86
+    headers: sellerShopHeaders()
87
+  })
88
+}

+ 59 - 0
ruoyi-ui/src/api/agri/seller/role.js

@@ -0,0 +1,59 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 查询当前店铺角色列表
5
+export function listSellerRole(query) {
6
+  return request({
7
+    url: '/agri/seller/role/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 查询商家端可分配菜单树
15
+export function sellerRoleMenuTree() {
16
+  return request({
17
+    url: '/agri/seller/role/menuTree',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 查询角色详情
24
+export function getSellerRole(roleId) {
25
+  return request({
26
+    url: '/agri/seller/role/' + roleId,
27
+    method: 'get',
28
+    headers: sellerShopHeaders()
29
+  })
30
+}
31
+
32
+// 创建角色
33
+export function addSellerRole(data) {
34
+  return request({
35
+    url: '/agri/seller/role',
36
+    method: 'post',
37
+    data: data,
38
+    headers: sellerShopHeaders()
39
+  })
40
+}
41
+
42
+// 修改角色
43
+export function updateSellerRole(data) {
44
+  return request({
45
+    url: '/agri/seller/role',
46
+    method: 'put',
47
+    data: data,
48
+    headers: sellerShopHeaders()
49
+  })
50
+}
51
+
52
+// 删除角色(支持批量,逗号分隔)
53
+export function delSellerRole(roleIds) {
54
+  return request({
55
+    url: '/agri/seller/role/' + roleIds,
56
+    method: 'delete',
57
+    headers: sellerShopHeaders()
58
+  })
59
+}

+ 33 - 0
ruoyi-ui/src/utils/sellerShop.js

@@ -0,0 +1,33 @@
1
+import cache from '@/plugins/cache'
2
+
3
+const SHOP_ID_KEY = 'sellerShopId'
4
+const SHOP_NAME_KEY = 'sellerShopName'
5
+
6
+/** 读取当前店铺 ID(写入 X-Shop-Id 请求头) */
7
+export function getSellerShopId() {
8
+  return cache.session.get(SHOP_ID_KEY)
9
+}
10
+
11
+/** 读取当前店铺名称 */
12
+export function getSellerShopName() {
13
+  return cache.session.get(SHOP_NAME_KEY)
14
+}
15
+
16
+/** 缓存商家端当前店铺上下文 */
17
+export function setSellerShopContext(shopId, shopName) {
18
+  if (shopId != null) {
19
+    cache.session.set(SHOP_ID_KEY, String(shopId))
20
+  }
21
+  if (shopName) {
22
+    cache.session.set(SHOP_NAME_KEY, shopName)
23
+  }
24
+}
25
+
26
+/** 商家端接口公共请求头 */
27
+export function sellerShopHeaders() {
28
+  const shopId = getSellerShopId()
29
+  if (shopId) {
30
+    return { 'X-Shop-Id': shopId }
31
+  }
32
+  return {}
33
+}

+ 548 - 0
ruoyi-ui/src/views/agri/seller/employee/index.vue

@@ -0,0 +1,548 @@
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="employeeName">
7
+          <el-input v-model="queryParams.employeeName" placeholder="模糊检索" clearable style="width: 220px" @keyup.enter.native="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item>
10
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
11
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
12
+        </el-form-item>
13
+      </el-form>
14
+    </el-card>
15
+
16
+    <br/>
17
+
18
+    <!-- 列表区 -->
19
+    <el-card shadow="never" class="table-card">
20
+      <div slot="header" class="card-header">
21
+        <span>员工列表</span>
22
+        <span class="header-tip">当前店铺:{{ quota.shopName || currentShopName || '—' }}</span>
23
+        <span class="header-tip" v-if="quota.merchantName">商户:{{ quota.merchantName }}</span>
24
+        <span class="quota-tip">员工 {{ quota.usedCount != null ? quota.usedCount : '—' }}/{{ quota.maxCount != null ? quota.maxCount : '—' }}</span>
25
+        <el-select
26
+          v-if="shopSwitchable && shopOptions.length > 1"
27
+          v-model="currentShopId"
28
+          size="small"
29
+          placeholder="切换店铺"
30
+          style="width: 200px; margin-left: 12px"
31
+          @change="handleShopSwitch"
32
+        >
33
+          <el-option
34
+            v-for="item in shopOptions"
35
+            :key="item.shopId"
36
+            :label="item.shopName"
37
+            :value="item.shopId"
38
+          />
39
+        </el-select>
40
+      </div>
41
+
42
+      <el-row :gutter="10" class="mb8">
43
+        <el-col :span="1.5">
44
+          <el-button
45
+            type="primary"
46
+            plain
47
+            icon="el-icon-plus"
48
+            size="mini"
49
+            :disabled="quotaFull"
50
+            @click="handleAdd"
51
+            v-hasPermi="['agri:seller:employee:add']"
52
+          >创建员工</el-button>
53
+        </el-col>
54
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="refreshPage"></right-toolbar>
55
+      </el-row>
56
+
57
+      <el-alert v-if="quotaFull" title="子管理员人数已达上限,无法新建或启用员工,请联系平台调整店铺设置" type="warning" :closable="false" show-icon class="mb12" />
58
+
59
+      <el-table border v-loading="loading" :data="employeeList">
60
+        <el-table-column label="员工账号" align="center" prop="loginAccount" min-width="120" :show-overflow-tooltip="true" />
61
+        <el-table-column label="员工名称" align="center" prop="employeeName" min-width="110" :show-overflow-tooltip="true" />
62
+        <el-table-column label="手机号" align="center" prop="mobile" width="120" />
63
+        <el-table-column label="邮箱" align="center" prop="email" min-width="140" :show-overflow-tooltip="true">
64
+          <template slot-scope="scope">
65
+            <span>{{ scope.row.email || '—' }}</span>
66
+          </template>
67
+        </el-table-column>
68
+        <el-table-column label="状态" align="center" prop="status" width="90">
69
+          <template slot-scope="scope">
70
+            <dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
71
+          </template>
72
+        </el-table-column>
73
+        <el-table-column label="操作" align="center" width="280" fixed="right">
74
+          <template slot-scope="scope">
75
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:employee:edit']">编辑</el-button>
76
+            <el-button size="mini" type="text" icon="el-icon-user" @click="handleEditProfile(scope.row)" v-hasPermi="['agri:seller:employee:edit']">修改账号</el-button>
77
+            <el-button size="mini" type="text" icon="el-icon-key" @click="handleResetPwd(scope.row)" v-hasPermi="['agri:seller:employee:resetPwd']">修改密码</el-button>
78
+            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['agri:seller:employee:remove']">删除</el-button>
79
+          </template>
80
+        </el-table-column>
81
+      </el-table>
82
+
83
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
84
+    </el-card>
85
+
86
+    <!-- 创建/编辑员工弹窗 -->
87
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body @close="cancelForm">
88
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
89
+        <el-form-item label="员工名称" prop="employeeName">
90
+          <el-input v-model="form.employeeName" placeholder="请输入员工名称" maxlength="30" />
91
+        </el-form-item>
92
+        <el-form-item label="手机号" prop="mobile">
93
+          <el-input v-model="form.mobile" placeholder="11位手机号,即登录账号" maxlength="11" :disabled="form.userId != null" />
94
+        </el-form-item>
95
+        <el-form-item v-if="form.userId == null" label="登录说明">
96
+          <span class="form-tip">手机号将作为员工登录账号;创建后须「修改密码」方可登录</span>
97
+        </el-form-item>
98
+        <el-form-item label="邮箱" prop="email">
99
+          <el-input v-model="form.email" placeholder="选填" maxlength="50" />
100
+        </el-form-item>
101
+        <el-form-item label="头像" prop="avatar">
102
+          <image-upload v-model="form.avatar" :limit="1" :file-size="5" :file-type="['png', 'jpg', 'jpeg']" />
103
+        </el-form-item>
104
+        <el-form-item label="状态" prop="status">
105
+          <el-radio-group v-model="form.status">
106
+            <el-radio
107
+              v-for="dict in dict.type.sys_normal_disable"
108
+              :key="dict.value"
109
+              :label="dict.value"
110
+            >{{ dict.label }}</el-radio>
111
+          </el-radio-group>
112
+        </el-form-item>
113
+        <el-form-item label="绑定角色" prop="roleIds">
114
+          <el-select v-model="form.roleIds" multiple placeholder="请至少选择一个角色" style="width: 100%">
115
+            <el-option
116
+              v-for="item in roleOptions"
117
+              :key="item.roleId"
118
+              :label="item.roleName"
119
+              :value="item.roleId"
120
+            />
121
+          </el-select>
122
+        </el-form-item>
123
+      </el-form>
124
+      <div slot="footer" class="dialog-footer">
125
+        <el-button type="primary" @click="submitForm">确 定</el-button>
126
+        <el-button @click="cancelForm">取 消</el-button>
127
+      </div>
128
+    </el-dialog>
129
+
130
+    <!-- 修改账号弹窗 -->
131
+    <el-dialog title="修改账号" :visible.sync="profileOpen" width="520px" append-to-body @close="cancelProfile">
132
+      <el-form ref="profileForm" :model="profileForm" :rules="profileRules" label-width="100px">
133
+        <el-form-item label="员工名称" prop="employeeName">
134
+          <el-input v-model="profileForm.employeeName" placeholder="请输入员工名称" maxlength="30" />
135
+        </el-form-item>
136
+        <el-form-item label="手机号" prop="mobile">
137
+          <el-input v-model="profileForm.mobile" placeholder="11位手机号" maxlength="11" />
138
+        </el-form-item>
139
+        <el-form-item label="邮箱" prop="email">
140
+          <el-input v-model="profileForm.email" placeholder="选填" maxlength="50" />
141
+        </el-form-item>
142
+      </el-form>
143
+      <div slot="footer" class="dialog-footer">
144
+        <el-button type="primary" @click="submitProfile">确 定</el-button>
145
+        <el-button @click="cancelProfile">取 消</el-button>
146
+      </div>
147
+    </el-dialog>
148
+
149
+    <!-- 修改密码弹窗 -->
150
+    <el-dialog title="修改密码" :visible.sync="pwdOpen" width="520px" append-to-body @close="cancelPwd">
151
+      <el-form ref="pwdForm" :model="pwdForm" :rules="pwdRules" label-width="100px">
152
+        <el-form-item label="员工名称">
153
+          <span class="readonly-text">{{ pwdForm.employeeName || '—' }}</span>
154
+        </el-form-item>
155
+        <el-form-item label="新密码" prop="password">
156
+          <el-input v-model="pwdForm.password" placeholder="请输入新密码" type="password" maxlength="20" show-password />
157
+        </el-form-item>
158
+        <el-form-item label="确认新密码" prop="confirmPassword">
159
+          <el-input v-model="pwdForm.confirmPassword" placeholder="请再次输入新密码" type="password" maxlength="20" show-password />
160
+        </el-form-item>
161
+      </el-form>
162
+      <div slot="footer" class="dialog-footer">
163
+        <el-button type="primary" @click="submitPwd">确 定</el-button>
164
+        <el-button @click="cancelPwd">取 消</el-button>
165
+      </div>
166
+    </el-dialog>
167
+  </div>
168
+</template>
169
+
170
+<script>
171
+import {
172
+  listSellerEmployee,
173
+  sellerEmployeeQuota,
174
+  sellerEmployeeRoleOptions,
175
+  getSellerEmployee,
176
+  addSellerEmployee,
177
+  updateSellerEmployee,
178
+  updateSellerEmployeeProfile,
179
+  resetSellerEmployeePassword,
180
+  delSellerEmployee
181
+} from "@/api/agri/seller/employee"
182
+import { getSellerContext, switchSellerShop } from "@/api/agri/seller/context"
183
+import { setSellerShopContext } from "@/utils/sellerShop"
184
+import passwordRule from "@/utils/passwordRule"
185
+
186
+export default {
187
+  name: "AgriSellerEmployee",
188
+  dicts: ['sys_normal_disable'],
189
+  mixins: [passwordRule],
190
+  data() {
191
+    const validateConfirmPassword = (rule, value, callback) => {
192
+      if (!value) {
193
+        callback(new Error("请再次输入新密码"))
194
+        return
195
+      }
196
+      if (value !== this.pwdForm.password) {
197
+        callback(new Error("两次输入的密码不一致"))
198
+        return
199
+      }
200
+      callback()
201
+    }
202
+    const validateRoleIds = (rule, value, callback) => {
203
+      if (!value || !value.length) {
204
+        callback(new Error("请至少选择一个角色"))
205
+        return
206
+      }
207
+      callback()
208
+    }
209
+    return {
210
+      loading: true,
211
+      showSearch: true,
212
+      total: 0,
213
+      employeeList: [],
214
+      title: "",
215
+      open: false,
216
+      profileOpen: false,
217
+      pwdOpen: false,
218
+      currentShopName: "",
219
+      currentShopId: undefined,
220
+      shopSwitchable: false,
221
+      shopOptions: [],
222
+      quota: {},
223
+      roleOptions: [],
224
+      queryParams: {
225
+        pageNum: 1,
226
+        pageSize: 10,
227
+        employeeName: undefined
228
+      },
229
+      form: {},
230
+      profileForm: {},
231
+      pwdForm: {},
232
+      rules: {
233
+        employeeName: [
234
+          { required: true, message: "员工名称不能为空", trigger: "blur" }
235
+        ],
236
+        mobile: [
237
+          { required: true, message: "手机号不能为空", trigger: "blur" },
238
+          { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: "blur" }
239
+        ],
240
+        email: [
241
+          { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }
242
+        ],
243
+        status: [
244
+          { required: true, message: "请选择状态", trigger: "change" }
245
+        ],
246
+        roleIds: [
247
+          { required: true, validator: validateRoleIds, trigger: "change" }
248
+        ]
249
+      },
250
+      profileRules: {
251
+        employeeName: [
252
+          { required: true, message: "员工名称不能为空", trigger: "blur" }
253
+        ],
254
+        mobile: [
255
+          { required: true, message: "手机号不能为空", trigger: "blur" },
256
+          { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: "blur" }
257
+        ],
258
+        email: [
259
+          { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }
260
+        ]
261
+      },
262
+      pwdRules: {
263
+        password: [
264
+          { required: true, message: "新密码不能为空", trigger: "blur" },
265
+          { min: 6, max: 20, message: "密码长度必须介于 6 和 20 之间", trigger: "blur" }
266
+        ],
267
+        confirmPassword: [
268
+          { required: true, validator: validateConfirmPassword, trigger: "blur" }
269
+        ]
270
+      }
271
+    }
272
+  },
273
+  computed: {
274
+    /** 配额已满时禁用创建 */
275
+    quotaFull() {
276
+      if (this.quota.usedCount == null || this.quota.maxCount == null) {
277
+        return false
278
+      }
279
+      return this.quota.usedCount >= this.quota.maxCount
280
+    }
281
+  },
282
+  created() {
283
+    this.pwdRules.password = this.pwdValidator
284
+    this.initPage()
285
+  },
286
+  methods: {
287
+    /** 初始化页面 */
288
+    initPage() {
289
+      this.loadShopContext().then(() => {
290
+        this.refreshPage()
291
+      }).catch(() => {
292
+        this.refreshPage()
293
+      })
294
+    },
295
+    /** 加载店铺上下文 */
296
+    loadShopContext() {
297
+      return getSellerContext().then(response => {
298
+        const data = response.data || {}
299
+        if (data.shopId != null) {
300
+          setSellerShopContext(data.shopId, data.shopName)
301
+          this.currentShopId = data.shopId
302
+          this.currentShopName = data.shopName || ""
303
+          this.shopSwitchable = data.switchable === true
304
+          this.shopOptions = data.shops || []
305
+        }
306
+      })
307
+    },
308
+    /** 切换店铺 */
309
+    handleShopSwitch(shopId) {
310
+      switchSellerShop({ shopId: shopId }).then(response => {
311
+        const data = response.data || {}
312
+        setSellerShopContext(data.shopId, data.shopName)
313
+        this.currentShopId = data.shopId
314
+        this.currentShopName = data.shopName || ""
315
+        this.queryParams.pageNum = 1
316
+        this.refreshPage()
317
+      })
318
+    },
319
+    /** 刷新列表与配额 */
320
+    refreshPage() {
321
+      this.loadQuota()
322
+      this.loadRoleOptions()
323
+      this.getList()
324
+    },
325
+    /** 加载配额 */
326
+    loadQuota() {
327
+      sellerEmployeeQuota().then(response => {
328
+        this.quota = response.data || {}
329
+      }).catch(() => {
330
+        this.quota = {}
331
+      })
332
+    },
333
+    /** 加载当前店铺可选角色 */
334
+    loadRoleOptions() {
335
+      sellerEmployeeRoleOptions().then(response => {
336
+        this.roleOptions = response.data || []
337
+      }).catch(() => {
338
+        this.roleOptions = []
339
+      })
340
+    },
341
+    /** 查询员工列表 */
342
+    getList() {
343
+      this.loading = true
344
+      listSellerEmployee(this.queryParams).then(response => {
345
+        this.employeeList = response.rows || []
346
+        this.total = response.total || 0
347
+        this.loading = false
348
+      }).catch(() => {
349
+        this.loading = false
350
+      })
351
+    },
352
+    handleQuery() {
353
+      this.queryParams.pageNum = 1
354
+      this.getList()
355
+    },
356
+    resetQuery() {
357
+      this.resetForm("queryForm")
358
+      this.handleQuery()
359
+    },
360
+    resetFormData() {
361
+      this.form = {
362
+        userId: undefined,
363
+        employeeName: undefined,
364
+        mobile: undefined,
365
+        email: undefined,
366
+        avatar: undefined,
367
+        status: "0",
368
+        roleIds: []
369
+      }
370
+      this.resetForm("form")
371
+    },
372
+    /** 打开创建员工 */
373
+    handleAdd() {
374
+      if (this.quotaFull) {
375
+        this.$modal.msgWarning("子管理员人数已达上限")
376
+        return
377
+      }
378
+      this.resetFormData()
379
+      this.open = true
380
+      this.title = "创建员工"
381
+    },
382
+    /** 打开编辑员工 */
383
+    handleUpdate(row) {
384
+      this.resetFormData()
385
+      getSellerEmployee(row.userId).then(response => {
386
+        const data = response.data || {}
387
+        this.form = {
388
+          userId: data.userId,
389
+          employeeName: data.employeeName,
390
+          mobile: data.mobile,
391
+          email: data.email,
392
+          avatar: data.avatar,
393
+          status: data.status != null ? data.status : "0",
394
+          roleIds: data.roleIds ? [].concat(data.roleIds) : []
395
+        }
396
+        this.open = true
397
+        this.title = "编辑员工"
398
+      })
399
+    },
400
+    cancelForm() {
401
+      this.open = false
402
+      this.resetFormData()
403
+    },
404
+    /** 提交创建或编辑 */
405
+    submitForm() {
406
+      this.$refs["form"].validate(valid => {
407
+        if (!valid) {
408
+          return
409
+        }
410
+        const payload = {
411
+          employeeName: this.form.employeeName,
412
+          mobile: this.form.mobile,
413
+          email: this.form.email,
414
+          avatar: this.form.avatar,
415
+          status: this.form.status,
416
+          roleIds: this.form.roleIds
417
+        }
418
+        if (this.form.userId != null) {
419
+          payload.userId = this.form.userId
420
+          updateSellerEmployee(payload).then(() => {
421
+            this.$modal.msgSuccess("修改成功")
422
+            this.open = false
423
+            this.refreshPage()
424
+          })
425
+        } else {
426
+          addSellerEmployee(payload).then(response => {
427
+            this.$modal.msgSuccess("创建成功,请为该员工设置登录密码")
428
+            this.open = false
429
+            this.refreshPage()
430
+            const userId = response.userId
431
+            if (userId) {
432
+              this.$modal.confirm("是否立即为该员工设置登录密码?").then(() => {
433
+                this.handleResetPwd({ userId: userId, employeeName: payload.employeeName })
434
+              }).catch(() => {})
435
+            }
436
+          })
437
+        }
438
+      })
439
+    },
440
+    /** 打开修改账号 */
441
+    handleEditProfile(row) {
442
+      getSellerEmployee(row.userId).then(response => {
443
+        const data = response.data || {}
444
+        this.profileForm = {
445
+          userId: data.userId,
446
+          employeeName: data.employeeName,
447
+          mobile: data.mobile,
448
+          email: data.email
449
+        }
450
+        this.profileOpen = true
451
+      })
452
+    },
453
+    cancelProfile() {
454
+      this.profileOpen = false
455
+      this.profileForm = {}
456
+      this.resetForm("profileForm")
457
+    },
458
+    submitProfile() {
459
+      this.$refs["profileForm"].validate(valid => {
460
+        if (!valid) {
461
+          return
462
+        }
463
+        updateSellerEmployeeProfile(this.profileForm).then(() => {
464
+          this.$modal.msgSuccess("修改成功")
465
+          this.profileOpen = false
466
+          this.getList()
467
+        })
468
+      })
469
+    },
470
+    /** 打开修改密码 */
471
+    handleResetPwd(row) {
472
+      this.pwdForm = {
473
+        userId: row.userId,
474
+        employeeName: row.employeeName,
475
+        password: undefined,
476
+        confirmPassword: undefined
477
+      }
478
+      this.pwdOpen = true
479
+      this.$nextTick(() => {
480
+        this.resetForm("pwdForm")
481
+      })
482
+    },
483
+    cancelPwd() {
484
+      this.pwdOpen = false
485
+      this.pwdForm = {}
486
+      this.resetForm("pwdForm")
487
+    },
488
+    submitPwd() {
489
+      this.$refs["pwdForm"].validate(valid => {
490
+        if (!valid) {
491
+          return
492
+        }
493
+        resetSellerEmployeePassword({
494
+          userId: this.pwdForm.userId,
495
+          password: this.pwdForm.password,
496
+          confirmPassword: this.pwdForm.confirmPassword
497
+        }).then(() => {
498
+          this.$modal.msgSuccess("修改成功,员工下次登录请使用新密码")
499
+          this.pwdOpen = false
500
+        })
501
+      })
502
+    },
503
+    /** 删除员工 */
504
+    handleDelete(row) {
505
+      this.$modal.confirm('是否确认删除员工「' + (row.employeeName || row.loginAccount) + '」?删除后将无法登录商家端。').then(() => {
506
+        return delSellerEmployee(row.userId)
507
+      }).then(() => {
508
+        this.$modal.msgSuccess("删除成功")
509
+        this.refreshPage()
510
+      }).catch(() => {})
511
+    }
512
+  }
513
+}
514
+</script>
515
+
516
+<style scoped>
517
+.search-card {
518
+  margin-bottom: 0;
519
+}
520
+.card-header {
521
+  display: flex;
522
+  align-items: center;
523
+  flex-wrap: wrap;
524
+  gap: 8px;
525
+}
526
+.header-tip {
527
+  color: #909399;
528
+  font-size: 13px;
529
+  font-weight: normal;
530
+}
531
+.quota-tip {
532
+  color: #606266;
533
+  font-size: 13px;
534
+  font-weight: normal;
535
+  margin-left: 4px;
536
+}
537
+.mb12 {
538
+  margin-bottom: 12px;
539
+}
540
+.form-tip {
541
+  color: #909399;
542
+  font-size: 12px;
543
+  line-height: 1.5;
544
+}
545
+.readonly-text {
546
+  color: #606266;
547
+}
548
+</style>

+ 396 - 0
ruoyi-ui/src/views/agri/seller/role/index.vue

@@ -0,0 +1,396 @@
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="roleName">
7
+          <el-input v-model="queryParams.roleName" placeholder="模糊检索" clearable style="width: 220px" @keyup.enter.native="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item>
10
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
11
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
12
+        </el-form-item>
13
+      </el-form>
14
+    </el-card>
15
+
16
+    <br/>
17
+
18
+    <!-- 列表区 -->
19
+    <el-card shadow="never" class="table-card">
20
+      <div slot="header" class="card-header">
21
+        <span>角色列表</span>
22
+        <span class="header-tip">当前店铺:{{ currentShopName || '—' }}</span>
23
+        <el-select
24
+          v-if="shopSwitchable && shopOptions.length > 1"
25
+          v-model="currentShopId"
26
+          size="small"
27
+          placeholder="切换店铺"
28
+          style="width: 200px; margin-left: 12px"
29
+          @change="handleShopSwitch"
30
+        >
31
+          <el-option
32
+            v-for="item in shopOptions"
33
+            :key="item.shopId"
34
+            :label="item.shopName"
35
+            :value="item.shopId"
36
+          />
37
+        </el-select>
38
+      </div>
39
+
40
+      <el-row :gutter="10" class="mb8">
41
+        <el-col :span="1.5">
42
+          <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['agri:seller:role:add']">创建角色</el-button>
43
+        </el-col>
44
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
45
+      </el-row>
46
+
47
+      <el-table border v-loading="loading" :data="roleList">
48
+        <el-table-column label="角色名称" align="center" prop="roleName" min-width="120" :show-overflow-tooltip="true" />
49
+        <el-table-column label="角色描述" align="center" prop="roleDesc" min-width="160" :show-overflow-tooltip="true">
50
+          <template slot-scope="scope">
51
+            <span>{{ scope.row.roleDesc || '—' }}</span>
52
+          </template>
53
+        </el-table-column>
54
+        <el-table-column label="状态" align="center" prop="status" width="100">
55
+          <template slot-scope="scope">
56
+            <dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
57
+          </template>
58
+        </el-table-column>
59
+        <el-table-column label="绑定员工" align="center" prop="bindCount" width="90">
60
+          <template slot-scope="scope">
61
+            <span>{{ scope.row.bindCount != null ? scope.row.bindCount : '—' }}</span>
62
+          </template>
63
+        </el-table-column>
64
+        <el-table-column label="创建时间" align="center" prop="createTime" width="160">
65
+          <template slot-scope="scope">
66
+            <span>{{ parseTime(scope.row.createTime) }}</span>
67
+          </template>
68
+        </el-table-column>
69
+        <el-table-column label="操作" align="center" width="140" fixed="right">
70
+          <template slot-scope="scope">
71
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:role:edit']">修改角色</el-button>
72
+            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['agri:seller:role:remove']">删除</el-button>
73
+          </template>
74
+        </el-table-column>
75
+      </el-table>
76
+
77
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
78
+    </el-card>
79
+
80
+    <!-- 创建/修改角色弹窗 -->
81
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body @close="cancel">
82
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
83
+        <el-form-item label="角色名称" prop="roleName">
84
+          <el-input v-model="form.roleName" placeholder="请输入角色名称" maxlength="30" />
85
+        </el-form-item>
86
+        <el-form-item label="角色描述" prop="roleDesc">
87
+          <el-input v-model="form.roleDesc" type="textarea" :rows="2" placeholder="选填" maxlength="200" show-word-limit />
88
+        </el-form-item>
89
+        <el-form-item label="状态" prop="status">
90
+          <el-radio-group v-model="form.status">
91
+            <el-radio
92
+              v-for="dict in dict.type.sys_normal_disable"
93
+              :key="dict.value"
94
+              :label="dict.value"
95
+            >{{ dict.label }}</el-radio>
96
+          </el-radio-group>
97
+        </el-form-item>
98
+        <el-form-item label="功能权限" prop="menuIds">
99
+          <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
100
+          <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox>
101
+          <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event)">父子联动</el-checkbox>
102
+          <el-tree
103
+            class="tree-border"
104
+            :data="menuOptions"
105
+            show-checkbox
106
+            ref="menu"
107
+            node-key="id"
108
+            :check-strictly="!form.menuCheckStrictly"
109
+            empty-text="加载中,请稍候"
110
+            :props="defaultProps"
111
+          />
112
+        </el-form-item>
113
+      </el-form>
114
+      <div slot="footer" class="dialog-footer">
115
+        <el-button type="primary" @click="submitForm">确 定</el-button>
116
+        <el-button @click="cancel">取 消</el-button>
117
+      </div>
118
+    </el-dialog>
119
+  </div>
120
+</template>
121
+
122
+<script>
123
+import {
124
+  listSellerRole,
125
+  sellerRoleMenuTree,
126
+  getSellerRole,
127
+  addSellerRole,
128
+  updateSellerRole,
129
+  delSellerRole
130
+} from "@/api/agri/seller/role"
131
+import { getSellerContext, switchSellerShop } from "@/api/agri/seller/context"
132
+import { setSellerShopContext } from "@/utils/sellerShop"
133
+
134
+export default {
135
+  name: "AgriSellerRole",
136
+  dicts: ['sys_normal_disable'],
137
+  data() {
138
+    return {
139
+      // 列表加载遮罩
140
+      loading: true,
141
+      // 是否显示检索区
142
+      showSearch: true,
143
+      // 列表总条数
144
+      total: 0,
145
+      // 角色列表数据
146
+      roleList: [],
147
+      // 弹窗标题
148
+      title: "",
149
+      // 是否显示弹窗
150
+      open: false,
151
+      // 当前店铺名称
152
+      currentShopName: "",
153
+      // 当前店铺 ID
154
+      currentShopId: undefined,
155
+      // 是否可切换店铺
156
+      shopSwitchable: false,
157
+      // 可切换店铺列表
158
+      shopOptions: [],
159
+      // 菜单树数据
160
+      menuOptions: [],
161
+      menuExpand: false,
162
+      menuNodeAll: false,
163
+      defaultProps: {
164
+        children: "children",
165
+        label: "label"
166
+      },
167
+      // 列表检索参数
168
+      queryParams: {
169
+        pageNum: 1,
170
+        pageSize: 10,
171
+        roleName: undefined
172
+      },
173
+      // 创建/编辑表单
174
+      form: {},
175
+      // 表单校验规则
176
+      rules: {
177
+        roleName: [
178
+          { required: true, message: "角色名称不能为空", trigger: "blur" }
179
+        ],
180
+        status: [
181
+          { required: true, message: "请选择状态", trigger: "change" }
182
+        ]
183
+      }
184
+    }
185
+  },
186
+  created() {
187
+    this.initPage()
188
+  },
189
+  methods: {
190
+    /** 初始化:加载店铺上下文后拉列表 */
191
+    initPage() {
192
+      this.loadShopContext().then(() => {
193
+        this.getList()
194
+      }).catch(() => {
195
+        this.getList()
196
+      })
197
+    },
198
+    /** 加载商家端当前店铺上下文,写入 X-Shop-Id 缓存 */
199
+    loadShopContext() {
200
+      return getSellerContext().then(response => {
201
+        const data = response.data || {}
202
+        if (data.shopId != null) {
203
+          setSellerShopContext(data.shopId, data.shopName)
204
+          this.currentShopId = data.shopId
205
+          this.currentShopName = data.shopName || ""
206
+          this.shopSwitchable = data.switchable === true
207
+          this.shopOptions = data.shops || []
208
+        }
209
+      })
210
+    },
211
+    /** 经营账号切换店铺 */
212
+    handleShopSwitch(shopId) {
213
+      switchSellerShop({ shopId: shopId }).then(response => {
214
+        const data = response.data || {}
215
+        setSellerShopContext(data.shopId, data.shopName)
216
+        this.currentShopId = data.shopId
217
+        this.currentShopName = data.shopName || ""
218
+        this.queryParams.pageNum = 1
219
+        this.getList()
220
+      })
221
+    },
222
+    /** 查询角色列表 */
223
+    getList() {
224
+      this.loading = true
225
+      listSellerRole(this.queryParams).then(response => {
226
+        this.roleList = response.rows || []
227
+        this.total = response.total || 0
228
+        this.loading = false
229
+      }).catch(() => {
230
+        this.loading = false
231
+      })
232
+    },
233
+    /** 搜索 */
234
+    handleQuery() {
235
+      this.queryParams.pageNum = 1
236
+      this.getList()
237
+    },
238
+    /** 重置检索 */
239
+    resetQuery() {
240
+      this.resetForm("queryForm")
241
+      this.handleQuery()
242
+    },
243
+    /** 加载商家端可分配菜单树 */
244
+    loadMenuTree() {
245
+      return sellerRoleMenuTree().then(response => {
246
+        this.menuOptions = response.data || []
247
+      })
248
+    },
249
+    /** 获取权限树选中节点(含半选父节点) */
250
+    getMenuAllCheckedKeys() {
251
+      const checkedKeys = this.$refs.menu.getCheckedKeys()
252
+      const halfCheckedKeys = this.$refs.menu.getHalfCheckedKeys()
253
+      checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
254
+      return checkedKeys
255
+    },
256
+    /** 展开/折叠权限树 */
257
+    handleCheckedTreeExpand(value) {
258
+      const treeList = this.menuOptions
259
+      for (let i = 0; i < treeList.length; i++) {
260
+        this.$refs.menu.store.nodesMap[treeList[i].id].expanded = value
261
+      }
262
+    },
263
+    /** 全选/全不选权限树 */
264
+    handleCheckedTreeNodeAll(value) {
265
+      this.$refs.menu.setCheckedNodes(value ? this.menuOptions : [])
266
+    },
267
+    /** 父子联动开关 */
268
+    handleCheckedTreeConnect(value) {
269
+      this.form.menuCheckStrictly = !!value
270
+    },
271
+    /** 重置表单 */
272
+    reset() {
273
+      if (this.$refs.menu) {
274
+        this.$refs.menu.setCheckedKeys([])
275
+      }
276
+      this.menuExpand = false
277
+      this.menuNodeAll = false
278
+      this.form = {
279
+        roleId: undefined,
280
+        roleName: undefined,
281
+        roleDesc: undefined,
282
+        status: "0",
283
+        menuCheckStrictly: true
284
+      }
285
+      this.resetForm("form")
286
+    },
287
+    /** 打开创建角色弹窗 */
288
+    handleAdd() {
289
+      this.reset()
290
+      this.loadMenuTree().then(() => {
291
+        this.open = true
292
+        this.title = "创建角色"
293
+      })
294
+    },
295
+    /** 打开修改角色弹窗并回显 */
296
+    handleUpdate(row) {
297
+      this.reset()
298
+      Promise.all([
299
+        this.loadMenuTree(),
300
+        getSellerRole(row.roleId)
301
+      ]).then(responses => {
302
+        const data = (responses[1].data) || {}
303
+        this.form = {
304
+          roleId: data.roleId,
305
+          roleName: data.roleName,
306
+          roleDesc: data.roleDesc,
307
+          status: data.status != null ? data.status : "0",
308
+          menuCheckStrictly: true
309
+        }
310
+        this.open = true
311
+        this.title = "修改角色"
312
+        this.$nextTick(() => {
313
+          if (this.$refs.menu && data.menuIds && data.menuIds.length) {
314
+            this.$refs.menu.setCheckedKeys(data.menuIds)
315
+          }
316
+        })
317
+      })
318
+    },
319
+    /** 关闭弹窗 */
320
+    cancel() {
321
+      this.open = false
322
+      this.reset()
323
+    },
324
+    /** 提交创建或修改 */
325
+    submitForm() {
326
+      this.$refs["form"].validate(valid => {
327
+        if (!valid) {
328
+          return
329
+        }
330
+        const menuIds = this.getMenuAllCheckedKeys()
331
+        if (!menuIds || !menuIds.length) {
332
+          this.$modal.msgWarning("请至少选择一项功能权限")
333
+          return
334
+        }
335
+        const payload = {
336
+          roleName: this.form.roleName,
337
+          roleDesc: this.form.roleDesc,
338
+          status: this.form.status,
339
+          menuIds: menuIds
340
+        }
341
+        if (this.form.roleId != null) {
342
+          payload.roleId = this.form.roleId
343
+          updateSellerRole(payload).then(() => {
344
+            this.$modal.msgSuccess("修改成功")
345
+            this.open = false
346
+            this.getList()
347
+          })
348
+        } else {
349
+          addSellerRole(payload).then(() => {
350
+            this.$modal.msgSuccess("创建成功")
351
+            this.open = false
352
+            this.getList()
353
+          })
354
+        }
355
+      })
356
+    },
357
+    /** 删除角色 */
358
+    handleDelete(row) {
359
+      const tip = row.bindCount > 0
360
+        ? '角色「' + row.roleName + '」已绑定 ' + row.bindCount + ' 名员工,删除可能失败。是否继续?'
361
+        : '是否确认删除角色「' + row.roleName + '」?'
362
+      this.$modal.confirm(tip).then(() => {
363
+        return delSellerRole(row.roleId)
364
+      }).then(() => {
365
+        this.getList()
366
+        this.$modal.msgSuccess("删除成功")
367
+      }).catch(() => {})
368
+    }
369
+  }
370
+}
371
+</script>
372
+
373
+<style scoped>
374
+.search-card {
375
+  margin-bottom: 0;
376
+}
377
+.card-header {
378
+  display: flex;
379
+  align-items: center;
380
+  flex-wrap: wrap;
381
+  gap: 8px;
382
+}
383
+.header-tip {
384
+  color: #909399;
385
+  font-size: 13px;
386
+  font-weight: normal;
387
+}
388
+.tree-border {
389
+  margin-top: 8px;
390
+  border: 1px solid #e5e6e7;
391
+  border-radius: 4px;
392
+  padding: 8px;
393
+  max-height: 360px;
394
+  overflow: auto;
395
+}
396
+</style>