Skip to content

Commit 65924bb

Browse files
YYQHoroYunaiV
authored andcommitted
!617 feat: 支持通过短信重置后台密码
* feat: 支持通过短信重置后台密码
1 parent 791d590 commit 65924bb

File tree

7 files changed

+298
-5
lines changed

7 files changed

+298
-5
lines changed

src/api/login/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,8 @@ export const getCode = (data) => {
8585
export const reqCheck = (data) => {
8686
return request.postOriginal({ url: 'system/captcha/check', data })
8787
}
88+
89+
// 通过短信重置密码
90+
export const smsResetPassword = (data) => {
91+
return request.post({ url: '/system/auth/sms-reset-password', data })
92+
}

src/locales/en.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ export default {
140140
btnQRCode: 'QR code sign in',
141141
qrcode: 'Scan the QR code to log in',
142142
btnRegister: 'Sign up',
143-
SmsSendMsg: 'code has been sent'
143+
SmsSendMsg: 'code has been sent',
144+
resetPassword: "Reset Password",
145+
resetPasswordSuccess: "Reset Password Success",
146+
invalidTenantName:"Invalid Tenant Name"
144147
},
145148
captcha: {
146149
verification: 'Please complete security verification',

src/locales/zh-CN.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ export default {
141141
btnQRCode: '二维码登录',
142142
qrcode: '扫描二维码登录',
143143
btnRegister: '注册',
144-
SmsSendMsg: '验证码已发送'
144+
SmsSendMsg: '验证码已发送',
145+
resetPassword: "重置密码",
146+
resetPasswordSuccess: "重置密码成功",
147+
invalidTenantName: "无效的租户名称"
145148
},
146149
captcha: {
147150
verification: '请完成安全验证',

src/views/Login/Login.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
6060
<!-- 三方登录 -->
6161
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
62+
<!-- 忘记密码 -->
63+
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
6264
</div>
6365
</Transition>
6466
</div>
@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app'
7375
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
7476
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
7577
76-
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
78+
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
7779
7880
defineOptions({ name: 'Login' })
7981
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<template>
2+
<el-form
3+
v-show="getShow"
4+
ref="formSmsResetPassword"
5+
:model="resetPasswordData"
6+
:rules="rules"
7+
class="login-form"
8+
label-position="top"
9+
label-width="120px"
10+
size="large"
11+
>
12+
<el-row style="margin-right: -10px; margin-left: -10px">
13+
<!-- 租户名 -->
14+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
15+
<el-form-item>
16+
<LoginFormTitle style="width: 100%" />
17+
</el-form-item>
18+
</el-col>
19+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
20+
<el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
21+
<el-input
22+
v-model="resetPasswordData.tenantName"
23+
:placeholder="t('login.tenantNamePlaceholder')"
24+
:prefix-icon="iconHouse"
25+
type="primary"
26+
link
27+
/>
28+
</el-form-item>
29+
</el-col>
30+
<!-- 手机号 -->
31+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
32+
<el-form-item prop="mobile">
33+
<el-input
34+
v-model="resetPasswordData.mobile"
35+
:placeholder="t('login.mobileNumberPlaceholder')"
36+
:prefix-icon="iconCellphone"
37+
/>
38+
</el-form-item>
39+
</el-col>
40+
<Verify
41+
ref="verify"
42+
:captchaType="captchaType"
43+
:imgSize="{ width: '400px', height: '200px' }"
44+
mode="pop"
45+
@success="getSmsCode"
46+
/>
47+
<!-- 验证码 -->
48+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
49+
<el-form-item prop="code">
50+
<el-row :gutter="5" justify="space-between" style="width: 100%">
51+
<el-col :span="24">
52+
<el-input
53+
v-model="resetPasswordData.code"
54+
:placeholder="t('login.codePlaceholder')"
55+
:prefix-icon="iconCircleCheck"
56+
>
57+
<template #append>
58+
<span
59+
v-if="mobileCodeTimer <= 0"
60+
class="getMobileCode"
61+
style="cursor: pointer"
62+
@click="getCode"
63+
>
64+
{{ t('login.getSmsCode') }}
65+
</span>
66+
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
67+
{{ mobileCodeTimer }}秒后可重新获取
68+
</span>
69+
</template>
70+
</el-input>
71+
<!-- </el-button> -->
72+
</el-col>
73+
</el-row>
74+
</el-form-item>
75+
</el-col>
76+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
77+
<el-form-item prop="password">
78+
<InputPassword
79+
v-model="resetPasswordData.password"
80+
:placeholder="t('login.passwordPlaceholder')"
81+
style="width: 100%"
82+
strength="true"
83+
/>
84+
</el-form-item>
85+
</el-col>
86+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
87+
<el-form-item prop="check_password">
88+
<InputPassword
89+
v-model="resetPasswordData.check_password"
90+
:placeholder="t('login.checkPassword')"
91+
style="width: 100%"
92+
strength="true"
93+
/>
94+
</el-form-item>
95+
</el-col>
96+
<!-- 登录按钮 / 返回按钮 -->
97+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
98+
<el-form-item>
99+
<XButton
100+
:loading="loginLoading"
101+
:title="t('login.resetPassword')"
102+
class="w-[100%]"
103+
type="primary"
104+
@click="resetPassword()"
105+
/>
106+
</el-form-item>
107+
</el-col>
108+
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
109+
<el-form-item>
110+
<XButton
111+
:loading="loginLoading"
112+
:title="t('login.backLogin')"
113+
class="w-[100%]"
114+
@click="handleBackLogin()"
115+
/>
116+
</el-form-item>
117+
</el-col>
118+
</el-row>
119+
</el-form>
120+
</template>
121+
<script lang="ts" setup>
122+
import type { RouteLocationNormalizedLoaded } from 'vue-router'
123+
124+
import { useIcon } from '@/hooks/web/useIcon'
125+
126+
import { sendSmsCode, smsResetPassword } from '@/api/login'
127+
import LoginFormTitle from './LoginFormTitle.vue'
128+
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
129+
import { ElLoading } from 'element-plus'
130+
import * as authUtil from '@/utils/auth'
131+
import * as LoginApi from '@/api/login'
132+
defineOptions({ name: 'ForgetPasswordForm' })
133+
const verify = ref()
134+
135+
const { t } = useI18n()
136+
const message = useMessage()
137+
const { currentRoute, push } = useRouter()
138+
const formSmsResetPassword = ref()
139+
const loginLoading = ref(false)
140+
const iconHouse = useIcon({ icon: 'ep:house' })
141+
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
142+
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
143+
const { validForm } = useFormValid(formSmsResetPassword)
144+
const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
145+
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
146+
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
147+
148+
const validatePass2 = (rule, value, callback) => {
149+
if (value === '') {
150+
callback(new Error('请再次输入密码'))
151+
} else if (value !== resetPasswordData.password) {
152+
callback(new Error('两次输入密码不一致!'))
153+
} else {
154+
callback()
155+
}
156+
}
157+
158+
const rules = {
159+
tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
160+
mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
161+
password: [
162+
{
163+
required: true,
164+
min: 4,
165+
max: 16,
166+
validator: validatePass2,
167+
trigger: 'blur',
168+
message: '密码长度为4到16位'
169+
}
170+
],
171+
check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
172+
code: [required]
173+
}
174+
175+
const resetPasswordData = reactive({
176+
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
177+
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
178+
tenantName: '',
179+
username: '',
180+
password: '',
181+
check_password: '',
182+
mobile: '',
183+
code: ''
184+
})
185+
186+
const smsVO = reactive({
187+
tenantName: '',
188+
mobile: '',
189+
captchaVerification: '',
190+
scene: 23
191+
})
192+
const mobileCodeTimer = ref(0)
193+
const redirect = ref<string>('')
194+
195+
// 获取验证码
196+
const getCode = async () => {
197+
// 情况一,未开启:则直接发送验证码
198+
if (resetPasswordData.captchaEnable === 'false') {
199+
await getSmsCode({})
200+
} else {
201+
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行发送验证码
202+
// 弹出验证码
203+
verify.value.show()
204+
}
205+
}
206+
207+
const getSmsCode = async (params) => {
208+
if (resetPasswordData.tenantEnable === 'true') {
209+
await getTenantId()
210+
}
211+
smsVO.captchaVerification = params.captchaVerification
212+
smsVO.mobile = resetPasswordData.mobile
213+
await sendSmsCode(smsVO).then(async () => {
214+
message.success(t('login.SmsSendMsg'))
215+
// 设置倒计时
216+
mobileCodeTimer.value = 60
217+
let msgTimer = setInterval(() => {
218+
mobileCodeTimer.value = mobileCodeTimer.value - 1
219+
if (mobileCodeTimer.value <= 0) {
220+
clearInterval(msgTimer)
221+
}
222+
}, 1000)
223+
})
224+
}
225+
watch(
226+
() => currentRoute.value,
227+
(route: RouteLocationNormalizedLoaded) => {
228+
redirect.value = route?.query?.redirect as string
229+
},
230+
{
231+
immediate: true
232+
}
233+
)
234+
235+
const getTenantId = async () => {
236+
if (resetPasswordData.tenantEnable === 'true') {
237+
const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
238+
if (res == null) {
239+
message.error(t('login.invalidTenantName'))
240+
throw t('login.invalidTenantName')
241+
}
242+
authUtil.setTenantId(res)
243+
}
244+
}
245+
246+
// 重置密码
247+
const resetPassword = async () => {
248+
const data = await validForm()
249+
if (!data) return
250+
await getTenantId()
251+
loginLoading.value = true
252+
await smsResetPassword(resetPasswordData)
253+
.then(async () => {
254+
message.success(t('login.resetPasswordSuccess'))
255+
setLoginState(LoginStateEnum.LOGIN)
256+
})
257+
.catch(() => {})
258+
.finally(() => {
259+
loginLoading.value = false
260+
setTimeout(() => {
261+
const loadingInstance = ElLoading.service()
262+
loadingInstance.close()
263+
}, 400)
264+
})
265+
}
266+
</script>
267+
268+
<style lang="scss" scoped>
269+
:deep(.anticon) {
270+
&:hover {
271+
color: var(--el-color-primary) !important;
272+
}
273+
}
274+
275+
.smsbtn {
276+
margin-top: 33px;
277+
}
278+
</style>

src/views/Login/components/LoginForm.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
</el-checkbox>
6060
</el-col>
6161
<el-col :offset="6" :span="12">
62-
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
62+
<el-link style="float: right" type="primary" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">{{
63+
t('login.forgetPassword') }}</el-link>
6364
</el-col>
6465
</el-row>
6566
</el-form-item>

src/views/Login/components/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue'
44
import RegisterForm from './RegisterForm.vue'
55
import QrCodeForm from './QrCodeForm.vue'
66
import SSOLoginVue from './SSOLogin.vue'
7+
import ForgetPasswordForm from './ForgetPasswordForm.vue'
78

8-
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
9+
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }

0 commit comments

Comments
 (0)