Files
hjc-web/app/composables/useHjcAuth.ts
T
weicw1996 32098b69ac fix(hjc-web): 修复刷新后登录态丢一半,以及顶栏企业名取错字段
- useHjcAuth:登录用户信息补持久化到 hjc_user cookie。原先用 useState,只活在内存与
  本次 SSR payload 里,刷新后归 null,顶栏企业名退化成占位的「企业」,登录态看着像丢了
  一半;H5 端把用户信息存本地存储,PC 端用同名 cookie 对齐。它**只用于展示**,鉴权一律
  以 JWT 为准(后端不读用户信息)。顺带抽出 setCredentials() 统一写入 token + 展示信息。
- HjcUserBar:loggedIn 改 computed —— 原来 ref(isLoggedIn()) 只在挂载那一刻求值一次,
  之后别处清凭据(401 处理、另一页签退出)本组件不跟着变;企业名改取 username 而非
  nickname —— 核心实例建号时会把 nickname 无条件覆盖成打码手机号,拿它当企业名是错的,
  username 才是「企业名称(登录账号)」。
- login:验证码图 object-cover 改 object-contain,避免被裁掉。
- tender/orders:改用 useRequestFetch()。SSR 期间裸 $fetch 不会把浏览器 Cookie 转发给
  站内 /api 代理,代理会变成匿名调用并返回 code=401,从而被 401 分支误判为「凭据失效」
  ——刷新页面等于把用户登出。
2026-09-14 19:01:31 +08:00

