Files
hjc-web/app/pages/forgot-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

272 lines
11 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-2xl rounded-lg border border-gray-200 p-8">
<h1 class="text-center text-2xl font-bold text-gray-900">忘记密码</h1>
<p class="mt-2 text-center text-sm text-gray-500">
提交企业材料后由平台人工审核审核通过后重置密码
</p>
<div class="mt-6 flex rounded-md border border-gray-200 text-sm">
<button
type="button"
class="flex-1 py-2"
:class="tab === 'apply' ? 'bg-blue-50 font-medium text-blue-600' : 'text-gray-600'"
@click="tab = 'apply'"
>
提交申请
</button>
<button
type="button"
class="flex-1 py-2"
:class="tab === 'query' ? 'bg-blue-50 font-medium text-blue-600' : 'text-gray-600'"
@click="tab = 'query'"
>
查询进度
</button>
</div>
<!-- ============ 提交申请 ============ -->
<form v-if="tab === 'apply'" class="mt-6 space-y-4" @submit.prevent="submitApply">
<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="applyForm.enterpriseName" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="请输入企业名称" />
</div>
<div>
<label class="text-sm text-gray-600">纳税人识别号</label>
<input v-model="applyForm.creditCode" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="请输入统一社会信用代码" />
</div>
<div>
<label class="text-sm text-gray-600">新密码</label>
<input v-model="applyForm.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="applyForm.confirmPassword" 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">授权委托书本次重新签署单张不超过 5M</label>
<div class="mt-1 flex items-center gap-2">
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="onPickHandbook" />
<button
type="button"
class="rounded-md border border-gray-300 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50"
@click="fileInput?.click()"
>
{{ handbookUrl ? '重新选择' : '选择图片' }}
</button>
<img v-if="handbookUrl" :src="handbookUrl" alt="授权委托书" class="h-10 w-16 rounded border border-gray-200 object-cover" />
<span v-if="handbookUrl" class="text-xs text-green-600">已上传</span>
</div>
<p class="mt-1 text-xs text-gray-400">
请上传本次新签署的授权委托书平台会与资质档案中留存的那份一并核对
</p>
</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>
<p v-if="submitted" class="text-center text-sm text-green-600">{{ submittedMessage }}</p>
</form>
<!-- ============ 查询进度 ============ -->
<form v-else class="mt-6 space-y-4" @submit.prevent="submitQuery">
<div>
<label class="text-sm text-gray-600">企业名称</label>
<input v-model="queryForm.enterpriseName" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" placeholder="请输入企业名称" />
</div>
<div>
<label class="text-sm text-gray-600">纳税人识别号</label>
<input v-model="queryForm.creditCode" 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>
<div v-if="queried" class="rounded-md border border-gray-200 p-4 text-sm">
<p v-if="!status" class="text-gray-500">
未查询到申请记录请核对企业名称与纳税人识别号是否与营业执照一致
</p>
<dl v-else class="space-y-2">
<div class="flex">
<dt class="w-24 shrink-0 text-gray-500">当前状态</dt>
<dd class="font-medium text-gray-900">{{ status.statusText }}</dd>
</div>
<div class="flex">
<dt class="w-24 shrink-0 text-gray-500">申请时间</dt>
<dd class="text-gray-700">{{ status.applyTime || '-' }}</dd>
</div>
<div v-if="status.auditTime" class="flex">
<dt class="w-24 shrink-0 text-gray-500">审核时间</dt>
<dd class="text-gray-700">{{ status.auditTime }}</dd>
</div>
<div v-if="status.resetTime" class="flex">
<dt class="w-24 shrink-0 text-gray-500">重置时间</dt>
<dd class="text-gray-700">{{ status.resetTime }}</dd>
</div>
<div v-if="status.rejectReason" class="flex">
<dt class="w-24 shrink-0 text-gray-500">驳回原因</dt>
<dd class="text-gray-700">{{ status.rejectReason }}</dd>
</div>
</dl>
<p v-if="status && status.status === 1" class="mt-3 rounded-md bg-amber-50 px-3 py-2 text-xs leading-relaxed text-amber-700">
审核已通过平台正在执行密码重置重置完成后状态会变为已重置」,届时可用新密码登录
</p>
</div>
</form>
<p class="mt-4 text-center text-sm text-gray-500">
<NuxtLink to="/login" class="text-blue-600 hover:underline">返回登录</NuxtLink>
</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { HjcPasswordApplyStatus } from '~/composables/useHjcPassword'
const { applyPasswordReset, fetchApplyStatus } = useHjcPassword()
const { siteInfo, fetchSiteInfo } = useSite()
// 先取站点信息,保证标题能带上品牌名(与其它页面一致)
await fetchSiteInfo()
usePageSeo(
{
title: '忘记密码',
path: '/forgot-password'
},
siteInfo.value
)
/** 密码强度:与后端(核心实例)一致,先在前端挡一道,免得提交后才被拒 */
const PASSWORD_PATTERN = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/
const MAX_UPLOAD_SIZE = 5 * 1024 * 1024
const tab = ref<'apply' | 'query'>('apply')
const loading = ref(false)
const error = ref('')
const submitted = ref(false)
const submittedMessage = ref('')
const applyForm = ref({
enterpriseName: '',
creditCode: '',
newPassword: '',
confirmPassword: ''
})
const handbookUrl = ref('')
const fileInput = ref<HTMLInputElement | null>(null)
const queryForm = ref({ enterpriseName: '', creditCode: '' })
const queried = ref(false)
const status = ref<HjcPasswordApplyStatus | null>(null)
/** 上传授权委托书:复用注册/资质页的匿名上传接口,单张 5M 上限与它们一致 */
async function onPickHandbook(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
if (file.size > MAX_UPLOAD_SIZE) {
error.value = '单张图片不超过 5M'
return
}
error.value = ''
try {
const fd = new FormData()
fd.append('file', file)
const res: any = await $fetch('/api/tender/upload', { method: 'POST', body: fd })
handbookUrl.value = res?.url || ''
if (!handbookUrl.value) error.value = '上传失败,请重试'
} catch (e: any) {
error.value = e?.data?.statusMessage || e?.data?.message || '上传失败,请重试'
}
}
async function submitApply() {
error.value = ''
submitted.value = false
const f = applyForm.value
if (!f.enterpriseName.trim() || !f.creditCode.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 (!handbookUrl.value) {
error.value = '请上传授权委托书'
return
}
loading.value = true
try {
const res = await applyPasswordReset({
enterpriseName: f.enterpriseName.trim(),
creditCode: f.creditCode.trim(),
newPassword: f.newPassword,
confirmPassword: f.confirmPassword,
handbookUrl: handbookUrl.value
})
if (!res.ok) {
error.value = res.message || '提交失败'
return
}
submitted.value = true
submittedMessage.value =
res.message ||
'申请已提交,请等待平台审核。审核期间可在「查询进度」查看结果;若长时间无进展,请核对所填信息是否与营业执照一致。'
// 提交后把条件带到查询页签,省掉用户再输一遍
queryForm.value.enterpriseName = f.enterpriseName.trim()
queryForm.value.creditCode = f.creditCode.trim()
} finally {
loading.value = false
}
}
async function submitQuery() {
error.value = ''
const f = queryForm.value
if (!f.enterpriseName.trim() || !f.creditCode.trim()) {
error.value = '请输入企业名称和纳税人识别号'
return
}
loading.value = true
try {
const res = await fetchApplyStatus(f.enterpriseName.trim(), f.creditCode.trim())
if (!res.ok) {
error.value = res.message || '查询失败'
return
}
status.value = res.data
queried.value = true
} finally {
loading.value = false
}
}
</script>