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 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,支持权限/显示控制 | 默认 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` |
281
- | le-home | 后台主框架 | emit `homeInit/command/userChangePwd` |
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) | - |
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
- - **行为**:登录成功后保存用户信息到 store、设置 token、成功提示
448
- - **样式**:全部通过 CSS 变量消费(login-card 背景 / logo 圆形 / 输入框边框文字 / 登录按钮渐变 / 验证码表头 / 装饰线),跟随 LeTheme 一键换肤
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
- - **关键 computed**:`effectiveHeaderMenu`、`headerMenuBgColor`、`headerMenuTextColor`、`headerMenuActiveTextColor`
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` 事件,回调里 `$forceUpdate` 强制刷新 el-menu 的 prop
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 变量,监听 `le-theme-change` 后 `$forceUpdate` 强制刷新
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
 
@@ -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>
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leisure-core",
3
- "version": "0.6.95",
3
+ "version": "0.7.07",
4
4
  "description": "leisure-core是京心数据基于vue2.x开发的一套后台管理系统桌面端组件库,封装了大量实用的UI控件模板,非常方便开发者快速搭建前端应用",
5
5
  "private": false,
6
6
  "author": "北方乐逍遥(zcx7878)",