187 lines
7.0 KiB
TypeScript
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.
import type { HjcUser } from '~/types/tender'
/**
* 后端 ApiResult.code 的登录态语义。
*
* <b>HTTP 状态码恒为 200</b>,失败一律靠 body 的 code 判定:
* - 0 成功
* - 401 未登录或 token 失效 → 清本地凭据并跳登录页
* - 403 已登录但无权限(message「没有访问权限」)→ 只提示,<b>不清 token、不跳登录页</b>
* - 其他非 0 → 业务失败,展示 message
*/
export const HJC_CODE_UNAUTHORIZED = 401
export const HJC_CODE_FORBIDDEN = 403
/** 登录凭据有效期(秒),token 与随行的展示信息同寿命 */
const AUTH_MAX_AGE = 60 * 60 * 24 * 7
/** 登录态(token 存 cookieSSR 安全;服务器端代理从 cookie 读取并转发 Authorization */
export function useHjcAuth() {
const token = useCookie<string | null>('hjc_token', {
maxAge: AUTH_MAX_AGE,
sameSite: 'lax'
})
/**
* 展示用登录用户信息,与 token 同寿命地持久化。
*
* `useState` 只活在内存与本次 SSR payload 里:刷新页面后 `hjc-user` 归 null
* 顶栏的企业名会从登录后的名称退化成占位的「企业」——登录态看着就像丢了一半。
* H5 端把用户信息存本地存储(`hjc_user`),PC 端用同名 cookie 对齐这一做法。
* 它**只用于展示**,鉴权一律以 JWT 为准(后端不读用户信息)。
*/
const userCookie = useCookie<HjcUser | null>('hjc_user', {
maxAge: AUTH_MAX_AGE,
sameSite: 'lax',
default: () => null
})
const user = useState<HjcUser | null>('hjc-user', () => userCookie.value ?? null)
function isLoggedIn() {
return !!token.value
}
/** 写入本次登录/注册拿到的凭据(token + 展示信息),内存与 cookie 一起写 */
function setCredentials(accessToken: string, loginUser: HjcUser | null) {
token.value = accessToken
user.value = loginUser
userCookie.value = loginUser
}
/** 清本地凭据(token 是无状态 JWT,后端无会话,清本地即退出) */
function clearCredentials() {
token.value = null
user.value = null
userCookie.value = null
}
/**
* 统一处理 ApiResult 的登录态失败:**只看 body.code,不看 HTTP 状态码**。
*
* @param res 服务器代理透传的 ApiResult
* @param redirect 401 跳登录页时带上,便于登录后回跳
* @returns handled=true 表示已按登录态问题处理完(401 已跳转 / 403 带提示文案);
* handled=false 表示不是登录态问题,交调用方按业务失败处理
*/
async function handleAuthCode(
res: any,
redirect = '/'
): Promise<{ handled: boolean; message?: string }> {
const code = Number(res?.code)
if (code === HJC_CODE_UNAUTHORIZED) {
// 注意:这一分支会写 cookie(服务端渲染时即 SSR 响应里的 Set-Cookie 清除)。
// 因此凡是**服务端渲染期间**发出的、需要登录态的站内请求,必须用
// `useRequestFetch()` 而不是裸 `$fetch`——裸 $fetch 不会把浏览器 Cookie 转发给
// 站内 /api 代理,代理会变成匿名调用并拿到 code=401,从而被这里误判为
// 「凭据失效」:刷新页面 = 把用户登出。参见 app/pages/tender/orders.vue。
clearCredentials()
await navigateTo(`/login?redirect=${encodeURIComponent(redirect)}`)
return { handled: true }
}
if (code === HJC_CODE_FORBIDDEN) {
// 已登录但无权限:不清 token、不跳登录页,仅提示后端 message
return { handled: true, message: res?.message || '没有访问权限' }
}
return { handled: false }
}
/**
* 拉取登录用图形验证码。
* 后端只返回图片(data:image/png;base64,...),不下发答案,需用户肉眼识别后输入。
*/
async function fetchCaptcha(): Promise<{ ok: boolean; image?: string; message?: string }> {
try {
// 带时间戳,避免浏览器/中间层缓存旧验证码图片
const res: any = await $fetch('/api/tender/captcha', { query: { t: Date.now() } })
if (res?.code === 0 && res.data?.image) {
return { ok: true, image: res.data.image }
}
return { ok: false, message: res?.message || '验证码获取失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.data?.message || e?.message || '验证码获取失败' }
}
}
/**
* 发送注册短信验证码。
* @param phone 经办人手机号(即账号手机号),后端据此发码
*/
async function sendSms(phone: string): Promise<{ ok: boolean; message?: string }> {
try {
const res: any = await $fetch('/api/tender/sms', {
method: 'POST',
body: { phone }
})
if (res?.code === 0) {
return { ok: true, message: res?.message || '验证码已发送' }
}
return { ok: false, message: res?.message || '验证码发送失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.data?.message || e?.message || '验证码发送失败' }
}
}
/**
* 企业登录
* @param code 图形验证码(必填,后端先校验非空)
*/
async function login(enterpriseName: string, password: string, code: string) {
try {
const res: any = await $fetch('/api/tender/login', {
method: 'POST',
body: { enterpriseName, password, code }
})
// mp-api 统一返回 ApiResult{code,message,data}code===0 成功
if (res?.code === 0 && res.data?.access_token) {
setCredentials(res.data.access_token, res.data.user || null)
return { ok: true, user: res.data.user }
}
return { ok: false, message: res?.message || '登录失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.message || '登录失败' }
}
}
/**
* 企业注册(一站式)
* @param payload 注册字段 + `code`(短信验证码,发给经办人手机号)
*/
async function register(payload: Record<string, any>) {
try {
const res: any = await $fetch('/api/tender/register', {
method: 'POST',
body: payload
})
if (res?.code === 0 && res.data?.access_token) {
setCredentials(res.data.access_token, res.data.user || null)
return { ok: true, user: res.data.user }
}
return { ok: false, message: res?.message || '注册失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.message || '注册失败' }
}
}
/** 退出登录:先通知后端(无服务端会话,仅为契约完整),再清本地凭据 */
async function logout() {
try {
await $fetch('/api/tender/logout', { method: 'POST' })
} catch {
// 退出接口失败不阻断本地退出:token 是无状态 JWT,清本地凭据即已登出
}
clearCredentials()
}
return {
token,
user,
isLoggedIn,
clearCredentials,
handleAuthCode,
fetchCaptcha,
sendSms,
login,
register,
logout
}
}