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)。
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<span class="text-gray-500">{{ displayName }}</span>
|
||||
<NuxtLink to="/tender/orders" class="text-blue-600 hover:underline">我的订单</NuxtLink>
|
||||
<NuxtLink to="/tender/qualification" class="text-blue-600 hover:underline">企业资质</NuxtLink>
|
||||
<NuxtLink to="/change-password" class="text-blue-600 hover:underline">修改密码</NuxtLink>
|
||||
<button class="text-gray-500 hover:text-red-600" @click="doLogout">退出</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 密码找回(材料审核制)与修改密码的前端封装。
|
||||
*
|
||||
* 独立成一支而不是塞进 `useHjcAuth`:后者已承担登录/注册/退出,这里只放密码相关的四条请求。
|
||||
*
|
||||
* 两条链路的失败语义不同,注意别混:
|
||||
* - **找回**(申请 / 查进度)是匿名的,后端对「企业不存在 / 信息不匹配」一律回同一句话,
|
||||
* 前端拿不到区分性的原因,也不该试图区分。
|
||||
* - **修改密码**的发送短信与提交都需登录态,401 要清凭据并跳登录页(交给 `handleAuthCode`)。
|
||||
*/
|
||||
export interface HjcPasswordApplyStatus {
|
||||
status: number
|
||||
statusText: string
|
||||
applyTime?: string | null
|
||||
auditTime?: string | null
|
||||
resetTime?: string | null
|
||||
rejectReason?: string | null
|
||||
}
|
||||
|
||||
export function useHjcPassword() {
|
||||
const { handleAuthCode } = useHjcAuth()
|
||||
|
||||
/** Nuxt 的 createError 会把后端的 statusMessage 放进 e.data,这里统一收口取文案 */
|
||||
function errMessage(e: any, fallback: string) {
|
||||
return e?.data?.statusMessage || e?.data?.message || e?.message || fallback
|
||||
}
|
||||
|
||||
/** 提交找回申请(匿名)。成功时后端的 message 是统一话术,不含任何企业存在性信息 */
|
||||
async function applyPasswordReset(payload: Record<string, any>) {
|
||||
try {
|
||||
const res: any = await $fetch('/api/tender/password/apply', { method: 'POST', body: payload })
|
||||
if (res?.code === 0) {
|
||||
return { ok: true, message: res?.message as string }
|
||||
}
|
||||
return { ok: false, message: res?.message || '提交失败' }
|
||||
} catch (e: any) {
|
||||
return { ok: false, message: errMessage(e, '提交失败') }
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询找回进度(匿名)。data 为 null 表示没有记录 */
|
||||
async function fetchApplyStatus(enterpriseName: string, creditCode: string) {
|
||||
try {
|
||||
const res: any = await $fetch('/api/tender/password/status', {
|
||||
query: { enterpriseName, creditCode }
|
||||
})
|
||||
if (res?.code === 0) {
|
||||
return { ok: true, data: (res.data || null) as HjcPasswordApplyStatus | null }
|
||||
}
|
||||
return { ok: false, data: null, message: res?.message || '查询失败' }
|
||||
} catch (e: any) {
|
||||
return { ok: false, data: null, message: errMessage(e, '查询失败') }
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送修改密码的短信验证码(登录态)。收件号由后端从登录态取,前端不能指定 */
|
||||
async function sendChangePasswordSms() {
|
||||
try {
|
||||
const res: any = await $fetch('/api/tender/password/sms', { method: 'POST' })
|
||||
if (res?.code === 0) {
|
||||
return { ok: true, message: res?.message || '验证码已发送' }
|
||||
}
|
||||
const auth = await handleAuthCode(res, '/change-password')
|
||||
if (auth.handled) return { ok: false, message: auth.message || '请先登录' }
|
||||
return { ok: false, message: res?.message || '验证码发送失败' }
|
||||
} catch (e: any) {
|
||||
return { ok: false, message: errMessage(e, '验证码发送失败') }
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交修改密码(登录态,旧密码 + 短信验证码) */
|
||||
async function changePassword(payload: Record<string, any>) {
|
||||
try {
|
||||
const res: any = await $fetch('/api/tender/password/change', { method: 'PUT', body: payload })
|
||||
if (res?.code === 0) {
|
||||
return { ok: true, message: res?.message || '密码修改成功' }
|
||||
}
|
||||
const auth = await handleAuthCode(res, '/change-password')
|
||||
if (auth.handled) return { ok: false, message: auth.message || '请先登录' }
|
||||
return { ok: false, message: res?.message || '修改密码失败' }
|
||||
} catch (e: any) {
|
||||
return { ok: false, message: errMessage(e, '修改密码失败') }
|
||||
}
|
||||
}
|
||||
|
||||
return { applyPasswordReset, fetchApplyStatus, sendChangePasswordSms, changePassword }
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<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>
|
||||
@@ -0,0 +1,259 @@
|
||||
<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 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>
|
||||
@@ -47,6 +47,9 @@
|
||||
还没有账号?
|
||||
<NuxtLink to="/register" class="text-blue-600 hover:underline">立即注册企业</NuxtLink>
|
||||
</p>
|
||||
<p class="mt-2 text-center text-sm text-gray-500">
|
||||
<NuxtLink to="/forgot-password" class="text-blue-600 hover:underline">忘记密码?</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, readBody } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 密码找回-提交申请(**匿名**,企业未登录时提交)
|
||||
* POST /api/tender/password/apply { enterpriseName, creditCode, newPassword, confirmPassword, handbookUrl }
|
||||
* 代理到 mp-api /api/hjc/auth/password/apply。
|
||||
*
|
||||
* 注意:该接口对「企业不存在 / 双要素不匹配 / 已有待审核申请」一律返回**同一句话且 data 为 null**,
|
||||
* 以免暴露某个企业是否在本平台注册过。前端因此不能靠 message 区分结果,只提示「已提交」并引导去查进度。
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const ctx = getTenantFromContext(event, config)
|
||||
const body = await readBody(event)
|
||||
|
||||
try {
|
||||
return await $fetch('/hjc/auth/password/apply', {
|
||||
baseURL: config.public.modulesApiBase,
|
||||
method: 'POST',
|
||||
headers: { TenantId: ctx.tenantId },
|
||||
query: { TenantId: ctx.tenantId },
|
||||
body
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.data?.message || error?.statusMessage || '提交失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 修改密码(**需登录态**,双因子:旧密码 + 账号绑定手机号短信验证码)
|
||||
* PUT /api/tender/password/change { oldPassword, smsCode, newPassword, confirmPassword }
|
||||
* 代理到 mp-api /api/hjc/auth/password/change。
|
||||
*
|
||||
* 原样透传 ApiResult:后端对「旧密码不正确 / 短信验证码不正确 / 密码强度不够」都有明确文案,
|
||||
* 由前端直接展示。
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const ctx = getTenantFromContext(event, config)
|
||||
const body = await readBody(event)
|
||||
const cookieToken = getCookie(event, 'hjc_token')
|
||||
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
|
||||
|
||||
try {
|
||||
return await $fetch('/hjc/auth/password/change', {
|
||||
baseURL: config.public.modulesApiBase,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
TenantId: ctx.tenantId,
|
||||
...(auth ? { Authorization: auth } : {})
|
||||
},
|
||||
query: { TenantId: ctx.tenantId },
|
||||
body
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.data?.message || error?.statusMessage || '修改密码失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, getHeader, getCookie } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 修改密码-发送短信验证码(**需登录态**)
|
||||
* POST /api/tender/password/sms(无 body)
|
||||
* 代理到 mp-api /api/hjc/auth/password/sms。
|
||||
*
|
||||
* 收件号由后端从登录态里取核心实例上的手机号(账号绑定手机号),**前端不能指定号码**——
|
||||
* 否则任何人都能给任意手机号发短信。
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const ctx = getTenantFromContext(event, config)
|
||||
const cookieToken = getCookie(event, 'hjc_token')
|
||||
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
|
||||
|
||||
try {
|
||||
return await $fetch('/hjc/auth/password/sms', {
|
||||
baseURL: config.public.modulesApiBase,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
TenantId: ctx.tenantId,
|
||||
...(auth ? { Authorization: auth } : {})
|
||||
},
|
||||
query: { TenantId: ctx.tenantId }
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.data?.message || error?.statusMessage || '验证码发送失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { $fetch } from 'ofetch'
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { getTenantFromContext } from '../../../utils/tenant'
|
||||
|
||||
/**
|
||||
* 密码找回-查询进度(**匿名**)
|
||||
* GET /api/tender/password/status?enterpriseName=&creditCode=
|
||||
* 代理到 mp-api /api/hjc/auth/password/apply/status。
|
||||
*
|
||||
* 返回的 data 为 null 表示「未查询到申请记录」(查不到企业与命中多条脏数据都归为这一种,
|
||||
* 同样不区分企业是否存在);有记录时只含状态与时间,**不含材料与新密码**。
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const ctx = getTenantFromContext(event, config)
|
||||
const query = getQuery(event)
|
||||
|
||||
try {
|
||||
return await $fetch('/hjc/auth/password/apply/status', {
|
||||
baseURL: config.public.modulesApiBase,
|
||||
method: 'GET',
|
||||
headers: { TenantId: ctx.tenantId },
|
||||
query: {
|
||||
TenantId: ctx.tenantId,
|
||||
enterpriseName: query.enterpriseName,
|
||||
creditCode: query.creditCode
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||
statusMessage: error?.data?.message || error?.statusMessage || '查询失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user