Skip to content

Vue Router Integration with Element Plus

Overview

Vue Router is the official routing library for Vue.js applications. When combined with Element Plus, it enables the creation of sophisticated single-page applications with rich UI components and seamless navigation. This guide covers comprehensive integration patterns, best practices, and advanced techniques.

Basic Setup and Configuration

Installation and Initial Setup

bash
# Install Vue Router
npm install vue-router@4

# For TypeScript projects
npm install @types/vue-router

Router Configuration

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from 'element-plus'

// Lazy-loaded components
const Home = () => import('../views/Home.vue')
const About = () => import('../views/About.vue')
const Dashboard = () => import('../views/Dashboard.vue')
const Profile = () => import('../views/Profile.vue')
const Settings = () => import('../views/Settings.vue')
const NotFound = () => import('../views/NotFound.vue')

// Admin components
const AdminLayout = () => import('../layouts/AdminLayout.vue')
const AdminDashboard = () => import('../views/admin/Dashboard.vue')
const UserManagement = () => import('../views/admin/UserManagement.vue')
const SystemSettings = () => import('../views/admin/SystemSettings.vue')

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      title: 'Home',
      requiresAuth: false,
      breadcrumb: [{ text: 'Home', to: '/' }]
    }
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    meta: {
      title: 'About Us',
      requiresAuth: false,
      breadcrumb: [
        { text: 'Home', to: '/' },
        { text: 'About', to: '/about' }
      ]
    }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: {
      title: 'Dashboard',
      requiresAuth: true,
      roles: ['user', 'admin'],
      breadcrumb: [
        { text: 'Home', to: '/' },
        { text: 'Dashboard', to: '/dashboard' }
      ]
    }
  },
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
    meta: {
      title: 'User Profile',
      requiresAuth: true,
      breadcrumb: [
        { text: 'Home', to: '/' },
        { text: 'Profile', to: '/profile' }
      ]
    }
  },
  {
    path: '/settings',
    name: 'Settings',
    component: Settings,
    meta: {
      title: 'Settings',
      requiresAuth: true,
      breadcrumb: [
        { text: 'Home', to: '/' },
        { text: 'Settings', to: '/settings' }
      ]
    }
  },
  
  // Admin routes with nested layout
  {
    path: '/admin',
    component: AdminLayout,
    meta: {
      requiresAuth: true,
      roles: ['admin']
    },
    children: [
      {
        path: '',
        name: 'AdminDashboard',
        component: AdminDashboard,
        meta: {
          title: 'Admin Dashboard',
          breadcrumb: [
            { text: 'Home', to: '/' },
            { text: 'Admin', to: '/admin' },
            { text: 'Dashboard', to: '/admin' }
          ]
        }
      },
      {
        path: 'users',
        name: 'UserManagement',
        component: UserManagement,
        meta: {
          title: 'User Management',
          breadcrumb: [
            { text: 'Home', to: '/' },
            { text: 'Admin', to: '/admin' },
            { text: 'Users', to: '/admin/users' }
          ]
        }
      },
      {
        path: 'settings',
        name: 'SystemSettings',
        component: SystemSettings,
        meta: {
          title: 'System Settings',
          breadcrumb: [
            { text: 'Home', to: '/' },
            { text: 'Admin', to: '/admin' },
            { text: 'Settings', to: '/admin/settings' }
          ]
        }
      }
    ]
  },
  
  // Dynamic routes
  {
    path: '/user/:id',
    name: 'UserDetail',
    component: () => import('../views/UserDetail.vue'),
    props: true,
    meta: {
      title: 'User Details',
      requiresAuth: true,
      breadcrumb: [
        { text: 'Home', to: '/' },
        { text: 'Users', to: '/users' },
        { text: 'Details', to: '' }
      ]
    }
  },
  
  // Catch-all route
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound,
    meta: {
      title: 'Page Not Found'
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // Smooth scroll behavior
    if (savedPosition) {
      return savedPosition
    } else if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    } else {
      return { top: 0, behavior: 'smooth' }
    }
  }
})

export default router
javascript
// router/guards.js
import { ElMessage, ElLoading } from 'element-plus'
import { useAuthStore } from '../stores/auth'
import { useAppStore } from '../stores/app'

let loadingInstance = null

