a6b65ae918
这些页面未调用 usePageSeo,SSR 产物里没有 <title>,浏览器标签页显示 URL。 - login / register / forgot-password / change-password / buy / renewal / preview 统一按 contact.vue 的既有写法:await fetchSiteInfo() 后调 usePageSeo, 标题自动带上站点品牌后缀 - preview 页仅供内部调试,追加 robots: noindex,nofollow 避免被收录 - cases.vue 是 /cases → /case 的纯重定向壳,无需标题,未改动
184 lines
6.0 KiB
Vue
184 lines
6.0 KiB
Vue
<template>
|
||
<div class="flex min-h-[60vh] items-center justify-center px-4 py-10">
|
||
<div class="w-full max-w-md rounded-lg border border-gray-200 p-8">
|
||
<h1 class="text-center text-2xl font-bold text-gray-900">修改密码</h1>
|
||
|
||
<!-- 改完提示去重新登录:旧 token 依然有效,但用户此刻应该用新密码进来 -->
|
||
<div v-if="done" class="mt-6 space-y-4">
|
||
<p class="rounded-md bg-green-50 px-3 py-3 text-sm leading-relaxed text-green-700">
|
||
密码已修改成功,请使用新密码重新登录。
|
||
</p>
|
||
<p class="text-xs leading-relaxed text-gray-500">
|
||
本机已退出登录。其它设备上已登录的会话<strong>不会</strong>因此失效,如需全部下线请联系平台客服。
|
||
</p>
|
||
<NuxtLink
|
||
to="/login"
|
||
class="block w-full rounded-md bg-blue-600 py-3 text-center font-medium text-white hover:bg-blue-700"
|
||
>
|
||
去登录
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<form v-else class="mt-6 space-y-4" @submit.prevent="submitForm">
|
||
<div class="rounded-md bg-amber-50 px-3 py-2 text-xs leading-relaxed text-amber-700">
|
||
修改密码需要同时验证「旧密码」与「账号绑定手机号的短信验证码」。
|
||
短信发到账号注册时绑定的手机号,不可自行指定。
|
||
</div>
|
||
|
||
<div>
|
||
<label class="text-sm text-gray-600">旧密码</label>
|
||
<input v-model="form.oldPassword" type="password" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="请输入当前登录密码" />
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm text-gray-600">短信验证码</label>
|
||
<div class="mt-1 flex items-center gap-2">
|
||
<input
|
||
v-model="form.smsCode"
|
||
class="w-full rounded-md border border-gray-300 px-4 py-2 text-sm"
|
||
placeholder="发送至账号绑定手机号"
|
||
autocomplete="off"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="h-[38px] w-32 shrink-0 rounded-md border border-gray-300 text-xs text-gray-600 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||
:disabled="countdown > 0 || sending"
|
||
@click="sendCode"
|
||
>
|
||
{{ countdown > 0 ? `${countdown} 秒后重发` : sending ? '发送中...' : '发送验证码' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="text-sm text-gray-600">新密码</label>
|
||
<input v-model="form.newPassword" type="password" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="至少 8 位,且包含字母和数字" />
|
||
</div>
|
||
<div>
|
||
<label class="text-sm text-gray-600">确认新密码</label>
|
||
<input v-model="form.confirmPassword" type="password" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="请再次输入新密码" />
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
class="w-full rounded-md bg-blue-600 py-3 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||
:disabled="loading"
|
||
>
|
||
{{ loading ? '提交中...' : '确认修改' }}
|
||
</button>
|
||
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
const { sendChangePasswordSms, changePassword } = useHjcPassword()
|
||
const { isLoggedIn, logout } = useHjcAuth()
|
||
const { siteInfo, fetchSiteInfo } = useSite()
|
||
|
||
// 先取站点信息,保证标题能带上品牌名(与其它页面一致)
|
||
await fetchSiteInfo()
|
||
|
||
usePageSeo(
|
||
{
|
||
title: '修改密码',
|
||
path: '/change-password'
|
||
},
|
||
siteInfo.value
|
||
)
|
||
|
||
const PASSWORD_PATTERN = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/
|
||
const SMS_COUNTDOWN_SECONDS = 60
|
||
|
||
const form = ref({
|
||
oldPassword: '',
|
||
smsCode: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
const loading = ref(false)
|
||
const sending = ref(false)
|
||
const error = ref('')
|
||
const done = ref(false)
|
||
const countdown = ref(0)
|
||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
function stopCountdown() {
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
countdown.value = 0
|
||
}
|
||
|
||
async function sendCode() {
|
||
error.value = ''
|
||
sending.value = true
|
||
try {
|
||
const res = await sendChangePasswordSms()
|
||
if (!res.ok) {
|
||
error.value = res.message || '验证码发送失败'
|
||
return
|
||
}
|
||
countdown.value = SMS_COUNTDOWN_SECONDS
|
||
countdownTimer = setInterval(() => {
|
||
countdown.value -= 1
|
||
if (countdown.value <= 0) stopCountdown()
|
||
}, 1000)
|
||
} finally {
|
||
sending.value = false
|
||
}
|
||
}
|
||
|
||
onUnmounted(stopCountdown)
|
||
|
||
async function submitForm() {
|
||
error.value = ''
|
||
const f = form.value
|
||
if (!f.oldPassword) {
|
||
error.value = '请输入旧密码'
|
||
return
|
||
}
|
||
if (!f.smsCode.trim()) {
|
||
error.value = '请输入短信验证码'
|
||
return
|
||
}
|
||
if (!f.newPassword || !f.confirmPassword) {
|
||
error.value = '请输入新密码并再次确认'
|
||
return
|
||
}
|
||
if (f.newPassword !== f.confirmPassword) {
|
||
error.value = '两次输入的新密码不一致'
|
||
return
|
||
}
|
||
if (!PASSWORD_PATTERN.test(f.newPassword)) {
|
||
error.value = '密码至少 8 位,且包含字母和数字'
|
||
return
|
||
}
|
||
if (f.newPassword === f.oldPassword) {
|
||
error.value = '新密码不能与旧密码相同'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const res = await changePassword({
|
||
oldPassword: f.oldPassword,
|
||
smsCode: f.smsCode.trim(),
|
||
newPassword: f.newPassword,
|
||
confirmPassword: f.confirmPassword
|
||
})
|
||
if (!res.ok) {
|
||
error.value = res.message || '修改密码失败'
|
||
return
|
||
}
|
||
// 与「改密后需重新登录」的习惯一致:清本机凭据。
|
||
// 只影响本机——token 是无状态 JWT,平台不做服务端登出与黑名单,页面上已如实告知。
|
||
if (isLoggedIn()) await logout()
|
||
done.value = true
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|