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 的纯重定向壳,无需标题,未改动
144 lines
5.0 KiB
Vue
144 lines
5.0 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>
|
||
<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>
|
||
|
||
<script setup lang="ts">
|
||
const { login, fetchCaptcha } = useHjcAuth()
|
||
const { siteInfo, fetchSiteInfo } = useSite()
|
||
const route = useRoute()
|
||
|
||
// 先取站点信息,保证标题能带上品牌名(与其它页面一致)
|
||
await fetchSiteInfo()
|
||
|
||
usePageSeo(
|
||
{
|
||
title: '企业登录',
|
||
path: '/login'
|
||
},
|
||
siteInfo.value
|
||
)
|
||
|
||
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>
|