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 的纯重定向壳,无需标题,未改动
272 lines
11 KiB
Vue
272 lines
11 KiB
Vue
<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>
|