leisure-core 0.6.95 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROJECT_GUIDE.md +62 -42
- package/le-home/src/main.vue +9 -160
- package/le-login/src/ForgotPassword.vue +613 -0
- package/le-login/src/main.vue +61 -0
- package/package.json +1 -1
package/PROJECT_GUIDE.md
CHANGED
|
@@ -53,7 +53,7 @@ leisure-core/
|
|
|
53
53
|
│ └── index.js # LeTheme 主题服务(set/register/init)
|
|
54
54
|
└── le-*/ # 各功能组件(共 36 个,见下文)
|
|
55
55
|
├── index.js # 组件注册入口(标准 install 模式)
|
|
56
|
-
└── src/main.vue # 组件实现(部分含 LeHeaderMenu.vue / rmixins.js 等辅助文件)
|
|
56
|
+
└── src/main.vue # 组件实现(部分含 LeHeaderMenu.vue / ForgotPassword.vue / rmixins.js 等辅助文件)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## 4. 架构约定
|
|
@@ -270,39 +270,39 @@ mounted() {
|
|
|
270
270
|
|
|
271
271
|
## 7. 组件清单总览
|
|
272
272
|
|
|
273
|
-
| 组件 | 一句话用途
|
|
274
|
-
| ------------------------------------------ |
|
|
275
|
-
| le-button | 封装 el-button,支持权限/显示控制
|
|
276
|
-
| le-button-qr | 点击生成二维码并下载
|
|
277
|
-
| le-button-msg | 带 popconfirm 确认的按钮
|
|
278
|
-
| le-button-attach | 文件上传按钮(Excel/CSV 校验)
|
|
279
|
-
| le-dialog-container | 弹窗容器
|
|
280
|
-
| le-login |
|
|
281
|
-
| le-home |
|
|
282
|
-
| le-form / le-form-item | 表单容器(透传 el-form)
|
|
283
|
-
| le-input / le-input-number | 输入框/数字输入封装
|
|
284
|
-
| le-input-advanced | 金额/数值带单位换算输入
|
|
285
|
-
| le-input-validate | 带格式校验的输入框
|
|
286
|
-
| le-span | 文本展示
|
|
287
|
-
| le-url | 超链接展示
|
|
288
|
-
| le-select / le-select-option | 下拉封装(透传)
|
|
289
|
-
| le-select-search | 远程搜索下拉
|
|
290
|
-
| le-select-multi | 多级树形多选
|
|
291
|
-
| le-date-picker | 日期选择
|
|
292
|
-
| le-date-picker-auto | 自动识别秒/毫秒时间戳的日期选择
|
|
293
|
-
| le-table-edit | 可编辑表格(单元格/行编辑)
|
|
294
|
-
| le-list | 列表页(查询/分页/增删改)
|
|
295
|
-
| le-list-form | 列表配套表单弹窗
|
|
296
|
-
| le-report | 报表(vxe-table + 复杂表头 + 导出)
|
|
297
|
-
| le-upload-table | 附件管理
|
|
298
|
-
| le-media / le-media-upload / le-media-list | 素材库三件套
|
|
299
|
-
| le-image | 图片展示(http→https)
|
|
300
|
-
| le-video | 视频展示
|
|
301
|
-
| le-upload | 通用上传
|
|
302
|
-
| le-area | 地区级联选择
|
|
303
|
-
| le-menu | 角色菜单配置
|
|
304
|
-
| le-role | 角色管理(含子组件 LeRoleSub)
|
|
305
|
-
| le-role-user | 角色用户管理(含 LeRoleUserSub)
|
|
273
|
+
| 组件 | 一句话用途 | 关键交互模式 |
|
|
274
|
+
| ------------------------------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
275
|
+
| le-button | 封装 el-button,支持权限/显示控制 | 默认 slot |
|
|
276
|
+
| le-button-qr | 点击生成二维码并下载 | emit `qrMake` |
|
|
277
|
+
| le-button-msg | 带 popconfirm 确认的按钮 | emit `click` |
|
|
278
|
+
| le-button-attach | 文件上传按钮(Excel/CSV 校验) | emit `before-upload/success/error` |
|
|
279
|
+
| le-dialog-container | 弹窗容器 | - |
|
|
280
|
+
| le-login | 登录页(含忘记密码弹窗) | emit `handleLodin/loginAfter/sendCaptcha/sendForgotCaptcha/verifyForgotCaptcha/resetPassword` |
|
|
281
|
+
| le-home | 后台主框架(含 header 菜单/系统设置弹窗) | emit `homeInit/command/userChangePwd/savePerInfo` |
|
|
282
|
+
| le-form / le-form-item | 表单容器(透传 el-form) | 透传 `$attrs/$listeners` |
|
|
283
|
+
| le-input / le-input-number | 输入框/数字输入封装 | 透传 |
|
|
284
|
+
| le-input-advanced | 金额/数值带单位换算输入 | emit `input` |
|
|
285
|
+
| le-input-validate | 带格式校验的输入框 | emit `input/blur/focus/clear` |
|
|
286
|
+
| le-span | 文本展示 | - |
|
|
287
|
+
| le-url | 超链接展示 | required `value` |
|
|
288
|
+
| le-select / le-select-option | 下拉封装(透传) | 默认 slot |
|
|
289
|
+
| le-select-search | 远程搜索下拉 | emit `remoteMethod` |
|
|
290
|
+
| le-select-multi | 多级树形多选 | emit `input/change/leaf-change/clear/visible-change` |
|
|
291
|
+
| le-date-picker | 日期选择 | - |
|
|
292
|
+
| le-date-picker-auto | 自动识别秒/毫秒时间戳的日期选择 | emit `input` |
|
|
293
|
+
| le-table-edit | 可编辑表格(单元格/行编辑) | emit `cell-save/row-save/update:data/data-change/edit-cancel` |
|
|
294
|
+
| le-list | 列表页(查询/分页/增删改) | emit `getList/delRow/handleStatus/editCurrentRow/detailCurrentRow/close` |
|
|
295
|
+
| le-list-form | 列表配套表单弹窗 | emit `form-ready/saveData/closeDialog` |
|
|
296
|
+
| le-report | 报表(vxe-table + 复杂表头 + 导出) | - |
|
|
297
|
+
| le-upload-table | 附件管理 | 通过 props 注入 5 个 API |
|
|
298
|
+
| le-media / le-media-upload / le-media-list | 素材库三件套 | 通过 props 注入 API |
|
|
299
|
+
| le-image | 图片展示(http→https) | - |
|
|
300
|
+
| le-video | 视频展示 | - |
|
|
301
|
+
| le-upload | 通用上传 | - |
|
|
302
|
+
| le-area | 地区级联选择 | emit `areaChange` |
|
|
303
|
+
| le-menu | 角色菜单配置 | emit `close` |
|
|
304
|
+
| le-role | 角色管理(含子组件 LeRoleSub) | - |
|
|
305
|
+
| le-role-user | 角色用户管理(含 LeRoleUserSub) | - |
|
|
306
306
|
|
|
307
307
|
## 8. 组件 API 详解
|
|
308
308
|
|
|
@@ -439,21 +439,30 @@ mounted() {
|
|
|
439
439
|
|
|
440
440
|
### 8.20 le-login
|
|
441
441
|
|
|
442
|
-
- **Props**:`home`、`title`、`title1`、`captchaRequired`
|
|
442
|
+
- **Props**:`home`、`title`、`title1`、`captchaRequired`、`showForgotPassword`(是否开启忘记密码功能,默认 `true`)、`emailForgotEnabled`(是否开启邮箱找回方式,默认 `true`)
|
|
443
443
|
- **Events**:
|
|
444
444
|
- `handleLodin(...)`:账号、密码、验证码、成功/失败回调
|
|
445
445
|
- `loginAfter(data)`:登录成功后通知
|
|
446
446
|
- `sendCaptcha(...)`:触发发送验证码,含成功/失败回调
|
|
447
|
-
-
|
|
448
|
-
-
|
|
447
|
+
- `sendForgotCaptcha({ account, type, success, fail })`:忘记密码 - 发送验证码,`type` 为 `phone`/`email`
|
|
448
|
+
- `verifyForgotCaptcha({ account, captcha, type, success, fail })`:忘记密码 - 验证验证码
|
|
449
|
+
- `resetPassword({ account, captcha, newPwd, type, success, fail })`:忘记密码 - 重置密码
|
|
450
|
+
- **子组件**:`ForgotPassword`(`src/ForgotPassword.vue`,独立的忘记密码弹窗组件,自包含三步流程逻辑)
|
|
451
|
+
- **行为**:登录成功后保存用户信息到 store、设置 token、成功提示;忘记密码完成后自动回填账号到登录表单
|
|
452
|
+
- **忘记密码流程**(ForgotPassword 组件):
|
|
453
|
+
- 三步流程:验证身份 → 设置新密码 → 完成
|
|
454
|
+
- 支持手机/邮箱两种找回方式切换(`emailEnabled` 控制)
|
|
455
|
+
- 自定义紧凑进度条(圆形数字标记 + 短标签 + 连接线,不换行)
|
|
456
|
+
- v-model 控制弹窗显示,事件透传给宿主
|
|
457
|
+
- **样式**:全部通过 CSS 变量消费(login-card 背景 / logo 圆形 / 输入框边框文字 / 登录按钮渐变 / 验证码表头 / 装饰线 / 忘记密码弹窗),跟随 LeTheme 一键换肤
|
|
449
458
|
|
|
450
459
|
### 8.21 le-home
|
|
451
460
|
|
|
452
461
|
- **Props**:`extraDropdownItems`、`companyInfo`、`showClose`、`headerMenu`、`showHeaderMenu`
|
|
453
462
|
- **Slots**:`otherContainer`、`rightComment`
|
|
454
|
-
- **Events**:`homeInit`、`command(command)`(修改密码/退出之外的命令)、`userChangePwd(params, callback)`
|
|
463
|
+
- **Events**:`homeInit`、`command(command)`(修改密码/退出之外的命令)、`userChangePwd(params, callback)`、`savePerInfo(params)`
|
|
455
464
|
- **子组件**:`LeHeaderMenu`(`src/LeHeaderMenu.vue`,递归菜单组件,支持任意深度嵌套)
|
|
456
|
-
-
|
|
465
|
+
- **行为**:挂载后注册键盘/鼠标事件,内置修改密码弹窗、系统设置弹窗
|
|
457
466
|
- **Header 布局**:
|
|
458
467
|
- 三栏结构:左(品牌名)/ 中(横向菜单,可选)/ 右(用户下拉)
|
|
459
468
|
- 有 headerMenu 时 span 为 `4:12:8`,无时为 `8:0:16`
|
|
@@ -463,12 +472,22 @@ mounted() {
|
|
|
463
472
|
- `_filterHiddenMenus(list)`:递归过滤 `ishide === 1` 的菜单项
|
|
464
473
|
- `handleHeaderMenuSelect(item)`:复用侧边栏导航逻辑
|
|
465
474
|
- `getCssVar(name, fallback)`:读 CSS 变量的 helper(定义在 methods 里)
|
|
466
|
-
-
|
|
475
|
+
- `submitSysPwd()`:提交修改密码表单,emit `userChangePwd`
|
|
476
|
+
- `submitSysPer()`:提交修改个人信息表单,emit `savePerInfo`
|
|
477
|
+
- `selectSkin(skinName)`:一键切换皮肤
|
|
478
|
+
- **关键 computed**:`effectiveHeaderMenu`、`headerMenuBgColor`、`headerMenuTextColor`、`headerMenuActiveTextColor`、`asideStyle`、`menuBackgroundColor`、`menuTextColor`、`menuActiveTextColor`、`availableSkins`、`currentSkin`
|
|
479
|
+
- **系统设置弹窗**:
|
|
480
|
+
- 用户下拉菜单中"系统设置"项触发,el-dialog + el-tabs 实现
|
|
481
|
+
- 三个 Tab:修改密码 / 修改个人信息 / 设置皮肤
|
|
482
|
+
- 修改密码:原密码 / 新密码 / 确认密码,含校验
|
|
483
|
+
- 修改个人信息:昵称 / 手机号,含校验
|
|
484
|
+
- 设置皮肤:皮肤卡片列表(默认蓝 / 深色 / 翠绿),点击即切换,当前项高亮
|
|
485
|
+
- footer 按钮根据当前 tab 动态显示
|
|
467
486
|
- **样式换肤支持**:
|
|
468
487
|
- 顶部 header 背景 / aside 背景 / 拖动条 / aside_open_close / contextmenu 全部使用 `var(--le-color-*)`
|
|
469
488
|
- el-menu 通过 `background-color` / `text-color` / `active-text-color` prop 动态绑定 CSS 变量(computed + `getCssVar`)
|
|
470
489
|
- 横向菜单弹出层通过全局 `.le-header-menu-popper` 样式绑定 header 背景色
|
|
471
|
-
- 监听 `window` 级 `le-theme-change`
|
|
490
|
+
- 监听 `window` 级 `le-theme-change` 事件,回调里更新 `currentSkin` 触发依赖 CSS 变量的 computed 重算
|
|
472
491
|
|
|
473
492
|
### 8.22 le-area
|
|
474
493
|
|
|
@@ -560,7 +579,8 @@ mounted() {
|
|
|
560
579
|
9. **CSS 变量 + 服务层的换肤架构**:变量层(`vars.css`)只管语义与 Element UI 覆盖;服务层(`LeTheme`)管运行时切换/持久化/事件派发;消费层按"CSS 直接 var"与"prop 绑定 + 事件监听"两种模式消费,解耦且可扩展
|
|
561
580
|
10. **避免 window 污染**:`LeTheme` 单例挂到 Vue 原型而非 `window`,符合组件库"库不污染全局"的规范
|
|
562
581
|
11. **新增皮肤 checklist**:品牌色族 → 功能色 → 文本边框容器 → framework 全部变量,缺一不可,否则局部不跟随换肤
|
|
563
|
-
12. **el-menu 等 prop 驱动组件的换肤坑**:不能只改外层 CSS,必须把 prop(`background-color` 等)绑定到 computed,再用 `getCssVar` 读 CSS
|
|
582
|
+
12. **el-menu 等 prop 驱动组件的换肤坑**:不能只改外层 CSS,必须把 prop(`background-color` 等)绑定到 computed,再用 `getCssVar` 读 CSS 变量;监听 `le-theme-change` 后更新 `currentSkin` 响应式变量,computed 里用 `return this.currentSkin, this.getCssVar(...)` 逗号表达式建立响应式依赖,避免 `$forceUpdate` 和 `:key` 重建闪烁
|
|
583
|
+
13. **弹窗组件化 + v-model 模式**:`ForgotPassword` 独立成子组件,通过 `v-model`(`value` prop + `input` event)控制显隐,内部自包含多步流程状态(step/form/计时器),父组件只需透传业务事件(`sendCaptcha`/`verifyCaptcha`/`resetPassword`),职责清晰且可独立复用
|
|
564
584
|
|
|
565
585
|
## 12. 版本与发布
|
|
566
586
|
|
package/le-home/src/main.vue
CHANGED
|
@@ -138,71 +138,6 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</el-main>
|
|
140
140
|
</el-container>
|
|
141
|
-
<!-- <el-dialog
|
|
142
|
-
title="修改密码"
|
|
143
|
-
ref="elDialog"
|
|
144
|
-
:visible.sync="showChangePwd"
|
|
145
|
-
width="30%"
|
|
146
|
-
:close-on-click-modal="false"
|
|
147
|
-
@close="cancelChangePwd"
|
|
148
|
-
:show-close="showClose"
|
|
149
|
-
custom-class="rounded-dialog"
|
|
150
|
-
center
|
|
151
|
-
>
|
|
152
|
-
<el-form
|
|
153
|
-
:model="ruleForm"
|
|
154
|
-
status-icon
|
|
155
|
-
:rules="rules"
|
|
156
|
-
ref="ruleForm"
|
|
157
|
-
label-width="100px"
|
|
158
|
-
class="demo-ruleForm"
|
|
159
|
-
>
|
|
160
|
-
<el-form-item label="新密码" prop="pwd">
|
|
161
|
-
<el-input
|
|
162
|
-
type="password"
|
|
163
|
-
v-model="ruleForm.pwd"
|
|
164
|
-
placeholder="请输入新密码"
|
|
165
|
-
autocomplete="off"
|
|
166
|
-
></el-input>
|
|
167
|
-
</el-form-item>
|
|
168
|
-
<el-form-item label="确认密码" prop="confirmPwd">
|
|
169
|
-
<el-input
|
|
170
|
-
type="password"
|
|
171
|
-
v-model="ruleForm.confirmPwd"
|
|
172
|
-
placeholder="请输入确认密码"
|
|
173
|
-
autocomplete="off"
|
|
174
|
-
></el-input>
|
|
175
|
-
</el-form-item>
|
|
176
|
-
</el-form>
|
|
177
|
-
<div
|
|
178
|
-
ref="pwdref"
|
|
179
|
-
style="display: flex; justify-content: center; color: red"
|
|
180
|
-
>
|
|
181
|
-
{{ pwdinfo }}
|
|
182
|
-
</div>
|
|
183
|
-
<div slot="footer">
|
|
184
|
-
<el-button
|
|
185
|
-
type="info"
|
|
186
|
-
v-if="showClose"
|
|
187
|
-
@click="cancelChangePwd('ruleForm')"
|
|
188
|
-
>取 消</el-button
|
|
189
|
-
>
|
|
190
|
-
<el-button type="primary" @click="saveChangePwd('ruleForm')"
|
|
191
|
-
>确认修改</el-button
|
|
192
|
-
>
|
|
193
|
-
</div>
|
|
194
|
-
</el-dialog> -->
|
|
195
|
-
<!-- <le-dialog-container :showDialog="showPerInfo" title="个人信息" width="30%">
|
|
196
|
-
<le-list-form
|
|
197
|
-
:formColumns="formColumns"
|
|
198
|
-
:formData="perInfo"
|
|
199
|
-
:columnsPerRow="1"
|
|
200
|
-
:handleStatus="2"
|
|
201
|
-
@closeDialog="cancelChangePerInfo"
|
|
202
|
-
@saveData="savePerInfo"
|
|
203
|
-
></le-list-form>
|
|
204
|
-
</le-dialog-container> -->
|
|
205
|
-
|
|
206
141
|
<!-- 系统设置对话框 -->
|
|
207
142
|
<el-dialog
|
|
208
143
|
:visible.sync="sysSettingsVisible"
|
|
@@ -222,14 +157,6 @@
|
|
|
222
157
|
label-width="100px"
|
|
223
158
|
style="padding: 20px 0"
|
|
224
159
|
>
|
|
225
|
-
<!-- <el-form-item label="原密码" prop="oldPwd">
|
|
226
|
-
<el-input
|
|
227
|
-
v-model="sysPwdForm.oldPwd"
|
|
228
|
-
type="password"
|
|
229
|
-
placeholder="请输入原密码"
|
|
230
|
-
show-password
|
|
231
|
-
></el-input>
|
|
232
|
-
</el-form-item> -->
|
|
233
160
|
<el-form-item label="新密码" prop="newPwd">
|
|
234
161
|
<el-input
|
|
235
162
|
v-model="sysPwdForm.newPwd"
|
|
@@ -313,7 +240,6 @@
|
|
|
313
240
|
</template>
|
|
314
241
|
</div>
|
|
315
242
|
</el-dialog>
|
|
316
|
-
|
|
317
243
|
<div v-show="contextMenuVisible">
|
|
318
244
|
<ul
|
|
319
245
|
:style="{ left: menuLeft + 'px', top: menuTop + 'px' }"
|
|
@@ -417,22 +343,6 @@ export default {
|
|
|
417
343
|
// changeUid: "",
|
|
418
344
|
tabNameCurrent: "",
|
|
419
345
|
tabsAll: [],
|
|
420
|
-
// ruleForm: {
|
|
421
|
-
// pwd: "",
|
|
422
|
-
// confirmPwd: "",
|
|
423
|
-
// },
|
|
424
|
-
// formColumns: [
|
|
425
|
-
// { prop: "nick", label: "昵称" },
|
|
426
|
-
// { prop: "phone", label: "手机号" },
|
|
427
|
-
// ],
|
|
428
|
-
// perInfo: {
|
|
429
|
-
// nick: this.$store.getters.userinfo.nick,
|
|
430
|
-
// phone: this.$store.getters.userinfo.phone,
|
|
431
|
-
// },
|
|
432
|
-
// rules: {
|
|
433
|
-
// pwd: [{ validator: validatePass, trigger: "blur" }],
|
|
434
|
-
// confirmPwd: [{ validator: validatePass2, trigger: "blur" }],
|
|
435
|
-
// },
|
|
436
346
|
//右键菜单
|
|
437
347
|
contextMenuVisible: false,
|
|
438
348
|
menuLeft: 0,
|
|
@@ -588,18 +498,6 @@ export default {
|
|
|
588
498
|
}
|
|
589
499
|
return item;
|
|
590
500
|
});
|
|
591
|
-
|
|
592
|
-
//代码2
|
|
593
|
-
// this.menuData = this.$store.getters.menu;
|
|
594
|
-
// this.menuData = this.$store.getters.menu.filter(function (item) {
|
|
595
|
-
// if (item.children) {
|
|
596
|
-
// item.children = item.children.filter(function (item1) {
|
|
597
|
-
// return item1.ishide === 0;
|
|
598
|
-
// });
|
|
599
|
-
// }
|
|
600
|
-
// return item;
|
|
601
|
-
// });
|
|
602
|
-
|
|
603
501
|
this.$emit("homeInit");
|
|
604
502
|
document.addEventListener("keydown", this.handleKeyDown);
|
|
605
503
|
|
|
@@ -720,7 +618,6 @@ export default {
|
|
|
720
618
|
},
|
|
721
619
|
handleCommand(command) {
|
|
722
620
|
if (command === "openSysSettings") {
|
|
723
|
-
// 打开系统设置对话框,默认密码 tab
|
|
724
621
|
this.sysSettingsTab = "pwd";
|
|
725
622
|
this.sysPwdForm = { oldPwd: "", newPwd: "", confirmPwd: "" };
|
|
726
623
|
this.sysPerForm = {
|
|
@@ -734,6 +631,15 @@ export default {
|
|
|
734
631
|
this.$emit("command", command); // 抛出外部命令
|
|
735
632
|
}
|
|
736
633
|
},
|
|
634
|
+
changPwd() {
|
|
635
|
+
this.sysSettingsTab = "pwd";
|
|
636
|
+
this.sysPwdForm = { oldPwd: "", newPwd: "", confirmPwd: "" };
|
|
637
|
+
this.sysPerForm = {
|
|
638
|
+
nick: this.$store.getters.userinfo.nick || "",
|
|
639
|
+
phone: this.$store.getters.userinfo.phone || "",
|
|
640
|
+
};
|
|
641
|
+
this.sysSettingsVisible = true;
|
|
642
|
+
},
|
|
737
643
|
// 系统设置 - 提交修改密码
|
|
738
644
|
submitSysPwd() {
|
|
739
645
|
this.$refs.sysPwdForm.validate((valid) => {
|
|
@@ -776,68 +682,11 @@ export default {
|
|
|
776
682
|
this.$leTheme.set(skinName);
|
|
777
683
|
this.$message.success("皮肤切换成功");
|
|
778
684
|
},
|
|
779
|
-
// changPwd() {
|
|
780
|
-
// this.changeUid = this.$store.getters.userinfo.id;
|
|
781
|
-
// this.ruleForm.pwd = "";
|
|
782
|
-
// this.ruleForm.confirmPwd = "";
|
|
783
|
-
// this.showChangePwd = true;
|
|
784
|
-
// },
|
|
785
|
-
// changPerson() {
|
|
786
|
-
// this.changeUid = this.$store.getters.userinfo.id;
|
|
787
|
-
// this.perInfo.nick = this.$store.getters.userinfo.nick;
|
|
788
|
-
// this.perInfo.phone = this.$store.getters.userinfo.phone;
|
|
789
|
-
// this.showPerInfo = true;
|
|
790
|
-
// },
|
|
791
685
|
validatePhone(phone) {
|
|
792
686
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
|
793
687
|
return phoneRegex.test(phone);
|
|
794
688
|
},
|
|
795
|
-
// cancelChangePwd() {
|
|
796
|
-
// this.showChangePwd = false;
|
|
797
|
-
// this.changeUid = "";
|
|
798
|
-
// this.ruleForm.pwd = "";
|
|
799
|
-
// this.ruleForm.confirmPwd = "";
|
|
800
|
-
// },
|
|
801
|
-
// cancelChangePerInfo() {
|
|
802
|
-
// this.showPerInfo = false;
|
|
803
|
-
// this.perInfo.nick = "";
|
|
804
|
-
// this.perInfo.phone = "";
|
|
805
|
-
// },
|
|
806
|
-
// savePerInfo(param) {
|
|
807
|
-
// if (param.phone) {
|
|
808
|
-
// if (!this.validatePhone(param.phone)) {
|
|
809
|
-
// this.$message.error("请输入有效的手机号");
|
|
810
|
-
// return;
|
|
811
|
-
// }
|
|
812
|
-
// }
|
|
813
|
-
|
|
814
|
-
// if (!param.nick && !param.phone) {
|
|
815
|
-
// this.$message.error("昵称和手机号至少填写一项");
|
|
816
|
-
// return;
|
|
817
|
-
// }
|
|
818
689
|
|
|
819
|
-
// let params = {
|
|
820
|
-
// uid: this.$store.getters.userinfo.id,
|
|
821
|
-
// nick: param.nick,
|
|
822
|
-
// phone: param.phone,
|
|
823
|
-
// };
|
|
824
|
-
// this.$emit("savePerInfo", params);
|
|
825
|
-
// },
|
|
826
|
-
// saveChangePwd(formName) {
|
|
827
|
-
// this.$refs[formName].validate((valid) => {
|
|
828
|
-
// if (valid) {
|
|
829
|
-
// let params = {
|
|
830
|
-
// pwd: this.ruleForm.pwd,
|
|
831
|
-
// confirmPwd: this.ruleForm.confirmPwd,
|
|
832
|
-
// uid: this.changeUid,
|
|
833
|
-
// };
|
|
834
|
-
// this.$emit("userChangePwd", params, this.changePwdAfter);
|
|
835
|
-
// } else {
|
|
836
|
-
// console.log("error submit!!");
|
|
837
|
-
// return false;
|
|
838
|
-
// }
|
|
839
|
-
// });
|
|
840
|
-
// },
|
|
841
690
|
changePwdAfter(res) {
|
|
842
691
|
if (res && res.data && res.data.code == "10000") {
|
|
843
692
|
this.$message.success("更新成功");
|
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
title="找回密码"
|
|
4
|
+
:visible.sync="dialogVisible"
|
|
5
|
+
width="480px"
|
|
6
|
+
:close-on-click-modal="false"
|
|
7
|
+
custom-class="forgot-dialog"
|
|
8
|
+
append-to-body
|
|
9
|
+
@closed="resetForm"
|
|
10
|
+
>
|
|
11
|
+
<!-- 自定义紧凑步骤条 -->
|
|
12
|
+
<div class="forgot-progress">
|
|
13
|
+
<div
|
|
14
|
+
v-for="(s, i) in steps"
|
|
15
|
+
:key="i"
|
|
16
|
+
class="progress-item"
|
|
17
|
+
:class="{
|
|
18
|
+
active: step >= i,
|
|
19
|
+
done: step > i,
|
|
20
|
+
}"
|
|
21
|
+
>
|
|
22
|
+
<div class="progress-circle">
|
|
23
|
+
<i v-if="step > i" class="el-icon-check"></i>
|
|
24
|
+
<span v-else>{{ i + 1 }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<span class="progress-label">{{ s }}</span>
|
|
27
|
+
<div class="progress-line" v-if="i < steps.length - 1"></div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<el-form
|
|
32
|
+
ref="forgotForm"
|
|
33
|
+
:model="form"
|
|
34
|
+
:rules="rules"
|
|
35
|
+
label-width="0"
|
|
36
|
+
class="forgot-form"
|
|
37
|
+
v-if="step < 2"
|
|
38
|
+
>
|
|
39
|
+
<!-- 步骤1:验证身份 -->
|
|
40
|
+
<div v-if="step === 0">
|
|
41
|
+
<!-- 找回方式切换:手机/邮箱 -->
|
|
42
|
+
<div class="forgot-tabs" v-if="emailEnabled">
|
|
43
|
+
<span
|
|
44
|
+
class="forgot-tab"
|
|
45
|
+
:class="{ active: type === 'phone' }"
|
|
46
|
+
@click="switchType('phone')"
|
|
47
|
+
>手机找回</span
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
class="forgot-tab"
|
|
51
|
+
:class="{ active: type === 'email' }"
|
|
52
|
+
@click="switchType('email')"
|
|
53
|
+
>邮箱找回</span
|
|
54
|
+
>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<el-form-item prop="account">
|
|
58
|
+
<el-input
|
|
59
|
+
v-model="form.account"
|
|
60
|
+
:placeholder="
|
|
61
|
+
type === 'phone' ? '请输入手机号' : '请输入邮箱'
|
|
62
|
+
"
|
|
63
|
+
:prefix-icon="
|
|
64
|
+
type === 'phone' ? 'el-icon-mobile-phone' : 'el-icon-message'
|
|
65
|
+
"
|
|
66
|
+
class="forgot-input"
|
|
67
|
+
/>
|
|
68
|
+
</el-form-item>
|
|
69
|
+
<el-form-item prop="captcha">
|
|
70
|
+
<div class="forgot-captcha-wrapper">
|
|
71
|
+
<el-input
|
|
72
|
+
v-model="form.captcha"
|
|
73
|
+
placeholder="请输入6位验证码"
|
|
74
|
+
prefix-icon="el-icon-key"
|
|
75
|
+
maxlength="6"
|
|
76
|
+
class="forgot-captcha-input"
|
|
77
|
+
></el-input>
|
|
78
|
+
<el-button
|
|
79
|
+
class="forgot-captcha-btn"
|
|
80
|
+
type="primary"
|
|
81
|
+
:disabled="isDis || isCounting"
|
|
82
|
+
:loading="sending"
|
|
83
|
+
@click="sendCaptcha"
|
|
84
|
+
>
|
|
85
|
+
{{ isCounting ? `${retime}秒` : "获取验证码" }}
|
|
86
|
+
</el-button>
|
|
87
|
+
</div>
|
|
88
|
+
</el-form-item>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- 步骤2:设置新密码 -->
|
|
92
|
+
<div v-if="step === 1">
|
|
93
|
+
<el-form-item prop="newPwd">
|
|
94
|
+
<el-input
|
|
95
|
+
v-model="form.newPwd"
|
|
96
|
+
type="password"
|
|
97
|
+
placeholder="请输入新密码(6-20位)"
|
|
98
|
+
show-password
|
|
99
|
+
prefix-icon="el-icon-lock"
|
|
100
|
+
class="forgot-input"
|
|
101
|
+
/>
|
|
102
|
+
</el-form-item>
|
|
103
|
+
<el-form-item prop="confirmPwd">
|
|
104
|
+
<el-input
|
|
105
|
+
v-model="form.confirmPwd"
|
|
106
|
+
type="password"
|
|
107
|
+
placeholder="请确认新密码"
|
|
108
|
+
show-password
|
|
109
|
+
prefix-icon="el-icon-lock"
|
|
110
|
+
class="forgot-input"
|
|
111
|
+
@keydown.enter.native="submit"
|
|
112
|
+
/>
|
|
113
|
+
</el-form-item>
|
|
114
|
+
</div>
|
|
115
|
+
</el-form>
|
|
116
|
+
|
|
117
|
+
<!-- 步骤3:完成 -->
|
|
118
|
+
<div v-if="step === 2" class="forgot-success">
|
|
119
|
+
<i class="el-icon-success"></i>
|
|
120
|
+
<p>密码修改成功!</p>
|
|
121
|
+
<span>请使用新密码登录系统</span>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<span slot="footer" class="dialog-footer">
|
|
125
|
+
<el-button @click="dialogVisible = false">
|
|
126
|
+
{{ step === 2 ? "关闭" : "取消" }}
|
|
127
|
+
</el-button>
|
|
128
|
+
<el-button
|
|
129
|
+
v-if="step === 0"
|
|
130
|
+
type="primary"
|
|
131
|
+
:loading="submitting"
|
|
132
|
+
@click="nextStep"
|
|
133
|
+
>下一步</el-button
|
|
134
|
+
>
|
|
135
|
+
<el-button
|
|
136
|
+
v-if="step === 1"
|
|
137
|
+
type="primary"
|
|
138
|
+
:loading="submitting"
|
|
139
|
+
@click="submit"
|
|
140
|
+
>确认修改</el-button
|
|
141
|
+
>
|
|
142
|
+
<el-button v-if="step === 2" type="primary" @click="finish"
|
|
143
|
+
>去登录</el-button
|
|
144
|
+
>
|
|
145
|
+
</span>
|
|
146
|
+
</el-dialog>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<script>
|
|
150
|
+
export default {
|
|
151
|
+
name: "ForgotPassword",
|
|
152
|
+
props: {
|
|
153
|
+
// 是否显示弹窗(v-model 绑定)
|
|
154
|
+
value: {
|
|
155
|
+
type: Boolean,
|
|
156
|
+
default: false,
|
|
157
|
+
},
|
|
158
|
+
// 是否开启邮箱找回方式
|
|
159
|
+
emailEnabled: {
|
|
160
|
+
type: Boolean,
|
|
161
|
+
default: true,
|
|
162
|
+
},
|
|
163
|
+
// 默认找回方式
|
|
164
|
+
defaultType: {
|
|
165
|
+
type: String,
|
|
166
|
+
default: "phone",
|
|
167
|
+
validator: (v) => v === "phone" || v === "email",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
data() {
|
|
171
|
+
return {
|
|
172
|
+
step: 0, // 0:验证身份 1:设置新密码 2:完成
|
|
173
|
+
steps: ["验证", "重置", "完成"], // 紧凑步骤标签
|
|
174
|
+
type: this.defaultType, // 找回方式:phone/email
|
|
175
|
+
submitting: false,
|
|
176
|
+
sending: false,
|
|
177
|
+
isCounting: false,
|
|
178
|
+
isDis: false,
|
|
179
|
+
retime: 60,
|
|
180
|
+
countdownTimer: null,
|
|
181
|
+
form: {
|
|
182
|
+
account: "",
|
|
183
|
+
captcha: "",
|
|
184
|
+
newPwd: "",
|
|
185
|
+
confirmPwd: "",
|
|
186
|
+
},
|
|
187
|
+
rules: {
|
|
188
|
+
account: [
|
|
189
|
+
{ required: true, message: "请输入账号", trigger: "blur" },
|
|
190
|
+
{
|
|
191
|
+
validator: (rule, value, callback) => {
|
|
192
|
+
if (!value) return callback();
|
|
193
|
+
if (this.type === "phone") {
|
|
194
|
+
if (!/^1[3-9]\d{9}$/.test(value)) {
|
|
195
|
+
callback(new Error("请输入正确的手机号"));
|
|
196
|
+
} else {
|
|
197
|
+
callback();
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
201
|
+
callback(new Error("请输入正确的邮箱"));
|
|
202
|
+
} else {
|
|
203
|
+
callback();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
trigger: "blur",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
captcha: [
|
|
211
|
+
{ required: true, message: "请输入验证码", trigger: "blur" },
|
|
212
|
+
{ len: 6, message: "验证码长度为6位", trigger: "blur" },
|
|
213
|
+
{
|
|
214
|
+
pattern: /^\d{6}$/,
|
|
215
|
+
message: "验证码必须是6位数字",
|
|
216
|
+
trigger: "blur",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
newPwd: [
|
|
220
|
+
{ required: true, message: "请输入新密码", trigger: "blur" },
|
|
221
|
+
{
|
|
222
|
+
min: 6,
|
|
223
|
+
max: 20,
|
|
224
|
+
message: "长度在 6 到 20 个字符",
|
|
225
|
+
trigger: "blur",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
confirmPwd: [
|
|
229
|
+
{ required: true, message: "请确认新密码", trigger: "blur" },
|
|
230
|
+
{
|
|
231
|
+
validator: (rule, value, callback) => {
|
|
232
|
+
if (value !== this.form.newPwd) {
|
|
233
|
+
callback(new Error("两次输入的密码不一致"));
|
|
234
|
+
} else {
|
|
235
|
+
callback();
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
trigger: "blur",
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
computed: {
|
|
245
|
+
dialogVisible: {
|
|
246
|
+
get() {
|
|
247
|
+
return this.value;
|
|
248
|
+
},
|
|
249
|
+
set(val) {
|
|
250
|
+
this.$emit("input", val);
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
watch: {
|
|
255
|
+
value(val) {
|
|
256
|
+
if (val) {
|
|
257
|
+
// 打开时重置到第一步
|
|
258
|
+
this.step = 0;
|
|
259
|
+
this.type = this.defaultType;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
beforeDestroy() {
|
|
264
|
+
if (this.countdownTimer) {
|
|
265
|
+
clearInterval(this.countdownTimer);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
methods: {
|
|
269
|
+
// 切换找回方式
|
|
270
|
+
switchType(t) {
|
|
271
|
+
if (this.type === t) return;
|
|
272
|
+
this.type = t;
|
|
273
|
+
this.form.account = "";
|
|
274
|
+
this.form.captcha = "";
|
|
275
|
+
if (this.$refs.forgotForm) {
|
|
276
|
+
this.$refs.forgotForm.clearValidate(["account", "captcha"]);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// 重置表单
|
|
281
|
+
resetForm() {
|
|
282
|
+
this.step = 0;
|
|
283
|
+
this.type = this.defaultType;
|
|
284
|
+
this.form = {
|
|
285
|
+
account: "",
|
|
286
|
+
captcha: "",
|
|
287
|
+
newPwd: "",
|
|
288
|
+
confirmPwd: "",
|
|
289
|
+
};
|
|
290
|
+
this.submitting = false;
|
|
291
|
+
this.sending = false;
|
|
292
|
+
if (this.$refs.forgotForm) {
|
|
293
|
+
this.$refs.forgotForm.resetFields();
|
|
294
|
+
}
|
|
295
|
+
this.$emit("closed");
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// 发送验证码
|
|
299
|
+
sendCaptcha() {
|
|
300
|
+
if (!this.form.account) {
|
|
301
|
+
this.$message.error(
|
|
302
|
+
this.type === "phone" ? "请输入手机号" : "请输入邮箱"
|
|
303
|
+
);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 先校验账号格式
|
|
308
|
+
const isPhone = this.type === "phone";
|
|
309
|
+
const valid = isPhone
|
|
310
|
+
? /^1[3-9]\d{9}$/.test(this.form.account)
|
|
311
|
+
: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.form.account);
|
|
312
|
+
if (!valid) {
|
|
313
|
+
this.$message.error(isPhone ? "请输入正确的手机号" : "请输入正确的邮箱");
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.sending = true;
|
|
318
|
+
|
|
319
|
+
// emit 给父组件,由父组件调用发送验证码接口
|
|
320
|
+
this.$emit("sendCaptcha", {
|
|
321
|
+
account: this.form.account,
|
|
322
|
+
type: this.type,
|
|
323
|
+
success: this.startCountdown,
|
|
324
|
+
fail: () => {
|
|
325
|
+
this.sending = false;
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// 倒计时
|
|
331
|
+
startCountdown() {
|
|
332
|
+
this.sending = false;
|
|
333
|
+
this.isCounting = true;
|
|
334
|
+
this.isDis = true;
|
|
335
|
+
this.retime = 60;
|
|
336
|
+
|
|
337
|
+
this.countdownTimer = setInterval(() => {
|
|
338
|
+
this.retime--;
|
|
339
|
+
if (this.retime <= 0) {
|
|
340
|
+
clearInterval(this.countdownTimer);
|
|
341
|
+
this.isCounting = false;
|
|
342
|
+
this.isDis = false;
|
|
343
|
+
this.retime = 60;
|
|
344
|
+
}
|
|
345
|
+
}, 1000);
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// 下一步:验证身份
|
|
349
|
+
nextStep() {
|
|
350
|
+
this.$refs.forgotForm.validateField(["account", "captcha"], (errors) => {
|
|
351
|
+
if (!errors) {
|
|
352
|
+
this.submitting = true;
|
|
353
|
+
// emit 给父组件验证验证码,成功后进入下一步
|
|
354
|
+
this.$emit("verifyCaptcha", {
|
|
355
|
+
account: this.form.account,
|
|
356
|
+
captcha: this.form.captcha,
|
|
357
|
+
type: this.type,
|
|
358
|
+
success: () => {
|
|
359
|
+
this.submitting = false;
|
|
360
|
+
this.step = 1;
|
|
361
|
+
this.$nextTick(() => {
|
|
362
|
+
if (this.$refs.forgotForm) {
|
|
363
|
+
this.$refs.forgotForm.clearValidate();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
fail: (msg) => {
|
|
368
|
+
this.submitting = false;
|
|
369
|
+
this.$message.error(msg || "验证失败");
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
// 确认修改密码
|
|
377
|
+
submit() {
|
|
378
|
+
this.$refs.forgotForm.validateField(["newPwd", "confirmPwd"], (errors) => {
|
|
379
|
+
if (!errors) {
|
|
380
|
+
this.submitting = true;
|
|
381
|
+
// emit 给父组件调用重置密码接口
|
|
382
|
+
this.$emit("resetPassword", {
|
|
383
|
+
account: this.form.account,
|
|
384
|
+
captcha: this.form.captcha,
|
|
385
|
+
newPwd: this.form.newPwd,
|
|
386
|
+
type: this.type,
|
|
387
|
+
success: () => {
|
|
388
|
+
this.submitting = false;
|
|
389
|
+
this.step = 2;
|
|
390
|
+
},
|
|
391
|
+
fail: (msg) => {
|
|
392
|
+
this.submitting = false;
|
|
393
|
+
this.$message.error(msg || "修改失败");
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
// 完成去登录
|
|
401
|
+
finish() {
|
|
402
|
+
this.dialogVisible = false;
|
|
403
|
+
// 把账号回填给父组件,方便用户直接登录
|
|
404
|
+
this.$emit("finish", {
|
|
405
|
+
account: this.form.account,
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
</script>
|
|
411
|
+
|
|
412
|
+
<style lang="scss">
|
|
413
|
+
.forgot-dialog {
|
|
414
|
+
border-radius: 16px;
|
|
415
|
+
overflow: hidden;
|
|
416
|
+
|
|
417
|
+
.el-dialog__header {
|
|
418
|
+
padding: 20px 24px;
|
|
419
|
+
border-bottom: 1px solid var(--le-color-border-light);
|
|
420
|
+
|
|
421
|
+
.el-dialog__title {
|
|
422
|
+
font-size: 18px;
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
color: var(--le-color-text-strong);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.el-dialog__body {
|
|
429
|
+
padding: 24px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.el-dialog__footer {
|
|
433
|
+
padding: 16px 24px 20px;
|
|
434
|
+
border-top: 1px solid var(--le-color-border-light);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* 自定义紧凑步骤条 */
|
|
438
|
+
.forgot-progress {
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
margin-bottom: 28px;
|
|
442
|
+
padding: 0 8px;
|
|
443
|
+
|
|
444
|
+
.progress-item {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
flex: 1;
|
|
448
|
+
position: relative;
|
|
449
|
+
|
|
450
|
+
&:last-child {
|
|
451
|
+
flex: 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.progress-circle {
|
|
455
|
+
width: 28px;
|
|
456
|
+
height: 28px;
|
|
457
|
+
border-radius: 50%;
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
justify-content: center;
|
|
461
|
+
font-size: 13px;
|
|
462
|
+
font-weight: 600;
|
|
463
|
+
flex-shrink: 0;
|
|
464
|
+
background: var(--le-color-fill);
|
|
465
|
+
color: var(--le-color-text-muted);
|
|
466
|
+
border: 2px solid var(--le-color-border);
|
|
467
|
+
transition: all 0.25s ease;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.progress-label {
|
|
471
|
+
margin-left: 8px;
|
|
472
|
+
font-size: 13px;
|
|
473
|
+
color: var(--le-color-text-muted);
|
|
474
|
+
white-space: nowrap;
|
|
475
|
+
transition: color 0.25s ease;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.progress-line {
|
|
479
|
+
flex: 1;
|
|
480
|
+
height: 2px;
|
|
481
|
+
background: var(--le-color-border);
|
|
482
|
+
margin: 0 10px;
|
|
483
|
+
min-width: 20px;
|
|
484
|
+
transition: background 0.25s ease;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* 当前/已完成状态 */
|
|
488
|
+
&.active {
|
|
489
|
+
.progress-circle {
|
|
490
|
+
background: var(--le-color-primary);
|
|
491
|
+
border-color: var(--le-color-primary);
|
|
492
|
+
color: #fff;
|
|
493
|
+
box-shadow: 0 4px 12px var(--le-color-primary-shadow-soft);
|
|
494
|
+
}
|
|
495
|
+
.progress-label {
|
|
496
|
+
color: var(--le-color-text-strong);
|
|
497
|
+
font-weight: 600;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
&.done {
|
|
502
|
+
.progress-circle {
|
|
503
|
+
background: var(--le-color-success);
|
|
504
|
+
border-color: var(--le-color-success);
|
|
505
|
+
color: #fff;
|
|
506
|
+
}
|
|
507
|
+
.progress-line {
|
|
508
|
+
background: var(--le-color-success);
|
|
509
|
+
}
|
|
510
|
+
.progress-label {
|
|
511
|
+
color: var(--le-color-text-regular);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* 找回方式切换 tabs */
|
|
518
|
+
.forgot-tabs {
|
|
519
|
+
display: flex;
|
|
520
|
+
margin-bottom: 18px;
|
|
521
|
+
background: var(--le-color-fill-light);
|
|
522
|
+
border-radius: 8px;
|
|
523
|
+
padding: 4px;
|
|
524
|
+
|
|
525
|
+
.forgot-tab {
|
|
526
|
+
flex: 1;
|
|
527
|
+
text-align: center;
|
|
528
|
+
padding: 8px 0;
|
|
529
|
+
font-size: 13px;
|
|
530
|
+
color: var(--le-color-text-regular);
|
|
531
|
+
cursor: pointer;
|
|
532
|
+
border-radius: 6px;
|
|
533
|
+
transition: all 0.2s ease;
|
|
534
|
+
user-select: none;
|
|
535
|
+
|
|
536
|
+
&:hover {
|
|
537
|
+
color: var(--le-color-text-strong);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
&.active {
|
|
541
|
+
background: var(--le-color-card-bg);
|
|
542
|
+
color: var(--le-color-primary);
|
|
543
|
+
font-weight: 600;
|
|
544
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.forgot-form {
|
|
550
|
+
.el-form-item {
|
|
551
|
+
margin-bottom: 20px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.forgot-captcha-wrapper {
|
|
555
|
+
display: flex;
|
|
556
|
+
gap: 10px;
|
|
557
|
+
align-items: center;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.forgot-captcha-input {
|
|
561
|
+
flex: 1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.forgot-captcha-btn {
|
|
565
|
+
min-width: 110px;
|
|
566
|
+
height: 44px;
|
|
567
|
+
border-radius: 8px;
|
|
568
|
+
flex-shrink: 0;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.forgot-input .el-input__inner,
|
|
573
|
+
.forgot-captcha-input .el-input__inner {
|
|
574
|
+
height: 44px;
|
|
575
|
+
border-radius: 8px;
|
|
576
|
+
font-size: 14px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.forgot-success {
|
|
580
|
+
text-align: center;
|
|
581
|
+
padding: 20px 0 10px;
|
|
582
|
+
|
|
583
|
+
i {
|
|
584
|
+
font-size: 64px;
|
|
585
|
+
color: var(--le-color-success);
|
|
586
|
+
margin-bottom: 16px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
p {
|
|
590
|
+
font-size: 18px;
|
|
591
|
+
font-weight: 600;
|
|
592
|
+
color: var(--le-color-text-strong);
|
|
593
|
+
margin: 0 0 8px 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
span {
|
|
597
|
+
font-size: 13px;
|
|
598
|
+
color: var(--le-color-text-regular);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.dialog-footer {
|
|
603
|
+
display: flex;
|
|
604
|
+
justify-content: flex-end;
|
|
605
|
+
gap: 10px;
|
|
606
|
+
|
|
607
|
+
.el-button {
|
|
608
|
+
min-width: 90px;
|
|
609
|
+
border-radius: 8px;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
</style>
|
package/le-login/src/main.vue
CHANGED
|
@@ -108,18 +108,36 @@
|
|
|
108
108
|
<span v-else>登录中...</span>
|
|
109
109
|
</el-button>
|
|
110
110
|
</el-form-item>
|
|
111
|
+
|
|
112
|
+
<!-- 忘记密码链接 -->
|
|
113
|
+
<div class="forgot-link" v-if="showForgotPassword">
|
|
114
|
+
<a href="javascript:;" @click="openForgotDialog">忘记密码?</a>
|
|
115
|
+
</div>
|
|
111
116
|
</el-form>
|
|
112
117
|
</div>
|
|
113
118
|
</div>
|
|
114
119
|
</div>
|
|
115
120
|
</div>
|
|
121
|
+
|
|
122
|
+
<!-- 忘记密码弹窗(独立组件) -->
|
|
123
|
+
<ForgotPassword
|
|
124
|
+
v-if="showForgotPassword"
|
|
125
|
+
v-model="forgotVisible"
|
|
126
|
+
:email-enabled="emailForgotEnabled"
|
|
127
|
+
@sendCaptcha="$emit('sendForgotCaptcha', $event)"
|
|
128
|
+
@verifyCaptcha="$emit('verifyForgotCaptcha', $event)"
|
|
129
|
+
@resetPassword="$emit('resetPassword', $event)"
|
|
130
|
+
@finish="onForgotFinish"
|
|
131
|
+
/>
|
|
116
132
|
</div>
|
|
117
133
|
</template>
|
|
118
134
|
|
|
119
135
|
<script>
|
|
120
136
|
import { setToken } from "../../le-libs/js/tokens";
|
|
137
|
+
import ForgotPassword from "./ForgotPassword.vue";
|
|
121
138
|
export default {
|
|
122
139
|
name: "le-login",
|
|
140
|
+
components: { ForgotPassword },
|
|
123
141
|
props: {
|
|
124
142
|
home: {
|
|
125
143
|
type: String,
|
|
@@ -138,6 +156,16 @@ export default {
|
|
|
138
156
|
type: Boolean,
|
|
139
157
|
default: true,
|
|
140
158
|
},
|
|
159
|
+
// 是否开启忘记密码功能
|
|
160
|
+
showForgotPassword: {
|
|
161
|
+
type: Boolean,
|
|
162
|
+
default: true,
|
|
163
|
+
},
|
|
164
|
+
// 是否开启邮箱找回密码方式
|
|
165
|
+
emailForgotEnabled: {
|
|
166
|
+
type: Boolean,
|
|
167
|
+
default: true,
|
|
168
|
+
},
|
|
141
169
|
},
|
|
142
170
|
data() {
|
|
143
171
|
return {
|
|
@@ -184,6 +212,8 @@ export default {
|
|
|
184
212
|
retime: 60, // 倒计时时间
|
|
185
213
|
isDis: false, // 按钮是否禁用
|
|
186
214
|
countdownTimer: null, // 倒计时定时器
|
|
215
|
+
// 忘记密码弹窗显示状态(由 ForgotPassword 子组件管理其余状态)
|
|
216
|
+
forgotVisible: false,
|
|
187
217
|
};
|
|
188
218
|
},
|
|
189
219
|
mounted() {
|
|
@@ -310,6 +340,20 @@ export default {
|
|
|
310
340
|
}
|
|
311
341
|
}, 1000);
|
|
312
342
|
},
|
|
343
|
+
|
|
344
|
+
// ============ 忘记密码功能 ============
|
|
345
|
+
|
|
346
|
+
// 打开忘记密码弹窗
|
|
347
|
+
openForgotDialog() {
|
|
348
|
+
this.forgotVisible = true;
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
// 忘记密码完成回调:回填账号到登录表单
|
|
352
|
+
onForgotFinish({ account }) {
|
|
353
|
+
if (account) {
|
|
354
|
+
this.formdata.account = account;
|
|
355
|
+
}
|
|
356
|
+
},
|
|
313
357
|
},
|
|
314
358
|
};
|
|
315
359
|
</script>
|
|
@@ -731,6 +775,23 @@ export default {
|
|
|
731
775
|
margin-top: 10px;
|
|
732
776
|
}
|
|
733
777
|
|
|
778
|
+
.forgot-link {
|
|
779
|
+
text-align: right;
|
|
780
|
+
margin-top: -10px;
|
|
781
|
+
padding: 0 2px;
|
|
782
|
+
|
|
783
|
+
a {
|
|
784
|
+
font-size: 13px;
|
|
785
|
+
color: var(--le-color-text-regular);
|
|
786
|
+
text-decoration: none;
|
|
787
|
+
transition: color 0.2s ease;
|
|
788
|
+
|
|
789
|
+
&:hover {
|
|
790
|
+
color: var(--le-color-primary);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
734
795
|
.login-btn {
|
|
735
796
|
width: 100%;
|
|
736
797
|
height: 52px;
|