// Global before guard
router.beforeEach(async (to, from, next) => {
  // Show loading for route changes
  if (to.name !== from.name) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: 'Loading...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  
  const authStore = useAuthStore()
  const appStore = useAppStore()
  
  // Set page title
  if (to.meta.title) {
    document.title = `${to.meta.title} - My App`
  }
  
  // Check authentication
  if (to.meta.requiresAuth) {
    if (!authStore.isAuthenticated) {
      ElMessage.warning('Please login to access this page')
      next({
        name: 'Login',
        query: { redirect: to.fullPath }
      })
      return
    }
    
    // Check user roles
    if (to.meta.roles && !to.meta.roles.includes(authStore.user.role)) {
      ElMessage.error('You do not have permission to access this page')
      next({ name: 'Dashboard' })
      return
    }
  }
  
  // Update breadcrumb
  if (to.meta.breadcrumb) {
    appStore.setBreadcrumb(to.meta.breadcrumb)
  }
  
  next()
})

// Global after guard
router.afterEach((to, from) => {
  // Hide loading
  if (loadingInstance) {
    loadingInstance.close()
    loadingInstance = null
  }
  
  // Track page view for analytics
  if (typeof gtag !== 'undefined') {
    gtag('config', 'GA_TRACKING_ID', {
      page_path: to.fullPath
    })
  }
})

// Global error handler
router.onError((error) => {
  console.error('Router error:', error)
  
  if (loadingInstance) {
    loadingInstance.close()
    loadingInstance = null
  }
  
  ElMessage.error('Navigation error occurred')
})
vue
<!-- components/Navigation/MainMenu.vue -->
<template>
  <el-menu
    :default-active="activeIndex"
    mode="horizontal"
    :router="true"
    class="main-menu"
    @select="handleSelect"
  >
    <el-menu-item index="/">
      <el-icon><House /></el-icon>
      <span>Home</span>
    </el-menu-item>
    
    <el-menu-item index="/about">
      <el-icon><InfoFilled /></el-icon>
      <span>About</span>
    </el-menu-item>
    
    <el-sub-menu index="/dashboard" v-if="isAuthenticated">
      <template #title>
        <el-icon><Monitor /></el-icon>
        <span>Dashboard</span>
      </template>
      <el-menu-item index="/dashboard">Overview</el-menu-item>
      <el-menu-item index="/dashboard/analytics">Analytics</el-menu-item>
      <el-menu-item index="/dashboard/reports">Reports</el-menu-item>
    </el-sub-menu>
    
    <el-sub-menu index="/admin" v-if="isAdmin">
      <template #title>
        <el-icon><Setting /></el-icon>
        <span>Admin</span>
      </template>
      <el-menu-item index="/admin">Dashboard</el-menu-item>
      <el-menu-item index="/admin/users">Users</el-menu-item>
      <el-menu-item index="/admin/settings">Settings</el-menu-item>
    </el-sub-menu>
    
    <div class="flex-grow" />
    
    <!-- User menu -->
    <el-sub-menu index="user" v-if="isAuthenticated">
      <template #title>
        <el-avatar :size="32" :src="user.avatar">
          <el-icon><User /></el-icon>
        </el-avatar>
        <span class="username">{{ user.name }}</span>
      </template>
      <el-menu-item index="/profile">
        <el-icon><User /></el-icon>
        <span>Profile</span>
      </el-menu-item>
      <el-menu-item index="/settings">
        <el-icon><Setting /></el-icon>
        <span>Settings</span>
      </el-menu-item>
      <el-menu-item @click="handleLogout">
        <el-icon><SwitchButton /></el-icon>
        <span>Logout</span>
      </el-menu-item>
    </el-sub-menu>
    
    <!-- Login button -->
    <el-menu-item index="/login" v-else>
      <el-icon><User /></el-icon>
      <span>Login</span>
    </el-menu-item>
  </el-menu>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth'
