Skip to content

Commit a3a563a

Browse files
committed
修复一个小bug,跳转
1 parent 030b24e commit a3a563a

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

web/src/composables/useAuth.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { ref, computed } from 'vue'
2+
import axios from 'axios'
3+
4+
// 认证状态管理
5+
const token = ref(localStorage.getItem('token') || '')
6+
const username = ref(localStorage.getItem('username') || '')
7+
8+
export function useAuth() {
9+
const isAuthenticated = computed(() => !!token.value)
10+
11+
const setToken = (newToken, newUsername) => {
12+
token.value = newToken
13+
username.value = newUsername
14+
if (newToken) {
15+
localStorage.setItem('token', newToken)
16+
localStorage.setItem('username', newUsername || '')
17+
} else {
18+
localStorage.removeItem('token')
19+
localStorage.removeItem('username')
20+
}
21+
}
22+
23+
const logout = () => {
24+
setToken('', '')
25+
}
26+
27+
return {
28+
token: computed(() => token.value),
29+
username: computed(() => username.value),
30+
isAuthenticated,
31+
setToken,
32+
logout
33+
}
34+
}
35+
36+
// 配置 Axios 拦截器
37+
export function setupAxiosInterceptors() {
38+
// 请求拦截器:自动添加 Authorization header
39+
axios.interceptors.request.use(
40+
(config) => {
41+
const currentToken = localStorage.getItem('token')
42+
if (currentToken) {
43+
config.headers.Authorization = `Bearer ${currentToken}`
44+
}
45+
return config
46+
},
47+
(error) => {
48+
return Promise.reject(error)
49+
}
50+
)
51+
52+
// 响应拦截器:处理 401 未授权
53+
axios.interceptors.response.use(
54+
(response) => response,
55+
(error) => {
56+
if (error.response?.status === 401) {
57+
// Token 过期或无效,清除并跳转登录
58+
localStorage.removeItem('token')
59+
localStorage.removeItem('username')
60+
token.value = ''
61+
username.value = ''
62+
63+
// 如果不在登录页,则跳转
64+
if (window.location.pathname !== '/login') {
65+
window.location.href = '/login'
66+
}
67+
}
68+
return Promise.reject(error)
69+
}
70+
)
71+
}

