You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8.0 KiB
8.0 KiB
02-路由和布局设计
目标
配置 Vue Router 路由系统,实现页面布局和导航。
前置要求
- 项目结构已初始化
- Vue Router 已安装
- 模拟数据服务已创建
实施步骤
步骤 1:创建路由配置
创建 src/router/index.ts:
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
name: 'Login',
component: () => import('@/views/auth/LoginView.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: () => import('@/views/layout/MainLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/home/HomeView.vue')
},
{
path: 'chat',
name: 'ChatList',
component: () => import('@/views/chat/ChatListView.vue')
},
{
path: 'chat/:id',
name: 'ChatDetail',
component: () => import('@/views/chat/ChatDetailView.vue')
},
{
path: 'constitution',
name: 'Constitution',
component: () => import('@/views/constitution/ConstitutionView.vue')
},
{
path: 'constitution/test',
name: 'ConstitutionTest',
component: () => import('@/views/constitution/ConstitutionTestView.vue')
},
{
path: 'constitution/result',
name: 'ConstitutionResult',
component: () => import('@/views/constitution/ConstitutionResultView.vue')
},
{
path: 'profile',
name: 'Profile',
component: () => import('@/views/profile/ProfileView.vue')
},
{
path: 'profile/health-record',
name: 'HealthRecord',
component: () => import('@/views/profile/HealthRecordView.vue')
}
]
}
]
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
next('/login')
} else if (to.path === '/login' && authStore.isLoggedIn) {
next('/')
} else {
next()
}
})
export default router
步骤 2:创建认证状态 Store
创建 src/stores/auth.ts:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
const isLoggedIn = computed(() => !!token.value)
function login(userData: User) {
user.value = userData
token.value = 'mock-token-' + userData.id
localStorage.setItem('token', token.value)
localStorage.setItem('user', JSON.stringify(userData))
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
// 初始化时从 localStorage 恢复用户信息
function init() {
const savedUser = localStorage.getItem('user')
if (savedUser && token.value) {
user.value = JSON.parse(savedUser)
}
}
init()
return { user, token, isLoggedIn, login, logout }
})
步骤 3:创建主布局
创建 src/views/layout/MainLayout.vue:
<template>
<el-container class="main-layout">
<!-- 侧边栏 -->
<el-aside width="220px" class="sidebar">
<div class="logo">
<el-icon size="28" color="#10B981"><FirstAidKit /></el-icon>
<span>AI健康助手</span>
</div>
<el-menu
:default-active="activeMenu"
router
class="sidebar-menu"
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/chat">
<el-icon><ChatDotRound /></el-icon>
<span>AI问答</span>
</el-menu-item>
<el-menu-item index="/constitution">
<el-icon><TrendCharts /></el-icon>
<span>体质分析</span>
</el-menu-item>
<el-menu-item index="/profile">
<el-icon><User /></el-icon>
<span>我的</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<el-header class="header">
<div class="header-left">
<span class="greeting">{{ greeting }},{{ authStore.user?.nickname || '用户' }}</span>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<el-avatar :size="36">
{{ authStore.user?.nickname?.charAt(0) || 'U' }}
</el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const activeMenu = computed(() => {
const path = route.path
if (path.startsWith('/chat')) return '/chat'
if (path.startsWith('/constitution')) return '/constitution'
if (path.startsWith('/profile')) return '/profile'
return path
})
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 12) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
})
const handleCommand = (command: string) => {
if (command === 'profile') {
router.push('/profile')
} else if (command === 'logout') {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
authStore.logout()
router.push('/login')
})
}
}
</script>
<style scoped lang="scss">
.main-layout {
height: 100vh;
}
.sidebar {
background: #fff;
border-right: 1px solid #E5E7EB;
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-bottom: 1px solid #E5E7EB;
span {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
}
.sidebar-menu {
border-right: none;
:deep(.el-menu-item.is-active) {
background-color: #ECFDF5;
color: #10B981;
}
}
}
.header {
background: #fff;
border-bottom: 1px solid #E5E7EB;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.greeting {
font-size: 16px;
color: #1F2937;
}
.el-avatar {
cursor: pointer;
background-color: #10B981;
}
}
.main-content {
background: #F3F4F6;
padding: 20px;
}
</style>
步骤 4:更新 App.vue
<template>
<router-view />
</template>
<style>
html, body, #app {
height: 100%;
margin: 0;
}
</style>
路由结构
/login - 登录页
/ - 主布局
├── / - 首页
├── /chat - 对话列表
├── /chat/:id - 对话详情
├── /constitution - 体质分析首页
├── /constitution/test - 体质问卷
├── /constitution/result - 体质结果
├── /profile - 个人中心
└── /profile/health-record - 健康档案
验收标准
- 路由配置正确
- 布局显示正常
- 导航切换正常
- 登录状态守卫正常
预计耗时
25-30 分钟
下一步
完成后进入 03-Web原型开发/03-登录页面.md