import {
  House,
  InfoFilled,
  Monitor,
  Setting,
  User,
  SwitchButton
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'

const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()

const activeIndex = computed(() => {
  // Handle nested routes
  if (route.path.startsWith('/admin')) {
    return '/admin'
  }
  if (route.path.startsWith('/dashboard')) {
    return '/dashboard'
  }
  return route.path
})

const isAuthenticated = computed(() => authStore.isAuthenticated)
const isAdmin = computed(() => authStore.user?.role === 'admin')
const user = computed(() => authStore.user)

const handleSelect = (index) => {
  // Custom handling for menu selection
  console.log('Menu selected:', index)
}

const handleLogout = async () => {
  try {
    await ElMessageBox.confirm(
      'Are you sure you want to logout?',
      'Confirm Logout',
      {
        confirmButtonText: 'Logout',
        cancelButtonText: 'Cancel',
        type: 'warning'
      }
    )
    
    await authStore.logout()
    ElMessage.success('Logged out successfully')
    router.push('/')
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('Logout failed')
    }
  }
}
</script>

<style scoped>
.main-menu {
  border-bottom: none;
}

.flex-grow {
  flex-grow: 1;
}

.username {
  margin-left: 8px;
}

.el-menu--horizontal .el-menu-item {
  height: 60px;
  line-height: 60px;
}

.el-menu--horizontal .el-sub-menu .el-sub-menu__title {
  height: 60px;
  line-height: 60px;
}
</style>
vue
<!-- components/Navigation/Sidebar.vue -->
<template>
  <el-aside :width="isCollapsed ? '64px' : '200px'" class="sidebar">
    <div class="sidebar-header">
      <el-button
        :icon="isCollapsed ? Expand : Fold"
        @click="toggleCollapse"
        text
        class="collapse-btn"
      />
    </div>
    
    <el-menu
      :default-active="activeIndex"
      :collapse="isCollapsed"
      :router="true"
      class="sidebar-menu"
      @select="handleSelect"
    >
      <el-menu-item index="/dashboard">
        <el-icon><Monitor /></el-icon>
        <template #title>Dashboard</template>
      </el-menu-item>
      
      <el-sub-menu index="analytics">
        <template #title>
          <el-icon><DataAnalysis /></el-icon>
          <span>Analytics</span>
        </template>
        <el-menu-item index="/analytics/overview">Overview</el-menu-item>
        <el-menu-item index="/analytics/reports">Reports</el-menu-item>
        <el-menu-item index="/analytics/charts">Charts</el-menu-item>
      </el-sub-menu>
      
      <el-sub-menu index="users">
        <template #title>
          <el-icon><User /></el-icon>
          <span>Users</span>
        </template>
        <el-menu-item index="/users/list">User List</el-menu-item>
        <el-menu-item index="/users/roles">Roles</el-menu-item>
        <el-menu-item index="/users/permissions">Permissions</el-menu-item>
      </el-sub-menu>
      
      <el-menu-item index="/settings">
        <el-icon><Setting /></el-icon>
        <template #title>Settings</template>
      </el-menu-item>
      
      <el-menu-item index="/help">
        <el-icon><QuestionFilled /></el-icon>
        <template #title>Help</template>
      </el-menu-item>
    </el-menu>
  </el-aside>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import {
  Monitor,
  DataAnalysis,
  User,
  Setting,
  QuestionFilled,
  Expand,
  Fold
} from '@element-plus/icons-vue'

const route = useRoute()
const isCollapsed = ref(false)

const activeIndex = computed(() => route.path)

const toggleCollapse = () => {
  isCollapsed.value = !isCollapsed.value
}

const handleSelect = (index) => {
  console.log('Sidebar menu selected:', index)
}
</script>

<style scoped>
.sidebar {
  background-color: var(--el-bg-color-page);
  border-right: 1px solid var(--el-border-color-light);
  transition: width 0.3s ease;
}

.sidebar-header {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 16px;
  border-bottom: 1px solid var(--el-border-color-light);
}

.collapse-btn {
  font-size: 18px;
}

.sidebar-menu {
  border-right: none;
  height: calc(100vh - 60px);
}

.sidebar-menu:not(.el-menu--collapse) {
  width: 200px;
}
</style>
vue
<!-- components/Navigation/Breadcrumb.vue -->
<template>
  <el-breadcrumb separator="/" class="app-breadcrumb">
    <transition-group name="breadcrumb" tag="div" class="breadcrumb-container">
      <el-breadcrumb-item
        v-for="(item, index) in breadcrumbItems"
        :key="item.path || item.text"
        :to="item.to"
        class="breadcrumb-item"
      >
        <el-icon v-if="item.icon" class="breadcrumb-icon">
          <component :is="item.icon" />
        </el-icon>
        {{ item.text }}
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '../../stores/app'
import { House } from '@element-plus/icons-vue'

