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.
13 KiB
13 KiB
02-路由和布局设计
目标
配置 Vue Router 路由系统,实现页面布局和导航守卫。
前置要求
- 项目结构已初始化
- Vue Router 已安装
实施步骤
步骤 1:创建用户 Store
创建 src/stores/user.ts:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref<any>(null)
const isLoggedIn = computed(() => !!token.value)
const surveyCompleted = computed(() => userInfo.value?.survey_completed || false)
function setToken(newToken: string) {
token.value = newToken
localStorage.setItem('token', newToken)
}
function setUserInfo(info: any) {
userInfo.value = info
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return {
token,
userInfo,
isLoggedIn,
surveyCompleted,
setToken,
setUserInfo,
logout,
}
})
步骤 2:创建路由配置
创建 src/router/index.ts:
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 布局组件
import MainLayout from '@/components/common/MainLayout.vue'
import AuthLayout from '@/components/common/AuthLayout.vue'
const routes: RouteRecordRaw[] = [
// 认证相关页面
{
path: '/auth',
component: AuthLayout,
children: [
{
path: 'login',
name: 'Login',
component: () => import('@/views/auth/Login.vue'),
meta: { title: '登录' }
},
{
path: 'register',
name: 'Register',
component: () => import('@/views/auth/Register.vue'),
meta: { title: '注册' }
},
],
},
// 健康调查(新用户必经)
{
path: '/survey',
component: MainLayout,
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Survey',
component: () => import('@/views/survey/Index.vue'),
meta: { title: '健康调查' }
},
],
},
// 体质测评
{
path: '/constitution',
component: MainLayout,
meta: { requiresAuth: true, requiresSurvey: true },
children: [
{
path: '',
name: 'Constitution',
component: () => import('@/views/constitution/Index.vue'),
meta: { title: '体质测评' }
},
{
path: 'result',
name: 'ConstitutionResult',
component: () => import('@/views/constitution/Result.vue'),
meta: { title: '体质结果' }
},
],
},
// 主要功能页面
{
path: '/',
component: MainLayout,
meta: { requiresAuth: true, requiresSurvey: true },
children: [
{
path: '',
redirect: '/chat'
},
{
path: 'chat',
name: 'Chat',
component: () => import('@/views/chat/Index.vue'),
meta: { title: 'AI问诊' }
},
{
path: 'chat/:id',
name: 'ChatDetail',
component: () => import('@/views/chat/Detail.vue'),
meta: { title: '对话详情' }
},
{
path: 'profile',
name: 'Profile',
component: () => import('@/views/profile/Index.vue'),
meta: { title: '个人中心' }
},
{
path: 'health-record',
name: 'HealthRecord',
component: () => import('@/views/profile/HealthRecord.vue'),
meta: { title: '健康档案' }
},
],
},
// 404
{
path: '/:pathMatch(.*)*',
redirect: '/'
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 设置页面标题
document.title = (to.meta.title as string) + ' - 健康AI助手' || '健康AI助手'
// 检查是否需要登录
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next({ path: '/auth/login', query: { redirect: to.fullPath } })
return
}
// 检查是否需要完成调查
if (to.meta.requiresSurvey && !userStore.surveyCompleted) {
// 如果已登录但未完成调查,跳转到调查页
if (to.path !== '/survey') {
next('/survey')
return
}
}
// 已登录用户访问登录页,跳转到首页
if ((to.path === '/auth/login' || to.path === '/auth/register') && userStore.isLoggedIn) {
next('/')
return
}
next()
})
export default router
步骤 3:创建认证布局组件
创建 src/components/common/AuthLayout.vue:
<template>
<div class="auth-layout">
<div class="auth-container">
<div class="auth-header">
<img src="@/assets/logo.svg" alt="Logo" class="logo" />
<h1>健康AI助手</h1>
<p>您的智能健康管家</p>
</div>
<div class="auth-content">
<router-view />
</div>
</div>
</div>
</template>
<style scoped>
.auth-layout {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.auth-container {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.auth-header {
text-align: center;
margin-bottom: 30px;
}
.logo {
width: 64px;
height: 64px;
margin-bottom: 16px;
}
.auth-header h1 {
font-size: 24px;
color: #333;
margin-bottom: 8px;
}
.auth-header p {
font-size: 14px;
color: #999;
}
</style>
步骤 4:创建主布局组件
创建 src/components/common/MainLayout.vue:
<template>
<el-container class="main-layout">
<!-- 侧边栏 -->
<el-aside :width="isCollapsed ? '64px' : '220px'" class="sidebar">
<div class="logo-container">
<img src="@/assets/logo.svg" alt="Logo" class="logo" />
<span v-if="!isCollapsed" class="logo-text">健康AI助手</span>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapsed"
router
class="sidebar-menu"
>
<el-menu-item index="/chat">
<el-icon><ChatDotRound /></el-icon>
<span>AI问诊</span>
</el-menu-item>
<el-menu-item index="/constitution">
<el-icon><User /></el-icon>
<span>体质测评</span>
</el-menu-item>
<el-menu-item index="/health-record">
<el-icon><Document /></el-icon>
<span>健康档案</span>
</el-menu-item>
<el-menu-item index="/profile">
<el-icon><Setting /></el-icon>
<span>个人中心</span>
</el-menu-item>
</el-menu>
<div class="sidebar-footer">
<el-button
:icon="isCollapsed ? 'Expand' : 'Fold'"
text
@click="toggleCollapse"
/>
</div>
</el-aside>
<!-- 主内容区 -->
<el-container>
<!-- 顶部导航 -->
<el-header class="header">
<div class="header-left">
<h2>{{ currentTitle }}</h2>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<div class="user-info">
<el-avatar :size="32" :src="userStore.userInfo?.avatar">
{{ userStore.userInfo?.nickname?.charAt(0) || 'U' }}
</el-avatar>
<span class="username">{{ userStore.userInfo?.nickname || '用户' }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<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 { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessageBox } from 'element-plus'
import {
ChatDotRound,
User,
Document,
Setting,
ArrowDown,
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const isCollapsed = ref(false)
const activeMenu = computed(() => route.path)
const currentTitle = computed(() => route.meta.title as string || '健康AI助手')
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const handleCommand = (command: string) => {
if (command === 'logout') {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
userStore.logout()
router.push('/auth/login')
})
} else if (command === 'profile') {
router.push('/profile')
}
}
</script>
<style scoped>
.main-layout {
height: 100vh;
}
.sidebar {
background: #304156;
display: flex;
flex-direction: column;
transition: width 0.3s;
}
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
width: 32px;
height: 32px;
}
.logo-text {
margin-left: 12px;
font-size: 16px;
font-weight: bold;
color: #fff;
white-space: nowrap;
}
.sidebar-menu {
flex: 1;
border-right: none;
background: transparent;
}
.sidebar-menu:not(.el-menu--collapse) {
width: 100%;
}
:deep(.el-menu-item) {
color: rgba(255, 255, 255, 0.7);
}
:deep(.el-menu-item:hover),
:deep(.el-menu-item.is-active) {
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
}
.sidebar-footer {
padding: 16px;
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.header-left h2 {
font-size: 18px;
font-weight: 500;
color: #333;
}
.user-info {
display: flex;
align-items: center;
cursor: pointer;
}
.username {
margin: 0 8px;
font-size: 14px;
color: #666;
}
.main-content {
background: #f5f7fa;
padding: 20px;
overflow-y: auto;
}
</style>
步骤 5:创建 Logo 占位
创建 src/assets/logo.svg:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="30" fill="#667eea"/>
<path d="M32 16c-8.8 0-16 7.2-16 16s7.2 16 16 16 16-7.2 16-16-7.2-16-16-16zm0 28c-6.6 0-12-5.4-12-12s5.4-12 12-12 12 5.4 12 12-5.4 12-12 12z" fill="#fff"/>
<path d="M32 24v16M24 32h16" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
</svg>
步骤 6:更新 App.vue
更新 src/App.vue:
<template>
<router-view />
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { getUserProfile } from '@/api/user'
const userStore = useUserStore()
onMounted(async () => {
// 如果已登录,获取用户信息
if (userStore.isLoggedIn) {
try {
const userInfo = await getUserProfile()
userStore.setUserInfo(userInfo)
} catch (error) {
// Token 可能已失效
userStore.logout()
}
}
})
</script>
步骤 7:创建用户 API
创建 src/api/user.ts:
import request from './request'
export const getUserProfile = () => {
return request.get('/user/profile')
}
export const updateUserProfile = (data: any) => {
return request.put('/user/profile', data)
}
export const getHealthProfile = () => {
return request.get('/user/health-profile')
}
需要创建的文件清单
| 文件路径 | 说明 |
|---|---|
src/stores/user.ts |
用户状态管理 |
src/router/index.ts |
路由配置 |
src/components/common/AuthLayout.vue |
认证布局 |
src/components/common/MainLayout.vue |
主布局 |
src/assets/logo.svg |
Logo 图标 |
src/App.vue |
根组件(更新) |
src/api/user.ts |
用户 API |
路由结构说明
| 路径 | 组件 | 说明 |
|---|---|---|
| /auth/login | Login.vue | 登录页 |
| /auth/register | Register.vue | 注册页 |
| /survey | Survey/Index.vue | 健康调查 |
| /constitution | Constitution/Index.vue | 体质测评 |
| /constitution/result | Constitution/Result.vue | 体质结果 |
| /chat | Chat/Index.vue | AI问诊列表 |
| /chat/:id | Chat/Detail.vue | 对话详情 |
| /profile | Profile/Index.vue | 个人中心 |
| /health-record | Profile/HealthRecord.vue | 健康档案 |
验收标准
- 路由配置正确加载
- 未登录自动跳转登录页
- 登录布局显示正常
- 主布局侧边栏显示正常
- 路由守卫逻辑正确
预计耗时
25-30 分钟
下一步
完成后进入 03-Web前端开发/03-用户认证页面.md