Kaynağa Gözat

财务管理

xsh_1997 1 hafta önce
ebeveyn
işleme
1291913765

+ 217 - 0
doc/店铺后台/财务中心/提现管理/提现管理前端技术方案.md

@@ -0,0 +1,217 @@
1
+# 提现管理 — 前端技术方案
2
+
3
+> **依据:** 《提现管理功能需求.md》v1.0、《提现管理技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** **提交提现**、**提现记录列表与检索**、提交区 **可用余额** 辅助展示;**不含** 平台审核、账户 CRUD、资金概览页。  
6
+> **实现状态:** `index.vue`、`api/agri/seller/finance/withdraw.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/finance/withdraw` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/stock/inbound/index.vue`(日期检索)、`agri/seller/finance/payAccount/index.vue`(财务模块) |
17
+| 布局 | 提交 `el-card` + `<br/>` + 检索 `el-card` + `<br/>` + 列表 `el-card` + `border` 表格 |
18
+| 账户下拉 | 复用 `@/api/agri/seller/finance/payAccount.js` → `listSellerPayAccountOptions` |
19
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
20
+
21
+---
22
+
23
+## 2. 业务要点(前端需体现)
24
+
25
+| 项 | 说明 |
26
+|----|------|
27
+| 唯一提交入口 | 本页 **POST submit**;无撤销/改待审单 |
28
+| 账户来源 | **账户管理** `GET /payAccount/options`;无账户时提示先添加 |
29
+| 可用余额 | `GET /balance`;提交前校验 **≤ availableBalance** |
30
+| 提现状态 | `1` 待审核 / `2` 审核不通过 / `3` 提现完成 |
31
+| 处理说明 | 待审核展示 **—**;终态只读展示 `processRemark` |
32
+| 账户摘要 | 列表优先 `accountSummary`;兜底拼接类型+脱敏+姓名 |
33
+| 检索 | 申请时间区间 + 提现状态下拉 |
34
+
35
+---
36
+
37
+## 3. 文件清单
38
+
39
+| 类型 | 路径 | 说明 |
40
+|------|------|------|
41
+| 页面 | `ruoyi-ui/src/views/agri/seller/finance/withdraw/index.vue` | 提交区 + 检索 + 记录列表 |
42
+| API | `ruoyi-ui/src/api/agri/seller/finance/withdraw.js` | list、balance、submit |
43
+| 协作 API | `api/agri/seller/finance/payAccount.js` | options 下拉 |
44
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
45
+
46
+**组件 name(keep-alive):** `AgriSellerFinanceWithdraw`
47
+
48
+**不提供:** 撤销待审核、修改待审单、页内添加账户、导出、按编号检索。
49
+
50
+---
51
+
52
+## 4. 菜单与路由
53
+
54
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
55
+|----------|----------|-------------------|----------|
56
+| 提现管理 | `agri/seller/finance/withdraw/index` | `seller/finance/withdraw` | `agri:seller:finance:withdraw:list` |
57
+
58
+**上级菜单:** 店铺经营管理端 → **财务中心**
59
+
60
+| 按钮权限 | 标识 | 页面落点 |
61
+|----------|------|----------|
62
+| 列表 + 余额 | `agri:seller:finance:withdraw:list` | 记录列表、可用余额 |
63
+| 提交提现 | `agri:seller:finance:withdraw:submit` | 「提交提现」 |
64
+
65
+---
66
+
67
+## 5. 页面结构(与代码一致)
68
+
69
+```text
70
+提现管理 index.vue
71
+├── 提交提现 submit-card
72
+│   ├── 可用店铺余额(GET /balance)
73
+│   ├── 无账户 warning 提示
74
+│   ├── 提现账户 accountId(options 下拉)
75
+│   ├── 提现金额 withdrawAmount(>0,两位小数)
76
+│   ├── 备注 remark(选填,≤200 字)
77
+│   └── 提交 / 重置
78
+├── <br/>
79
+├── 检索区 search-card
80
+│   ├── 申请时间 daterange → beginApplyTime / endApplyTime
81
+│   ├── 提现状态 withdrawStatus
82
+│   └── 搜索 / 重置
83
+├── <br/>
84
+└── 提现记录 table-card
85
+    ├── el-table border
86
+    │   ├── 提现编号 withdrawNo
87
+    │   ├── 申请时间 applyTime
88
+    │   ├── 提现账户 accountSummary
89
+    │   ├── 提现金额 withdrawAmount
90
+    │   ├── 备注 remark
91
+    │   ├── 提现状态 withdrawStatusText(tag)
92
+    │   └── 提现处理说明 processRemark
93
+    └── pagination
94
+```
95
+
96
+---
97
+
98
+## 6. 提交提现
99
+
100
+### 6.1 接口
101
+
102
+**GET** `/balance` → `{ availableBalance }`
103
+
104
+**POST** `/submit`
105
+
106
+```json
107
+{
108
+  "accountId": 1,
109
+  "withdrawAmount": 100.00,
110
+  "remark": "选填"
111
+}
112
+```
113
+
114
+**成功:** `msgSuccess('提交成功')` → 刷新余额与列表、重置表单。
115
+
116
+### 6.2 前端校验
117
+
118
+| 规则 | 说明 |
119
+|------|------|
120
+| 未选账户 | 「请选择提现账户」 |
121
+| 金额空或 ≤0 | 「请输入有效的提现金额」 |
122
+| 超过余额 | 「提现金额不能超过可用余额」 |
123
+| 备注 | maxlength 200 |
124
+
125
+后端另校验账户有效性、余额并发(FOR UPDATE)。
126
+
127
+---
128
+
129
+## 7. 提现记录列表
130
+
131
+### 7.1 接口
132
+
133
+**GET** `/list?pageNum=&pageSize=&withdrawStatus=&beginApplyTime=&endApplyTime=`
134
+
135
+| 参数 | 说明 |
136
+|------|------|
137
+| beginApplyTime | `yyyy-MM-dd` 开始(含 00:00:00) |
138
+| endApplyTime | `yyyy-MM-dd` 结束(含 23:59:59) |
139
+| withdrawStatus | 空=全部;`1`/`2`/`3` |
140
+
141
+> **联调说明:** 后端 `SellerWithdrawQuery` 须包含 `beginApplyTime`、`endApplyTime` 字段,与 Mapper XML 一致;前端已按平铺 Query 传参。
142
+
143
+### 7.2 状态展示
144
+
145
+| withdrawStatus | 文案 | Tag |
146
+|----------------|------|-----|
147
+| `1` | 待审核 | warning |
148
+| `2` | 审核不通过 | danger |
149
+| `3` | 提现完成 | success |
150
+
151
+---
152
+
153
+## 8. 店铺上下文(X-Shop-Id)
154
+
155
+| 步骤 | 说明 |
156
+|------|------|
157
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
158
+| 全部 API | `sellerShopHeaders()` |
159
+| Navbar 切店 | `location.reload()` 整页刷新 |
160
+
161
+---
162
+
163
+## 9. API 封装
164
+
165
+**模块:** `@/api/agri/seller/finance/withdraw.js`
166
+
167
+| 方法 | HTTP | 路径 | 权限 |
168
+|------|------|------|------|
169
+| `listSellerWithdraws` | GET | `/list` | withdraw:list |
170
+| `getSellerWithdrawBalance` | GET | `/balance` | withdraw:list |
171
+| `submitSellerWithdraw` | POST | `/submit` | withdraw:submit |
172
+
173
+---
174
+
175
+## 10. 空状态与错误提示
176
+
177
+| 场景 | 文案 |
178
+|------|------|
179
+| 无收款账户 | warning:请先至账户管理添加 |
180
+| 记录无数据 | 「暂无提现记录」 |
181
+| 检索无结果 | 「未找到符合条件的提现记录」 |
182
+| 提交成功 | 「提交成功」 |
183
+| 后端账户无效 | 「提现账户不存在或已删除」 |
184
+
185
+---
186
+
187
+## 11. 与兄弟模块边界(前端)
188
+
189
+| 模块 | 关系 |
190
+|------|------|
191
+| **账户管理** | options 下拉;无 inline 添加 |
192
+| **资金概览** | 提交后写「余额提现」流水;本页只读余额 |
193
+| **平台提现审核** | 审核结果回写状态与处理说明 |
194
+
195
+---
196
+
197
+## 12. 联调检查清单
198
+
199
+- [ ] 菜单挂载 `agri/seller/finance/withdraw/index`
200
+- [ ] 无账户时不可提交并提示
201
+- [ ] 提交成功生成待审核记录
202
+- [ ] 超余额提交被后端拒绝
203
+- [ ] 日期区间、状态筛选生效
204
+- [ ] 平台审核后列表状态与处理说明更新
205
+- [ ] Navbar 切店后数据刷新
206
+
207
+---
208
+
209
+## 13. 版本记录
210
+
211
+| 版本 | 说明 |
212
+|------|------|
213
+| **v1.0** | 首版:提交区 + 记录检索 + API 封装;对齐需求 v1.0 |
214
+
215
+---
216
+
217
+*文档版本:v1.0 · 依据《提现管理功能需求.md》v1.0、《提现管理技术方案.md》v1.0*

+ 187 - 0
doc/店铺后台/财务中心/账户管理/账户管理前端技术方案.md

@@ -0,0 +1,187 @@
1
+# 账户管理 — 前端技术方案
2
+
3
+> **依据:** 《账户管理功能需求.md》v1.0、《账户管理技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺 **提现收款账户** 列表、**添加 / 编辑 / 删除**;**不含** 提现提交、平台审核、余额/流水。  
6
+> **实现状态:** `index.vue`、`api/agri/seller/finance/payAccount.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/finance/payAccount` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/employee/index.vue`(CRUD 弹窗)、`agri/seller/finance/overview/index.vue`(财务模块上下文) |
17
+| 布局 | 列表 `el-card` + `border` 表格 + 分页 |
18
+| 表单 | `el-dialog` width=520px(`append-to-body`) |
19
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
20
+
21
+---
22
+
23
+## 2. 业务要点(前端需体现)
24
+
25
+| 项 | 说明 |
26
+|----|------|
27
+| 账户类型 | `1` 银行卡 / `2` 支付宝 / `3` 微信 |
28
+| 类型锁定 | **添加时选定**;**编辑不可改类型**(PA-A3) |
29
+| 列表账号 | 展示后端 **脱敏** 字段 `accountNoMask`,**不展示明文** |
30
+| 编辑账号 | 列表无明文;编辑须 **重新输入完整账号** |
31
+| 删除 | 二次确认;逻辑删除;**不影响** 已提交提现快照 |
32
+| 检索 | **非本期**;仅分页列表 |
33
+| 与资金概览 | 银行卡类型计入概览「银行卡数量」 |
34
+
35
+---
36
+
37
+## 3. 文件清单
38
+
39
+| 类型 | 路径 | 说明 |
40
+|------|------|------|
41
+| 列表页 | `ruoyi-ui/src/views/agri/seller/finance/payAccount/index.vue` | 列表 + 添加/编辑弹窗 |
42
+| API | `ruoyi-ui/src/api/agri/seller/finance/payAccount.js` | list、options、add、edit、remove |
43
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
44
+
45
+**组件 name(keep-alive):** `AgriSellerFinancePayAccount`
46
+
47
+**不提供:** 关键词检索、批量删除、默认账户、导出、详情页、页内实名认证。
48
+
49
+---
50
+
51
+## 4. 菜单与路由
52
+
53
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
54
+|----------|----------|-------------------|----------|
55
+| 账户管理 | `agri/seller/finance/payAccount/index` | `seller/finance/payAccount` | `agri:seller:finance:payAccount:list` |
56
+
57
+**上级菜单:** 店铺经营管理端 → **财务中心**
58
+
59
+| 按钮权限 | 标识 | 页面落点 |
60
+|----------|------|----------|
61
+| 列表 | `agri:seller:finance:payAccount:list` | 进入页面 |
62
+| 添加 | `agri:seller:finance:payAccount:add` | 「添加账户」、POST |
63
+| 编辑 | `agri:seller:finance:payAccount:edit` | 「编辑」、PUT |
64
+| 删除 | `agri:seller:finance:payAccount:remove` | 「删除」、DELETE |
65
+
66
+> `GET /options` 权限为 `agri:seller:finance:withdraw:list`,供 **提现管理** 调用,非本页使用。
67
+
68
+---
69
+
70
+## 5. 页面结构(与代码一致)
71
+
72
+```text
73
+账户管理 index.vue
74
+├── 列表区 table-card
75
+│   ├── 添加账户(权限 add)
76
+│   ├── right-toolbar(刷新)
77
+│   ├── el-table border
78
+│   │   ├── 收款方式 accountTypeText
79
+│   │   ├── 真实姓名 realName
80
+│   │   ├── 账号 accountNoMask
81
+│   │   ├── 添加时间 createTime
82
+│   │   └── 操作:编辑 | 删除
83
+│   └── pagination
84
+└── 添加/编辑弹窗 el-dialog
85
+    ├── 收款方式 accountType(编辑 disabled)
86
+    ├── 真实姓名 realName
87
+    └── 账号 accountNo(标签随类型变化;编辑展示当前脱敏提示)
88
+```
89
+
90
+---
91
+
92
+## 6. 表单与校验
93
+
94
+### 6.1 字段映射(PayAccountSaveDTO)
95
+
96
+| 字段 | 添加 | 编辑 |
97
+|------|------|------|
98
+| accountType | 必选 | 只读展示,随 payload 原样提交 |
99
+| realName | 必填,≤64 | 可改 |
100
+| accountNo | 必填明文 | 必填明文(重新输入) |
101
+| accountId | — | 必填 |
102
+
103
+### 6.2 前端校验
104
+
105
+| 规则 | 说明 |
106
+|------|------|
107
+| 收款方式未选 | 「请选择收款方式」 |
108
+| 姓名为空 | 「请填写真实姓名」 |
109
+| 账号为空 | 按类型提示(银行卡号/支付宝号/微信号) |
110
+| 银行卡号 | **纯数字**(`/^\d+$/`) |
111
+| 支付宝/微信 | 非空即可 |
112
+
113
+成功提示:添加「添加成功」、编辑「保存成功」、删除「删除成功」。
114
+
115
+---
116
+
117
+## 7. 删除确认
118
+
119
+**文案:** 确认删除该{类型}账户「{脱敏账号}」吗?删除后不可在提现时选择,已提交的提现单不受影响。
120
+
121
+---
122
+
123
+## 8. 店铺上下文(X-Shop-Id)
124
+
125
+| 步骤 | 说明 |
126
+|------|------|
127
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
128
+| 全部 API | `sellerShopHeaders()` |
129
+| Navbar 切店 | `location.reload()` 整页刷新 |
130
+
131
+---
132
+
133
+## 9. API 封装
134
+
135
+**模块:** `@/api/agri/seller/finance/payAccount.js`
136
+
137
+| 方法 | HTTP | 路径 | 权限 |
138
+|------|------|------|------|
139
+| `listSellerPayAccounts` | GET | `/list` | payAccount:list |
140
+| `listSellerPayAccountOptions` | GET | `/options` | withdraw:list |
141
+| `addSellerPayAccount` | POST | `/` | payAccount:add |
142
+| `updateSellerPayAccount` | PUT | `/` | payAccount:edit |
143
+| `delSellerPayAccount` | DELETE | `/{accountId}` | payAccount:remove |
144
+
145
+---
146
+
147
+## 10. 空状态与错误提示
148
+
149
+| 场景 | 文案 |
150
+|------|------|
151
+| 列表无数据 | 「暂无收款账户,请先添加」 |
152
+| 后端类型无效 | 「收款方式无效」 |
153
+| 账户不存在 | 「收款账户不存在」 |
154
+
155
+---
156
+
157
+## 11. 与兄弟模块边界(前端)
158
+
159
+| 模块 | 关系 |
160
+|------|------|
161
+| **提现管理** | 提交前须本模块有账户;下拉调 `GET /options` |
162
+| **资金概览** | 只读银行卡数量;本页维护后概览刷新可见 |
163
+| **平台提现审核** | 读提现快照;**不读**本模块实时数据 |
164
+
165
+---
166
+
167
+## 12. 联调检查清单
168
+
169
+- [ ] 菜单挂载 `agri/seller/finance/payAccount/index`
170
+- [ ] 添加三类账户成功并列表可见
171
+- [ ] 列表账号为脱敏展示
172
+- [ ] 编辑时类型不可改、须填完整账号
173
+- [ ] 删除后列表移除、options 不可选
174
+- [ ] 权限:无 add/edit/remove 时按钮隐藏
175
+- [ ] Navbar 切店后列表刷新
176
+
177
+---
178
+
179
+## 13. 版本记录
180
+
181
+| 版本 | 说明 |
182
+|------|------|
183
+| **v1.0** | 首版:列表/添加/编辑/删除/API 封装;对齐需求 v1.0 |
184
+
185
+---
186
+
187
+*文档版本:v1.0 · 依据《账户管理功能需求.md》v1.0、《账户管理技术方案.md》v1.0*

