healthapp
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

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