Files
hjc-web/app/pages/change-password.vue
T
weicw1996 a6b65ae918 feat(hjc-web): 为登录/注册/找回密码等 7 个页面补 title 标签
这些页面未调用 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 的纯重定向壳,无需标题,未改动
2026-09-19 02:05:10 +08:00

184 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>