diff --git a/app/components/HjcUserBar.vue b/app/components/HjcUserBar.vue
index b302800..8432f11 100644
--- a/app/components/HjcUserBar.vue
+++ b/app/components/HjcUserBar.vue
@@ -4,6 +4,7 @@
{{ displayName }}
我的订单
企业资质
+ 修改密码
diff --git a/app/composables/useHjcPassword.ts b/app/composables/useHjcPassword.ts
new file mode 100644
index 0000000..ae0a7f0
--- /dev/null
+++ b/app/composables/useHjcPassword.ts
@@ -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) {
+ 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) {
+ 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 }
+}
diff --git a/app/pages/change-password.vue b/app/pages/change-password.vue
new file mode 100644
index 0000000..edad1e2
--- /dev/null
+++ b/app/pages/change-password.vue
@@ -0,0 +1,171 @@
+
+
+
+
修改密码
+
+
+
+
+ 密码已修改成功,请使用新密码重新登录。
+
+
+ 本机已退出登录。其它设备上已登录的会话不会因此失效,如需全部下线请联系平台客服。
+
+
+ 去登录
+
+
+
+
+
+
+
+
+
diff --git a/app/pages/forgot-password.vue b/app/pages/forgot-password.vue
new file mode 100644
index 0000000..dc3abc4
--- /dev/null
+++ b/app/pages/forgot-password.vue
@@ -0,0 +1,259 @@
+
+
+
+
忘记密码
+
+ 提交企业材料后由平台人工审核,审核通过后重置密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 返回登录
+
+
+
+
+
+
diff --git a/app/pages/login.vue b/app/pages/login.vue
index 1e50745..d8b0fac 100644
--- a/app/pages/login.vue
+++ b/app/pages/login.vue
@@ -47,6 +47,9 @@
还没有账号?
立即注册企业
+
+ 忘记密码?
+
diff --git a/server/api/tender/password/apply.post.ts b/server/api/tender/password/apply.post.ts
new file mode 100644
index 0000000..8137a4e
--- /dev/null
+++ b/server/api/tender/password/apply.post.ts
@@ -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 || '提交失败'
+ })
+ }
+})
diff --git a/server/api/tender/password/change.put.ts b/server/api/tender/password/change.put.ts
new file mode 100644
index 0000000..08ef259
--- /dev/null
+++ b/server/api/tender/password/change.put.ts
@@ -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 || '修改密码失败'
+ })
+ }
+})
diff --git a/server/api/tender/password/sms.post.ts b/server/api/tender/password/sms.post.ts
new file mode 100644
index 0000000..508c46c
--- /dev/null
+++ b/server/api/tender/password/sms.post.ts
@@ -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 || '验证码发送失败'
+ })
+ }
+})
diff --git a/server/api/tender/password/status.get.ts b/server/api/tender/password/status.get.ts
new file mode 100644
index 0000000..479d1bb
--- /dev/null
+++ b/server/api/tender/password/status.get.ts
@@ -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 || '查询失败'
+ })
+ }
+})