xsh_1997 před 2 týdny
rodič
revize
d58007e696

+ 219 - 0
doc/店铺后台/店铺设置/店铺设置前端技术方案.md

@@ -0,0 +1,219 @@
1
+# 店铺设置 — 前端技术方案
2
+
3
+> **依据:** 《店铺设置功能需求.md》v1.0、《店铺设置技术方案.md》v1.1  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端(店铺经营管理端)** 当前店铺资料展示/编辑、员工数量概览;**不** 维护平台全局策略、**不** 做员工 CRUD。  
6
+> **实现状态:** 页面与 API 封装已落地,待菜单配置及 `/agri/seller/shop` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI(与 RuoYi-Vue 一致) |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/role/index.vue`(店铺上下文)、`agri/org/shop/index.vue`(店名/电话/头像校验) |
17
+| 布局 | 资料区 `el-card` + `<br/>` + 员工概览 `el-card`(本模块无检索/表格) |
18
+| 图片 | `image-upload` / `image-preview`(头像,5MB,png/jpg/jpeg) |
19
+| 店铺切换 | **仅 Navbar**;业务页 **禁止** 店铺选择器(见 `doc/前端设计/前端设计.md` §5) |
20
+
21
+---
22
+
23
+## 2. 文件清单
24
+
25
+| 类型 | 路径 | 说明 |
26
+|------|------|------|
27
+| 设置页 | `ruoyi-ui/src/views/agri/seller/shop/index.vue` | 资料展示、编辑弹窗、员工概览 |
28
+| 店铺 API | `ruoyi-ui/src/api/agri/seller/shop.js` | 资料 GET/PUT、员工统计 GET |
29
+| 店铺上下文 | `ruoyi-ui/src/api/agri/seller/context.js` | 已有,复用 |
30
+| 店铺工具 | `ruoyi-ui/src/utils/sellerShop.js` | 已有,复用 |
31
+
32
+**组件 name(keep-alive):** `AgriSellerShop`
33
+
34
+---
35
+
36
+## 3. 菜单与路由
37
+
38
+若依路由由 **后端菜单** 动态加载,前端需配置:
39
+
40
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
41
+|----------|----------|-------------------|----------|
42
+| 店铺设置 | `agri/seller/shop/index` | `seller/shop` | `agri:seller:shop:query` |
43
+
44
+**上级菜单:** 店铺经营管理端 → **店铺设置**(或同级入口,以运营配置为准)
45
+
46
+| 按钮权限 | 标识 | 说明 |
47
+|----------|------|------|
48
+| 查看资料 / 员工概览 | `agri:seller:shop:query` | 页面进入与只读数据 |
49
+| 编辑店铺资料 | `agri:seller:shop:edit` | 「编辑」按钮与 PUT 保存 |
50
+
51
+**协作菜单:** 跳转员工管理须 `agri:seller:employee:list`;无权限时「前往员工管理」按钮隐藏。
52
+
53
+> **命名区分:** 商家端 **`agri:seller:shop:*`** 与平台 **`agri:shop:setting`**(全局策略)**不可混用**。
54
+
55
+---
56
+
57
+## 4. 页面结构
58
+
59
+```text
60
+店铺设置
61
+├── 区块 A:店铺资料(el-card + el-descriptions border)
62
+│   ├── 工具栏:编辑(v-hasPermi edit)
63
+│   └── 只读展示:
64
+│       ├── 店铺头像、店铺名称
65
+│       ├── 创建时间、店铺状态(开业/停业)
66
+│       ├── 所属商户、客服电话(biz_shop.shop_phone)
67
+│       ├── 负责人姓名、联系电话(商户联系人,后端脱敏)
68
+│       └── 店铺简介
69
+├── <br/>
70
+├── 区块 B:员工概览(el-card)
71
+│   ├── 工具栏:前往员工管理 → /seller/employee
72
+│   └── 指标:
73
+│       ├── 已有员工数 totalCount(当前店,含停用)
74
+│       ├── 停用员工数 disabledCount
75
+│       └── 商户配额 usedCount / maxCount(各店合计 / 平台上限)
76
+└── 编辑弹窗(560px)
77
+    ├── 店铺名称 shopName(必填,≤128)
78
+    ├── 店铺头像 shopAvatar(必填,image-upload)
79
+    ├── 店铺简介 shopDesc(选填,≤1000)
80
+    └── 客服电话 shopPhone(选填,格式校验)
81
+```
82
+
83
+**不提供:** 页内店铺选择器、营业状态切换、员工 CRUD、平台全局策略编辑。
84
+
85
+---
86
+
87
+## 5. 店铺上下文(X-Shop-Id)
88
+
89
+| 步骤 | 说明 |
90
+|------|------|
91
+| 页面 `created` | `GET /agri/seller/context` → `setSellerShopContext(shopId, shopName)` |
92
+| 业务请求 | `sellerShopHeaders()` 注入 `X-Shop-Id` |
93
+| 切换店铺 | Navbar `PUT /agri/seller/context/shop` 成功后 **`location.reload()`**,本页随整页刷新 |
94
+| 数据范围 | 全部接口仅针对 **当前店铺**;切换后不得展示上一店数据 |
95
+
96
+---
97
+
98
+## 6. API 封装
99
+
100
+**模块:** `ruoyi-ui/src/api/agri/seller/shop.js`
101
+
102
+| 方法 | HTTP | 路径 | 权限 | 说明 |
103
+|------|------|------|------|------|
104
+| `getSellerShopProfile()` | GET | `/agri/seller/shop` | query | 返回 `SellerShopProfileVO` |
105
+| `updateSellerShopProfile(data)` | PUT | `/agri/seller/shop` | edit | Body:`shopName/shopAvatar/shopDesc/shopPhone` |
106
+| `getSellerShopEmployeeStats()` | GET | `/agri/seller/shop/employeeStats` | query | 返回 `SellerShopEmployeeStatsVO` |
107
+
108
+### 6.1 资料 VO 字段映射
109
+
110
+| 接口字段 | 界面标签 | 编辑 |
111
+|----------|----------|:----:|
112
+| shopAvatar | 店铺头像 | ✓ |
113
+| shopName | 店铺名称 | ✓ |
114
+| createTime | 创建时间 | ✗ |
115
+| shopStatus | 店铺状态(0 开业 / 1 停业) | ✗ |
116
+| merchantName | 所属商户 | ✗ |
117
+| shopPhone | 客服电话 | ✓ |
118
+| contactName | 负责人姓名 | ✗ |
119
+| contactPhone | 联系电话(脱敏) | ✗ |
120
+| shopDesc | 店铺简介 | ✓ |
121
+
122
+### 6.2 员工统计 VO
123
+
124
+| 字段 | 界面说明 |
125
+|------|----------|
126
+| totalCount | 当前店已有员工数(正常+停用,不含经营账号) |
127
+| disabledCount | 当前店停用员工数 |
128
+| usedCount | 商户级已占用名额(各店员工合计) |
129
+| maxCount | 平台全局子管理员上限(只读) |
130
+
131
+---
132
+
133
+## 7. 交互与校验
134
+
135
+### 7.1 加载流程
136
+
137
+```text
138
+created → loadShopContext
139
+       → 并行 GET /shop + GET /shop/employeeStats
140
+       → 渲染 descriptions + 统计卡片
141
+```
142
+
143
+### 7.2 编辑流程
144
+
145
+```text
146
+点击「编辑」→ 弹窗预填四字段
147
+           → 校验通过 → PUT /agri/seller/shop
148
+           → msgSuccess → 关闭弹窗 → 重新 GET 资料
149
+```
150
+
151
+### 7.3 前端校验规则
152
+
153
+| 字段 | 规则 |
154
+|------|------|
155
+| shopName | 必填 |
156
+| shopAvatar | 必填(change 触发) |
157
+| shopDesc | 选填,maxlength 1000 |
158
+| shopPhone | 选填;有值时 `/^[0-9\-+()\s]{5,20}$/`(对齐平台店铺管理) |
159
+
160
+后端额外校验(前端透传 msg):店名全平台唯一、简介过长、无权/无店铺等。
161
+
162
+### 7.4 空值展示
163
+
164
+未填字段统一显示 **「—」**;联系人未完善不阻断编辑。
165
+
166
+### 7.5 跳转员工管理
167
+
168
+```javascript
169
+this.$router.push({ path: '/seller/employee' })
170
+```
171
+
172
+返回本页时可在 `activated`(若 keep-alive)或用户手动刷新时重新拉 `employeeStats`;Navbar 切店会整页 reload。
173
+
174
+---
175
+
176
+## 8. 权限与按钮
177
+
178
+```html
179
+v-hasPermi="['agri:seller:shop:edit']"      <!-- 编辑 -->
180
+v-hasPermi="['agri:seller:employee:list']"    <!-- 前往员工管理 -->
181
+```
182
+
183
+仅有 `query` 无 `edit` 时:可看资料与统计,**无** 编辑按钮。
184
+
185
+---
186
+
187
+## 9. 与后端 / 平台协作
188
+
189
+| 关联 | 说明 |
190
+|------|------|
191
+| 平台 · 店铺管理 | PUT 后 `biz_shop` 同源,平台列表无需同步任务 |
192
+| 平台 · 商户管理 | `contactName/contactPhone` 只读 join |
193
+| 平台 · 店铺设置 | `maxCount` 只读;文案提示联系平台调整 |
194
+| 员工管理 | 统计口径一致;CRUD 在 `/agri/seller/employee` |
195
+| 商品列表等 | 共用 `X-Shop-Id` 与 Navbar 切店行为 |
196
+
197
+---
198
+
199
+## 10. 联调检查清单
200
+
201
+- [ ] 菜单 SQL:`component = agri/seller/shop/index`,perms `agri:seller:shop:query|edit`
202
+- [ ] 经营账号多店:Navbar 切店后资料/统计随店变化
203
+- [ ] 员工账号:固定绑定店,不可切店
204
+- [ ] 保存成功后平台店铺列表名称/头像/简介/电话一致
205
+- [ ] `usedCount/maxCount` 与 `GET /agri/seller/employee/quota` 一致
206
+- [ ] 无 `employee:list` 权限时不展示跳转按钮
207
+- [ ] 店名重复、头像为空、电话格式错误等后端 msg 正常弹出
208
+
209
+---
210
+
211
+## 11. 修订记录
212
+
213
+| 版本 | 说明 |
214
+|------|------|
215
+| **v1.0** | 首版;对齐功能需求 v1.0、后端技术方案 v1.1;实现资料展示/编辑弹窗、员工概览与跳转 |
216
+
217
+---
218
+
219
+*文档版本:v1.0 · 关联《店铺设置功能需求.md》v1.0、《店铺设置技术方案.md》v1.1*

+ 247 - 0
doc/店铺后台/订单管理/全部订单/全部订单前端技术方案.md

@@ -0,0 +1,247 @@
1
+# 全部订单 — 前端技术方案
2
+
3
+> **依据:** 《全部订单功能需求.md》v1.0、《全部订单技术方案.md》v1.0.2  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺订单列表、检索、详情、整单发货、物流更新、送达登记、删除已关闭单;**不** 创建订单、**不** 手工关闭。  
6
+> **实现状态:** 页面与 API 封装已落地,待菜单配置及 `/agri/seller/order` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/goods/index.vue`(页签+检索+列表)、`agri/member/orders.vue`(订单列展示) |
17
+| 布局 | 检索 `el-card` + `<br/>` + 列表 `el-card` + `border` 表格 |
18
+| 详情 | 右侧 `el-drawer`(与商品详情一致) |
19
+| 店铺切换 | **仅 Navbar**;业务页不展示店铺选择器 |
20
+
21
+---
22
+
23
+## 2. 文件清单
24
+
25
+| 类型 | 路径 | 说明 |
26
+|------|------|------|
27
+| 列表页 | `ruoyi-ui/src/views/agri/seller/order/index.vue` | 页签、检索、列表、履约弹窗 |
28
+| 详情抽屉 | `ruoyi-ui/src/views/agri/seller/order/detail.vue` | 分区展示、物流时间轴、操作按钮 |
29
+| 订单 API | `ruoyi-ui/src/api/agri/seller/order.js` | list、detail、ship、logistics、delivered、remove |
30
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
31
+
32
+**组件 name(keep-alive):** `AgriSellerOrder`
33
+
34
+---
35
+
36
+## 3. 菜单与路由
37
+
38
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
39
+|----------|----------|-------------------|----------|
40
+| 全部订单 | `agri/seller/order/index` | `seller/order` | `agri:seller:order:list` |
41
+
42
+**上级菜单:** 店铺经营管理端 → **订单管理**
43
+
44
+| 按钮权限 | 标识 | 说明 |
45
+|----------|------|------|
46
+| 列表 | `agri:seller:order:list` | 页签列表 |
47
+| 详情 | `agri:seller:order:query` | 查看详情抽屉 |
48
+| 发货/物流/送达 | `agri:seller:order:ship` | 去发货、更新物流、商品到货 |
49
+| 删除 | `agri:seller:order:remove` | 已关闭 → 已删除 |
50
+
51
+> 与平台 `agri:order:*` **不可混用**。
52
+
53
+---
54
+
55
+## 4. 页面结构
56
+
57
+```text
58
+全部订单
59
+├── 检索区(el-card)
60
+│   ├── 订单编号 orderNo
61
+│   ├── 商品名称 goodsName
62
+│   ├── 发货状态 shipStatus(0待支付/1待发货/2已发货)
63
+│   ├── 收货人 consigneeName、收货手机 consigneeMobile、收货地址 consigneeAddress
64
+│   ├── 配送方式 deliveryType(1物流/2商家配送)
65
+│   └── 下单时间 beginTime~endTime(daterange)
66
+├── 列表区(el-card + border 表格)
67
+│   ├── 状态页签:全部 | 待发货 | 已发货 | 已关闭 | 已完成 | 已删除
68
+│   ├── 列:订单信息、订单金额、会员名称、收货人信息、配送方式、订单状态、操作
69
+│   └── 分页
70
+├── 详情抽屉 detail.vue
71
+│   ├── 基本信息 / 商品明细 / 收货 / 物流 / 关闭信息 / 物流节点时间轴
72
+│   └── 操作:去发货、更新物流、商品到货、删除(按状态显隐)
73
+├── 发货弹窗(待发货 · 整单)
74
+├── 更新物流弹窗(已发货)
75
+└── 商品到货弹窗(已发货)
76
+```
77
+
78
+**不提供:** 页内店铺选择、手工关闭、代确认收货、拆单/部分发货。
79
+
80
+---
81
+
82
+## 5. 状态页签与 Query
83
+
84
+| 页签 | `statusTab` | 传参 `orderStatus` | 说明 |
85
+|------|-------------|-------------------|------|
86
+| 全部 | `all` | 不传 | 含待支付;**排除** 已删除(后端 `!= '5'`) |
87
+| 待发货 | `1` | `1` | 可「去发货」 |
88
+| 已发货 | `2` | `2` | 可更新物流、商品到货 |
89
+| 已关闭 | `4` | `4` | 可删除 |
90
+| 已完成 | `3` | `3` | 只读 |
91
+| 已删除 | `5` | `5` | 只读审计 |
92
+
93
+页签与高级检索 **AND** 组合;切换页签重置 `pageNum=1` 并刷新列表。
94
+
95
+**深链:** 支持 `?orderStatus=1` 等路由参数预设页签(供后续「发货管理」跳转)。
96
+
97
+---
98
+
99
+## 6. 店铺上下文(X-Shop-Id)
100
+
101
+| 步骤 | 说明 |
102
+|------|------|
103
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
104
+| 列表/详情/履约 | `sellerShopHeaders()` |
105
+| Navbar 切店 | `location.reload()` 整页刷新 |
106
+
107
+---
108
+
109
+## 7. API 封装
110
+
111
+**模块:** `ruoyi-ui/src/api/agri/seller/order.js`
112
+
113
+| 方法 | HTTP | 路径 |
114
+|------|------|------|
115
+| `listSellerOrder(query)` | GET | `/agri/seller/order/list` |
116
+| `getSellerOrder(orderId)` | GET | `/agri/seller/order/{orderId}` |
117
+| `shipSellerOrder(orderId, data)` | POST | `/agri/seller/order/{orderId}/ship` |
118
+| `logisticsSellerOrder(orderId, data)` | POST | `/agri/seller/order/{orderId}/logistics` |
119
+| `deliveredSellerOrder(orderId, data)` | POST | `/agri/seller/order/{orderId}/delivered` |
120
+| `delSellerOrder(orderId)` | DELETE | `/agri/seller/order/{orderId}` |
121
+
122
+### 7.1 列表 Query 参数
123
+
124
+与 `SellerOrderQuery` 对齐:`orderStatus`、`orderNo`、`goodsName`、`shipStatus`、`consigneeName`、`consigneeMobile`、`consigneeAddress`、`deliveryType`、`beginTime`、`endTime`、`pageNum`、`pageSize`。
125
+
126
+### 7.2 列表行 `OrderListRowVO`
127
+
128
+| 字段 | 界面 |
129
+|------|------|
130
+| orderNo, createTime, items[], itemCount | 「订单信息」列 |
131
+| payAmount, freightAmount | 「订单金额」 |
132
+| memberNickName | 会员名称 |
133
+| consigneeName/Mobile/Address | 收货人(手机已脱敏) |
134
+| deliveryTypeLabel | 配送方式 |
135
+| orderStatus, orderStatusLabel | 状态 Tag |
136
+
137
+首行商品 +「共 N 件」展示多商品摘要。
138
+
139
+### 7.3 详情 `OrderDetailVO`
140
+
141
+分区:基本信息、商品明细表、收货信息、物流信息(按 deliveryType 显隐字段)、关闭信息、**traceList 时间轴(倒序,后端已排序)**。
142
+
143
+---
144
+
145
+## 8. 操作与状态矩阵
146
+
147
+| orderStatus | 列表/详情操作 |
148
+|-------------|---------------|
149
+| `0` 待支付 | 仅详情(**全部**页签可见) |
150
+| `1` 待发货 | 详情、去发货 |
151
+| `2` 已发货 | 详情、更新物流、商品到货 |
152
+| `3` 已完成 | 仅详情 |
153
+| `4` 已关闭 | 详情、删除 |
154
+| `5` 已删除 | 仅详情 |
155
+
156
+权限:`v-hasPermi` 控制按钮显隐。
157
+
158
+---
159
+
160
+## 9. 履约弹窗
161
+
162
+### 9.1 去发货 `OrderShipDTO`
163
+
164
+| 字段 | 规则 |
165
+|------|------|
166
+| shipTime | 必填,默认当前时间 |
167
+| deliveryType | `1` 物流 / `2` 商家配送 |
168
+| deliveryType=1 | logisticsCompany + trackingNo 必填 |
169
+| deliveryType=2 | vehicleNo + courierName + courierMobile(11 位手机)必填 |
170
+| shipRemark | 选填 |
171
+| 发货商品 | 只读表格,整单展示;文案提示不可拆单 |
172
+
173
+打开弹窗前 `GET` 详情拉取 `items`。
174
+
175
+### 9.2 更新物流 `OrderLogisticsDTO`
176
+
177
+| 字段 | 规则 |
178
+|------|------|
179
+| traceTime | 必填 |
180
+| content | 必填(运输状态描述) |
181
+
182
+### 9.3 商品到货 `OrderDeliveredDTO`
183
+
184
+| 字段 | 规则 |
185
+|------|------|
186
+| traceTime | 必填 |
187
+| content | 选填 |
188
+
189
+弹窗底部提示:登记送达 **不替代** 买家确认收货。
190
+
191
+### 9.4 删除
192
+
193
+`$modal.confirm` 二次确认 → `DELETE /{orderId}` → 刷新列表;若详情打开则关闭抽屉。
194
+
195
+---
196
+
197
+## 10. 状态展示
198
+
199
+前端本地映射(后端亦返回 `orderStatusLabel`):
200
+
201
+| 值 | 文案 | Tag |
202
+|----|------|-----|
203
+| 0 | 待支付 | warning |
204
+| 1 | 待发货 | primary |
205
+| 2 | 已发货 | default |
206
+| 3 | 已完成 | success |
207
+| 4 | 已关闭 | info |
208
+| 5 | 已删除 | info |
209
+
210
+后续可接入字典 `biz_order_status`、`biz_delivery_type`(与后端常量一致)。
211
+
212
+---
213
+
214
+## 11. 与平台 / 其他模块
215
+
216
+| 关联 | 说明 |
217
+|------|------|
218
+| 平台订单管理 | 同源状态机;平台代发货后商家端不可重复发货 |
219
+| 会员管理 | 会员名只读;不展示累计消费 |
220
+| 商品列表 | 明细为下单快照 |
221
+| 发货管理(待建) | 可深链 `seller/order?orderStatus=1` |
222
+| Navbar 切店 | 整页 reload,列表随店刷新 |
223
+
224
+---
225
+
226
+## 12. 联调检查清单
227
+
228
+- [ ] 菜单:`agri/seller/order/index`,权限 `agri:seller:order:*`
229
+- [ ] **全部** 页签含待支付且仅可查看
230
+- [ ] 待发货整单发货后进入「已发货」页签
231
+- [ ] 物流/送达多次追加 trace,主状态仍为已发货
232
+- [ ] 仅已关闭可删除,删除后在「已删除」可见
233
+- [ ] 检索 + 页签组合过滤正确
234
+- [ ] 切店后列表仅为新店订单
235
+- [ ] 无 ship/remove 权限时按钮隐藏
236
+
237
+---
238
+
239
+## 13. 修订记录
240
+
241
+| 版本 | 说明 |
242
+|------|------|
243
+| **v1.0** | 首版;对齐功能需求 v1.0、后端技术方案 v1.0.2;列表/详情/三种履约弹窗/删除 |
244
+
245
+---
246
+
247
+*文档版本:v1.0 · 关联《全部订单功能需求.md》v1.0、《全部订单技术方案.md》v1.0.2*

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

@@ -0,0 +1,60 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 查询当前店铺订单列表
5
+export function listSellerOrder(query) {
6
+  return request({
7
+    url: '/agri/seller/order/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 查询订单详情
15
+export function getSellerOrder(orderId) {
16
+  return request({
17
+    url: '/agri/seller/order/' + orderId,
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 整单发货
24
+export function shipSellerOrder(orderId, data) {
25
+  return request({
26
+    url: '/agri/seller/order/' + orderId + '/ship',
27
+    method: 'post',
28
+    data: data,
29
+    headers: sellerShopHeaders()
30
+  })
31
+}
32
+
33
+// 更新物流
34
+export function logisticsSellerOrder(orderId, data) {
35
+  return request({
36
+    url: '/agri/seller/order/' + orderId + '/logistics',
37
+    method: 'post',
38
+    data: data,
39
+    headers: sellerShopHeaders()
40
+  })
41
+}
42
+
43
+// 登记送达
44
+export function deliveredSellerOrder(orderId, data) {
45
+  return request({
46
+    url: '/agri/seller/order/' + orderId + '/delivered',
47
+    method: 'post',
48
+    data: data,
49
+    headers: sellerShopHeaders()
50
+  })
51
+}
52
+
53
+// 删除已关闭订单
54
+export function delSellerOrder(orderId) {
55
+  return request({
56
+    url: '/agri/seller/order/' + orderId,
57
+    method: 'delete',
58
+    headers: sellerShopHeaders()
59
+  })
60
+}

+ 30 - 0
ruoyi-ui/src/api/agri/seller/shop.js

@@ -0,0 +1,30 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 查询当前店铺资料
5
+export function getSellerShopProfile() {
6
+  return request({
7
+    url: '/agri/seller/shop',
8
+    method: 'get',
9
+    headers: sellerShopHeaders()
10
+  })
11
+}
12
+
13
+// 修改当前店铺资料
14
+export function updateSellerShopProfile(data) {
15
+  return request({
16
+    url: '/agri/seller/shop',
17
+    method: 'put',
18
+    data: data,
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 查询当前店铺员工概览
24
+export function getSellerShopEmployeeStats() {
25
+  return request({
26
+    url: '/agri/seller/shop/employeeStats',
27
+    method: 'get',
28
+    headers: sellerShopHeaders()
29
+  })
30
+}

+ 262 - 0
ruoyi-ui/src/views/agri/seller/order/detail.vue

@@ -0,0 +1,262 @@
1
+<template>
2
+  <el-drawer
3
+    :title="'订单详情 · ' + (detail.orderNo || '')"
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.orderNo || '—' }}</el-descriptions-item>
14
+        <el-descriptions-item label="订单状态">
15
+          <el-tag size="small" :type="orderStatusTag(detail.orderStatus)">
16
+            {{ detail.orderStatusLabel || orderStatusLabel(detail.orderStatus) }}
17
+          </el-tag>
18
+        </el-descriptions-item>
19
+        <el-descriptions-item label="下单时间">{{ parseTime(detail.createTime) || '—' }}</el-descriptions-item>
20
+        <el-descriptions-item label="会员名称">{{ detail.memberNickName || '—' }}</el-descriptions-item>
21
+        <el-descriptions-item label="实付金额">{{ formatAmount(detail.payAmount) }}</el-descriptions-item>
22
+        <el-descriptions-item label="运费">{{ formatAmount(detail.freightAmount) }}</el-descriptions-item>
23
+        <el-descriptions-item label="配送方式">{{ detail.deliveryTypeLabel || '—' }}</el-descriptions-item>
24
+        <el-descriptions-item label="支付状态">{{ payStatusLabel(detail.payStatus) }}</el-descriptions-item>
25
+        <el-descriptions-item label="支付时间">{{ parseTime(detail.payTime) || '—' }}</el-descriptions-item>
26
+        <el-descriptions-item label="发货时间">{{ parseTime(detail.shipTime) || '—' }}</el-descriptions-item>
27
+        <el-descriptions-item label="完成时间">{{ parseTime(detail.finishTime) || '—' }}</el-descriptions-item>
28
+      </el-descriptions>
29
+
30
+      <h4 class="section-header">商品明细</h4>
31
+      <el-table v-if="detail.items && detail.items.length" border size="small" :data="detail.items" class="mb16">
32
+        <el-table-column label="商品" min-width="220">
33
+          <template slot-scope="scope">
34
+            <div class="goods-cell">
35
+              <image-preview v-if="scope.row.goodsImage" :src="scope.row.goodsImage" :width="50" :height="50" />
36
+              <div>
37
+                <div>{{ scope.row.goodsName || '—' }}</div>
38
+                <div class="sub-text">{{ scope.row.goodsSpec || '默认' }}</div>
39
+              </div>
40
+            </div>
41
+          </template>
42
+        </el-table-column>
43
+        <el-table-column label="单价" prop="unitPrice" width="90" align="center" />
44
+        <el-table-column label="数量" prop="quantity" width="70" align="center" />
45
+        <el-table-column label="小计" prop="lineAmount" width="90" align="center" />
46
+      </el-table>
47
+      <div v-else class="empty-tip mb16">暂无商品明细</div>
48
+
49
+      <h4 class="section-header">收货信息</h4>
50
+      <el-descriptions :column="2" border size="small" class="mb16">
51
+        <el-descriptions-item label="收货人">{{ detail.consigneeName || '—' }}</el-descriptions-item>
52
+        <el-descriptions-item label="联系电话">{{ detail.consigneeMobile || '—' }}</el-descriptions-item>
53
+        <el-descriptions-item label="收货地址" :span="2">{{ detail.consigneeAddress || '—' }}</el-descriptions-item>
54
+      </el-descriptions>
55
+
56
+      <h4 v-if="hasLogisticsInfo" class="section-header">物流信息</h4>
57
+      <el-descriptions v-if="hasLogisticsInfo" :column="2" border size="small" class="mb16">
58
+        <template v-if="detail.deliveryType === '1'">
59
+          <el-descriptions-item label="物流公司">{{ detail.logisticsCompany || '—' }}</el-descriptions-item>
60
+          <el-descriptions-item label="快递单号">{{ detail.trackingNo || '—' }}</el-descriptions-item>
61
+        </template>
62
+        <template v-if="detail.deliveryType === '2'">
63
+          <el-descriptions-item label="车辆号码">{{ detail.vehicleNo || '—' }}</el-descriptions-item>
64
+          <el-descriptions-item label="配送员">{{ detail.courierName || '—' }}</el-descriptions-item>
65
+          <el-descriptions-item label="配送员手机">{{ detail.courierMobile || '—' }}</el-descriptions-item>
66
+        </template>
67
+        <el-descriptions-item v-if="detail.shipRemark" label="发货备注" :span="2">{{ detail.shipRemark }}</el-descriptions-item>
68
+      </el-descriptions>
69
+
70
+      <h4 v-if="detail.closeType || detail.closeReason" class="section-header">关闭信息</h4>
71
+      <el-descriptions v-if="detail.closeType || detail.closeReason" :column="2" border size="small" class="mb16">
72
+        <el-descriptions-item label="关闭类型">{{ detail.closeTypeLabel || '—' }}</el-descriptions-item>
73
+        <el-descriptions-item label="关闭原因" :span="2">{{ detail.closeReason || '—' }}</el-descriptions-item>
74
+      </el-descriptions>
75
+
76
+      <h4 v-if="traceList.length" class="section-header">物流节点</h4>
77
+      <el-timeline v-if="traceList.length" class="mb16">
78
+        <el-timeline-item
79
+          v-for="(item, index) in traceList"
80
+          :key="index"
81
+          :timestamp="parseTime(item.traceTime)"
82
+          placement="top"
83
+        >
84
+          <div class="trace-title">{{ item.traceTypeLabel || traceTypeLabel(item.traceType) }}</div>
85
+          <div class="trace-content">{{ item.content || '—' }}</div>
86
+          <div v-if="item.createBy" class="trace-meta">操作人:{{ item.createBy }}</div>
87
+        </el-timeline-item>
88
+      </el-timeline>
89
+
90
+      <div class="action-bar">
91
+        <el-button
92
+          v-if="canShip(detail)"
93
+          type="primary"
94
+          @click="$emit('ship', detail)"
95
+          v-hasPermi="['agri:seller:order:ship']"
96
+        >去发货</el-button>
97
+        <el-button
98
+          v-if="canLogistics(detail)"
99
+          type="primary"
100
+          plain
101
+          @click="$emit('logistics', detail)"
102
+          v-hasPermi="['agri:seller:order:ship']"
103
+        >更新物流</el-button>
104
+        <el-button
105
+          v-if="canDelivered(detail)"
106
+          type="success"
107
+          plain
108
+          @click="$emit('delivered', detail)"
109
+          v-hasPermi="['agri:seller:order:ship']"
110
+        >商品到货</el-button>
111
+        <el-button
112
+          v-if="canDelete(detail)"
113
+          type="danger"
114
+          plain
115
+          @click="$emit('delete', detail)"
116
+          v-hasPermi="['agri:seller:order:remove']"
117
+        >删除订单</el-button>
118
+      </div>
119
+    </div>
120
+  </el-drawer>
121
+</template>
122
+
123
+<script>
124
+import { getSellerOrder } from "@/api/agri/seller/order"
125
+
126
+export default {
127
+  name: "SellerOrderDetail",
128
+  props: {
129
+    visible: { type: Boolean, default: false },
130
+    orderId: { type: [Number, String], default: null }
131
+  },
132
+  data() {
133
+    return {
134
+      localVisible: false,
135
+      loading: false,
136
+      detail: {}
137
+    }
138
+  },
139
+  computed: {
140
+    traceList() {
141
+      return this.detail.traceList || []
142
+    },
143
+    hasLogisticsInfo() {
144
+      const d = this.detail
145
+      return d.deliveryType === '1' && (d.logisticsCompany || d.trackingNo)
146
+        || d.deliveryType === '2' && (d.vehicleNo || d.courierName || d.courierMobile)
147
+        || d.shipRemark
148
+    }
149
+  },
150
+  watch: {
151
+    visible(val) {
152
+      this.localVisible = val
153
+      if (val && this.orderId) {
154
+        this.loadDetail()
155
+      }
156
+    },
157
+    localVisible(val) {
158
+      this.$emit("update:visible", val)
159
+    },
160
+    orderId(val) {
161
+      if (this.localVisible && val) {
162
+        this.loadDetail()
163
+      }
164
+    }
165
+  },
166
+  methods: {
167
+    loadDetail() {
168
+      this.loading = true
169
+      getSellerOrder(this.orderId).then(response => {
170
+        this.detail = response.data || {}
171
+        this.loading = false
172
+      }).catch(() => {
173
+        this.loading = false
174
+      })
175
+    },
176
+    reload() {
177
+      if (this.orderId) {
178
+        this.loadDetail()
179
+      }
180
+    },
181
+    formatAmount(val) {
182
+      return val != null ? val : '—'
183
+    },
184
+    payStatusLabel(status) {
185
+      if (status === '0') return '未支付'
186
+      if (status === '1') return '已支付'
187
+      return '—'
188
+    },
189
+    orderStatusLabel(status) {
190
+      const map = { "0": "待支付", "1": "待发货", "2": "已发货", "3": "已完成", "4": "已关闭", "5": "已删除" }
191
+      return map[status] || "—"
192
+    },
193
+    orderStatusTag(status) {
194
+      const map = { "0": "warning", "1": "primary", "2": "", "3": "success", "4": "info", "5": "info" }
195
+      return map[status] || "info"
196
+    },
197
+    traceTypeLabel(type) {
198
+      const map = { "1": "已发货", "2": "运输更新", "3": "已送达", "4": "确认收货" }
199
+      return map[type] || "—"
200
+    },
201
+    canShip(row) {
202
+      return row && row.orderStatus === '1'
203
+    },
204
+    canLogistics(row) {
205
+      return row && row.orderStatus === '2'
206
+    },
207
+    canDelivered(row) {
208
+      return row && row.orderStatus === '2'
209
+    },
210
+    canDelete(row) {
211
+      return row && row.orderStatus === '4'
212
+    }
213
+  }
214
+}
215
+</script>
216
+
217
+<style scoped lang="scss">
218
+.drawer-content {
219
+  padding: 0 20px 20px;
220
+}
221
+.section-header {
222
+  margin: 16px 0 10px;
223
+  font-size: 15px;
224
+  color: #303133;
225
+  border-left: 3px solid #409EFF;
226
+  padding-left: 8px;
227
+}
228
+.mb16 {
229
+  margin-bottom: 16px;
230
+}
231
+.goods-cell {
232
+  display: flex;
233
+  align-items: center;
234
+  gap: 8px;
235
+}
236
+.sub-text {
237
+  color: #909399;
238
+  font-size: 12px;
239
+  margin-top: 2px;
240
+}
241
+.empty-tip {
242
+  color: #909399;
243
+  font-size: 13px;
244
+}
245
+.trace-title {
246
+  font-weight: 600;
247
+  margin-bottom: 4px;
248
+}
249
+.trace-content {
250
+  color: #606266;
251
+}
252
+.trace-meta {
253
+  color: #909399;
254
+  font-size: 12px;
255
+  margin-top: 4px;
256
+}
257
+.action-bar {
258
+  margin-top: 20px;
259
+  padding-top: 16px;
260
+  border-top: 1px solid #ebeef5;
261
+}
262
+</style>

+ 695 - 0
ruoyi-ui/src/views/agri/seller/order/index.vue

@@ -0,0 +1,695 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 检索区 -->
4
+    <el-card shadow="never" class="search-card">
5
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
6
+        <el-form-item label="订单编号" prop="orderNo">
7
+          <el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
8
+        </el-form-item>
9
+        <el-form-item label="商品名称" prop="goodsName">
10
+          <el-input v-model="queryParams.goodsName" placeholder="商品名称" clearable style="width: 140px" @keyup.enter.native="handleQuery" />
11
+        </el-form-item>
12
+        <el-form-item label="发货状态" prop="shipStatus">
13
+          <el-select v-model="queryParams.shipStatus" placeholder="全部" clearable style="width: 120px">
14
+            <el-option label="待支付" value="0" />
15
+            <el-option label="待发货" value="1" />
16
+            <el-option label="已发货" value="2" />
17
+          </el-select>
18
+        </el-form-item>
19
+        <el-form-item label="收货人" prop="consigneeName">
20
+          <el-input v-model="queryParams.consigneeName" placeholder="收货人" clearable style="width: 110px" />
21
+        </el-form-item>
22
+        <el-form-item label="收货手机" prop="consigneeMobile">
23
+          <el-input v-model="queryParams.consigneeMobile" placeholder="手机号" clearable style="width: 130px" />
24
+        </el-form-item>
25
+        <el-form-item label="收货地址" prop="consigneeAddress">
26
+          <el-input v-model="queryParams.consigneeAddress" placeholder="地址关键词" clearable style="width: 140px" />
27
+        </el-form-item>
28
+        <el-form-item label="配送方式" prop="deliveryType">
29
+          <el-select v-model="queryParams.deliveryType" placeholder="全部" clearable style="width: 120px">
30
+            <el-option label="物流配送" value="1" />
31
+            <el-option label="商家配送" value="2" />
32
+          </el-select>
33
+        </el-form-item>
34
+        <el-form-item label="下单时间">
35
+          <el-date-picker
36
+            v-model="dateRange"
37
+            type="daterange"
38
+            value-format="yyyy-MM-dd"
39
+            range-separator="至"
40
+            start-placeholder="开始日期"
41
+            end-placeholder="结束日期"
42
+            style="width: 240px"
43
+          />
44
+        </el-form-item>
45
+        <el-form-item>
46
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
47
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
48
+        </el-form-item>
49
+      </el-form>
50
+    </el-card>
51
+
52
+    <br/>
53
+
54
+    <!-- 列表区 -->
55
+    <el-card shadow="never" class="table-card">
56
+      <el-tabs v-model="statusTab" @tab-click="handleTabClick">
57
+        <el-tab-pane label="全部" name="all" />
58
+        <el-tab-pane label="待发货" name="1" />
59
+        <el-tab-pane label="已发货" name="2" />
60
+        <el-tab-pane label="已关闭" name="4" />
61
+        <el-tab-pane label="已完成" name="3" />
62
+        <el-tab-pane label="已删除" name="5" />
63
+      </el-tabs>
64
+
65
+      <el-row :gutter="10" class="mb8">
66
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
67
+      </el-row>
68
+
69
+      <el-table border v-loading="loading" :data="orderList">
70
+        <el-table-column label="订单信息" align="left" min-width="280">
71
+          <template slot-scope="scope">
72
+            <div class="order-info">
73
+              <div class="order-no">订单号:{{ scope.row.orderNo || '—' }}</div>
74
+              <div class="order-time">下单:{{ parseTime(scope.row.createTime) || '—' }}</div>
75
+              <div v-if="scope.row.items && scope.row.items.length" class="goods-info">
76
+                <image-preview v-if="scope.row.items[0].goodsImage" :src="scope.row.items[0].goodsImage" :width="44" :height="44" />
77
+                <div class="goods-text">
78
+                  <div>{{ scope.row.items[0].goodsName || '—' }}</div>
79
+                  <div class="sub-text">
80
+                    {{ scope.row.items[0].goodsSpec || '默认' }}
81
+                    × {{ scope.row.items[0].quantity }}
82
+                    · ¥{{ scope.row.items[0].unitPrice != null ? scope.row.items[0].unitPrice : '—' }}
83
+                  </div>
84
+                  <div v-if="scope.row.itemCount > 1" class="more-tip">共 {{ scope.row.itemCount }} 件商品</div>
85
+                </div>
86
+              </div>
87
+            </div>
88
+          </template>
89
+        </el-table-column>
90
+        <el-table-column label="订单金额" align="center" width="110">
91
+          <template slot-scope="scope">
92
+            <div>¥{{ scope.row.payAmount != null ? scope.row.payAmount : '—' }}</div>
93
+            <div v-if="Number(scope.row.freightAmount) > 0" class="sub-text">含运费 ¥{{ scope.row.freightAmount }}</div>
94
+          </template>
95
+        </el-table-column>
96
+        <el-table-column label="会员名称" align="center" prop="memberNickName" width="100" :show-overflow-tooltip="true">
97
+          <template slot-scope="scope">
98
+            <span>{{ scope.row.memberNickName || '—' }}</span>
99
+          </template>
100
+        </el-table-column>
101
+        <el-table-column label="收货人信息" align="left" min-width="160">
102
+          <template slot-scope="scope">
103
+            <div>{{ scope.row.consigneeName || '—' }}</div>
104
+            <div class="sub-text">{{ scope.row.consigneeMobile || '—' }}</div>
105
+            <div class="sub-text addr-text">{{ scope.row.consigneeAddress || '—' }}</div>
106
+          </template>
107
+        </el-table-column>
108
+        <el-table-column label="配送方式" align="center" width="100">
109
+          <template slot-scope="scope">
110
+            <span>{{ scope.row.deliveryTypeLabel || '—' }}</span>
111
+          </template>
112
+        </el-table-column>
113
+        <el-table-column label="订单状态" align="center" width="90">
114
+          <template slot-scope="scope">
115
+            <el-tag size="small" :type="orderStatusTag(scope.row.orderStatus)">
116
+              {{ scope.row.orderStatusLabel || orderStatusLabel(scope.row.orderStatus) }}
117
+            </el-tag>
118
+          </template>
119
+        </el-table-column>
120
+        <el-table-column label="操作" align="center" width="220" fixed="right">
121
+          <template slot-scope="scope">
122
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['agri:seller:order:query']">详情</el-button>
123
+            <el-button
124
+              v-if="canShip(scope.row)"
125
+              size="mini"
126
+              type="text"
127
+              icon="el-icon-truck"
128
+              @click="handleShip(scope.row)"
129
+              v-hasPermi="['agri:seller:order:ship']"
130
+            >去发货</el-button>
131
+            <el-button
132
+              v-if="canLogistics(scope.row)"
133
+              size="mini"
134
+              type="text"
135
+              icon="el-icon-position"
136
+              @click="handleLogistics(scope.row)"
137
+              v-hasPermi="['agri:seller:order:ship']"
138
+            >更新物流</el-button>
139
+            <el-button
140
+              v-if="canDelivered(scope.row)"
141
+              size="mini"
142
+              type="text"
143
+              icon="el-icon-circle-check"
144
+              @click="handleDelivered(scope.row)"
145
+              v-hasPermi="['agri:seller:order:ship']"
146
+            >商品到货</el-button>
147
+            <el-button
148
+              v-if="canDelete(scope.row)"
149
+              size="mini"
150
+              type="text"
151
+              icon="el-icon-delete"
152
+              @click="handleDelete(scope.row)"
153
+              v-hasPermi="['agri:seller:order:remove']"
154
+            >删除</el-button>
155
+          </template>
156
+        </el-table-column>
157
+      </el-table>
158
+
159
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
160
+    </el-card>
161
+
162
+    <!-- 详情抽屉 -->
163
+    <seller-order-detail
164
+      ref="orderDetail"
165
+      :visible.sync="detailOpen"
166
+      :order-id="currentOrderId"
167
+      @ship="handleShipFromDetail"
168
+      @logistics="handleLogisticsFromDetail"
169
+      @delivered="handleDeliveredFromDetail"
170
+      @delete="handleDeleteFromDetail"
171
+    />
172
+
173
+    <!-- 发货弹窗 -->
174
+    <el-dialog title="去发货" :visible.sync="shipOpen" width="680px" append-to-body @close="cancelShip">
175
+      <el-form ref="shipFormRef" :model="shipForm" :rules="shipRules" label-width="110px">
176
+        <el-form-item label="发货时间" prop="shipTime">
177
+          <el-date-picker
178
+            v-model="shipForm.shipTime"
179
+            type="datetime"
180
+            value-format="yyyy-MM-dd HH:mm:ss"
181
+            placeholder="选择发货时间"
182
+            style="width: 100%"
183
+          />
184
+        </el-form-item>
185
+        <el-form-item label="配送方式" prop="deliveryType">
186
+          <el-radio-group v-model="shipForm.deliveryType">
187
+            <el-radio label="1">物流配送</el-radio>
188
+            <el-radio label="2">商家配送</el-radio>
189
+          </el-radio-group>
190
+        </el-form-item>
191
+        <template v-if="shipForm.deliveryType === '1'">
192
+          <el-form-item label="物流公司" prop="logisticsCompany">
193
+            <el-input v-model="shipForm.logisticsCompany" placeholder="请输入物流公司" maxlength="64" />
194
+          </el-form-item>
195
+          <el-form-item label="快递单号" prop="trackingNo">
196
+            <el-input v-model="shipForm.trackingNo" placeholder="请输入快递单号" maxlength="64" />
197
+          </el-form-item>
198
+        </template>
199
+        <template v-if="shipForm.deliveryType === '2'">
200
+          <el-form-item label="车辆号码" prop="vehicleNo">
201
+            <el-input v-model="shipForm.vehicleNo" placeholder="请输入车辆号码" maxlength="32" />
202
+          </el-form-item>
203
+          <el-form-item label="配送员" prop="courierName">
204
+            <el-input v-model="shipForm.courierName" placeholder="请输入配送员姓名" maxlength="32" />
205
+          </el-form-item>
206
+          <el-form-item label="配送员手机" prop="courierMobile">
207
+            <el-input v-model="shipForm.courierMobile" placeholder="请输入配送员手机号" maxlength="20" />
208
+          </el-form-item>
209
+        </template>
210
+        <el-form-item label="发货备注" prop="shipRemark">
211
+          <el-input v-model="shipForm.shipRemark" type="textarea" :rows="2" placeholder="选填" maxlength="200" show-word-limit />
212
+        </el-form-item>
213
+        <el-form-item label="发货商品">
214
+          <el-table border size="small" :data="shipItems">
215
+            <el-table-column label="商品" min-width="180">
216
+              <template slot-scope="scope">
217
+                <div class="goods-cell">
218
+                  <image-preview v-if="scope.row.goodsImage" :src="scope.row.goodsImage" :width="40" :height="40" />
219
+                  <span>{{ scope.row.goodsName || '—' }}</span>
220
+                </div>
221
+              </template>
222
+            </el-table-column>
223
+            <el-table-column label="规格" prop="goodsSpec" width="90">
224
+              <template slot-scope="scope">
225
+                <span>{{ scope.row.goodsSpec || '默认' }}</span>
226
+              </template>
227
+            </el-table-column>
228
+            <el-table-column label="数量" prop="quantity" width="70" align="center" />
229
+          </el-table>
230
+          <div class="form-tip">须整单一次发完,不可拆单</div>
231
+        </el-form-item>
232
+      </el-form>
233
+      <div slot="footer" class="dialog-footer">
234
+        <el-button type="primary" @click="submitShip">确 定</el-button>
235
+        <el-button @click="cancelShip">取 消</el-button>
236
+      </div>
237
+    </el-dialog>
238
+
239
+    <!-- 更新物流弹窗 -->
240
+    <el-dialog title="更新物流信息" :visible.sync="logisticsOpen" width="520px" append-to-body @close="cancelLogistics">
241
+      <el-form ref="logisticsFormRef" :model="logisticsForm" :rules="traceRules" label-width="110px">
242
+        <el-form-item label="运输更新时间" prop="traceTime">
243
+          <el-date-picker
244
+            v-model="logisticsForm.traceTime"
245
+            type="datetime"
246
+            value-format="yyyy-MM-dd HH:mm:ss"
247
+            placeholder="选择时间"
248
+            style="width: 100%"
249
+          />
250
+        </el-form-item>
251
+        <el-form-item label="运输状态描述" prop="content">
252
+          <el-input v-model="logisticsForm.content" type="textarea" :rows="3" placeholder="如:到达 XX 中转站" maxlength="500" show-word-limit />
253
+        </el-form-item>
254
+      </el-form>
255
+      <div slot="footer" class="dialog-footer">
256
+        <el-button type="primary" @click="submitLogistics">确 定</el-button>
257
+        <el-button @click="cancelLogistics">取 消</el-button>
258
+      </div>
259
+    </el-dialog>
260
+
261
+    <!-- 商品到货弹窗 -->
262
+    <el-dialog title="商品到货登记" :visible.sync="deliveredOpen" width="520px" append-to-body @close="cancelDelivered">
263
+      <el-form ref="deliveredFormRef" :model="deliveredForm" :rules="deliveredRules" label-width="100px">
264
+        <el-form-item label="送达时间" prop="traceTime">
265
+          <el-date-picker
266
+            v-model="deliveredForm.traceTime"
267
+            type="datetime"
268
+            value-format="yyyy-MM-dd HH:mm:ss"
269
+            placeholder="选择送达时间"
270
+            style="width: 100%"
271
+          />
272
+        </el-form-item>
273
+        <el-form-item label="送达说明" prop="content">
274
+          <el-input v-model="deliveredForm.content" type="textarea" :rows="3" placeholder="选填" maxlength="500" show-word-limit />
275
+        </el-form-item>
276
+      </el-form>
277
+      <div class="form-tip">登记送达不替代买家确认收货,订单仍为「已发货」直至买家确认</div>
278
+      <div slot="footer" class="dialog-footer">
279
+        <el-button type="primary" @click="submitDelivered">确 定</el-button>
280
+        <el-button @click="cancelDelivered">取 消</el-button>
281
+      </div>
282
+    </el-dialog>
283
+  </div>
284
+</template>
285
+
286
+<script>
287
+import {
288
+  listSellerOrder,
289
+  getSellerOrder,
290
+  shipSellerOrder,
291
+  logisticsSellerOrder,
292
+  deliveredSellerOrder,
293
+  delSellerOrder
294
+} from "@/api/agri/seller/order"
295
+import { getSellerContext } from "@/api/agri/seller/context"
296
+import { setSellerShopContext } from "@/utils/sellerShop"
297
+import SellerOrderDetail from "./detail"
298
+
299
+export default {
300
+  name: "AgriSellerOrder",
301
+  components: { SellerOrderDetail },
302
+  data() {
303
+    const validateLogisticsCompany = (rule, value, callback) => {
304
+      if (this.shipForm.deliveryType !== '1') {
305
+        callback()
306
+        return
307
+      }
308
+      if (!value || !String(value).trim()) {
309
+        callback(new Error("请输入物流公司"))
310
+        return
311
+      }
312
+      callback()
313
+    }
314
+    const validateTrackingNo = (rule, value, callback) => {
315
+      if (this.shipForm.deliveryType !== '1') {
316
+        callback()
317
+        return
318
+      }
319
+      if (!value || !String(value).trim()) {
320
+        callback(new Error("请输入快递单号"))
321
+        return
322
+      }
323
+      callback()
324
+    }
325
+    const validateVehicleNo = (rule, value, callback) => {
326
+      if (this.shipForm.deliveryType !== '2') {
327
+        callback()
328
+        return
329
+      }
330
+      if (!value || !String(value).trim()) {
331
+        callback(new Error("请输入车辆号码"))
332
+        return
333
+      }
334
+      callback()
335
+    }
336
+    const validateCourierName = (rule, value, callback) => {
337
+      if (this.shipForm.deliveryType !== '2') {
338
+        callback()
339
+        return
340
+      }
341
+      if (!value || !String(value).trim()) {
342
+        callback(new Error("请输入配送员姓名"))
343
+        return
344
+      }
345
+      callback()
346
+    }
347
+    const validateCourierMobile = (rule, value, callback) => {
348
+      if (this.shipForm.deliveryType !== '2') {
349
+        callback()
350
+        return
351
+      }
352
+      if (!value || !String(value).trim()) {
353
+        callback(new Error("请输入配送员手机号"))
354
+        return
355
+      }
356
+      if (!/^1[3-9]\d{9}$/.test(String(value).trim())) {
357
+        callback(new Error("请输入正确的手机号"))
358
+        return
359
+      }
360
+      callback()
361
+    }
362
+    return {
363
+      loading: false,
364
+      showSearch: true,
365
+      total: 0,
366
+      orderList: [],
367
+      statusTab: "all",
368
+      dateRange: [],
369
+      detailOpen: false,
370
+      currentOrderId: null,
371
+      shipOpen: false,
372
+      shipItems: [],
373
+      shipForm: {},
374
+      logisticsOpen: false,
375
+      logisticsForm: {},
376
+      deliveredOpen: false,
377
+      deliveredForm: {},
378
+      queryParams: {
379
+        pageNum: 1,
380
+        pageSize: 10,
381
+        orderStatus: undefined,
382
+        orderNo: undefined,
383
+        goodsName: undefined,
384
+        shipStatus: undefined,
385
+        consigneeName: undefined,
386
+        consigneeMobile: undefined,
387
+        consigneeAddress: undefined,
388
+        deliveryType: undefined
389
+      },
390
+      shipRules: {
391
+        shipTime: [{ required: true, message: "请选择发货时间", trigger: "change" }],
392
+        deliveryType: [{ required: true, message: "请选择配送方式", trigger: "change" }],
393
+        logisticsCompany: [{ validator: validateLogisticsCompany, trigger: "blur" }],
394
+        trackingNo: [{ validator: validateTrackingNo, trigger: "blur" }],
395
+        vehicleNo: [{ validator: validateVehicleNo, trigger: "blur" }],
396
+        courierName: [{ validator: validateCourierName, trigger: "blur" }],
397
+        courierMobile: [{ validator: validateCourierMobile, trigger: "blur" }]
398
+      },
399
+      traceRules: {
400
+        traceTime: [{ required: true, message: "请选择运输更新时间", trigger: "change" }],
401
+        content: [{ required: true, message: "请填写运输状态描述", trigger: "blur" }]
402
+      },
403
+      deliveredRules: {
404
+        traceTime: [{ required: true, message: "请选择送达时间", trigger: "change" }]
405
+      }
406
+    }
407
+  },
408
+  created() {
409
+    this.applyRouteTab()
410
+    this.initPage()
411
+  },
412
+  methods: {
413
+    applyRouteTab() {
414
+      const tab = this.$route.query.orderStatus
415
+      if (tab === undefined || tab === null || tab === '') {
416
+        return
417
+      }
418
+      const allowed = ['all', '1', '2', '3', '4', '5']
419
+      if (allowed.includes(String(tab))) {
420
+        this.statusTab = String(tab) === 'all' ? 'all' : String(tab)
421
+        this.syncTabToQuery()
422
+      }
423
+    },
424
+    syncTabToQuery() {
425
+      this.queryParams.orderStatus = this.statusTab === 'all' ? undefined : this.statusTab
426
+    },
427
+    initPage() {
428
+      this.syncTabToQuery()
429
+      this.loadShopContext().then(() => {
430
+        this.getList()
431
+      }).catch(() => {
432
+        this.getList()
433
+      })
434
+    },
435
+    loadShopContext() {
436
+      return getSellerContext().then(response => {
437
+        const data = response.data || {}
438
+        if (data.shopId != null) {
439
+          setSellerShopContext(data.shopId, data.shopName)
440
+        }
441
+      })
442
+    },
443
+    getList() {
444
+      this.loading = true
445
+      listSellerOrder(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
446
+        this.orderList = response.rows || []
447
+        this.total = response.total || 0
448
+        this.loading = false
449
+      }).catch(() => {
450
+        this.loading = false
451
+      })
452
+    },
453
+    handleTabClick() {
454
+      this.syncTabToQuery()
455
+      this.queryParams.pageNum = 1
456
+      this.getList()
457
+    },
458
+    handleQuery() {
459
+      this.queryParams.pageNum = 1
460
+      this.getList()
461
+    },
462
+    resetQuery() {
463
+      this.dateRange = []
464
+      this.resetForm("queryForm")
465
+      this.syncTabToQuery()
466
+      this.queryParams.pageNum = 1
467
+      this.getList()
468
+    },
469
+    handleDetail(row) {
470
+      this.currentOrderId = row.orderId
471
+      this.detailOpen = true
472
+    },
473
+    orderStatusLabel(status) {
474
+      const map = { "0": "待支付", "1": "待发货", "2": "已发货", "3": "已完成", "4": "已关闭", "5": "已删除" }
475
+      return map[status] || "—"
476
+    },
477
+    orderStatusTag(status) {
478
+      const map = { "0": "warning", "1": "primary", "2": "", "3": "success", "4": "info", "5": "info" }
479
+      return map[status] || "info"
480
+    },
481
+    canShip(row) {
482
+      return row.orderStatus === '1'
483
+    },
484
+    canLogistics(row) {
485
+      return row.orderStatus === '2'
486
+    },
487
+    canDelivered(row) {
488
+      return row.orderStatus === '2'
489
+    },
490
+    canDelete(row) {
491
+      return row.orderStatus === '4'
492
+    },
493
+    nowDateTime() {
494
+      return this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
495
+    },
496
+    prepareShipDialog(orderId) {
497
+      this.currentOrderId = orderId
498
+      return getSellerOrder(orderId).then(response => {
499
+        const data = response.data || {}
500
+        this.shipItems = data.items || []
501
+        this.resetShipForm()
502
+        this.shipOpen = true
503
+      })
504
+    },
505
+    handleShip(row) {
506
+      this.prepareShipDialog(row.orderId)
507
+    },
508
+    handleShipFromDetail(detail) {
509
+      this.prepareShipDialog(detail.orderId)
510
+    },
511
+    resetShipForm() {
512
+      this.shipForm = {
513
+        shipTime: this.nowDateTime(),
514
+        deliveryType: "1",
515
+        logisticsCompany: undefined,
516
+        trackingNo: undefined,
517
+        vehicleNo: undefined,
518
+        courierName: undefined,
519
+        courierMobile: undefined,
520
+        shipRemark: undefined
521
+      }
522
+      this.resetForm("shipFormRef")
523
+    },
524
+    cancelShip() {
525
+      this.shipOpen = false
526
+      this.resetShipForm()
527
+    },
528
+    submitShip() {
529
+      this.$refs["shipFormRef"].validate(valid => {
530
+        if (!valid) {
531
+          return
532
+        }
533
+        const payload = {
534
+          shipTime: this.shipForm.shipTime,
535
+          deliveryType: this.shipForm.deliveryType,
536
+          shipRemark: this.shipForm.shipRemark || undefined
537
+        }
538
+        if (this.shipForm.deliveryType === '1') {
539
+          payload.logisticsCompany = this.shipForm.logisticsCompany
540
+          payload.trackingNo = this.shipForm.trackingNo
541
+        } else {
542
+          payload.vehicleNo = this.shipForm.vehicleNo
543
+          payload.courierName = this.shipForm.courierName
544
+          payload.courierMobile = this.shipForm.courierMobile
545
+        }
546
+        shipSellerOrder(this.currentOrderId, payload).then(() => {
547
+          this.$modal.msgSuccess("发货成功")
548
+          this.shipOpen = false
549
+          this.getList()
550
+          if (this.detailOpen && this.$refs.orderDetail) {
551
+            this.$refs.orderDetail.reload()
552
+          }
553
+        })
554
+      })
555
+    },
556
+    openTraceDialog(type, orderId) {
557
+      this.currentOrderId = orderId
558
+      if (type === 'logistics') {
559
+        this.logisticsForm = {
560
+          traceTime: this.nowDateTime(),
561
+          content: undefined
562
+        }
563
+        this.resetForm("logisticsFormRef")
564
+        this.logisticsOpen = true
565
+      } else {
566
+        this.deliveredForm = {
567
+          traceTime: this.nowDateTime(),
568
+          content: undefined
569
+        }
570
+        this.resetForm("deliveredFormRef")
571
+        this.deliveredOpen = true
572
+      }
573
+    },
574
+    handleLogistics(row) {
575
+      this.openTraceDialog('logistics', row.orderId)
576
+    },
577
+    handleLogisticsFromDetail(detail) {
578
+      this.openTraceDialog('logistics', detail.orderId)
579
+    },
580
+    cancelLogistics() {
581
+      this.logisticsOpen = false
582
+      this.logisticsForm = {}
583
+      this.resetForm("logisticsFormRef")
584
+    },
585
+    submitLogistics() {
586
+      this.$refs["logisticsFormRef"].validate(valid => {
587
+        if (!valid) {
588
+          return
589
+        }
590
+        logisticsSellerOrder(this.currentOrderId, this.logisticsForm).then(() => {
591
+          this.$modal.msgSuccess("物流信息已更新")
592
+          this.logisticsOpen = false
593
+          this.getList()
594
+          if (this.detailOpen && this.$refs.orderDetail) {
595
+            this.$refs.orderDetail.reload()
596
+          }
597
+        })
598
+      })
599
+    },
600
+    handleDelivered(row) {
601
+      this.openTraceDialog('delivered', row.orderId)
602
+    },
603
+    handleDeliveredFromDetail(detail) {
604
+      this.openTraceDialog('delivered', detail.orderId)
605
+    },
606
+    cancelDelivered() {
607
+      this.deliveredOpen = false
608
+      this.deliveredForm = {}
609
+      this.resetForm("deliveredFormRef")
610
+    },
611
+    submitDelivered() {
612
+      this.$refs["deliveredFormRef"].validate(valid => {
613
+        if (!valid) {
614
+          return
615
+        }
616
+        deliveredSellerOrder(this.currentOrderId, this.deliveredForm).then(() => {
617
+          this.$modal.msgSuccess("送达登记成功")
618
+          this.deliveredOpen = false
619
+          this.getList()
620
+          if (this.detailOpen && this.$refs.orderDetail) {
621
+            this.$refs.orderDetail.reload()
622
+          }
623
+        })
624
+      })
625
+    },
626
+    handleDelete(row) {
627
+      this.confirmDelete(row.orderId)
628
+    },
629
+    handleDeleteFromDetail(detail) {
630
+      this.confirmDelete(detail.orderId)
631
+    },
632
+    confirmDelete(orderId) {
633
+      this.$modal.confirm('确认删除该已关闭订单?删除后将移至「已删除」页签,默认列表不再展示。').then(() => {
634
+        return delSellerOrder(orderId)
635
+      }).then(() => {
636
+        this.$modal.msgSuccess("删除成功")
637
+        this.getList()
638
+        if (this.detailOpen && this.currentOrderId === orderId) {
639
+          this.detailOpen = false
640
+        }
641
+      }).catch(() => {})
642
+    }
643
+  }
644
+}
645
+</script>
646
+
647
+<style scoped lang="scss">
648
+.order-info {
649
+  .order-no {
650
+    font-weight: 600;
651
+    margin-bottom: 4px;
652
+  }
653
+  .order-time {
654
+    color: #909399;
655
+    font-size: 12px;
656
+    margin-bottom: 8px;
657
+  }
658
+}
659
+.goods-info {
660
+  display: flex;
661
+  align-items: flex-start;
662
+  gap: 8px;
663
+}
664
+.goods-text {
665
+  flex: 1;
666
+  min-width: 0;
667
+}
668
+.goods-cell {
669
+  display: flex;
670
+  align-items: center;
671
+  gap: 8px;
672
+}
673
+.sub-text {
674
+  color: #909399;
675
+  font-size: 12px;
676
+  line-height: 1.4;
677
+}
678
+.more-tip {
679
+  color: #909399;
680
+  font-size: 12px;
681
+  margin-top: 2px;
682
+}
683
+.addr-text {
684
+  display: -webkit-box;
685
+  -webkit-line-clamp: 2;
686
+  -webkit-box-orient: vertical;
687
+  overflow: hidden;
688
+}
689
+.form-tip {
690
+  color: #909399;
691
+  font-size: 12px;
692
+  margin-top: 8px;
693
+  line-height: 1.5;
694
+}
695
+</style>

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

@@ -0,0 +1,293 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 店铺资料 -->
4
+    <el-card shadow="never" class="profile-card" v-loading="profileLoading">
5
+      <div slot="header" class="card-header">
6
+        <span class="card-title">店铺资料</span>
7
+        <el-button
8
+          type="primary"
9
+          plain
10
+          icon="el-icon-edit"
11
+          size="mini"
12
+          @click="handleEdit"
13
+          v-hasPermi="['agri:seller:shop:edit']"
14
+        >编辑</el-button>
15
+      </div>
16
+      <el-descriptions :column="2" border size="medium">
17
+        <el-descriptions-item label="店铺头像">
18
+          <image-preview v-if="profile.shopAvatar" :src="profile.shopAvatar" :width="64" :height="64" />
19
+          <span v-else>—</span>
20
+        </el-descriptions-item>
21
+        <el-descriptions-item label="店铺名称">{{ displayText(profile.shopName) }}</el-descriptions-item>
22
+        <el-descriptions-item label="创建时间">{{ profile.createTime ? parseTime(profile.createTime) : '—' }}</el-descriptions-item>
23
+        <el-descriptions-item label="店铺状态">
24
+          <el-tag v-if="profile.shopStatus === '0'" type="success" size="small">开业</el-tag>
25
+          <el-tag v-else-if="profile.shopStatus === '1'" type="info" size="small">停业</el-tag>
26
+          <span v-else>—</span>
27
+        </el-descriptions-item>
28
+        <el-descriptions-item label="所属商户">{{ displayText(profile.merchantName) }}</el-descriptions-item>
29
+        <el-descriptions-item label="客服电话">{{ displayText(profile.shopPhone) }}</el-descriptions-item>
30
+        <el-descriptions-item label="负责人姓名">{{ displayText(profile.contactName) }}</el-descriptions-item>
31
+        <el-descriptions-item label="联系电话">{{ displayText(profile.contactPhone) }}</el-descriptions-item>
32
+        <el-descriptions-item label="店铺简介" :span="2">{{ displayText(profile.shopDesc) }}</el-descriptions-item>
33
+      </el-descriptions>
34
+    </el-card>
35
+
36
+    <br/>
37
+
38
+    <!-- 员工概览 -->
39
+    <el-card shadow="never" class="stats-card" v-loading="statsLoading">
40
+      <div slot="header" class="card-header">
41
+        <span class="card-title">员工概览</span>
42
+        <el-button
43
+          type="primary"
44
+          plain
45
+          icon="el-icon-user"
46
+          size="mini"
47
+          @click="goEmployeeManage"
48
+          v-hasPermi="['agri:seller:employee:list']"
49
+        >前往员工管理</el-button>
50
+      </div>
51
+      <el-row :gutter="24" class="stats-row">
52
+        <el-col :xs="12" :sm="6">
53
+          <div class="stat-block">
54
+            <div class="stat-value">{{ employeeStats.totalCount != null ? employeeStats.totalCount : '—' }}</div>
55
+            <div class="stat-label">已有员工数</div>
56
+            <div class="stat-tip">含正常与停用,不含经营账号</div>
57
+          </div>
58
+        </el-col>
59
+        <el-col :xs="12" :sm="6">
60
+          <div class="stat-block">
61
+            <div class="stat-value">{{ employeeStats.disabledCount != null ? employeeStats.disabledCount : '—' }}</div>
62
+            <div class="stat-label">停用员工数</div>
63
+          </div>
64
+        </el-col>
65
+        <el-col :xs="12" :sm="6">
66
+          <div class="stat-block">
67
+            <div class="stat-value">
68
+              <template v-if="employeeStats.usedCount != null && employeeStats.maxCount != null">
69
+                {{ employeeStats.usedCount }} / {{ employeeStats.maxCount }}
70
+              </template>
71
+              <span v-else>—</span>
72
+            </div>
73
+            <div class="stat-label">商户员工配额</div>
74
+            <div class="stat-tip">已用 / 平台上限(各店合计)</div>
75
+          </div>
76
+        </el-col>
77
+        <el-col :xs="12" :sm="6">
78
+          <div class="stat-block stat-block-muted">
79
+            <div class="stat-label">配额说明</div>
80
+            <div class="stat-desc">子管理员上限由平台「店铺设置」配置,本页只读展示;明细维护请前往员工管理。</div>
81
+          </div>
82
+        </el-col>
83
+      </el-row>
84
+    </el-card>
85
+
86
+    <!-- 编辑店铺资料弹窗 -->
87
+    <el-dialog title="编辑店铺资料" :visible.sync="open" width="560px" append-to-body @close="cancel">
88
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
89
+        <el-form-item label="店铺名称" prop="shopName">
90
+          <el-input v-model="form.shopName" placeholder="请输入店铺名称" maxlength="128" show-word-limit />
91
+        </el-form-item>
92
+        <el-form-item label="店铺头像" prop="shopAvatar">
93
+          <image-upload v-model="form.shopAvatar" :limit="1" :file-size="5" :file-type="['png', 'jpg', 'jpeg']" />
94
+        </el-form-item>
95
+        <el-form-item label="店铺简介" prop="shopDesc">
96
+          <el-input v-model="form.shopDesc" type="textarea" :rows="3" placeholder="选填" maxlength="1000" show-word-limit />
97
+        </el-form-item>
98
+        <el-form-item label="客服电话" prop="shopPhone">
99
+          <el-input v-model="form.shopPhone" placeholder="选填,手机或固话" maxlength="20" />
100
+        </el-form-item>
101
+      </el-form>
102
+      <div slot="footer" class="dialog-footer">
103
+        <el-button type="primary" @click="submitForm">保 存</el-button>
104
+        <el-button @click="cancel">取 消</el-button>
105
+      </div>
106
+    </el-dialog>
107
+  </div>
108
+</template>
109
+
110
+<script>
111
+import { getSellerShopProfile, updateSellerShopProfile, getSellerShopEmployeeStats } from "@/api/agri/seller/shop"
112
+import { getSellerContext } from "@/api/agri/seller/context"
113
+import { setSellerShopContext } from "@/utils/sellerShop"
114
+
115
+export default {
116
+  name: "AgriSellerShop",
117
+  data() {
118
+    /** 客服电话:选填;有值时校验格式(与平台店铺管理一致) */
119
+    const validateShopPhone = (rule, value, callback) => {
120
+      if (!value || !String(value).trim()) {
121
+        callback()
122
+        return
123
+      }
124
+      if (!/^[0-9\-+()\s]{5,20}$/.test(String(value).trim())) {
125
+        callback(new Error("请输入正确的电话格式"))
126
+        return
127
+      }
128
+      callback()
129
+    }
130
+    return {
131
+      profileLoading: false,
132
+      statsLoading: false,
133
+      open: false,
134
+      profile: {},
135
+      employeeStats: {},
136
+      form: {},
137
+      rules: {
138
+        shopName: [{ required: true, message: "请输入店铺名称", trigger: "blur" }],
139
+        shopAvatar: [{ required: true, message: "请上传店铺头像", trigger: "change" }],
140
+        shopDesc: [{ max: 1000, message: "店铺简介过长", trigger: "blur" }],
141
+        shopPhone: [{ validator: validateShopPhone, trigger: "blur" }]
142
+      }
143
+    }
144
+  },
145
+  created() {
146
+    this.initPage()
147
+  },
148
+  methods: {
149
+    /** 初始化:加载店铺上下文后并行拉资料与员工统计 */
150
+    initPage() {
151
+      this.loadShopContext().then(() => {
152
+        this.loadPageData()
153
+      }).catch(() => {
154
+        this.loadPageData()
155
+      })
156
+    },
157
+    /** 加载商家端当前店铺上下文 */
158
+    loadShopContext() {
159
+      return getSellerContext().then(response => {
160
+        const data = response.data || {}
161
+        if (data.shopId != null) {
162
+          setSellerShopContext(data.shopId, data.shopName)
163
+        }
164
+      })
165
+    },
166
+    /** 并行加载店铺资料与员工概览 */
167
+    loadPageData() {
168
+      this.loadProfile()
169
+      this.loadEmployeeStats()
170
+    },
171
+    /** 查询当前店铺资料 */
172
+    loadProfile() {
173
+      this.profileLoading = true
174
+      getSellerShopProfile().then(response => {
175
+        this.profile = response.data || {}
176
+        this.profileLoading = false
177
+      }).catch(() => {
178
+        this.profileLoading = false
179
+      })
180
+    },
181
+    /** 查询员工概览 */
182
+    loadEmployeeStats() {
183
+      this.statsLoading = true
184
+      getSellerShopEmployeeStats().then(response => {
185
+        this.employeeStats = response.data || {}
186
+        this.statsLoading = false
187
+      }).catch(() => {
188
+        this.statsLoading = false
189
+      })
190
+    },
191
+    /** 空值展示占位 */
192
+    displayText(value) {
193
+      if (value === null || value === undefined || String(value).trim() === "") {
194
+        return "—"
195
+      }
196
+      return value
197
+    },
198
+    /** 打开编辑弹窗 */
199
+    handleEdit() {
200
+      this.reset()
201
+      this.form = {
202
+        shopName: this.profile.shopName || "",
203
+        shopAvatar: this.profile.shopAvatar || "",
204
+        shopDesc: this.profile.shopDesc || "",
205
+        shopPhone: this.profile.shopPhone || ""
206
+      }
207
+      this.open = true
208
+    },
209
+    /** 提交编辑 */
210
+    submitForm() {
211
+      this.$refs["form"].validate(valid => {
212
+        if (!valid) {
213
+          return
214
+        }
215
+        const payload = {
216
+          shopName: this.form.shopName.trim(),
217
+          shopAvatar: this.form.shopAvatar,
218
+          shopDesc: this.form.shopDesc ? this.form.shopDesc.trim() : undefined,
219
+          shopPhone: this.form.shopPhone && String(this.form.shopPhone).trim()
220
+            ? this.form.shopPhone.trim()
221
+            : undefined
222
+        }
223
+        updateSellerShopProfile(payload).then(() => {
224
+          this.$modal.msgSuccess("保存成功")
225
+          this.open = false
226
+          this.loadProfile()
227
+        })
228
+      })
229
+    },
230
+    /** 关闭编辑弹窗 */
231
+    cancel() {
232
+      this.open = false
233
+      this.reset()
234
+    },
235
+    reset() {
236
+      this.form = {
237
+        shopName: undefined,
238
+        shopAvatar: undefined,
239
+        shopDesc: undefined,
240
+        shopPhone: undefined
241
+      }
242
+      this.resetForm("form")
243
+    },
244
+    /** 跳转员工管理(同一当前店铺上下文) */
245
+    goEmployeeManage() {
246
+      this.$router.push({ path: "/seller/employee" })
247
+    }
248
+  }
249
+}
250
+</script>
251
+
252
+<style scoped lang="scss">
253
+.card-header {
254
+  display: flex;
255
+  align-items: center;
256
+  justify-content: space-between;
257
+}
258
+.card-title {
259
+  font-weight: 600;
260
+  font-size: 15px;
261
+}
262
+.stats-row {
263
+  margin-top: 4px;
264
+}
265
+.stat-block {
266
+  padding: 16px;
267
+  border: 1px solid #ebeef5;
268
+  border-radius: 4px;
269
+  min-height: 120px;
270
+  background: #fafafa;
271
+}
272
+.stat-block-muted {
273
+  background: #fff;
274
+}
275
+.stat-value {
276
+  font-size: 28px;
277
+  font-weight: 600;
278
+  color: #303133;
279
+  line-height: 1.2;
280
+  margin-bottom: 8px;
281
+}
282
+.stat-label {
283
+  font-size: 14px;
284
+  color: #606266;
285
+  margin-bottom: 4px;
286
+}
287
+.stat-tip,
288
+.stat-desc {
289
+  font-size: 12px;
290
+  color: #909399;
291
+  line-height: 1.5;
292
+}
293
+</style>