const route = useRoute()
const appStore = useAppStore()

const breadcrumbItems = computed(() => {
  // Get breadcrumb from store or route meta
  const breadcrumb = appStore.breadcrumb || route.meta.breadcrumb || []
  
  // Add home icon to first item
  return breadcrumb.map((item, index) => ({
    ...item,
    icon: index === 0 ? House : null
  }))
})
</script>

<style scoped>
.app-breadcrumb {
  padding: 12px 16px;
  background-color: var(--el-bg-color);
  border-bottom: 1px solid var(--el-border-color-light);
}

.breadcrumb-container {
  display: flex;
  align-items: center;
}

.breadcrumb-item {
  display: flex;
  align-items: center;
}

.breadcrumb-icon {
  margin-right: 4px;
  font-size: 14px;
}

/* Breadcrumb transitions */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
  transition: all 0.3s ease;
}

.breadcrumb-enter-from {
  opacity: 0;
  transform: translateX(-20px);
}

.breadcrumb-leave-to {
  opacity: 0;
  transform: translateX(20px);
}
</style>

Advanced Routing Patterns

Route-based Code Splitting

javascript
// router/lazy-loading.js
import { defineAsyncComponent } from 'vue'
import { ElLoading } from 'element-plus'

// Enhanced lazy loading with loading states
export const createLazyComponent = (importFn, options = {}) => {
  return defineAsyncComponent({
    loader: importFn,
    
    loadingComponent: {
      template: `
        <div class="lazy-loading">
          <el-skeleton :rows="5" animated />
        </div>
      `
    },
    
    errorComponent: {
      template: `
        <div class="lazy-error">
          <el-result
            icon="error"
            title="Loading Error"
            sub-title="Failed to load component"
          >
            <template #extra>
              <el-button type="primary" @click="$emit('retry')">
                Retry
              </el-button>
            </template>
          </el-result>
        </div>
      `
    },
    
    delay: options.delay || 200,
    timeout: options.timeout || 10000,
    
    onError(error, retry, fail, attempts) {
      console.error('Component loading error:', error)
      
      if (attempts <= 3) {
        // Retry up to 3 times
        setTimeout(retry, 1000 * attempts)
      } else {
        fail()
      }
    }
  })
}

// Route-specific lazy components
export const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: createLazyComponent(
      () => import('../views/Dashboard.vue'),
      { delay: 100 }
    )
  },
  {
    path: '/analytics',
    name: 'Analytics',
    component: createLazyComponent(
      () => import('../views/Analytics.vue'),
      { delay: 300 } // Longer delay for heavy components
    )
  }
]

Dynamic Route Generation

javascript
// router/dynamic-routes.js
import { useAuthStore } from '../stores/auth'

// Generate routes based on user permissions
export const generateDynamicRoutes = (userPermissions) => {
  const routes = []
  
  // Base routes available to all users
  routes.push(
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home.vue')
    },
    {
      path: '/profile',
      name: 'Profile',
      component: () => import('../views/Profile.vue'),
      meta: { requiresAuth: true }
    }
  )
  
  // Admin routes
  if (userPermissions.includes('admin')) {
    routes.push(
      {
        path: '/admin',
        name: 'Admin',
        component: () => import('../layouts/AdminLayout.vue'),
        children: [
          {
            path: 'dashboard',
            name: 'AdminDashboard',
            component: () => import('../views/admin/Dashboard.vue')
          },
          {
            path: 'users',
            name: 'UserManagement',
            component: () => import('../views/admin/Users.vue')
          }
        ]
      }
    )
  }
  
  // Manager routes
  if (userPermissions.includes('manager')) {
    routes.push(
      {
        path: '/reports',
        name: 'Reports',
        component: () => import('../views/Reports.vue')
      }
    )
  }
  
  return routes
}

// Add routes dynamically after authentication
export const addDynamicRoutes = (router, userPermissions) => {
  const dynamicRoutes = generateDynamicRoutes(userPermissions)
  
  dynamicRoutes.forEach(route => {
    router.addRoute(route)
  })
}