+ 221 - 0
doc/店铺后台/财务中心/资金概览/资金概览前端技术方案.md

@@ -0,0 +1,221 @@
1
+# 资金概览 — 前端技术方案
2
+
3
+> **依据:** 《资金概览功能需求.md》v1.0、《资金概览技术方案.md》v1.0  
4
+> **前端规范:** `doc/前端设计/前端设计.md`  
5
+> **范围:** 仅 **ruoyi-ui 商家端** 当前店铺 **账户概览**(只读)与 **收支概况流水**(只读分页);**不含** 提现申请、账户维护、平台审核、导出/筛选。  
6
+> **实现状态:** `index.vue`、`api/agri/seller/finance/overview.js` **已按 v1.0 落地**;待菜单配置及 `/agri/seller/finance/overview` 联调。
7
+
8
+---
9
+
10
+## 1. 技术栈与写法约定
11
+
12
+| 项 | 说明 |
13
+|----|------|
14
+| 框架 | Vue 2 + Element UI |
15
+| 请求 | `@/utils/request` + `sellerShopHeaders()` 携带 **`X-Shop-Id`** |
16
+| 参考页面 | `agri/seller/shop/index.vue`(概览 stat-block)、`agri/seller/stock/log/index.vue`(只读列表) |
17
+| 布局 | 概览 `el-card` + `<br/>` + 流水 `el-card` + `border` 表格 |
18
+| 店铺切换 | **仅 Navbar**;业务页禁止展示店铺选择器 |
19
+
20
+---
21
+
22
+## 2. 业务要点(前端需体现)
23
+
24
+| 项 | 说明 |
25
+|----|------|
26
+| 只读 | **无** 改余额/冻结、发起提现、维护账户 |
27
+| 概览指标 | 可用余额、待结算、冻结、用户名、银行卡数量 |
28
+| 待结算 | 后端实时汇总订单,**非** wallet 字段 |
29
+| 银行卡数 | **仅** 统计银行卡类型(`account_type=1`) |
30
+| 流水 | 变动时间 **倒序**;四类变动原因与后端文案一致 |
31
+| 变动展示 | 前端格式化「变更值 + 变更后」(需求 §3.4) |
32
+| 业务编号 | **只读展示**;跳转订单/提现 **非本期** |
33
+
34
+---
35
+
36
+## 3. 文件清单
37
+
38
+| 类型 | 路径 | 说明 |
39
+|------|------|------|
40
+| 页面 | `ruoyi-ui/src/views/agri/seller/finance/overview/index.vue` | 概览 + 收支流水 |
41
+| API | `ruoyi-ui/src/api/agri/seller/finance/overview.js` | summary、logs |
42
+| 店铺上下文 | `api/agri/seller/context.js` + `utils/sellerShop.js` | X-Shop-Id |
43
+
44
+**组件 name(keep-alive):** `AgriSellerFinanceOverview`
45
+
46
+**不提供:** 检索筛选、导出、业务编号下钻、概览指标下钻、提现/账户入口按钮。
47
+
48
+---
49
+
50
+## 4. 菜单与路由
51
+
52
+| 菜单名称 | 组件路径 | 路由 path(建议) | 权限标识 |
53
+|----------|----------|-------------------|----------|
54
+| 资金概览 | `agri/seller/finance/overview/index` | `seller/finance/overview` | `agri:seller:finance:overview:query` |
55
+
56
+**上级菜单:** 店铺经营管理端 → **财务中心**
57
+
58
+| 按钮权限 | 标识 | 页面落点 |
59
+|----------|------|----------|
60
+| 概览 + 流水 | `agri:seller:finance:overview:query` | 进入页面(无分写权限) |
61
+
62
+---
63
+
64
+## 5. 页面结构(与代码一致)
65
+
66
+```text
67
+资金概览 index.vue
68
+├── 账户概览 overview-card(v-loading)
69
+│   ├── 三指标 stat-block
70
+│   │   ├── 可用店铺余额 availableBalance
71
+│   │   ├── 待结算金额 pendingSettleAmount
72
+│   │   └── 冻结金额 frozenAmount
73
+│   └── el-descriptions border
74
+│       ├── 用户名 username
75
+│       └── 银行卡数量 bankAccountCount
76
+├── <br/>
77
+└── 收支概况 table-card
78
+    ├── el-table border(empty-text:暂无收支记录)
79
+    │   ├── 店铺名称 shopName
80
+    │   ├── 变动时间 changeTime
81
+    │   ├── 业务编号 bizNo
82
+    │   ├── 变动余额资金 balanceChange + balanceAfter
83
+    │   ├── 变动冻结资金 frozenChange + frozenAfter
84
+    │   └── 变动原因 changeReasonText
85
+    └── pagination
86
+```
87
+
88
+---
89
+
90
+## 6. 账户概览
91
+
92
+### 6.1 接口
93
+
94
+**GET** `/agri/seller/finance/overview/summary`
95
+
96
+| 字段 | 说明 |
97
+|------|------|
98
+| availableBalance | 可用店铺余额 |
99
+| pendingSettleAmount | 待结算金额 |
100
+| frozenAmount | 冻结金额 |
101
+| username | 当前登录用户名 |
102
+| bankAccountCount | 银行卡条数 |
103
+
104
+无 wallet 时余额/冻结后端返回 `0.00`。
105
+
106
+### 6.2 金额展示
107
+
108
+| 场景 | 规则 |
109
+|------|------|
110
+| 概览金额 | `formatMoney`:`¥` + 两位小数 + 千分位 |
111
+| null | 展示 `—` |
112
+| 为 0 | **仍展示** `¥0.00` |
113
+
114
+---
115
+
116
+## 7. 收支概况列表
117
+
118
+### 7.1 接口
119
+
120
+**GET** `/agri/seller/finance/overview/logs?pageNum=&pageSize=`
121
+
122
+**无** 筛选参数(非本期)。
123
+
124
+### 7.2 变动列格式化(前端)
125
+
126
+```text
127
+{±change}(余额:{after})
128
+{±change}(冻结:{after})
129
+```
130
+
131
+| 规则 | 说明 |
132
+|------|------|
133
+| change 为 0 或 null | 变更值展示 `0` |
134
+| change > 0 | 前缀 `+` |
135
+| after | 两位小数 + 千分位(无 ¥) |
136
+| after 为 null | `—` |
137
+| 样式 | 正数 `.change-in` 绿色;负数 `.change-out` 红色 |
138
+
139
+**示例:**
140
+
141
+- 订单收支:`+128.00(余额:1,280.00)` / `0(冻结:200.00)`
142
+- 余额提现:`−500.00(余额:780.00)` / `+500.00(冻结:700.00)`
143
+
144
+### 7.3 变动原因
145
+
146
+优先 `changeReasonText`;兜底映射:
147
+
148
+| changeReason | 文案 |
149
+|--------------|------|
150
+| `1` | 订单收支 |
151
+| `2` | 余额提现 |
152
+| `3` | 提现驳回 |
153
+| `4` | 提现完成 |
154
+
155
+---
156
+
157
+## 8. 店铺上下文(X-Shop-Id)
158
+
159
+| 步骤 | 说明 |
160
+|------|------|
161
+| `created` | `GET /agri/seller/context` → `setSellerShopContext` |
162
+| 全部 API | `sellerShopHeaders()` |
163
+| Navbar 切店 | `location.reload()` 整页刷新 |
164
+
165
+---
166
+
167
+## 9. API 封装
168
+
169
+**模块:** `@/api/agri/seller/finance/overview.js`
170
+
171
+| 方法 | HTTP | 路径 | 权限 |
172
+|------|------|------|------|
173
+| `getSellerFundOverviewSummary` | GET | `/summary` | query |
174
+| `listSellerFundOverviewLogs` | GET | `/logs` | query |
175
+
176
+---
177
+
178
+## 10. 空状态与错误提示
179
+
180
+| 场景 | 文案 |
181
+|------|------|
182
+| 流水无数据 | 「暂无收支记录」 |
183
+| 概览余额为 0 | 仍展示 `¥0.00` |
184
+| 无银行卡 | 银行卡数量 `0` |
185
+| 用户名空 | `—` |
186
+
187
+---
188
+
189
+## 11. 与兄弟模块边界(前端)
190
+
191
+| 模块 | 关系 |
192
+|------|------|
193
+| **提现管理** | 写操作在提现模块;本页 **无** 提交入口 |
194
+| **账户管理** | 银行卡维护在账户模块;本页 **只读** 数量 |
195
+| **全部订单** | 订单收支由确认收货触发;本页 **只读** 流水 |
196
+| **售后/退款** | 负向流水 **非本期** |
197
+
198
+---
199
+
200
+## 12. 联调检查清单
201
+
202
+- [ ] 菜单挂载 `agri/seller/finance/overview/index`
203
+- [ ] summary 四块指标与当前店铺一致
204
+- [ ] 待结算随订单状态变化
205
+- [ ] 确认收货后出现「订单收支」流水
206
+- [ ] 提现 submit 后出现「余额提现」流水
207
+- [ ] 变动列格式符合 §3.4
208
+- [ ] 分页倒序(最新在前)
209
+- [ ] Navbar 切店后概览与列表刷新
210
+
211
+---
212
+
213
+## 13. 版本记录
214
+
215
+| 版本 | 说明 |
216
+|------|------|
217
+| **v1.0** | 首版:账户概览 + 收支流水分页 + 金额/变动格式化 + API 封装 |
218
+
219
+---
220
+
221
+*文档版本:v1.0 · 依据《资金概览功能需求.md》v1.0、《资金概览技术方案.md》v1.0*

