feat: 重构HTTP客户端架构和认证系统
核心改进: - HTTP客户端:工厂函数模式,支持自定义拦截器和401/403处理 - 认证服务:函数式实现,消除this绑定问题,支持业务码+HTTP状态码双通道 - Token管理:简化为直接实例导出,移除bind()和箭头函数包装 - 路由守卫:优化逻辑,移除冗余代码,更简洁易维护 技术亮点: - 统一401/403错误处理(业务code和HTTP status双检查) - 自动刷新token并重试请求,保留自定义拦截器 - 分层清晰:clientAxios (Mono) -> http (应用) -> AuthService - 支持扩展:业务代码可创建自定义HTTP实例并添加拦截器 文件变更: - 新增 AuthService.js (函数式) 和 Login.vue - 重构 http.js、token-manager.js、router/index.js - 删除 TokenInput.vue、utils/auth.js 等冗余文件 - 更新所有API调用点使用直接实例导入 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
488
frontend/app/web-gold/src/views/auth/Login.vue
Normal file
488
frontend/app/web-gold/src/views/auth/Login.vue
Normal file
@@ -0,0 +1,488 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- 背景动画 -->
|
||||
<div class="bg-animation">
|
||||
<div class="floating-shapes">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
<div class="shape shape-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单卡片 -->
|
||||
<div class="login-card">
|
||||
<div class="card-header">
|
||||
<h1 class="title">欢迎回来</h1>
|
||||
<p class="subtitle">登录您的账户以继续</p>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" centered class="login-tabs">
|
||||
<a-tab-pane key="sms" tab="短信登录">
|
||||
<a-form
|
||||
:model="smsForm"
|
||||
:rules="smsRules"
|
||||
ref="smsFormRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item name="mobile" label="手机号">
|
||||
<a-input
|
||||
v-model:value="smsForm.mobile"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
:maxlength="11"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="code" label="验证码">
|
||||
<div class="code-input">
|
||||
<a-input
|
||||
v-model:value="smsForm.code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:maxlength="6"
|
||||
>
|
||||
<template #prefix>
|
||||
<SafetyOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="codeCountdown > 0"
|
||||
:loading="sendingCode"
|
||||
@click="sendSmsCode"
|
||||
class="send-code-btn"
|
||||
>
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s后重发` : '发送验证码' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
block
|
||||
:loading="loggingIn"
|
||||
@click="handleSmsLogin"
|
||||
class="login-btn"
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="password" tab="密码登录">
|
||||
<a-form
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
ref="passwordFormRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item name="mobile" label="手机号">
|
||||
<a-input
|
||||
v-model:value="passwordForm.mobile"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
:maxlength="11"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="passwordForm.password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
@pressEnter="handlePasswordLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
block
|
||||
:loading="loggingIn"
|
||||
@click="handlePasswordLogin"
|
||||
class="login-btn"
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<div class="card-footer">
|
||||
<p class="tip">登录即表示您同意我们的服务条款和隐私政策</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PhoneOutlined,
|
||||
SafetyOutlined,
|
||||
LockOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import authService from '@/services/AuthService'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 标签页
|
||||
const activeTab = ref('sms')
|
||||
|
||||
// 短信登录表单
|
||||
const smsForm = reactive({
|
||||
mobile: '',
|
||||
code: ''
|
||||
})
|
||||
const smsFormRef = ref()
|
||||
|
||||
// 密码登录表单
|
||||
const passwordForm = reactive({
|
||||
mobile: '',
|
||||
password: ''
|
||||
})
|
||||
const passwordFormRef = ref()
|
||||
|
||||
// 状态
|
||||
const sendingCode = ref(false)
|
||||
const loggingIn = ref(false)
|
||||
const codeCountdown = ref(0)
|
||||
|
||||
// 验证码倒计时
|
||||
let countdownTimer = null
|
||||
|
||||
// 表单验证规则
|
||||
const smsRules = {
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码为6位数字', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const passwordRules = {
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 4, max: 16, message: '密码长度为4-16位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
async function sendSmsCode() {
|
||||
try {
|
||||
await smsFormRef.value.validateFields(['mobile'])
|
||||
sendingCode.value = true
|
||||
|
||||
await authService.sendSmsCode(smsForm.mobile, 1)
|
||||
|
||||
message.success('验证码已发送')
|
||||
|
||||
// 开始倒计时
|
||||
codeCountdown.value = 60
|
||||
countdownTimer = setInterval(() => {
|
||||
codeCountdown.value--
|
||||
if (codeCountdown.value <= 0) {
|
||||
clearInterval(countdownTimer)
|
||||
}
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
} else if (error.message) {
|
||||
message.error(error.message)
|
||||
} else {
|
||||
message.error('发送验证码失败')
|
||||
}
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 短信登录
|
||||
async function handleSmsLogin() {
|
||||
try {
|
||||
await smsFormRef.value.validateFields()
|
||||
loggingIn.value = true
|
||||
|
||||
const result = await authService.loginBySms(smsForm.mobile, smsForm.code)
|
||||
|
||||
message.success('登录成功')
|
||||
|
||||
// 获取redirect参数,跳转到之前的页面或默认首页
|
||||
const redirect = router.currentRoute.value.query.redirect
|
||||
setTimeout(() => {
|
||||
router.push(redirect || '/content-style/benchmark')
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
} else {
|
||||
message.error('登录失败,请检查输入信息')
|
||||
}
|
||||
} finally {
|
||||
loggingIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
async function handlePasswordLogin() {
|
||||
try {
|
||||
await passwordFormRef.value.validateFields()
|
||||
loggingIn.value = true
|
||||
|
||||
await authService.loginByPassword(passwordForm.mobile, passwordForm.password)
|
||||
|
||||
message.success('登录成功')
|
||||
|
||||
// 获取redirect参数,跳转到之前的页面或默认首页
|
||||
const redirect = router.currentRoute.value.query.redirect
|
||||
setTimeout(() => {
|
||||
router.push(redirect || '/content-style/benchmark')
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.message) {
|
||||
message.error(error.response.data.message)
|
||||
} else {
|
||||
message.error('登录失败,请检查输入信息')
|
||||
}
|
||||
} finally {
|
||||
loggingIn.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景动画 */
|
||||
.bg-animation {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.floating-shapes {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
opacity: 0.3;
|
||||
animation: float 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: #ffffff;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: #ffd700;
|
||||
top: 50%;
|
||||
right: -150px;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
background: #ff6b9d;
|
||||
bottom: -80px;
|
||||
left: 30%;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
.shape-4 {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
background: #00f2fe;
|
||||
top: 20%;
|
||||
left: 10%;
|
||||
animation-delay: -15s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(100px, -100px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-80px, 80px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 50px 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
.login-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-tabs :deep(.ant-tabs-tab) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.login-tabs :deep(.ant-tabs-tab-active) {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.login-tabs :deep(.ant-tabs-ink-bar) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e8e8e8;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper:hover),
|
||||
:deep(.ant-input-affix-wrapper-focused) {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
:deep(.ant-input) {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 验证码输入组 */
|
||||
.code-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-btn {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 底部提示 */
|
||||
.card-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import { UserPromptApi } from '@/api/userPrompt'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GradientButton from '@/components/GradientButton.vue'
|
||||
import { setJSON, getJSON } from '@/utils/storage'
|
||||
import authService from '@/services/AuthService'
|
||||
|
||||
const promptStore = usePromptStore()
|
||||
const userStore = useUserStore()
|
||||
@@ -379,6 +380,20 @@ async function generateCopywriting() {
|
||||
if (!isResolved) {
|
||||
errorOccurred = true
|
||||
ctrl.abort()
|
||||
|
||||
// 尝试解析错误中的状态码和业务码
|
||||
const status = err?.response?.status
|
||||
const data = err?.response?.data
|
||||
|
||||
// 处理 401/403 认证错误
|
||||
if (status === 401 || status === 403 ||
|
||||
(data && typeof data.code === 'number' && (data.code === 401 || data.code === 403))) {
|
||||
authService.handleAuthError(err, () => {
|
||||
window.location.href = '/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const errorMsg = err?.message || '网络请求失败'
|
||||
console.error('SSE请求错误:', err)
|
||||
message.error(errorMsg)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UserPromptApi } from '@/api/userPrompt'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import GradientButton from '@/components/GradientButton.vue'
|
||||
import { getVoiceText } from '@gold/hooks/web/useVoiceText'
|
||||
import authService from '@/services/AuthService'
|
||||
|
||||
defineOptions({ name: 'ForecastView' })
|
||||
|
||||
@@ -272,6 +273,20 @@ async function handleGenerate() {
|
||||
errorOccurred = true
|
||||
isResolved = true
|
||||
ctrl.abort()
|
||||
|
||||
// 尝试解析错误中的状态码和业务码
|
||||
const status = err?.response?.status
|
||||
const data = err?.response?.data
|
||||
|
||||
// 处理 401/403 认证错误
|
||||
if (status === 401 || status === 403 ||
|
||||
(data && typeof data.code === 'number' && (data.code === 401 || data.code === 403))) {
|
||||
authService.handleAuthError(err, () => {
|
||||
window.location.href = '/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const errorMsg = err?.message || '网络请求失败'
|
||||
console.error('SSE请求错误:', err)
|
||||
message.error(errorMsg)
|
||||
|
||||
Reference in New Issue
Block a user