// Remove dynamic routes on logout
export const removeDynamicRoutes = (router, routeNames) => {
  routeNames.forEach(name => {
    if (router.hasRoute(name)) {
      router.removeRoute(name)
    }
  })
}

Route Transitions

vue
<!-- App.vue with route transitions -->
<template>
  <div id="app">
    <MainNavigation />
    
    <el-container>
      <Sidebar v-if="showSidebar" />
      
      <el-main class="main-content">
        <Breadcrumb />
        
        <router-view v-slot="{ Component, route }">
          <transition
            :name="getTransitionName(route)"
            mode="out-in"
            @before-enter="onBeforeEnter"
            @after-enter="onAfterEnter"
          >
            <keep-alive :include="cachedViews">
              <component :is="Component" :key="route.fullPath" />
            </keep-alive>
          </transition>
        </router-view>
      </el-main>
    </el-container>
  </div>
</template>

<script setup>
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from './stores/app'
import MainNavigation from './components/Navigation/MainNavigation.vue'
import Sidebar from './components/Navigation/Sidebar.vue'
import Breadcrumb from './components/Navigation/Breadcrumb.vue'

const route = useRoute()
const appStore = useAppStore()

const showSidebar = computed(() => {
  return !['Login', 'Register', 'NotFound'].includes(route.name)
})

const cachedViews = computed(() => appStore.cachedViews)

const getTransitionName = (route) => {
  // Different transitions for different route types
  if (route.meta.transition) {
    return route.meta.transition
  }
  
  if (route.path.startsWith('/admin')) {
    return 'slide-left'
  }
  
  if (route.path.startsWith('/dashboard')) {
    return 'fade'
  }
  
  return 'slide-right'
}

const onBeforeEnter = () => {
  // Show loading indicator
  appStore.setLoading(true)
}

const onAfterEnter = () => {
  // Hide loading indicator
  appStore.setLoading(false)
}
</script>

<style>
/* Route transition animations */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.slide-left-enter-active,
.slide-left-leave-active {
  transition: all 0.3s ease;
}

.slide-left-enter-from {
  transform: translateX(100%);
  opacity: 0;
}

.slide-left-leave-to {
  transform: translateX(-100%);
  opacity: 0;
}

.slide-right-enter-active,
.slide-right-leave-active {
  transition: all 0.3s ease;
}

.slide-right-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}

.slide-right-leave-to {
  transform: translateX(100%);
  opacity: 0;
}

.main-content {
  min-height: calc(100vh - 60px);
  padding: 0;
}
</style>

Route-based Data Fetching

Data Fetching Strategies

javascript
// composables/useRouteData.js
import { ref, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElLoading, ElMessage } from 'element-plus'

export const useRouteData = (fetchFn, options = {}) => {
  const route = useRoute()
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const {
    immediate = true,
    watchParams = true,
    showLoading = true,
    loadingText = 'Loading...',
    errorHandler = null
  } = options
  
  let loadingInstance = null
  
  const fetchData = async () => {
    try {
      loading.value = true
      error.value = null
      
      if (showLoading) {
        loadingInstance = ElLoading.service({
          lock: true,
          text: loadingText,
          background: 'rgba(0, 0, 0, 0.7)'
        })
      }
      
      const result = await fetchFn(route.params, route.query)
      data.value = result
      
    } catch (err) {
      error.value = err
      
      if (errorHandler) {
        errorHandler(err)
      } else {
        ElMessage.error(`Failed to load data: ${err.message}`)
      }
    } finally {
      loading.value = false
      
      if (loadingInstance) {
        loadingInstance.close()
        loadingInstance = null
      }
    }
  }
  
  // Watch route parameters for changes
  if (watchParams) {
    watch(
      () => [route.params, route.query],
      () => fetchData(),
      { deep: true }
    )
  }
  
  // Initial fetch
  if (immediate) {
    onMounted(fetchData)
  }
  
  return {
    data,
    loading,
    error,
    fetchData,
    refetch: fetchData
  }
}

Route-specific Data Loading

