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