Files
hjc-web/app/pages/change-password.vue
T
weicw1996 81ef9317c7 feat(tender): 忘记密码与修改密码页(10 套模板通用)
密码找回(材料审核制,见 ADR 0008)
- app/pages/forgot-password.vue:两个页签——「提交申请」(企业名称 + 纳税人识别号 + 新密码 + 确认
  + 授权委托书,复用 POST /api/tender/upload,并照 HjcEnterpriseForm 做 5M 校验)与「查询进度」
  (双要素查询,展示状态与时间、驳回原因)。提交后自动把条件带到查询页签。文案纪律:
  「已通过」≠「已重置」。

修改密码(登录态,双因子)
- app/pages/change-password.vue:旧密码 + 短信验证码(60s 倒计时,照 register.vue)+ 新密码 +
  确认;成功后清本机凭据并显示「去登录」面板。

BFF 与封装
- server/api/tender/password/{apply.post,status.get,sms.post,change.put}.ts:四条都照
  register.post.ts 的透传范式(modulesApiBase + TenantId(header 与 query 都带)+ 原样返回
  ApiResult,HTTP 恒 200)。前两条**匿名**(与 sms/upload 一致,不读 cookie);后两条需登录态,
  从 cookie hjc_token 或 Authorization 头取 token 拼 Bearer。
- app/composables/useHjcPassword.ts:四条请求单独成一支,不塞进 useHjcAuth——找回的两条是匿名的、
  不做 401 跳转;修改密码的两条经 handleAuthCode 处理登录态。

接线
- app/pages/login.vue:加「忘记密码?」入口。
- app/components/HjcUserBar.vue:已登录菜单加「修改密码」。

落点说明:新页面直接放 app/pages/*.vue——静态段路由优先于 app/pages/[slug].vue 的动态段,故不必
动 useTemplate.ts / templates/index.ts,与 login.vue、register.vue 完全同款做法,10 套模板自动可用。

验证:针对本次文件 npx eslint 0 error;pnpm run build 成功且四条新 BFF 都出现在产物里。
未验:未起 mp-java 做端到端(见 .scratch/hjc-password/issues/06)。
2026-09-17 02:35:45 +08:00

172 lines
5.8 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 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>