221 lines
5.1 KiB
Vue
221 lines
5.1 KiB
Vue
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
|
||
import { useLoginStore } from '@/stores/login'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const loginStore = useLoginStore()
|
||
|
||
const currentPassword = ref('')
|
||
const newPassword = ref('')
|
||
const confirmPassword = ref('')
|
||
const errorMessage = ref('')
|
||
const isSubmitting = ref(false)
|
||
|
||
function getAfterChangeRedirect() {
|
||
const redirect = route.query.redirect
|
||
if (
|
||
typeof redirect === 'string' &&
|
||
redirect.startsWith('/') &&
|
||
!redirect.startsWith('//') &&
|
||
redirect !== '/change-password'
|
||
) {
|
||
return redirect
|
||
}
|
||
return '/workbench/home'
|
||
}
|
||
|
||
function validatePassword() {
|
||
if (!currentPassword.value || !newPassword.value || !confirmPassword.value) {
|
||
return '请完整填写当前密码、新密码和确认密码'
|
||
}
|
||
if (newPassword.value.length < 12 || newPassword.value.length > 128) {
|
||
return '新密码长度必须为 12 至 128 位'
|
||
}
|
||
if (
|
||
!/[A-Z]/.test(newPassword.value) ||
|
||
!/[a-z]/.test(newPassword.value) ||
|
||
!/\d/.test(newPassword.value) ||
|
||
!/[^A-Za-z0-9]/.test(newPassword.value)
|
||
) {
|
||
return '新密码必须包含大写字母、小写字母、数字和特殊字符'
|
||
}
|
||
if (newPassword.value !== confirmPassword.value) {
|
||
return '两次输入的新密码不一致'
|
||
}
|
||
if (newPassword.value === currentPassword.value) {
|
||
return '新密码不能与当前密码相同'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
const validationMessage = validatePassword()
|
||
if (validationMessage) {
|
||
errorMessage.value = validationMessage
|
||
return
|
||
}
|
||
|
||
errorMessage.value = ''
|
||
isSubmitting.value = true
|
||
try {
|
||
await loginStore.changePassword({
|
||
currentPassword: currentPassword.value,
|
||
newPassword: newPassword.value,
|
||
})
|
||
await router.replace({
|
||
name: 'login',
|
||
query: { passwordChanged: '1', redirect: getAfterChangeRedirect() },
|
||
})
|
||
} catch (error: unknown) {
|
||
errorMessage.value =
|
||
error instanceof Error && error.message ? error.message : '密码修改失败,请稍后重试'
|
||
} finally {
|
||
isSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleLogout() {
|
||
try {
|
||
await loginStore.logout()
|
||
} catch {
|
||
loginStore.clearSession()
|
||
}
|
||
await router.replace({ name: 'login' })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<main class="password-page">
|
||
<form class="password-card" novalidate @submit.prevent="handleSubmit">
|
||
<div class="password-brand">医签通</div>
|
||
<h1>{{ loginStore.user?.passwordChangeRequired ? '首次登录,请修改密码' : '修改密码' }}</h1>
|
||
<p class="password-description">
|
||
{{
|
||
loginStore.user?.passwordChangeRequired
|
||
? '为保护患者信息,设置新密码后需要重新登录。'
|
||
: '修改密码后需要重新登录。'
|
||
}}
|
||
</p>
|
||
|
||
<label>
|
||
<span>当前密码</span>
|
||
<input v-model="currentPassword" type="password" autocomplete="current-password" />
|
||
</label>
|
||
<label>
|
||
<span>新密码</span>
|
||
<input v-model="newPassword" type="password" autocomplete="new-password" />
|
||
</label>
|
||
<label>
|
||
<span>确认新密码</span>
|
||
<input v-model="confirmPassword" type="password" autocomplete="new-password" />
|
||
</label>
|
||
|
||
<p class="password-policy">12–128 位,须同时包含大小写字母、数字和特殊字符。</p>
|
||
<p v-if="errorMessage" class="password-error" role="alert">{{ errorMessage }}</p>
|
||
|
||
<button class="password-submit" type="submit" :disabled="isSubmitting">
|
||
{{ isSubmitting ? '修改中…' : '修改密码并重新登录' }}
|
||
</button>
|
||
<button class="password-logout" type="button" :disabled="isSubmitting" @click="handleLogout">
|
||
退出当前账号
|
||
</button>
|
||
</form>
|
||
</main>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.password-page {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--login-gradient);
|
||
}
|
||
|
||
.password-card {
|
||
width: min(420px, calc(100% - 32px));
|
||
padding: 32px 36px;
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
box-shadow: var(--sh-login);
|
||
}
|
||
|
||
.password-brand {
|
||
color: var(--brand);
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
text-align: center;
|
||
}
|
||
|
||
h1 {
|
||
margin: 8px 0;
|
||
color: var(--ink);
|
||
font-size: 22px;
|
||
text-align: center;
|
||
}
|
||
|
||
.password-description,
|
||
.password-policy {
|
||
color: var(--mut);
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
text-align: center;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
label span {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: var(--mut);
|
||
font-size: 12px;
|
||
}
|
||
|
||
input {
|
||
width: 100%;
|
||
padding: 9px 10px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
outline: none;
|
||
}
|
||
|
||
input:focus {
|
||
border-color: var(--brand);
|
||
}
|
||
|
||
.password-error {
|
||
color: var(--err);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.password-submit,
|
||
.password-logout {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border-radius: 7px;
|
||
}
|
||
|
||
.password-submit {
|
||
color: #fff;
|
||
font-weight: 700;
|
||
background: var(--brand);
|
||
}
|
||
|
||
.password-logout {
|
||
margin-top: 8px;
|
||
color: var(--mut);
|
||
background: transparent;
|
||
}
|
||
|
||
button:disabled {
|
||
cursor: wait;
|
||
opacity: 0.65;
|
||
}
|
||
</style>
|