web/src/pages/LoginPage.vue

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<template>
2+
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 px-4">
3+
<div class="max-w-md w-full">
4+
<!-- Logo -->
5+
<div class="text-center mb-8">
6+
<h1 class="font-bold text-4xl tf-brand mb-2">
7+
<span class="tf-letter">T</span><span class="tf-letter">i</span><span class="tf-letter">n</span><span class="tf-letter">y</span><span class="tf-letter">F</span><span class="tf-letter">l</span><span class="tf-letter">o</span><span class="tf-letter">w</span>
8+
</h1>
9+
<p class="text-gray-600">{{ isLogin ? '登录到你的账户' : '创建新账户' }}</p>
10+
</div>
11+
12+
<!-- 登录/注册表单卡片 -->
13+
<div class="bg-white rounded-2xl shadow-lg p-8 border border-gray-100">
14+
<div class="mb-6">
15+
<div class="flex gap-2 p-1 bg-gray-100 rounded-lg">
16+
<button
17+
@click="isLogin = true"
18+
:class="[
19+
'flex-1 py-2 px-4 rounded-md font-medium transition-all',
20+
isLogin ? 'bg-white shadow text-blue-600' : 'text-gray-600 hover:text-gray-900'
21+
]"
22+
>
23+
登录
24+
</button>
25+
<button
26+
@click="isLogin = false"
27+
:class="[
28+
'flex-1 py-2 px-4 rounded-md font-medium transition-all',
29+
!isLogin ? 'bg-white shadow text-blue-600' : 'text-gray-600 hover:text-gray-900'
30+
]"
31+
>
32+
注册
33+
</button>
34+
</div>
35+
</div>
36+
37+
<form @submit.prevent="handleSubmit" class="space-y-4">
38+
<!-- 用户名 -->
39+
<div>
40+
<label class="block text-sm font-medium text-gray-700 mb-1">用户名</label>
41+
<input
42+
v-model="formData.username"
43+
type="text"
44+
required
45+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
46+
placeholder="请输入用户名"
47+
/>
48+
</div>
49+
50+
<!-- 邮箱(仅注册) -->
51+
<div v-if="!isLogin">
52+
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
53+
<input
54+
v-model="formData.email"
55+
type="email"
56+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
57+
placeholder="请输入邮箱(可选)"
58+
/>
59+
</div>
60+
61+
<!-- 密码 -->
62+
<div>
63+
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
64+
<input
65+
v-model="formData.password"
66+
type="password"
67+
required
68+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
69+
:placeholder="isLogin ? '请输入密码' : '请设置密码(至少6位)'"
70+
:minlength="isLogin ? 1 : 6"
71+
/>
72+
</div>
73+
74+
<!-- 错误提示 -->
75+
<div v-if="error" class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
76+
{{ error }}
77+
</div>
78+
79+
<!-- 提交按钮 -->
80+
<button
81+
type="submit"
82+
:disabled="loading"
83+
@click="console.log('按钮被点击了!')"
84+
class="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition disabled:opacity-50 disabled:cursor-not-allowed"
85+
>
86+
{{ loading ? '处理中...' : (isLogin ? '登录' : '注册') }}
87+
</button>
88+
</form>
89+
90+
<!-- 匿名访问提示 -->
91+
<div class="mt-6 text-center">
92+
<button
93+
@click="skipLogin"
94+
class="text-sm text-gray-500 hover:text-gray-700 transition"
95+
>
96+
暂时跳过,匿名访问 →
97+
</button>
98+
</div>
99+
</div>
100+
101+
<!-- 底部提示 -->
102+
<div class="mt-6 text-center text-sm text-gray-500">
103+
{{ isLogin ? '还没有账户?' : '已有账户?' }}
104+
<button
105+
@click="isLogin = !isLogin"
106+
class="text-blue-600 hover:text-blue-700 font-medium ml-1"
107+
>
108+
{{ isLogin ? '立即注册' : '立即登录' }}
109+
</button>
110+
</div>
111+
</div>
112+
</div>
113+
</template>
114+
115+
<script setup>
116+
import { ref, onMounted } from 'vue'
117+
import { useRouter } from 'vue-router'
118+
import axios from 'axios'
119+
120+
console.log('LoginPage.vue 组件正在加载...')
121+
122+
const router = useRouter()
123+
const isLogin = ref(true)
124+
const loading = ref(false)
125+
const error = ref('')
126+
127+
const formData = ref({
128+
username: '',
129+
email: '',
130+
password: ''
131+
})
132+
133+
onMounted(() => {
134+
console.log('LoginPage 组件已挂载!')
135+
console.log('测试:点击"立即注册"按钮应该能切换标签')
136+
})
137+
138+
const handleSubmit = async () => {
139+
console.log('=== 开始登录/注册 ===', { isLogin: isLogin.value, formData: formData.value })
140+
error.value = ''
141+
loading.value = true
142+
143+
try {
144+
const endpoint = isLogin.value ? '/api/auth/login' : '/api/auth/register'
145+
const payload = isLogin.value
146+
? { username: formData.value.username, password: formData.value.password }
147+
: { username: formData.value.username, email: formData.value.email || undefined, password: formData.value.password }
148+
149+
console.log('发送请求:', { endpoint, payload })
150+
const response = await axios.post(endpoint, payload)
151+
console.log('收到响应:', response)
152+
const data = response.data
153+
154+
// 处理返回结果
155+
let token = null
156+
if (data.code === 0 && data.data) {
157+
// Result<AuthResponse> 包装格式
158+
token = data.data.token
159+
console.log('提取 Token (Result格式):', token)
160+
} else if (data.token) {
161+
// 直接返回 AuthResponse
162+
token = data.token
163+
console.log('提取 Token (直接格式):', token)
164+
}
165+
166+
if (!token) {
167+
console.error('未找到有效 Token:', data)
168+
throw new Error('登录失败:未返回有效 Token')
169+
}
170+
171+
// 存储 Token
172+
localStorage.setItem('token', token)
173+
localStorage.setItem('username', data.data?.username || data.username || formData.value.username)
174+
console.log('Token 已存储,准备跳转首页')
175+
176+
// 跳转到首页
177+
router.push('/')
178+
} catch (err) {
179+
console.error('登录/注册错误:', err)
180+
if (err.response) {
181+
// 后端返回的错误
182+
console.error('后端错误响应:', err.response.data)
183+
const msg = err.response.data?.message || err.response.data?.msg || err.response.data
184+
error.value = typeof msg === 'string' ? msg : (isLogin.value ? '登录失败,请检查用户名和密码' : '注册失败,请稍后重试')
185+
} else if (err.request) {
186+
console.error('请求未收到响应:', err.request)
187+
error.value = '网络错误,请检查后端服务是否启动'
188+
} else {
189+
console.error('请求配置错误:', err.message)
190+
error.value = isLogin.value ? '登录失败,请稍后重试' : '注册失败,请稍后重试'
191+
}
192+
} finally {
193+
loading.value = false
194+
console.log('=== 登录/注册流程结束 ===')
195+
}
196+
}
197+
198+
const skipLogin = () => {
199+
// 清除 Token,以匿名身份访问
200+
localStorage.removeItem('token')
201+
localStorage.removeItem('username')
202+
router.push('/')
203+
}
204+
</script>
205+
206+
<style scoped>
207+
.tf-brand {
208+
background-image: linear-gradient(135deg, #6B72FF 0%, #8A6BFF 60%, #A66BFF 100%);
209+
-webkit-background-clip: text;
210+
background-clip: text;
211+
color: transparent;
212+
letter-spacing: 0.5px;
213+
}
214+
215+
.tf-letter {
216+
display: inline-block;
217+
animation: bounce 0.6s ease-in-out;
218+
}
219+
220+
.tf-letter:nth-child(1) { animation-delay: 0.1s; }
221+
.tf-letter:nth-child(2) { animation-delay: 0.2s; }
222+
.tf-letter:nth-child(3) { animation-delay: 0.3s; }
223+
.tf-letter:nth-child(4) { animation-delay: 0.4s; }
224+
.tf-letter:nth-child(5) { animation-delay: 0.5s; }
225+
.tf-letter:nth-child(6) { animation-delay: 0.6s; }
226+
.tf-letter:nth-child(7) { animation-delay: 0.7s; }
227+
.tf-letter:nth-child(8) { animation-delay: 0.8s; }
228+
229+
@keyframes bounce {
230+
0%, 100% { transform: translateY(0); }
231+
50% { transform: translateY(-10px); }
232+
}
233+
</style>

0 commit comments

Comments
 (0)