Commit e7411267 authored by Administrator's avatar Administrator
Browse files

completed step 2 except for real process for JWT token retrivement and authentication

parent e3d38c80
# Step 2: 配置Vue Router和路由守卫
## 目标
配置Vue Router路由系统,实现路由守卫进行认证控制,并创建基础的页面组件。
## 步骤
### 2.1 创建页面组件
首先创建基础的页面组件,为路由系统做准备。
#### 2.1.1 创建登录页面
创建 `src/views/LoginView.vue`
```vue
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
if (!email.value || !password.value) {
error.value = '请填写邮箱和密码'
return
}
loading.value = true
error.value = ''
try {
const result = await authStore.login({
email: email.value,
password: password.value,
})
if (!result.success) {
error.value = result.error || '登录失败'
}
} catch {
error.value = '登录过程中发生错误'
} finally {
loading.value = false
}
}
</script>
<template>
<v-container fluid fill-height>
<v-row align="center" justify="center">
<v-col cols="12" sm="8" md="4">
<v-card class="elevation-12">
<v-toolbar color="primary" dark flat>
<v-toolbar-title>学生管理系统 - 登录</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert v-if="error" type="error" variant="tonal" class="mb-4">
{{ error }}
</v-alert>
<v-form @submit.prevent="handleLogin">
<v-text-field
v-model="email"
label="邮箱"
name="email"
prepend-icon="mdi-email"
type="email"
required
/>
<v-text-field
v-model="password"
label="密码"
name="password"
prepend-icon="mdi-lock"
type="password"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" :loading="loading" @click="handleLogin"> 登录 </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.v-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>
```
#### 2.1.2 创建主页
创建 `src/views/HomeView.vue`
```vue
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<v-container>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="text-h4"> 欢迎来到学生管理系统 </v-card-title>
<v-card-text>
<p class="text-body-1">这是一个基于Vue 3和Vuetify构建的现代化学生管理系统。</p>
<v-alert type="info" variant="tonal" class="mt-4">
当前功能正在开发中,更多功能即将推出。
</v-alert>
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="router.push('/profile')"> 个人资料 </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
```
#### 2.1.3 创建个人资料页面
创建 `src/views/ProfileView.vue`
```vue
<script setup>
import { ref } from 'vue'
const user = ref({
name: '张三',
email: 'zhangsan@example.com',
studentId: '2024001',
major: '计算机科学与技术',
grade: '大二'
})
</script>
<template>
<v-container>
<v-row>
<v-col cols="12" md="8" offset-md="2">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-account</v-icon>
个人资料
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="user.name"
label="姓名"
readonly
prepend-icon="mdi-account"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.email"
label="邮箱"
readonly
prepend-icon="mdi-email"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.studentId"
label="学号"
readonly
prepend-icon="mdi-card-account-details"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.major"
label="专业"
readonly
prepend-icon="mdi-school"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.grade"
label="年级"
readonly
prepend-icon="mdi-account-group"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
```
### 2.2 配置路由
更新 `src/router/index.js`
```javascript
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: true },
},
{
path: '/login',
name: 'login',
component: LoginView,
meta: { requiresAuth: false },
},
{
path: '/profile',
name: 'profile',
component: ProfileView,
meta: { requiresAuth: true },
},
{
path: '/:pathMatch(.*)*',
redirect: '/',
},
],
})
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token') // 简单的认证检查
if (to.meta.requiresAuth && !isAuthenticated) {
// 需要认证但未登录,重定向到登录页
next('/login')
} else if (to.path === '/login' && isAuthenticated) {
// 已登录用户访问登录页,重定向到主页
next('/')
} else {
next()
}
})
export default router
```
### 2.3 更新主应用组件
更新 `src/App.vue` 以支持路由导航:
```vue
<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'
import { useAuthStore } from './stores/auth'
const route = useRoute()
const authStore = useAuthStore()
const isAuthenticated = computed(() => {
return localStorage.getItem('token')
})
const currentRoute = computed(() => route.name)
const handleLogout = () => {
authStore.logout()
}
</script>
<template>
<v-app>
<!-- 导航栏 -->
<v-app-bar v-if="isAuthenticated" app color="primary" dark>
<v-app-bar-title>学生管理系统</v-app-bar-title>
<v-spacer />
<v-btn
:to="{ name: 'home' }"
:color="currentRoute === 'home' ? 'white' : 'transparent'"
variant="text"
>
主页
</v-btn>
<v-btn
:to="{ name: 'profile' }"
:color="currentRoute === 'profile' ? 'white' : 'transparent'"
variant="text"
>
个人资料
</v-btn>
<v-btn color="white" variant="text" @click="handleLogout"> 登出 </v-btn>
</v-app-bar>
<!-- 主要内容区域 -->
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<style scoped>
.v-app-bar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style>
```
### 2.4 创建认证状态管理
创建 `src/stores/auth.js`
```javascript
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
export const useAuthStore = defineStore('auth', () => {
const router = useRouter()
// 状态
const token = ref(localStorage.getItem('token') || null)
const user = ref(null)
// 计算属性
const isAuthenticated = computed(() => !!token.value)
// 方法
const login = async (credentials) => {
try {
// TODO: 调用登录API
console.log('登录凭证:', credentials)
// 模拟登录成功
const mockToken = 'mock-jwt-token-' + Date.now()
const mockUser = {
id: 1,
name: '张三',
email: credentials.email,
role: 'student',
}
// 保存认证信息
token.value = mockToken
user.value = mockUser
localStorage.setItem('token', mockToken)
localStorage.setItem('user', JSON.stringify(mockUser))
// 跳转到主页
router.push('/')
return { success: true }
} catch (error) {
console.error('登录失败:', error)
return { success: false, error: error.message }
}
}
const logout = () => {
// 清除认证信息
token.value = null
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
// 跳转到登录页
router.push('/login')
}
const initializeAuth = () => {
const savedToken = localStorage.getItem('token')
const savedUser = localStorage.getItem('user')
if (savedToken && savedUser) {
token.value = savedToken
user.value = JSON.parse(savedUser)
}
}
return {
token,
user,
isAuthenticated,
login,
logout,
initializeAuth,
}
})
```
### 2.5 更新登录页面以使用状态管理
```vue
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
if (!email.value || !password.value) {
error.value = '请填写邮箱和密码'
return
}
loading.value = true
error.value = ''
try {
const result = await authStore.login({
email: email.value,
password: password.value,
})
if (!result.success) {
error.value = result.error || '登录失败'
}
} catch (err) {
error.value = '登录过程中发生错误'
} finally {
loading.value = false
}
}
</script>
<template>
<v-container fluid fill-height>
<v-row align="center" justify="center">
<v-col cols="12" sm="8" md="4">
<v-card class="elevation-12">
<v-toolbar color="primary" dark flat>
<v-toolbar-title>学生管理系统 - 登录</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert v-if="error" type="error" variant="tonal" class="mb-4">
{{ error }}
</v-alert>
<v-form @submit.prevent="handleLogin">
<v-text-field
v-model="email"
label="邮箱"
name="email"
prepend-icon="mdi-email"
type="email"
required
/>
<v-text-field
v-model="password"
label="密码"
name="password"
prepend-icon="mdi-lock"
type="password"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" :loading="loading" @click="handleLogin"> 登录 </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.v-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>
```
### 2.6 更新主应用以初始化认证状态
```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 引入Vuetify
import vuetify from './plugins/vuetify'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(vuetify)
// 初始化认证状态
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
authStore.initializeAuth()
app.mount('#app')
```
## 阶段性验证
### 验证1:基础路由功能
1. 启动开发服务器:`npm run dev`
2. 访问 `http://localhost:5173`
3. 应该自动重定向到登录页面(因为未认证)
4. 在登录页面输入任意邮箱和密码
5. 点击登录后应该跳转到主页
### 验证2:路由守卫功能
1. 在主页点击"登出"按钮
2. 应该跳转回登录页面
3. 尝试直接访问 `http://localhost:5173/profile`
4. 应该重定向到登录页面
### 验证3:导航功能
1. 登录后应该看到顶部导航栏
2. 点击"主页"和"个人资料"应该能正常切换
3. 点击"登出"应该清除认证状态并跳转到登录页
## 完成标志
- [x] 创建了三个基础页面组件(登录、主页、个人资料)
- [x] 配置了Vue Router路由系统
- [x] 实现了路由守卫进行认证控制
- [x] 创建了认证状态管理store
- [x] 更新了主应用组件支持导航
- [x] 所有路由功能正常工作
- [x] 认证状态持久化正常
## 下一步
进入Step 3:完善状态管理和用户界面优化
## 注意事项
1. 当前使用的是模拟的认证逻辑,实际项目中需要连接真实的后端API
2. 路由守卫使用localStorage进行简单的token检查,生产环境中需要更安全的实现
3. 个人资料页面当前为只读模式,如需编辑功能可参考完整版本的实现
4. 统一使用Pinia状态管理进行登录和登出操作,提供更好的状态管理
5. 登出功能统一在导航栏中提供,避免重复的登出按钮
<script setup></script>
<script setup>
import { computed } from 'vue'
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
const isAuthenticated = computed(() => {
return localStorage.getItem('token')
})
const handleLogout = () => {
authStore.logout()
}
</script>
<template>
<v-app>
<!-- 导航栏 -->
<v-app-bar v-if="isAuthenticated" app color="primary" dark>
<v-app-bar-title>学生管理系统</v-app-bar-title>
<v-spacer />
<v-btn
:to="{ name: 'home' }"
color="white"
variant="text"
>
主页
</v-btn>
<v-btn
:to="{ name: 'profile' }"
color="white"
variant="text"
>
个人资料
</v-btn>
<v-btn color="white" variant="text" @click="handleLogout"> 登出 </v-btn>
</v-app-bar>
<!-- 主要内容区域 -->
<v-main>
<v-container>
<v-row>
<v-col>
<v-card>
<v-card-title>Vue + Vuetify项目初始化成功</v-card-title>
<v-card-text>
<p>这是一个使用Vuetify组件的示例卡片</p>
</v-card-text>
<v-card-actions>
<v-btn color="primary">主要按钮</v-btn>
<v-btn color="secondary">次要按钮</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
<router-view />
</v-main>
</v-app>
</template>
<style scoped></style>
<style scoped>
.v-app-bar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style>
......@@ -8,9 +8,15 @@ import router from './router'
import vuetify from './plugins/vuetify'
const app = createApp(App)
const pinia = createPinia()
app.use(createPinia())
app.use(pinia)
app.use(router)
app.use(vuetify) // 添加这一行以使用Vuetify
app.use(vuetify)
// 初始化认证状态
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
authStore.initializeAuth()
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
routes: [
{
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: true },
},
{
path: '/login',
name: 'login',
component: LoginView,
meta: { requiresAuth: false },
},
{
path: '/profile',
name: 'profile',
component: ProfileView,
meta: { requiresAuth: true },
},
{
path: '/:pathMatch(.*)*',
redirect: '/',
},
],
})
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token') // 简单的认证检查
if (to.meta.requiresAuth && !isAuthenticated) {
// 需要认证但未登录,重定向到登录页
next('/login')
} else if (to.path === '/login' && isAuthenticated) {
// 已登录用户访问登录页,重定向到主页
next('/')
} else {
next()
}
})
export default router
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
export const useAuthStore = defineStore('auth', () => {
const router = useRouter()
// 状态
const token = ref(localStorage.getItem('token') || null)
const user = ref(null)
// 计算属性
const isAuthenticated = computed(() => !!token.value)
// 方法
const login = async (credentials) => {
try {
// TODO: 调用登录API
console.log('登录凭证:', credentials)
// 模拟登录成功
const mockToken = 'mock-jwt-token-' + Date.now()
const mockUser = {
id: 1,
name: '张三',
email: credentials.email,
role: 'student',
}
// 保存认证信息
token.value = mockToken
user.value = mockUser
localStorage.setItem('token', mockToken)
localStorage.setItem('user', JSON.stringify(mockUser))
// 跳转到主页
router.push('/')
return { success: true }
} catch (error) {
console.error('登录失败:', error)
return { success: false, error: error.message }
}
}
const logout = () => {
// 清除认证信息
token.value = null
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
// 跳转到登录页
router.push('/login')
}
const initializeAuth = () => {
const savedToken = localStorage.getItem('token')
const savedUser = localStorage.getItem('user')
if (savedToken && savedUser) {
token.value = savedToken
user.value = JSON.parse(savedUser)
}
}
return {
token,
user,
isAuthenticated,
login,
logout,
initializeAuth,
}
})
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<v-container>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="text-h4"> 欢迎来到学生管理系统 </v-card-title>
<v-card-text>
<p class="text-body-1">这是一个基于Vue 3和Vuetify构建的现代化学生管理系统。</p>
<v-alert type="info" variant="tonal" class="mt-4">
当前功能正在开发中,更多功能即将推出。
</v-alert>
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="router.push('/profile')"> 个人资料 </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
if (!email.value || !password.value) {
error.value = '请填写邮箱和密码'
return
}
loading.value = true
error.value = ''
try {
const result = await authStore.login({
email: email.value,
password: password.value,
})
if (!result.success) {
error.value = result.error || '登录失败'
}
} catch {
error.value = '登录过程中发生错误'
} finally {
loading.value = false
}
}
</script>
<template>
<v-container fluid fill-height>
<v-row align="center" justify="center">
<v-col cols="12" sm="8" md="4">
<v-card class="elevation-12">
<v-toolbar color="primary" dark flat>
<v-toolbar-title>学生管理系统 - 登录</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert v-if="error" type="error" variant="tonal" class="mb-4">
{{ error }}
</v-alert>
<v-form @submit.prevent="handleLogin">
<v-text-field
v-model="email"
label="邮箱"
name="email"
prepend-icon="mdi-email"
type="email"
required
/>
<v-text-field
v-model="password"
label="密码"
name="password"
prepend-icon="mdi-lock"
type="password"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" :loading="loading" @click="handleLogin"> 登录 </v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.v-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>
<script setup>
import { ref } from 'vue'
const user = ref({
name: '张三',
email: 'zhangsan@example.com',
studentId: '2024001',
major: '计算机科学与技术',
grade: '大二',
})
</script>
<template>
<v-container>
<v-row>
<v-col cols="12" md="8" offset-md="2">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-account</v-icon>
个人资料
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="user.name"
label="姓名"
readonly
prepend-icon="mdi-account"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.email"
label="邮箱"
readonly
prepend-icon="mdi-email"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.studentId"
label="学号"
readonly
prepend-icon="mdi-card-account-details"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.major"
label="专业"
readonly
prepend-icon="mdi-school"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="user.grade"
label="年级"
readonly
prepend-icon="mdi-account-group"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment