1. axios封装:request.ts
作用:统一处理请求,自动携带token,401跳转
ts
// src/api/request.ts
import axios from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器:自动添加token
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器:统一处理错误
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// token过期,清除token
localStorage.removeItem('token')
// 跳转到登录页
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
2. 接口定义:auth.ts
作用:定义所有登录相关的接口
ts
// src/api/auth.ts
import request from './request'
// 登录接口
export const loginApi = (username: string, password: string) => {
return request.post('/login', {
username,
password
})
}
// 获取用户信息接口
export const getUserInfoApi = () => {
return request.get('/user-info')
}
3. Store:auth.ts
作用:管理登录状态,token存localStorage,用户信息只存内存,刷新页面重新调接口获取最新用户信息
ts
// src/store/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { loginApi, getUserInfoApi } from '@/api/auth'
export const useAuthStore = defineStore('auth', () => {
// token需要持久化,因为请求要用
const token = ref(localStorage.getItem('token') || '')
// 用户信息只存在内存里,刷新就没了
const userInfo = ref<any>(null)
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const username = computed(() => userInfo.value?.username || '')
const nickname = computed(() => userInfo.value?.nickname || '')
// 登录
const login = async (user: string, pwd: string, remember: boolean) => {
try {
// 1. 调用登录接口
const res = await loginApi(user, pwd) as any
// 2. 保存token(必须存,不然刷新就丢了)
token.value = res.token
localStorage.setItem('token', res.token)
// 3. 获取用户信息(存内存)
await getUserInfo()
// 4. 如果记住用户名,保存用户名(不是密码)
if (remember) {
localStorage.setItem('savedUsername', user)
} else {
localStorage.removeItem('savedUsername')
}
return true
} catch (error) {
console.error('登录失败', error)
return false
}
}
// 获取用户信息(每次刷新页面都要重新获取)
const getUserInfo = async () => {
// 如果没有token,不获取
if (!token.value) {
return null
}
try {
const res = await getUserInfoApi() as any
userInfo.value = res
return res
} catch (error) {
console.error('获取用户信息失败', error)
// 如果获取失败(比如token过期),清除登录状态
logout()
throw error
}
}
// 退出登录
const logout = () => {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
// 注意:记住的用户名不清除,方便下次登录
}
return {
token,
userInfo,
isLoggedIn,
username,
nickname,
login,
logout,
getUserInfo
}
})
4. 路由:index.ts
作用:控制页面访问权限,刷新页面时重新获取用户信息
ts
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/store/auth'
const routes = [
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue')
},
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// 如果已登录且要去登录页,先退出再过去
if (to.path === '/login' && authStore.isLoggedIn) {
authStore.logout()
next()
return
}
// 如果需要登录
if (to.meta.requiresAuth) {
// 如果有token
if (authStore.isLoggedIn) {
// 如果没有用户信息,说明是刷新页面,需要重新获取
if (!authStore.userInfo) {
try {
await authStore.getUserInfo()
next()
} catch (error) {
// 获取用户信息失败(token过期等),已经自动退出
next('/login')
}
} else {
// 有用户信息,直接放行
next()
}
} else {
// 没token,去登录
next('/login')
}
} else {
next()
}
})
export default router
5. 登录页:Login.vue
作用:用户输入账号密码
vue
<!-- src/views/Login.vue -->
<template>
<div class="login">
<h2>用户登录</h2>
<div class="form">
<div class="field">
<label>用户名:</label>
<input
v-model="form.username"
type="text"
placeholder="请输入用户名"
/>
</div>
<div class="field">
<label>密码:</label>
<input
v-model="form.password"
type="password"
placeholder="请输入密码"
/>
</div>
<div class="field">
<label>
<input type="checkbox" v-model="form.remember" />
记住用户名
</label>
</div>
<div v-if="error" class="error">{{ error }}</div>
<button
@click="handleLogin"
:disabled="!form.username || !form.password || loading"
>
{{ loading ? '登录中...' : '登录' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
const router = useRouter()
const authStore = useAuthStore()
const form = reactive({
username: '',
password: '',
remember: false
})
const loading = ref(false)
const error = ref('')
// 页面加载时,如果有保存的用户名就自动填充
onMounted(() => {
const savedUsername = localStorage.getItem('savedUsername')
if (savedUsername) {
form.username = savedUsername
form.remember = true
}
})
const handleLogin = async () => {
if (!form.username || !form.password) {
error.value = '请输入用户名和密码'
return
}
loading.value = true
error.value = ''
try {
const success = await authStore.login(
form.username,
form.password,
form.remember
)
if (success) {
router.push('/')
} else {
error.value = '用户名或密码错误'
}
} catch (e: any) {
error.value = e.response?.data?.message || '登录失败,请稍后重试'
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login {
max-width: 400px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
.field {
margin-bottom: 15px;
}
.field label {
display: block;
margin-bottom: 5px;
}
.field input[type="text"],
.field input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.error {
color: red;
margin-bottom: 10px;
}
button {
width: 100%;
padding: 10px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
6. 首页:Home.vue
作用:展示用户信息,处理加载状态
vue
<!-- src/views/Home.vue -->
<template>
<div class="home">
<h1>首页</h1>
<!-- 加载中状态 -->
<div v-if="loading" class="loading">
加载用户信息中...
</div>
<!-- 展示用户信息 -->
<div v-else-if="authStore.userInfo" class="user-info">
<h2>用户信息</h2>
<p><strong>用户名:</strong> {{ authStore.userInfo.username }}</p>
<p><strong>昵称:</strong> {{ authStore.userInfo.nickname || '未设置' }}</p>
<p><strong>邮箱:</strong> {{ authStore.userInfo.email || '未设置' }}</p>
<p><strong>手机:</strong> {{ authStore.userInfo.phone || '未设置' }}</p>
<p><strong>角色:</strong> {{ authStore.userInfo.roles?.join(', ') || '普通用户' }}</p>
</div>
<button @click="handleLogout" class="logout-btn">
退出登录
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
// 页面加载时确保用户信息是最新的
onMounted(async () => {
// 如果store里没有用户信息(比如刷新页面),就获取一下
if (!authStore.userInfo) {
loading.value = true
try {
await authStore.getUserInfo()
} catch (error) {
console.error('获取用户信息失败', error)
} finally {
loading.value = false
}
}
})
const handleLogout = () => {
authStore.logout()
router.push('/login')
}
</script>
<style scoped>
.home {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.user-info {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.user-info p {
margin: 10px 0;
}
.loading {
color: #999;
text-align: center;
padding: 20px;
}
.logout-btn {
padding: 10px 20px;
background: #ff4d4f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
7. 其他必需文件
ts
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
vue
<!-- src/App.vue -->
<template>
<router-view />
</template>
ts
// src/vite-env.d.ts
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}