+ 21 - 0
ruoyi-ui/src/api/agri/seller/finance/overview.js

@@ -0,0 +1,21 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 账户概览
5
+export function getSellerFundOverviewSummary() {
6
+  return request({
7
+    url: '/agri/seller/finance/overview/summary',
8
+    method: 'get',
9
+    headers: sellerShopHeaders()
10
+  })
11
+}
12
+
13
+// 收支概况流水
14
+export function listSellerFundOverviewLogs(query) {
15
+  return request({
16
+    url: '/agri/seller/finance/overview/logs',
17
+    method: 'get',
18
+    params: query,
19
+    headers: sellerShopHeaders()
20
+  })
21
+}

+ 50 - 0
ruoyi-ui/src/api/agri/seller/finance/payAccount.js

@@ -0,0 +1,50 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 收款账户列表
5
+export function listSellerPayAccounts(query) {
6
+  return request({
7
+    url: '/agri/seller/finance/payAccount/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 提现可选账户(供提现管理协作)
15
+export function listSellerPayAccountOptions() {
16
+  return request({
17
+    url: '/agri/seller/finance/payAccount/options',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 添加收款账户
24
+export function addSellerPayAccount(data) {
25
+  return request({
26
+    url: '/agri/seller/finance/payAccount',
27
+    method: 'post',
28
+    data: data,
29
+    headers: sellerShopHeaders()
30
+  })
31
+}
32
+
33
+// 编辑收款账户
34
+export function updateSellerPayAccount(data) {
35
+  return request({
36
+    url: '/agri/seller/finance/payAccount',
37
+    method: 'put',
38
+    data: data,
39
+    headers: sellerShopHeaders()
40
+  })
41
+}
42
+
43
+// 删除收款账户
44
+export function delSellerPayAccount(accountId) {
45
+  return request({
46
+    url: '/agri/seller/finance/payAccount/' + accountId,
47
+    method: 'delete',
48
+    headers: sellerShopHeaders()
49
+  })
50
+}

+ 31 - 0
ruoyi-ui/src/api/agri/seller/finance/withdraw.js

@@ -0,0 +1,31 @@
1
+import request from '@/utils/request'
2
+import { sellerShopHeaders } from '@/utils/sellerShop'
3
+
4
+// 提现记录列表
5
+export function listSellerWithdraws(query) {
6
+  return request({
7
+    url: '/agri/seller/finance/withdraw/list',
8
+    method: 'get',
9
+    params: query,
10
+    headers: sellerShopHeaders()
11
+  })
12
+}
13
+
14
+// 可用余额(提交页辅助)
15
+export function getSellerWithdrawBalance() {
16
+  return request({
17
+    url: '/agri/seller/finance/withdraw/balance',
18
+    method: 'get',
19
+    headers: sellerShopHeaders()
20
+  })
21
+}
22
+
23
+// 提交提现申请
24
+export function submitSellerWithdraw(data) {
25
+  return request({
26
+    url: '/agri/seller/finance/withdraw/submit',
27
+    method: 'post',
28
+    data: data,
29
+    headers: sellerShopHeaders()
30
+  })
31
+}

+ 287 - 0
ruoyi-ui/src/views/agri/seller/finance/overview/index.vue

@@ -0,0 +1,287 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 账户概览 -->
4
+    <el-card shadow="never" class="overview-card" v-loading="summaryLoading">
5
+      <div slot="header" class="card-header">
6
+        <span class="card-title">账户概览</span>
7
+      </div>
8
+      <el-row :gutter="24" class="stats-row">
9
+        <el-col :xs="24" :sm="8">
10
+          <div class="stat-block stat-block-primary">
11
+            <div class="stat-value">{{ formatMoney(summary.availableBalance) }}</div>
12
+            <div class="stat-label">可用店铺余额</div>
13
+            <div class="stat-tip">订单结算完成、可发起提现的金额</div>
14
+          </div>
15
+        </el-col>
16
+        <el-col :xs="24" :sm="8">
17
+          <div class="stat-block">
18
+            <div class="stat-value">{{ formatMoney(summary.pendingSettleAmount) }}</div>
19
+            <div class="stat-label">待结算金额</div>
20
+            <div class="stat-tip">已支付未完成订单实付合计,确认收货后转入余额</div>
21
+          </div>
22
+        </el-col>
23
+        <el-col :xs="24" :sm="8">
24
+          <div class="stat-block">
25
+            <div class="stat-value">{{ formatMoney(summary.frozenAmount) }}</div>
26
+            <div class="stat-label">冻结金额</div>
27
+            <div class="stat-tip">提现审核中临时冻结的金额</div>
28
+          </div>
29
+        </el-col>
30
+      </el-row>
31
+      <el-descriptions :column="2" border size="medium" class="account-info">
32
+        <el-descriptions-item label="用户名">{{ displayText(summary.username) }}</el-descriptions-item>
33
+        <el-descriptions-item label="银行卡数量">
34
+          {{ summary.bankAccountCount != null ? summary.bankAccountCount : '—' }}
35
+        </el-descriptions-item>
36
+      </el-descriptions>
37
+    </el-card>
38
+
39
+    <br/>
40
+
41
+    <!-- 收支概况 -->
42
+    <el-card shadow="never" class="table-card">
43
+      <div slot="header" class="card-header">
44
+        <span class="card-title">收支概况</span>
45
+      </div>
46
+
47
+      <el-table border v-loading="loading" :data="logList" :empty-text="emptyTableText">
48
+        <el-table-column label="店铺名称" align="center" prop="shopName" min-width="120" :show-overflow-tooltip="true">
49
+          <template slot-scope="scope">
50
+            <span>{{ scope.row.shopName || '—' }}</span>
51
+          </template>
52
+        </el-table-column>
53
+        <el-table-column label="变动时间" align="center" width="160">
54
+          <template slot-scope="scope">
55
+            <span>{{ parseTime(scope.row.changeTime) || '—' }}</span>
56
+          </template>
57
+        </el-table-column>
58
+        <el-table-column label="业务编号" align="center" prop="bizNo" min-width="160" :show-overflow-tooltip="true">
59
+          <template slot-scope="scope">
60
+            <span>{{ scope.row.bizNo || '—' }}</span>
61
+          </template>
62
+        </el-table-column>
63
+        <el-table-column label="变动余额资金" align="center" min-width="180">
64
+          <template slot-scope="scope">
65
+            <span :class="changeClass(scope.row.balanceChange)">
66
+              {{ formatFundChange(scope.row.balanceChange, scope.row.balanceAfter, '余额') }}
67
+            </span>
68
+          </template>
69
+        </el-table-column>
70
+        <el-table-column label="变动冻结资金" align="center" min-width="180">
71
+          <template slot-scope="scope">
72
+            <span :class="changeClass(scope.row.frozenChange)">
73
+              {{ formatFundChange(scope.row.frozenChange, scope.row.frozenAfter, '冻结') }}
74
+            </span>
75
+          </template>
76
+        </el-table-column>
77
+        <el-table-column label="变动原因" align="center" width="110">
78
+          <template slot-scope="scope">
79
+            <span>{{ scope.row.changeReasonText || changeReasonLabel(scope.row.changeReason) }}</span>
80
+          </template>
81
+        </el-table-column>
82
+      </el-table>
83
+
84
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
85
+    </el-card>
86
+  </div>
87
+</template>
88
+
89
+<script>
90
+import {
91
+  getSellerFundOverviewSummary,
92
+  listSellerFundOverviewLogs
93
+} from "@/api/agri/seller/finance/overview"
94
+import { getSellerContext } from "@/api/agri/seller/context"
95
+import { setSellerShopContext } from "@/utils/sellerShop"
96
+
97
+const CHANGE_REASON_MAP = {
98
+  "1": "订单收支",
99
+  "2": "余额提现",
100
+  "3": "提现驳回",
101
+  "4": "提现完成"
102
+}
103
+
104
+export default {
105
+  name: "AgriSellerFinanceOverview",
106
+  data() {
107
+    return {
108
+      summaryLoading: false,
109
+      loading: false,
110
+      total: 0,
111
+      summary: {},
112
+      logList: [],
113
+      queryParams: {
114
+        pageNum: 1,
115
+        pageSize: 10
116
+      }
117
+    }
118
+  },
119
+  computed: {
120
+    emptyTableText() {
121
+      return "暂无收支记录"
122
+    }
123
+  },
124
+  created() {
125
+    this.initPage()
126
+  },
127
+  methods: {
128
+    /** 初始化:加载店铺上下文后拉概览与流水 */
129
+    initPage() {
130
+      this.loadShopContext().then(() => {
131
+        this.loadSummary()
132
+        this.getList()
133
+      }).catch(() => {
134
+        this.loadSummary()
135
+        this.getList()
136
+      })
137
+    },
138
+    /** 加载商家端当前店铺上下文 */
139
+    loadShopContext() {
140
+      return getSellerContext().then(response => {
141
+        const data = response.data || {}
142
+        if (data.shopId != null) {
143
+          setSellerShopContext(data.shopId, data.shopName)
144
+        }
145
+      })
146
+    },
147
+    /** 查询账户概览 */
148
+    loadSummary() {
149
+      this.summaryLoading = true
150
+      getSellerFundOverviewSummary().then(response => {
151
+        this.summary = response.data || {}
152
+        this.summaryLoading = false
153
+      }).catch(() => {
154
+        this.summaryLoading = false
155
+      })
156
+    },
157
+    /** 查询收支流水列表 */
158
+    getList() {
159
+      this.loading = true
160
+      listSellerFundOverviewLogs(this.queryParams).then(response => {
161
+        this.logList = response.rows || []
162
+        this.total = response.total || 0
163
+        this.loading = false
164
+      }).catch(() => {
165
+        this.loading = false
166
+      })
167
+    },
168
+    /** 空值展示占位 */
169
+    displayText(value) {
170
+      if (value === null || value === undefined || String(value).trim() === "") {
171
+        return "—"
172
+      }
173
+      return value
174
+    },
175
+    /** 金额格式化:¥ + 两位小数 + 千分位 */
176
+    formatMoney(val) {
177
+      if (val == null || val === "") {
178
+        return "—"
179
+      }
180
+      const num = Number(val)
181
+      if (isNaN(num)) {
182
+        return "—"
183
+      }
184
+      const fixed = num.toFixed(2)
185
+      const parts = fixed.split(".")
186
+      parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
187
+      return "¥" + parts.join(".")
188
+    },
189
+    /** 变动后金额(不带货币符号,带千分位) */
190
+    formatAfterAmount(val) {
191
+      if (val == null || val === "") {
192
+        return "—"
193
+      }
194
+      const num = Number(val)
195
+      if (isNaN(num)) {
196
+        return "—"
197
+      }
198
+      const fixed = num.toFixed(2)
199
+      const parts = fixed.split(".")
200
+      parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
201
+      return parts.join(".")
202
+    },
203
+    /** 变动值(带 +/- 符号) */
204
+    formatChangeValue(change) {
205
+      if (change == null || change === "") {
206
+        return "0"
207
+      }
208
+      const num = Number(change)
209
+      if (isNaN(num) || num === 0) {
210
+        return "0"
211
+      }
212
+      const sign = num > 0 ? "+" : ""
213
+      return sign + num.toFixed(2)
214
+    },
215
+    /** 变动列展示:变更值 + 变更后 */
216
+    formatFundChange(change, after, label) {
217
+      const changeStr = this.formatChangeValue(change)
218
+      const afterStr = this.formatAfterAmount(after)
219
+      return `${changeStr}(${label}:${afterStr})`
220
+    },
221
+    /** 正负变动样式 */
222
+    changeClass(change) {
223
+      if (change == null || change === "" || Number(change) === 0) {
224
+        return ""
225
+      }
226
+      return Number(change) > 0 ? "change-in" : "change-out"
227
+    },
228
+    /** 变动原因文案兜底 */
229
+    changeReasonLabel(reason) {
230
+      return CHANGE_REASON_MAP[reason] || "—"
231
+    }
232
+  }
233
+}
234
+</script>
235
+
236
+<style scoped lang="scss">
237
+.card-header {
238
+  display: flex;
239
+  align-items: center;
240
+  justify-content: space-between;
241
+}
242
+.card-title {
243
+  font-weight: 600;
244
+  font-size: 15px;
245
+}
246
+.stats-row {
247
+  margin-bottom: 16px;
248
+}
249
+.stat-block {
250
+  padding: 16px;
251
+  border: 1px solid #ebeef5;
252
+  border-radius: 4px;
253
+  min-height: 120px;
254
+  background: #fafafa;
255
+  margin-bottom: 12px;
256
+}
257
+.stat-block-primary {
258
+  background: #f0f9eb;
259
+  border-color: #e1f3d8;
260
+}
261
+.stat-value {
262
+  font-size: 28px;
263
+  font-weight: 600;
264
+  color: #303133;
265
+  line-height: 1.2;
266
+  margin-bottom: 8px;
267
+}
268
+.stat-label {
269
+  font-size: 14px;
270
+  color: #606266;
271
+  margin-bottom: 4px;
272
+}
273
+.stat-tip {
274
+  font-size: 12px;
275
+  color: #909399;
276
+  line-height: 1.5;
277
+}
278
+.account-info {
279
+  margin-top: 4px;
280
+}
281
+.change-in {
282
+  color: #67c23a;
283
+}
284
+.change-out {
285
+  color: #f56c6c;
286
+}
287
+</style>

+ 318 - 0
ruoyi-ui/src/views/agri/seller/finance/payAccount/index.vue

@@ -0,0 +1,318 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 列表区 -->
4
+    <el-card shadow="never" class="table-card">
5
+      <div slot="header" class="card-header">
6
+        <span class="card-title">收款账户</span>
7
+      </div>
8
+
9
+      <el-row :gutter="10" class="mb8">
10
+        <el-col :span="1.5">
11
+          <el-button
12
+            type="primary"
13
+            plain
14
+            icon="el-icon-plus"
15
+            size="mini"
16
+            @click="handleAdd"
17
+            v-hasPermi="['agri:seller:finance:payAccount:add']"
18
+          >添加账户</el-button>
19
+        </el-col>
20
+        <right-toolbar :showSearch="false" @queryTable="getList"></right-toolbar>
21
+      </el-row>
22
+
23
+      <el-table border v-loading="loading" :data="accountList" :empty-text="emptyTableText">
24
+        <el-table-column label="收款方式" align="center" width="100">
25
+          <template slot-scope="scope">
26
+            <span>{{ scope.row.accountTypeText || accountTypeLabel(scope.row.accountType) }}</span>
27
+          </template>
28
+        </el-table-column>
29
+        <el-table-column label="真实姓名" align="center" prop="realName" width="120" :show-overflow-tooltip="true">
30
+          <template slot-scope="scope">
31
+            <span>{{ scope.row.realName || '—' }}</span>
32
+          </template>
33
+        </el-table-column>
34
+        <el-table-column label="账号" align="center" min-width="180" :show-overflow-tooltip="true">
35
+          <template slot-scope="scope">
36
+            <span>{{ scope.row.accountNoMask || '—' }}</span>
37
+          </template>
38
+        </el-table-column>
39
+        <el-table-column label="添加时间" align="center" width="160">
40
+          <template slot-scope="scope">
41
+            <span>{{ parseTime(scope.row.createTime) || '—' }}</span>
42
+          </template>
43
+        </el-table-column>
44
+        <el-table-column label="操作" align="center" width="140" fixed="right">
45
+          <template slot-scope="scope">
46
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['agri:seller:finance:payAccount:edit']">编辑</el-button>
47
+            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['agri:seller:finance:payAccount:remove']">删除</el-button>
48
+          </template>
49
+        </el-table-column>
50
+      </el-table>
51
+
52
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
53
+    </el-card>
54
+
55
+    <!-- 添加/编辑弹窗 -->
56
+    <el-dialog :title="title" :visible.sync="open" width="520px" append-to-body @close="cancel">
57
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" size="small">
58
+        <el-form-item label="收款方式" prop="accountType">
59
+          <el-radio-group v-model="form.accountType" :disabled="isEdit" @change="handleAccountTypeChange">
60
+            <el-radio v-for="item in accountTypeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
61
+          </el-radio-group>
62
+        </el-form-item>
63
+        <el-form-item label="真实姓名" prop="realName">
64
+          <el-input v-model="form.realName" placeholder="请输入真实姓名" maxlength="64" show-word-limit />
65
+        </el-form-item>
66
+        <el-form-item :label="accountNoLabel" prop="accountNo">
67
+          <el-input v-model="form.accountNo" :placeholder="accountNoPlaceholder" maxlength="128" />
68
+          <p v-if="isEdit && form.accountNoMask" class="form-tip">当前账号:{{ form.accountNoMask }}(修改请填写完整账号)</p>
69
+        </el-form-item>
70
+      </el-form>
71
+      <div slot="footer" class="dialog-footer">
72
+        <el-button type="primary" @click="submitForm">确 定</el-button>
73
+        <el-button @click="cancel">取 消</el-button>
74
+      </div>
75
+    </el-dialog>
76
+  </div>
77
+</template>
78
+
79
+<script>
80
+import {
81
+  listSellerPayAccounts,
82
+  addSellerPayAccount,
83
+  updateSellerPayAccount,
84
+  delSellerPayAccount
85
+} from "@/api/agri/seller/finance/payAccount"
86
+import { getSellerContext } from "@/api/agri/seller/context"
87
+import { setSellerShopContext } from "@/utils/sellerShop"
88
+
89
+const ACCOUNT_TYPE_MAP = {
90
+  "1": "银行卡",
91
+  "2": "支付宝",
92
+  "3": "微信"
93
+}
94
+
95
+export default {
96
+  name: "AgriSellerFinancePayAccount",
97
+  data() {
98
+    const validateAccountNo = (rule, value, callback) => {
99
+      if (!value || !String(value).trim()) {
100
+        callback(new Error(this.accountNoRequiredMsg))
101
+        return
102
+      }
103
+      if (this.form.accountType === "1" && !/^\d+$/.test(String(value).trim())) {
104
+        callback(new Error("请输入正确的银行卡号"))
105
+        return
106
+      }
107
+      callback()
108
+    }
109
+    return {
110
+      loading: false,
111
+      total: 0,
112
+      accountList: [],
113
+      queryParams: {
114
+        pageNum: 1,
115
+        pageSize: 10
116
+      },
117
+      open: false,
118
+      title: "",
119
+      isEdit: false,
120
+      form: {},
121
+      accountTypeOptions: [
122
+        { value: "1", label: "银行卡" },
123
+        { value: "2", label: "支付宝" },
124
+        { value: "3", label: "微信" }
125
+      ],
126
+      rules: {
127
+        accountType: [{ required: true, message: "请选择收款方式", trigger: "change" }],
128
+        realName: [{ required: true, message: "请填写真实姓名", trigger: "blur" }],
129
+        accountNo: [{ validator: validateAccountNo, trigger: "blur" }]
130
+      }
131
+    }
132
+  },
133
+  computed: {
134
+    emptyTableText() {
135
+      return "暂无收款账户,请先添加"
136
+    },
137
+    accountNoLabel() {
138
+      if (this.form.accountType === "1") {
139
+        return "银行卡号"
140
+      }
141
+      if (this.form.accountType === "2") {
142
+        return "支付宝号"
143
+      }
144
+      if (this.form.accountType === "3") {
145
+        return "微信号"
146
+      }
147
+      return "账号"
148
+    },
149
+    accountNoPlaceholder() {
150
+      if (this.form.accountType === "1") {
151
+        return "请输入银行卡号"
152
+      }
153
+      if (this.form.accountType === "2") {
154
+        return "请输入支付宝号"
155
+      }
156
+      if (this.form.accountType === "3") {
157
+        return "请输入微信号"
158
+      }
159
+      return "请先选择收款方式"
160
+    },
161
+    accountNoRequiredMsg() {
162
+      if (this.form.accountType === "1") {
163
+        return "请填写银行卡号"
164
+      }
165
+      if (this.form.accountType === "2") {
166
+        return "请填写支付宝号"
167
+      }
168
+      if (this.form.accountType === "3") {
169
+        return "请填写微信号"
170
+      }
171
+      return "请填写账号"
172
+    }
173
+  },
174
+  created() {
175
+    this.initPage()
176
+  },
177
+  methods: {
178
+    accountTypeLabel(type) {
179
+      return ACCOUNT_TYPE_MAP[type] || "—"
180
+    },
181
+    /** 初始化:加载店铺上下文后拉列表 */
182
+    initPage() {
183
+      this.loadShopContext().then(() => {
184
+        this.getList()
185
+      }).catch(() => {
186
+        this.getList()
187
+      })
188
+    },
189
+    /** 加载商家端当前店铺上下文 */
190
+    loadShopContext() {
191
+      return getSellerContext().then(response => {
192
+        const data = response.data || {}
193
+        if (data.shopId != null) {
194
+          setSellerShopContext(data.shopId, data.shopName)
195
+        }
196
+      })
197
+    },
198
+    /** 查询账户列表 */
199
+    getList() {
200
+      this.loading = true
201
+      listSellerPayAccounts(this.queryParams).then(response => {
202
+        this.accountList = response.rows || []
203
+        this.total = response.total || 0
204
+        this.loading = false
205
+      }).catch(() => {
206
+        this.loading = false
207
+      })
208
+    },
209
+    /** 重置表单 */
210
+    reset() {
211
+      this.form = {
212
+        accountId: undefined,
213
+        accountType: "1",
214
+        realName: undefined,
215
+        accountNo: undefined,
216
+        accountNoMask: undefined
217
+      }
218
+      this.isEdit = false
219
+      if (this.$refs.form) {
220
+        this.resetForm("form")
221
+      }
222
+    },
223
+    /** 打开添加弹窗 */
224
+    handleAdd() {
225
+      this.reset()
226
+      this.title = "添加账户"
227
+      this.open = true
228
+    },
229
+    /** 打开编辑弹窗 */
230
+    handleUpdate(row) {
231
+      this.reset()
232
+      this.isEdit = true
233
+      this.title = "编辑账户"
234
+      this.form = {
235
+        accountId: row.accountId,
236
+        accountType: row.accountType,
237
+        realName: row.realName,
238
+        accountNo: undefined,
239
+        accountNoMask: row.accountNoMask
240
+      }
241
+      this.open = true
242
+    },
243
+    /** 切换收款方式时清空账号校验 */
244
+    handleAccountTypeChange() {
245
+      this.form.accountNo = undefined
246
+      this.$nextTick(() => {
247
+        if (this.$refs.form) {
248
+          this.$refs.form.clearValidate("accountNo")
249
+        }
250
+      })
251
+    },
252
+    /** 提交添加/编辑 */
253
+    submitForm() {
254
+      this.$refs.form.validate(valid => {
255
+        if (!valid) {
256
+          return
257
+        }
258
+        const payload = {
259
+          accountType: this.form.accountType,
260
+          realName: String(this.form.realName).trim(),
261
+          accountNo: String(this.form.accountNo).trim()
262
+        }
263
+        if (this.form.accountId != null) {
264
+          payload.accountId = this.form.accountId
265
+          updateSellerPayAccount(payload).then(() => {
266
+            this.$modal.msgSuccess("保存成功")
267
+            this.open = false
268
+            this.getList()
269
+          })
270
+        } else {
271
+          addSellerPayAccount(payload).then(() => {
272
+            this.$modal.msgSuccess("添加成功")
273
+            this.open = false
274
+            this.getList()
275
+          })
276
+        }
277
+      })
278
+    },
279
+    /** 删除账户 */
280
+    handleDelete(row) {
281
+      const name = row.accountTypeText || this.accountTypeLabel(row.accountType)
282
+      const mask = row.accountNoMask || ""
283
+      this.$modal.confirm(`确认删除该${name}账户「${mask}」吗?删除后不可在提现时选择,已提交的提现单不受影响。`).then(() => {
284
+        return delSellerPayAccount(row.accountId)
285
+      }).then(() => {
286
+        this.$modal.msgSuccess("删除成功")
287
+        this.getList()
288
+      }).catch(() => {})
289
+    },
290
+    /** 关闭弹窗 */
291
+    cancel() {
292
+      this.open = false
293
+      this.reset()
294
+    }
295
+  }
296
+}
297
+</script>
298
+
299
+<style scoped lang="scss">
300
+.card-header {
301
+  display: flex;
302
+  align-items: center;
303
+  justify-content: space-between;
304
+}
305
+.card-title {
306
+  font-weight: 600;
307
+  font-size: 15px;
308
+}
309
+.mb8 {
310
+  margin-bottom: 8px;
311
+}
312
+.form-tip {
313
+  margin: 6px 0 0;
314
+  font-size: 12px;
315
+  color: #909399;
316
+  line-height: 1.5;
317
+}
318
+</style>

+ 438 - 0
ruoyi-ui/src/views/agri/seller/finance/withdraw/index.vue

@@ -0,0 +1,438 @@
1
+<template>
2
+  <div class="app-container">
3
+    <!-- 提交提现 -->
4
+    <el-card shadow="never" class="submit-card" v-loading="balanceLoading">
5
+      <div slot="header" class="card-header">
6
+        <span class="card-title">提交提现</span>
7
+        <span class="balance-tip">可用店铺余额:<strong>{{ formatMoney(availableBalance) }}</strong></span>
8
+      </div>
9
+
10
+      <el-alert
11
+        v-if="accountOptions.length === 0"
12
+        title="暂无收款账户,请先在「账户管理」中添加银行卡/支付宝/微信账户后再提交提现"
13
+        type="warning"
14
+        :closable="false"
15
+        show-icon
16
+        class="mb12"
17
+      />
18
+
19
+      <el-form ref="submitForm" :model="submitForm" :rules="submitRules" label-width="100px" size="small" class="submit-form">
20
+        <el-row :gutter="24">
21
+          <el-col :xs="24" :sm="12" :md="8">
22
+            <el-form-item label="提现账户" prop="accountId">
23
+              <el-select
24
+                v-model="submitForm.accountId"
25
+                placeholder="请选择提现账户"
26
+                filterable
27
+                clearable
28
+                style="width: 100%"
29
+                :disabled="accountOptions.length === 0"
30
+              >
31
+                <el-option
32
+                  v-for="item in accountOptions"
33
+                  :key="item.accountId"
34
+                  :label="item.label || buildAccountLabel(item)"
35
+                  :value="item.accountId"
36
+                />
37
+              </el-select>
38
+            </el-form-item>
39
+          </el-col>
40
+          <el-col :xs="24" :sm="12" :md="8">
41
+            <el-form-item label="提现金额" prop="withdrawAmount">
42
+              <el-input-number
43
+                v-model="submitForm.withdrawAmount"
44
+                :min="0.01"
45
+                :precision="2"
46
+                :step="1"
47
+                controls-position="right"
48
+                style="width: 100%"
49
+                placeholder="请输入提现金额"
50
+              />
51
+            </el-form-item>
52
+          </el-col>
53
+          <el-col :xs="24" :sm="24" :md="8">
54
+            <el-form-item label="备注" prop="remark">
55
+              <el-input
56
+                v-model="submitForm.remark"
57
+                type="textarea"
58
+                :rows="2"
59
+                placeholder="选填"
60
+                maxlength="200"
61
+                show-word-limit
62
+              />
63
+            </el-form-item>
64
+          </el-col>
65
+        </el-row>
66
+        <el-form-item>
67
+          <el-button
68
+            type="primary"
69
+            :loading="submitting"
70
+            :disabled="accountOptions.length === 0"
71
+            @click="handleSubmit"
72
+            v-hasPermi="['agri:seller:finance:withdraw:submit']"
73
+          >提交提现</el-button>
74
+          <el-button @click="resetSubmitForm">重 置</el-button>
75
+        </el-form-item>
76
+      </el-form>
77
+    </el-card>
78
+
79
+    <br/>
80
+
81
+    <!-- 检索区 -->
82
+    <el-card shadow="never" class="search-card">
83
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
84
+        <el-form-item label="申请时间">
85
+          <el-date-picker
86
+            v-model="dateRange"
87
+            type="daterange"
88
+            value-format="yyyy-MM-dd"
89
+            range-separator="至"
90
+            start-placeholder="开始日期"
91
+            end-placeholder="结束日期"
92
+            style="width: 240px"
93
+          />
94
+        </el-form-item>
95
+        <el-form-item label="提现状态" prop="withdrawStatus">
96
+          <el-select v-model="queryParams.withdrawStatus" placeholder="全部" clearable style="width: 140px">
97
+            <el-option v-for="item in withdrawStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
98
+          </el-select>
99
+        </el-form-item>
100
+        <el-form-item>
101
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
102
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
103
+        </el-form-item>
104
+      </el-form>
105
+    </el-card>
106
+
107
+    <br/>
108
+
109
+    <!-- 提现记录 -->
110
+    <el-card shadow="never" class="table-card">
111
+      <div slot="header" class="card-header">
112
+        <span class="card-title">提现记录</span>
113
+      </div>
114
+
115
+      <el-row :gutter="10" class="mb8">
116
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
117
+      </el-row>
118
+
119
+      <el-table border v-loading="loading" :data="withdrawList" :empty-text="emptyTableText">
120
+        <el-table-column label="提现编号" align="center" prop="withdrawNo" min-width="170" :show-overflow-tooltip="true">
121
+          <template slot-scope="scope">
122
+            <span>{{ scope.row.withdrawNo || '—' }}</span>
123
+          </template>
124
+        </el-table-column>
125
+        <el-table-column label="申请时间" align="center" width="160">
126
+          <template slot-scope="scope">
127
+            <span>{{ parseTime(scope.row.applyTime) || '—' }}</span>
128
+          </template>
129
+        </el-table-column>
130
+        <el-table-column label="提现账户" align="center" min-width="200" :show-overflow-tooltip="true">
131
+          <template slot-scope="scope">
132
+            <span>{{ accountSummaryText(scope.row) }}</span>
133
+          </template>
134
+        </el-table-column>
135
+        <el-table-column label="提现金额" align="center" width="110">
136
+          <template slot-scope="scope">
137
+            <span>{{ formatMoney(scope.row.withdrawAmount) }}</span>
138
+          </template>
139
+        </el-table-column>
140
+        <el-table-column label="备注" align="center" min-width="120" :show-overflow-tooltip="true">
141
+          <template slot-scope="scope">
142
+            <span>{{ scope.row.remark || '—' }}</span>
143
+          </template>
144
+        </el-table-column>
145
+        <el-table-column label="提现状态" align="center" width="110">
146
+          <template slot-scope="scope">
147
+            <el-tag size="small" :type="withdrawStatusTag(scope.row.withdrawStatus)">
148
+              {{ scope.row.withdrawStatusText || withdrawStatusLabel(scope.row.withdrawStatus) }}
149
+            </el-tag>
150
+          </template>
151
+        </el-table-column>
152
+        <el-table-column label="提现处理说明" align="center" min-width="140" :show-overflow-tooltip="true">
153
+          <template slot-scope="scope">
154
+            <span>{{ processRemarkText(scope.row) }}</span>
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
+  </div>
162
+</template>
163
+
164
+<script>
165
+import {
166
+  listSellerWithdraws,
167
+  getSellerWithdrawBalance,
168
+  submitSellerWithdraw
169
+} from "@/api/agri/seller/finance/withdraw"
170
+import { listSellerPayAccountOptions } from "@/api/agri/seller/finance/payAccount"
171
+import { getSellerContext } from "@/api/agri/seller/context"
172
+import { setSellerShopContext } from "@/utils/sellerShop"
173
+
174
+const WITHDRAW_STATUS_MAP = {
175
+  "1": "待审核",
176
+  "2": "审核不通过",
177
+  "3": "提现完成"
178
+}
179
+
180
+const ACCOUNT_TYPE_MAP = {
181
+  "1": "银行卡",
182
+  "2": "支付宝",
183
+  "3": "微信"
184
+}
185
+
186
+export default {
187
+  name: "AgriSellerFinanceWithdraw",
188
+  data() {
189
+    const validateWithdrawAmount = (rule, value, callback) => {
190
+      if (value == null || value === "") {
191
+        callback(new Error("请输入有效的提现金额"))
192
+        return
193
+      }
194
+      const num = Number(value)
195
+      if (isNaN(num) || num <= 0) {
196
+        callback(new Error("请输入有效的提现金额"))
197
+        return
198
+      }
199
+      const balance = Number(this.availableBalance)
200
+      if (!isNaN(balance) && num > balance) {
201
+        callback(new Error("提现金额不能超过可用余额"))
202
+        return
203
+      }
204
+      callback()
205
+    }
206
+    return {
207
+      loading: false,
208
+      balanceLoading: false,
209
+      submitting: false,
210
+      showSearch: true,
211
+      total: 0,
212
+      availableBalance: null,
213
+      accountOptions: [],
214
+      withdrawList: [],
215
+      dateRange: [],
216
+      submitForm: {
217
+        accountId: undefined,
218
+        withdrawAmount: undefined,
219
+        remark: undefined
220
+      },
221
+      submitRules: {
222
+        accountId: [{ required: true, message: "请选择提现账户", trigger: "change" }],
223
+        withdrawAmount: [{ validator: validateWithdrawAmount, trigger: "blur" }]
224
+      },
225
+      queryParams: {
226
+        pageNum: 1,
227
+        pageSize: 10,
228
+        withdrawStatus: undefined,
229
+        beginApplyTime: undefined,
230
+        endApplyTime: undefined
231
+      },
232
+      withdrawStatusOptions: [
233
+        { value: "1", label: "待审核" },
234
+        { value: "2", label: "审核不通过" },
235
+        { value: "3", label: "提现完成" }
236
+      ]
237
+    }
238
+  },
239
+  computed: {
240
+    hasSearchFilter() {
241
+      return !!(this.queryParams.withdrawStatus || (this.dateRange && this.dateRange.length))
242
+    },
243
+    emptyTableText() {
244
+      return this.hasSearchFilter ? "未找到符合条件的提现记录" : "暂无提现记录"
245
+    }
246
+  },
247
+  created() {
248
+    this.initPage()
249
+  },
250
+  methods: {
251
+    /** 初始化:加载店铺上下文后拉数据 */
252
+    initPage() {
253
+      this.loadShopContext().then(() => {
254
+        this.loadBalance()
255
+        this.loadAccountOptions()
256
+        this.getList()
257
+      }).catch(() => {
258
+        this.loadBalance()
259
+        this.loadAccountOptions()
260
+        this.getList()
261
+      })
262
+    },
263
+    /** 加载商家端当前店铺上下文 */
264
+    loadShopContext() {
265
+      return getSellerContext().then(response => {
266
+        const data = response.data || {}
267
+        if (data.shopId != null) {
268
+          setSellerShopContext(data.shopId, data.shopName)
269
+        }
270
+      })
271
+    },
272
+    /** 查询可用余额 */
273
+    loadBalance() {
274
+      this.balanceLoading = true
275
+      getSellerWithdrawBalance().then(response => {
276
+        const data = response.data || {}
277
+        this.availableBalance = data.availableBalance
278
+        this.balanceLoading = false
279
+      }).catch(() => {
280
+        this.balanceLoading = false
281
+      })
282
+    },
283
+    /** 加载提现账户下拉 */
284
+    loadAccountOptions() {
285
+      listSellerPayAccountOptions().then(response => {
286
+        this.accountOptions = response.data || []
287
+      }).catch(() => {
288
+        this.accountOptions = []
289
+      })
290
+    },
291
+    /** 构建查询参数(含申请时间区间) */
292
+    buildQueryParams() {
293
+      const params = { ...this.queryParams }
294
+      if (this.dateRange && this.dateRange.length === 2) {
295
+        params.beginApplyTime = this.dateRange[0]
296
+        params.endApplyTime = this.dateRange[1]
297
+      } else {
298
+        params.beginApplyTime = undefined
299
+        params.endApplyTime = undefined
300
+      }
301
+      return params
302
+    },
303
+    /** 查询提现记录 */
304
+    getList() {
305
+      this.loading = true
306
+      listSellerWithdraws(this.buildQueryParams()).then(response => {
307
+        this.withdrawList = response.rows || []
308
+        this.total = response.total || 0
309
+        this.loading = false
310
+      }).catch(() => {
311
+        this.loading = false
312
+      })
313
+    },
314
+    handleQuery() {
315
+      this.queryParams.pageNum = 1
316
+      this.getList()
317
+    },
318
+    resetQuery() {
319
+      this.dateRange = []
320
+      this.resetForm("queryForm")
321
+      this.queryParams.pageNum = 1
322
+      this.queryParams.beginApplyTime = undefined
323
+      this.queryParams.endApplyTime = undefined
324
+      this.getList()
325
+    },
326
+    /** 提交提现 */
327
+    handleSubmit() {
328
+      this.$refs.submitForm.validate(valid => {
329
+        if (!valid) {
330
+          return
331
+        }
332
+        this.submitting = true
333
+        submitSellerWithdraw({
334
+          accountId: this.submitForm.accountId,
335
+          withdrawAmount: Number(this.submitForm.withdrawAmount),
336
+          remark: this.submitForm.remark ? String(this.submitForm.remark).trim() : undefined
337
+        }).then(() => {
338
+          this.$modal.msgSuccess("提交成功")
339
+          this.resetSubmitForm()
340
+          this.loadBalance()
341
+          this.getList()
342
+          this.submitting = false
343
+        }).catch(() => {
344
+          this.submitting = false
345
+        })
346
+      })
347
+    },
348
+    /** 重置提交表单 */
349
+    resetSubmitForm() {
350
+      this.submitForm = {
351
+        accountId: undefined,
352
+        withdrawAmount: undefined,
353
+        remark: undefined
354
+      }
355
+      if (this.$refs.submitForm) {
356
+        this.resetForm("submitForm")
357
+      }
358
+    },
359
+    /** 金额格式化 */
360
+    formatMoney(val) {
361
+      if (val == null || val === "") {
362
+        return "—"
363
+      }
364
+      const num = Number(val)
365
+      if (isNaN(num)) {
366
+        return "—"
367
+      }
368
+      const fixed = num.toFixed(2)
369
+      const parts = fixed.split(".")
370
+      parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
371
+      return "¥" + parts.join(".")
372
+    },
373
+    /** 账户下拉 label 兜底 */
374
+    buildAccountLabel(item) {
375
+      const type = item.accountTypeText || ACCOUNT_TYPE_MAP[item.accountType] || ""
376
+      const mask = item.accountNoMask || ""
377
+      const name = item.realName || ""
378
+      return [type, mask, name].filter(Boolean).join(" ")
379
+    },
380
+    /** 列表提现账户摘要 */
381
+    accountSummaryText(row) {
382
+      if (row.accountSummary) {
383
+        return row.accountSummary
384
+      }
385
+      const type = row.accountTypeText || ACCOUNT_TYPE_MAP[row.accountType] || ""
386
+      const mask = row.accountNoMask || ""
387
+      const name = row.accountRealName || ""
388
+      const text = [type, mask, name].filter(Boolean).join(" ")
389
+      return text || "—"
390
+    },
391
+    /** 处理说明:待审核展示 — */
392
+    processRemarkText(row) {
393
+      if (row.withdrawStatus === "1") {
394
+        return "—"
395
+      }
396
+      return row.processRemark || "—"
397
+    },
398
+    withdrawStatusLabel(status) {
399
+      return WITHDRAW_STATUS_MAP[status] || "—"
400
+    },
401
+    withdrawStatusTag(status) {
402
+      const map = { "1": "warning", "2": "danger", "3": "success" }
403
+      return map[status] || "info"
404
+    }
405
+  }
406
+}
407
+</script>
408
+
409
+<style scoped lang="scss">
410
+.card-header {
411
+  display: flex;
412
+  align-items: center;
413
+  justify-content: space-between;
414
+  flex-wrap: wrap;
415
+  gap: 8px;
416
+}
417
+.card-title {
418
+  font-weight: 600;
419
+  font-size: 15px;
420
+}
421
+.balance-tip {
422
+  font-size: 13px;
423
+  color: #606266;
424
+  strong {
425
+    color: #303133;
426
+    font-size: 15px;
427
+  }
428
+}
429
+.submit-form {
430
+  max-width: 1200px;
431
+}
432
+.mb8 {
433
+  margin-bottom: 8px;
434
+}
435
+.mb12 {
436
+  margin-bottom: 12px;
437
+}
438
+</style>