Files
hjc-web/app/pages/login.vue
T
weicw1996 d0c2dd17cb feat(tender): 登录加图形验证码、注册加短信验证码;改按 body.code 判定 401/403
- 新增 captcha / sms / logout 三个 Nuxt server 代理
- 修复代理把 ApiResult 拍扁成 data 的问题(my-orders、enterprise、order、pay、
  mark-paid):登录态失败被前端当成「没有订单 / 未提交资质」,无法区分
- 新增 handleAuthCode 统一处理:401 → 清凭据并跳 /login?redirect=…;
  403 → 只提示「没有访问权限」,不清 token、不跳登录页
- 登录页与注册页分别接入图形验证码与短信验证码(含 60s 倒计时)
- 修复回跳开放重定向:redirect 参数安全解码且只接受站内路径
  (拒绝 //evil.com、https://… 及反斜杠变体 /\evil.com)
- 下单成功但发起支付失败时停在支付步,避免用户重提交产生重复订单
2026-09-10 23:44:19 +08:00

128 lines
4.6 KiB
Vue
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.
<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-cover"
@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>