32098b69ac
- 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 分支误判为「凭据失效」 ——刷新页面等于把用户登出。
128 lines
4.6 KiB
Vue
128 lines
4.6 KiB
Vue
<template>
|
||
<div class="min-h-[60vh] flex items-center justify-center px-4">
|
||
<div class="w-full max-w-md rounded-lg border border-gray-200 p-8">
|
||
<h1 class="text-2xl font-bold text-center text-gray-900">企业登录</h1>
|
||
<form class="mt-6 space-y-4" @submit.prevent="submitForm">
|
||
<div>
|
||
<label class="text-sm text-gray-600">企业名称</label>
|
||
<input v-model="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="password" type="password" 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>
|
||
<div class="mt-1 flex items-center gap-2">
|
||
<input
|
||
v-model="code"
|
||
class="w-full rounded-md border border-gray-300 px-4 py-2 text-sm"
|
||
placeholder="请输入图片中的验证码"
|
||
autocomplete="off"
|
||
/>
|
||
<img
|
||
v-if="captchaImage"
|
||
:src="captchaImage"
|
||
alt="图形验证码"
|
||
title="看不清?点击换一张"
|
||
class="h-[38px] w-28 shrink-0 cursor-pointer rounded-md border border-gray-300 object-contain"
|
||
@click="refreshCaptcha"
|
||
/>
|
||
<button
|
||
v-else
|
||
type="button"
|
||
class="h-[38px] w-28 shrink-0 rounded-md border border-gray-300 text-xs text-gray-600 hover:bg-gray-50"
|
||
@click="refreshCaptcha"
|
||
>
|
||
获取验证码
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="w-full rounded-md bg-blue-600 py-3 text-white font-medium 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>
|
||
<p class="mt-4 text-center text-sm text-gray-500">
|
||
还没有账号?
|
||
<NuxtLink to="/register" class="text-blue-600 hover:underline">立即注册企业</NuxtLink>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
const { login, fetchCaptcha } = useHjcAuth()
|
||
const route = useRoute()
|
||
const enterpriseName = ref('')
|
||
const password = ref('')
|
||
/** 图形验证码(后端只下发图片,答案由用户肉眼识别后输入) */
|
||
const code = ref('')
|
||
const captchaImage = ref('')
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
|
||
/** 拉取/刷新图形验证码图片;失败时提示并允许点击重试 */
|
||
async function refreshCaptcha() {
|
||
code.value = ''
|
||
const res = await fetchCaptcha()
|
||
if (res.ok && res.image) {
|
||
captchaImage.value = res.image
|
||
return
|
||
}
|
||
captchaImage.value = ''
|
||
// 不覆盖已有错误(如登录失败 message),仅在无错误时提示验证码本身的问题
|
||
if (!error.value) error.value = res.message || '验证码获取失败,请点击重试'
|
||
}
|
||
|
||
/**
|
||
* 登录成功后的去向:`redirect` 可能被 encodeURIComponent 过(如 /buy%3Fid%3D3),
|
||
* 这里安全解码;仅接受站内路径,避免开放重定向。
|
||
*/
|
||
function resolveRedirect() {
|
||
const raw = (route.query.redirect as string) || ''
|
||
if (!raw) return '/tender'
|
||
let target = raw
|
||
try {
|
||
target = decodeURIComponent(raw)
|
||
} catch {
|
||
target = raw
|
||
}
|
||
// 必须以单个 '/' 开头:排除 http(s)://、协议相对地址 //evil.com,
|
||
// 以及反斜杠变体 /\evil.com(部分浏览器把 '\' 归一成 '/',等价于 //evil.com)
|
||
return target.startsWith('/') && !target.startsWith('//') && !target.includes('\\') ? target : '/tender'
|
||
}
|
||
|
||
onMounted(refreshCaptcha)
|
||
|
||
async function submitForm() {
|
||
error.value = ''
|
||
if (!enterpriseName.value.trim()) {
|
||
error.value = '请输入企业名称'
|
||
return
|
||
}
|
||
if (!password.value) {
|
||
error.value = '请输入密码'
|
||
return
|
||
}
|
||
if (!code.value.trim()) {
|
||
error.value = '请输入图形验证码'
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const res = await login(enterpriseName.value.trim(), password.value, code.value.trim())
|
||
if (res.ok) {
|
||
return navigateTo(resolveRedirect())
|
||
}
|
||
error.value = res.message || '登录失败'
|
||
// 验证码一次性,失败后换一张,避免用户用同一个码反复提交
|
||
await refreshCaptcha()
|
||
} catch (e: any) {
|
||
error.value = e?.message || '登录失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|