vue
<!-- views/UserDetail.vue -->
<template>
  <div class="user-detail">
    <el-page-header @back="goBack" :content="pageTitle">
      <template #extra>
        <el-button-group>
          <el-button :icon="Edit" @click="editUser">Edit</el-button>
          <el-button :icon="Delete" type="danger" @click="deleteUser">
            Delete
          </el-button>
        </el-button-group>
      </template>
    </el-page-header>
    
    <el-card v-if="user" class="user-card">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="Name">
          {{ user.name }}
        </el-descriptions-item>
        <el-descriptions-item label="Email">
          {{ user.email }}
        </el-descriptions-item>
        <el-descriptions-item label="Role">
          <el-tag :type="getRoleType(user.role)">{{ user.role }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="Status">
          <el-tag :type="user.active ? 'success' : 'danger'">
            {{ user.active ? 'Active' : 'Inactive' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="Created At">
          {{ formatDate(user.createdAt) }}
        </el-descriptions-item>
        <el-descriptions-item label="Last Login">
          {{ formatDate(user.lastLogin) }}
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
    
    <!-- User activity timeline -->
    <el-card v-if="activities" class="activity-card">
      <template #header>
        <h3>Recent Activity</h3>
      </template>
      
      <el-timeline>
        <el-timeline-item
          v-for="activity in activities"
          :key="activity.id"
          :timestamp="formatDate(activity.timestamp)"
          :type="getActivityType(activity.type)"
        >
          {{ activity.description }}
        </el-timeline-item>
      </el-timeline>
    </el-card>
    
    <!-- Error state -->
    <el-result
      v-if="error"
      icon="error"
      title="Failed to Load User"
      :sub-title="error.message"
    >
      <template #extra>
        <el-button type="primary" @click="refetch">Retry</el-button>
      </template>
    </el-result>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useRouteData } from '../composables/useRouteData'
import { userApi } from '../api/users'
import { Edit, Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'

const props = defineProps({
  id: {
    type: String,
    required: true
  }
})

const router = useRouter()

// Fetch user data
const {
  data: user,
  loading: userLoading,
  error,
  refetch
} = useRouteData(
  async (params) => {
    const response = await userApi.getUser(params.id)
    return response.data
  },
  {
    loadingText: 'Loading user details...'
  }
)

// Fetch user activities
const {
  data: activities,
  loading: activitiesLoading
} = useRouteData(
  async (params) => {
    const response = await userApi.getUserActivities(params.id)
    return response.data
  },
  {
    showLoading: false
  }
)

const pageTitle = computed(() => {
  return user.value ? `User: ${user.value.name}` : 'User Details'
})

const goBack = () => {
  router.go(-1)
}

const editUser = () => {
  router.push(`/users/${props.id}/edit`)
}

const deleteUser = async () => {
  try {
    await ElMessageBox.confirm(
      'This will permanently delete the user. Continue?',
      'Warning',
      {
        confirmButtonText: 'Delete',
        cancelButtonText: 'Cancel',
        type: 'warning'
      }
    )
    
    await userApi.deleteUser(props.id)
    ElMessage.success('User deleted successfully')
    router.push('/users')
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('Failed to delete user')
    }
  }
}

const getRoleType = (role) => {
  const types = {
    admin: 'danger',
    manager: 'warning',
    user: 'info'
  }
  return types[role] || 'info'
}

const getActivityType = (type) => {
  const types = {
    login: 'primary',
    logout: 'info',
    update: 'warning',
    delete: 'danger'
  }
  return types[type] || 'primary'
}

const formatDate = (date) => {
  return new Date(date).toLocaleString()
}
</script>

<style scoped>
.user-detail {
  padding: 20px;
}

.user-card,
.activity-card {
  margin-top: 20px;
}

.el-page-header {
  margin-bottom: 20px;
}
</style>

Best Practices and Performance

Route Caching Strategy

javascript
// stores/routeCache.js
import { defineStore } from 'pinia'

export const useRouteCacheStore = defineStore('routeCache', {
  state: () => ({
    cachedViews: [],
    cachedData: new Map(),
    maxCacheSize: 10
  }),
  
  actions: {
    addCachedView(view) {
      if (this.cachedViews.includes(view.name)) return
      
      if (this.cachedViews.length >= this.maxCacheSize) {
        // Remove oldest cached view
        const removed = this.cachedViews.shift()
        this.cachedData.delete(removed)
      }
      
      this.cachedViews.push(view.name)
    },
    
    removeCachedView(view) {
      const index = this.cachedViews.indexOf(view.name)
      if (index > -1) {
        this.cachedViews.splice(index, 1)
        this.cachedData.delete(view.name)
      }
    },
    
    clearCache() {
      this.cachedViews = []
      this.cachedData.clear()
    },
    
    setCachedData(routeName, data) {
      this.cachedData.set(routeName, {
        data,
        timestamp: Date.now()
      })
    },
    
    getCachedData(routeName, maxAge = 5 * 60 * 1000) {
      const cached = this.cachedData.get(routeName)
      
      if (!cached) return null
      
      if (Date.now() - cached.timestamp > maxAge) {
        this.cachedData.delete(routeName)
        return null
      }
      
      return cached.data
    }
  }
})

SEO and Meta Management

javascript
// composables/useMeta.js
import { watch } from 'vue'
import { useRoute } from 'vue-router'

export const useMeta = () => {
  const route = useRoute()
  
  const setMeta = (meta) => {
    // Set page title
    if (meta.title) {
      document.title = `${meta.title} - My App`
    }
    
    // Set meta description
    if (meta.description) {
      updateMetaTag('description', meta.description)
    }
    
    // Set Open Graph tags
    if (meta.ogTitle) {
      updateMetaTag('og:title', meta.ogTitle, 'property')
    }
    
    if (meta.ogDescription) {
      updateMetaTag('og:description', meta.ogDescription, 'property')
    }
    
    if (meta.ogImage) {
      updateMetaTag('og:image', meta.ogImage, 'property')
    }
    
    // Set canonical URL
    if (meta.canonical) {
      updateCanonicalLink(meta.canonical)
    }
  }
  
  const updateMetaTag = (name, content, attribute = 'name') => {
    let element = document.querySelector(`meta[${attribute}="${name}"]`)
    
    if (!element) {
      element = document.createElement('meta')
      element.setAttribute(attribute, name)
      document.head.appendChild(element)
    }
    
    element.setAttribute('content', content)
  }
  
  const updateCanonicalLink = (href) => {
    let element = document.querySelector('link[rel="canonical"]')
    
    if (!element) {
      element = document.createElement('link')
      element.setAttribute('rel', 'canonical')
      document.head.appendChild(element)
    }
    
    element.setAttribute('href', href)
  }
  
  // Watch route changes
  watch(
    () => route.meta,
    (meta) => {
      if (meta) {
        setMeta(meta)
      }
    },
    { immediate: true }
  )
  
  return {
    setMeta
  }
}

Error Handling

vue
<!-- components/ErrorBoundary.vue -->
<template>
  <div class="error-boundary">
    <slot v-if="!hasError" />
    
    <el-result
      v-else
      icon="error"
      :title="errorTitle"
      :sub-title="errorMessage"
    >
      <template #extra>
        <el-button type="primary" @click="retry">
          Try Again
        </el-button>
        <el-button @click="goHome">
          Go Home
        </el-button>
      </template>
    </el-result>
  </div>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'

const router = useRouter()
const hasError = ref(false)
const errorTitle = ref('Something went wrong')
const errorMessage = ref('An unexpected error occurred')

onErrorCaptured((error, instance, info) => {
  console.error('Error caught by boundary:', error, info)
  
  hasError.value = true
  errorTitle.value = 'Component Error'
  errorMessage.value = error.message || 'An error occurred while rendering this component'
  
  // Report error to monitoring service
  if (window.Sentry) {
    window.Sentry.captureException(error, {
      contexts: {
        vue: {
          componentName: instance?.$options.name || 'Unknown',
          errorInfo: info
        }
      }
    })
  }
  
  return false // Prevent error from propagating
})

const retry = () => {
  hasError.value = false
  errorTitle.value = 'Something went wrong'
  errorMessage.value = 'An unexpected error occurred'
}

const goHome = () => {
  router.push('/')
}
</script>

<style scoped>
.error-boundary {
  min-height: 400px;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

This comprehensive guide covers all aspects of Vue Router integration with Element Plus, from basic setup to advanced patterns like dynamic routing, data fetching, and error handling. The examples provide a solid foundation for building robust, scalable applications with excellent user experience.

Element Plus Study Guide