Skip to content

Pinia State Management with Element Plus

Overview

This guide demonstrates how to effectively integrate Pinia state management with Element Plus components, covering advanced patterns, best practices, and real-world implementation scenarios.

Basic Pinia Setup with Element Plus

Store Configuration

typescript
// src/stores/index.ts
import { createPinia } from 'pinia'
import { markRaw } from 'vue'
import type { Router } from 'vue-router'

// Pinia plugins
import { createPersistedState } from 'pinia-plugin-persistedstate'
import { createPiniaLogger } from 'pinia-plugin-logger'

const pinia = createPinia()

// Add persistence plugin
pinia.use(createPersistedState({
  storage: localStorage,
  serializer: {
    serialize: JSON.stringify,
    deserialize: JSON.parse
  }
}))

// Add logger plugin for development
if (import.meta.env.DEV) {
  pinia.use(createPiniaLogger({
    disabled: false,
    logErrors: true
  }))
}

// Router plugin for navigation in stores
pinia.use(({ store }) => {
  store.router = markRaw(router)
})

export default pinia

// Type augmentation for router
declare module 'pinia' {
  export interface PiniaCustomProperties {
    router: Router
  }
}

User Authentication Store

typescript
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { User, LoginCredentials, AuthResponse } from '@/types/auth'
import { authAPI } from '@/api/auth'
import { useUserStore } from './user'
import { usePermissionStore } from './permission'

export const useAuthStore = defineStore('auth', () => {
  // State
  const token = ref<string | null>(null)
  const refreshToken = ref<string | null>(null)
  const isLoading = ref(false)
  const loginAttempts = ref(0)
  const lastLoginTime = ref<Date | null>(null)
  
  // Getters
  const isAuthenticated = computed(() => {
    return !!token.value && !!userStore.currentUser
  })
  
  const isTokenExpired = computed(() => {
    if (!token.value) return true
    
    try {
      const payload = JSON.parse(atob(token.value.split('.')[1]))
      return Date.now() >= payload.exp * 1000
    } catch {
      return true
    }
  })
  
  const timeUntilExpiry = computed(() => {
    if (!token.value) return 0
    
    try {
      const payload = JSON.parse(atob(token.value.split('.')[1]))
      return Math.max(0, payload.exp * 1000 - Date.now())
    } catch {
      return 0
    }
  })
  
  // Store dependencies
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()
  
  // Actions
  const login = async (credentials: LoginCredentials) => {
    isLoading.value = true
    
    try {
      // Rate limiting check
      if (loginAttempts.value >= 5) {
        throw new Error('Too many login attempts. Please try again later.')
      }
      
      const response: AuthResponse = await authAPI.login(credentials)
      
      // Store tokens
      token.value = response.accessToken
      refreshToken.value = response.refreshToken
      lastLoginTime.value = new Date()
      
      // Update user store
      await userStore.setCurrentUser(response.user)
      
      // Update permissions
      await permissionStore.loadUserPermissions(response.user.id)
      
      // Reset login attempts
      loginAttempts.value = 0
      
      // Setup token refresh timer
      setupTokenRefresh()
      
      ElMessage.success('Login successful!')
      
      return response
    } catch (error: any) {
      loginAttempts.value++
      
      ElMessage.error({
        message: error.message || 'Login failed',
        duration: 5000,
        showClose: true
      })
      
      throw error
    } finally {
      isLoading.value = false
    }
  }
  
  const logout = async (showConfirmation = true) => {
    if (showConfirmation) {
      try {
        await ElMessageBox.confirm(
          'Are you sure you want to logout?',
          'Confirm Logout',
          {
            confirmButtonText: 'Logout',
            cancelButtonText: 'Cancel',
            type: 'warning',
            confirmButtonClass: 'el-button--danger'
          }
        )
      } catch {
        return // User cancelled
      }
    }
    
    isLoading.value = true
    
    try {
      // Call logout API if token exists
      if (token.value) {
        await authAPI.logout(refreshToken.value!)
      }
    } catch (error) {
      console.warn('Logout API call failed:', error)
    } finally {
      // Clear all auth data
      token.value = null
      refreshToken.value = null
      lastLoginTime.value = null
      loginAttempts.value = 0
      
      // Clear user data
      userStore.$reset()
      permissionStore.$reset()
      
      // Clear token refresh timer
      clearTokenRefresh()
      
      isLoading.value = false
      
      ElMessage.success('Logged out successfully')
      
      // Redirect to login page
      router.push({ name: 'Login' })
    }
  }
  
  const refreshAccessToken = async () => {
    if (!refreshToken.value) {
      throw new Error('No refresh token available')
    }
    
    try {
      const response = await authAPI.refreshToken(refreshToken.value)
      
      token.value = response.accessToken
      refreshToken.value = response.refreshToken
      
      // Setup new refresh timer
      setupTokenRefresh()
      
      return response.accessToken
    } catch (error) {
      // Refresh failed, logout user
      await logout(false)
      throw error
    }
  }
  
  const checkAuthStatus = async () => {
    if (!token.value) return false
    
    if (isTokenExpired.value) {
      try {
        await refreshAccessToken()
        return true
      } catch {
        return false
      }
    }
    
    return true
  }
  
  // Token refresh management
  let refreshTimer: NodeJS.Timeout | null = null
  
  const setupTokenRefresh = () => {
    clearTokenRefresh()
    
    if (timeUntilExpiry.value > 0) {
      // Refresh token 5 minutes before expiry
      const refreshTime = Math.max(0, timeUntilExpiry.value - 5 * 60 * 1000)
      
      refreshTimer = setTimeout(async () => {
        try {
          await refreshAccessToken()
        } catch (error) {
          console.error('Auto token refresh failed:', error)
        }
      }, refreshTime)
    }
  }
  
  const clearTokenRefresh = () => {
    if (refreshTimer) {
      clearTimeout(refreshTimer)
      refreshTimer = null
    }
  }
  
  // Initialize auth state
  const initialize = async () => {
    if (token.value && !isTokenExpired.value) {
      try {
        // Verify token with server
        const user = await authAPI.getCurrentUser()
        await userStore.setCurrentUser(user)
        await permissionStore.loadUserPermissions(user.id)
        
        setupTokenRefresh()
        return true
      } catch (error) {
        console.warn('Token verification failed:', error)
        await logout(false)
        return false
      }
    }
    
    return false
  }
  
  return {
    // State
    token,
    refreshToken,
    isLoading,
    loginAttempts,
    lastLoginTime,
    
    // Getters
    isAuthenticated,
    isTokenExpired,
    timeUntilExpiry,
    
    // Actions
    login,
    logout,
    refreshAccessToken,
    checkAuthStatus,
    initialize
  }
}, {
  persist: {
    key: 'auth-store',
    paths: ['token', 'refreshToken', 'lastLoginTime'],
    storage: localStorage
  }
})

UI State Management Store

typescript
// src/stores/ui.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { ComponentSize } from 'element-plus'

export type Theme = 'light' | 'dark' | 'auto'
export type Language = 'en' | 'zh' | 'es' | 'fr' | 'de'

export const useUIStore = defineStore('ui', () => {
  // State
  const theme = ref<Theme>('auto')
  const language = ref<Language>('en')
  const componentSize = ref<ComponentSize>('default')
  const sidebarCollapsed = ref(false)
  const loading = ref(false)
  const loadingText = ref('')
  const breadcrumbs = ref<Array<{ title: string; path?: string }>>([])
  
  // Notification settings
  const notificationsEnabled = ref(true)
  const soundEnabled = ref(true)
  const desktopNotifications = ref(false)
  
  // Layout settings
  const headerFixed = ref(true)
  const sidebarFixed = ref(true)
  const showTabs = ref(true)
  const showFooter = ref(true)
  
  // Getters
  const isDarkMode = computed(() => {
    if (theme.value === 'auto') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
    }
    return theme.value === 'dark'
  })
  
  const currentThemeClass = computed(() => {
    return isDarkMode.value ? 'dark' : 'light'
  })
  
  const isRTL = computed(() => {
    return ['ar', 'he', 'fa'].includes(language.value)
  })
  
  // Actions
  const setTheme = (newTheme: Theme) => {
    theme.value = newTheme
    updateThemeClass()
  }
  
  const setLanguage = (newLanguage: Language) => {
    language.value = newLanguage
    updateDocumentLanguage()
  }
  
  const setComponentSize = (size: ComponentSize) => {
    componentSize.value = size
  }
  
  const toggleSidebar = () => {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }
  
  const setSidebarCollapsed = (collapsed: boolean) => {
    sidebarCollapsed.value = collapsed
  }
  
  const showLoading = (text = 'Loading...') => {
    loading.value = true
    loadingText.value = text
  }
  
  const hideLoading = () => {
    loading.value = false
    loadingText.value = ''
  }
  
  const setBreadcrumbs = (crumbs: Array<{ title: string; path?: string }>) => {
    breadcrumbs.value = crumbs
  }
  
  const addBreadcrumb = (crumb: { title: string; path?: string }) => {
    breadcrumbs.value.push(crumb)
  }
  
  const clearBreadcrumbs = () => {
    breadcrumbs.value = []
  }
  
  const updateNotificationSettings = (settings: {
    enabled?: boolean
    sound?: boolean
    desktop?: boolean
  }) => {
    if (settings.enabled !== undefined) {
      notificationsEnabled.value = settings.enabled
    }
    if (settings.sound !== undefined) {
      soundEnabled.value = settings.sound
    }
    if (settings.desktop !== undefined) {
      desktopNotifications.value = settings.desktop
      
      // Request permission for desktop notifications
      if (settings.desktop && 'Notification' in window) {
        Notification.requestPermission()
      }
    }
  }
  
  const updateLayoutSettings = (settings: {
    headerFixed?: boolean
    sidebarFixed?: boolean
    showTabs?: boolean
    showFooter?: boolean
  }) => {
    if (settings.headerFixed !== undefined) {
      headerFixed.value = settings.headerFixed
    }
    if (settings.sidebarFixed !== undefined) {
      sidebarFixed.value = settings.sidebarFixed
    }
    if (settings.showTabs !== undefined) {
      showTabs.value = settings.showTabs
    }
    if (settings.showFooter !== undefined) {
      showFooter.value = settings.showFooter
    }
  }
  
  // Helper functions
  const updateThemeClass = () => {
    const html = document.documentElement
    html.classList.remove('light', 'dark')
    html.classList.add(currentThemeClass.value)
    
    // Update CSS custom properties
    if (isDarkMode.value) {
      html.style.setProperty('--el-color-primary', '#409eff')
      html.style.setProperty('--el-bg-color', '#141414')
      html.style.setProperty('--el-text-color-primary', '#e5eaf3')
    } else {
      html.style.removeProperty('--el-color-primary')
      html.style.removeProperty('--el-bg-color')
      html.style.removeProperty('--el-text-color-primary')
    }
  }
  
  const updateDocumentLanguage = () => {
    document.documentElement.lang = language.value
    document.documentElement.dir = isRTL.value ? 'rtl' : 'ltr'
  }
  
  // Initialize
  const initialize = () => {
    updateThemeClass()
    updateDocumentLanguage()
    
    // Listen for system theme changes
    if (theme.value === 'auto') {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
      mediaQuery.addEventListener('change', updateThemeClass)
    }
  }
  
  return {
    // State
    theme,
    language,
    componentSize,
    sidebarCollapsed,
    loading,
    loadingText,
    breadcrumbs,
    notificationsEnabled,
    soundEnabled,
    desktopNotifications,
    headerFixed,
    sidebarFixed,
    showTabs,
    showFooter,
    
    // Getters
    isDarkMode,
    currentThemeClass,
    isRTL,
    
    // Actions
    setTheme,
    setLanguage,
    setComponentSize,
    toggleSidebar,
    setSidebarCollapsed,
    showLoading,
    hideLoading,
    setBreadcrumbs,
    addBreadcrumb,
    clearBreadcrumbs,
    updateNotificationSettings,
    updateLayoutSettings,
    initialize
  }
}, {
  persist: {
    key: 'ui-store',
    paths: [
      'theme',
      'language',
      'componentSize',
      'sidebarCollapsed',
      'notificationsEnabled',
      'soundEnabled',
      'desktopNotifications',
      'headerFixed',
      'sidebarFixed',
      'showTabs',
      'showFooter'
    ]
  }
})

Data Table Store with Element Plus Integration

typescript
// src/stores/dataTable.ts
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import type { TableColumnCtx } from 'element-plus'

export interface TableColumn {
  prop: string
  label: string
  width?: string | number
  minWidth?: string | number
  fixed?: boolean | 'left' | 'right'
  sortable?: boolean | 'custom'
  filterable?: boolean
  searchable?: boolean
  type?: 'selection' | 'index' | 'expand'
  formatter?: (row: any, column: TableColumnCtx<any>, cellValue: any) => string
  visible?: boolean
}

export interface TableState {
  data: any[]
  loading: boolean
  total: number
  currentPage: number
  pageSize: number
  sortBy: string
  sortOrder: 'ascending' | 'descending' | null
  filters: Record<string, any>
  searchQuery: string
  selectedRows: any[]
  expandedRows: any[]
}

export const useDataTableStore = defineStore('dataTable', () => {
  // State
  const tables = reactive<Record<string, TableState>>({})
  const columns = reactive<Record<string, TableColumn[]>>({})
  
  // Getters
  const getTableState = computed(() => (tableId: string) => {
    return tables[tableId] || createDefaultTableState()
  })
  
  const getTableColumns = computed(() => (tableId: string) => {
    return columns[tableId] || []
  })
  
  const getVisibleColumns = computed(() => (tableId: string) => {
    return (columns[tableId] || []).filter(col => col.visible !== false)
  })
  
  const getSearchableColumns = computed(() => (tableId: string) => {
    return (columns[tableId] || []).filter(col => col.searchable)
  })
  
  const getFilteredData = computed(() => (tableId: string) => {
    const state = tables[tableId]
    if (!state) return []
    
    let filteredData = [...state.data]
    
    // Apply search filter
    if (state.searchQuery) {
      const searchableColumns = getSearchableColumns.value(tableId)
      filteredData = filteredData.filter(row => {
        return searchableColumns.some(col => {
          const value = row[col.prop]
          return value && value.toString().toLowerCase().includes(state.searchQuery.toLowerCase())
        })
      })
    }
    
    // Apply column filters
    Object.entries(state.filters).forEach(([prop, filterValue]) => {
      if (filterValue !== null && filterValue !== undefined && filterValue !== '') {
        filteredData = filteredData.filter(row => {
          const value = row[prop]
          if (Array.isArray(filterValue)) {
            return filterValue.includes(value)
          }
          return value === filterValue
        })
      }
    })
    
    // Apply sorting
    if (state.sortBy && state.sortOrder) {
      filteredData.sort((a, b) => {
        const aVal = a[state.sortBy]
        const bVal = b[state.sortBy]
        
        let result = 0
        if (aVal < bVal) result = -1
        else if (aVal > bVal) result = 1
        
        return state.sortOrder === 'ascending' ? result : -result
      })
    }
    
    return filteredData
  })
  
  const getPaginatedData = computed(() => (tableId: string) => {
    const state = tables[tableId]
    const filteredData = getFilteredData.value(tableId)
    
    if (!state || state.pageSize <= 0) return filteredData
    
    const start = (state.currentPage - 1) * state.pageSize
    const end = start + state.pageSize
    
    return filteredData.slice(start, end)
  })
  
  // Actions
  const initializeTable = (tableId: string, initialColumns: TableColumn[] = []) => {
    if (!tables[tableId]) {
      tables[tableId] = createDefaultTableState()
    }
    
    if (initialColumns.length > 0) {
      columns[tableId] = initialColumns.map(col => ({
        ...col,
        visible: col.visible !== false
      }))
    }
  }
  
  const setTableData = (tableId: string, data: any[], total?: number) => {
    if (!tables[tableId]) {
      initializeTable(tableId)
    }
    
    tables[tableId].data = data
    tables[tableId].total = total || data.length
    tables[tableId].loading = false
  }
  
  const setLoading = (tableId: string, loading: boolean) => {
    if (!tables[tableId]) {
      initializeTable(tableId)
    }
    
    tables[tableId].loading = loading
  }
  
  const updatePagination = (tableId: string, page: number, pageSize?: number) => {
    if (!tables[tableId]) return
    
    tables[tableId].currentPage = page
    if (pageSize) {
      tables[tableId].pageSize = pageSize
    }
  }
  
  const updateSort = (tableId: string, sortBy: string, sortOrder: 'ascending' | 'descending' | null) => {
    if (!tables[tableId]) return
    
    tables[tableId].sortBy = sortBy
    tables[tableId].sortOrder = sortOrder
  }
  
  const updateFilter = (tableId: string, prop: string, value: any) => {
    if (!tables[tableId]) return
    
    tables[tableId].filters[prop] = value
    tables[tableId].currentPage = 1 // Reset to first page when filtering
  }
  
  const updateSearch = (tableId: string, query: string) => {
    if (!tables[tableId]) return
    
    tables[tableId].searchQuery = query
    tables[tableId].currentPage = 1 // Reset to first page when searching
  }
  
  const clearFilters = (tableId: string) => {
    if (!tables[tableId]) return
    
    tables[tableId].filters = {}
    tables[tableId].searchQuery = ''
    tables[tableId].currentPage = 1
  }
  
  const setSelectedRows = (tableId: string, rows: any[]) => {
    if (!tables[tableId]) return
    
    tables[tableId].selectedRows = rows
  }
  
  const toggleRowSelection = (tableId: string, row: any) => {
    if (!tables[tableId]) return
    
    const selectedRows = tables[tableId].selectedRows
    const index = selectedRows.findIndex(r => r.id === row.id)
    
    if (index > -1) {
      selectedRows.splice(index, 1)
    } else {
      selectedRows.push(row)
    }
  }
  
  const clearSelection = (tableId: string) => {
    if (!tables[tableId]) return
    
    tables[tableId].selectedRows = []
  }
  
  const updateColumnVisibility = (tableId: string, prop: string, visible: boolean) => {
    if (!columns[tableId]) return
    
    const column = columns[tableId].find(col => col.prop === prop)
    if (column) {
      column.visible = visible
    }
  }
  
  const reorderColumns = (tableId: string, oldIndex: number, newIndex: number) => {
    if (!columns[tableId]) return
    
    const cols = columns[tableId]
    const [movedColumn] = cols.splice(oldIndex, 1)
    cols.splice(newIndex, 0, movedColumn)
  }
  
  const exportTableData = (tableId: string, format: 'csv' | 'excel' = 'csv') => {
    const data = getFilteredData.value(tableId)
    const visibleColumns = getVisibleColumns.value(tableId)
    
    if (data.length === 0) {
      ElMessage.warning('No data to export')
      return
    }
    
    try {
      if (format === 'csv') {
        exportToCSV(data, visibleColumns, tableId)
      } else {
        exportToExcel(data, visibleColumns, tableId)
      }
      
      ElMessage.success(`Data exported successfully as ${format.toUpperCase()}`)
    } catch (error) {
      ElMessage.error('Export failed')
      console.error('Export error:', error)
    }
  }
  
  const resetTable = (tableId: string) => {
    if (tables[tableId]) {
      tables[tableId] = createDefaultTableState()
    }
  }
  
  // Helper functions
  function createDefaultTableState(): TableState {
    return {
      data: [],
      loading: false,
      total: 0,
      currentPage: 1,
      pageSize: 20,
      sortBy: '',
      sortOrder: null,
      filters: {},
      searchQuery: '',
      selectedRows: [],
      expandedRows: []
    }
  }
  
  function exportToCSV(data: any[], columns: TableColumn[], filename: string) {
    const headers = columns.map(col => col.label).join(',')
    const rows = data.map(row => 
      columns.map(col => {
        const value = row[col.prop]
        return typeof value === 'string' && value.includes(',') 
          ? `"${value}"` 
          : value
      }).join(',')
    )
    
    const csvContent = [headers, ...rows].join('\n')
    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
    
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.download = `${filename}-${new Date().toISOString().split('T')[0]}.csv`
    link.click()
  }
  
  function exportToExcel(data: any[], columns: TableColumn[], filename: string) {
    // This would require a library like xlsx
    console.log('Excel export not implemented yet')
  }
  
  return {
    // State
    tables,
    columns,
    
    // Getters
    getTableState,
    getTableColumns,
    getVisibleColumns,
    getSearchableColumns,
    getFilteredData,
    getPaginatedData,
    
    // Actions
    initializeTable,
    setTableData,
    setLoading,
    updatePagination,
    updateSort,
    updateFilter,
    updateSearch,
    clearFilters,
    setSelectedRows,
    toggleRowSelection,
    clearSelection,
    updateColumnVisibility,
    reorderColumns,
    exportTableData,
    resetTable
  }
})

Store Composition and Integration

vue
<!-- src/components/DataTableManager.vue -->
<template>
  <div class="data-table-manager">
    <!-- Table Controls -->
    <div class="table-controls">
      <el-row :gutter="16" justify="space-between">
        <el-col :span="12">
          <el-input
            v-model="searchQuery"
            placeholder="Search..."
            :prefix-icon="Search"
            clearable
            @input="handleSearch"
          />
        </el-col>
        
        <el-col :span="12" class="text-right">
          <el-button-group>
            <el-button :icon="Refresh" @click="refreshData">
              Refresh
            </el-button>
            
            <el-dropdown @command="handleExport">
              <el-button :icon="Download">
                Export<el-icon class="el-icon--right"><arrow-down /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="csv">Export as CSV</el-dropdown-item>
                  <el-dropdown-item command="excel">Export as Excel</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            
            <el-button :icon="Setting" @click="showColumnSettings = true">
              Columns
            </el-button>
          </el-button-group>
        </el-col>
      </el-row>
    </div>
    
    <!-- Data Table -->
    <el-table
      ref="tableRef"
      :data="paginatedData"
      :loading="tableState.loading"
      v-loading="tableState.loading"
      element-loading-text="Loading data..."
      @sort-change="handleSortChange"
      @selection-change="handleSelectionChange"
      @filter-change="handleFilterChange"
      stripe
      border
      height="600"
    >
      <!-- Selection Column -->
      <el-table-column
        v-if="selectable"
        type="selection"
        width="55"
        fixed="left"
      />
      
      <!-- Index Column -->
      <el-table-column
        v-if="showIndex"
        type="index"
        label="#"
        width="60"
        fixed="left"
      />
      
      <!-- Dynamic Columns -->
      <el-table-column
        v-for="column in visibleColumns"
        :key="column.prop"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        :min-width="column.minWidth"
        :fixed="column.fixed"
        :sortable="column.sortable"
        :filters="getColumnFilters(column)"
        :filter-method="column.filterable ? undefined : null"
        :formatter="column.formatter"
        show-overflow-tooltip
      >
        <template v-if="column.type === 'actions'" #default="{ row, $index }">
          <slot name="actions" :row="row" :index="$index" />
        </template>
      </el-table-column>
    </el-table>
    
    <!-- Pagination -->
    <div class="table-pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="filteredTotal"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
    
    <!-- Column Settings Dialog -->
    <el-dialog
      v-model="showColumnSettings"
      title="Column Settings"
      width="500px"
    >
      <div class="column-settings">
        <el-checkbox-group v-model="selectedColumns">
          <div
            v-for="column in allColumns"
            :key="column.prop"
            class="column-item"
          >
            <el-checkbox :label="column.prop">
              {{ column.label }}
            </el-checkbox>
          </div>
        </el-checkbox-group>
      </div>
      
      <template #footer>
        <el-button @click="showColumnSettings = false">Cancel</el-button>
        <el-button type="primary" @click="applyColumnSettings">
          Apply
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import {
  ElTable,
  ElTableColumn,
  ElInput,
  ElButton,
  ElButtonGroup,
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElPagination,
  ElDialog,
  ElCheckbox,
  ElCheckboxGroup,
  ElRow,
  ElCol,
  ElIcon
} from 'element-plus'
import {
  Search,
  Refresh,
  Download,
  Setting,
  ArrowDown
} from '@element-plus/icons-vue'
import { useDataTableStore } from '@/stores/dataTable'
import type { TableColumn } from '@/stores/dataTable'

interface Props {
  tableId: string
  columns: TableColumn[]
  dataLoader: () => Promise<any[]>
  selectable?: boolean
  showIndex?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  selectable: false,
  showIndex: false
})

const emit = defineEmits<{
  refresh: []
  selectionChange: [rows: any[]]
}>()

// Store
const dataTableStore = useDataTableStore()

// Refs
const tableRef = ref<InstanceType<typeof ElTable>>()
const showColumnSettings = ref(false)
const selectedColumns = ref<string[]>([])

// Computed
const tableState = computed(() => dataTableStore.getTableState(props.tableId))
const allColumns = computed(() => dataTableStore.getTableColumns(props.tableId))
const visibleColumns = computed(() => dataTableStore.getVisibleColumns(props.tableId))
const paginatedData = computed(() => dataTableStore.getPaginatedData(props.tableId))
const filteredTotal = computed(() => dataTableStore.getFilteredData(props.tableId).length)

// Reactive properties
const searchQuery = computed({
  get: () => tableState.value.searchQuery,
  set: (value) => dataTableStore.updateSearch(props.tableId, value)
})

const currentPage = computed({
  get: () => tableState.value.currentPage,
  set: (value) => dataTableStore.updatePagination(props.tableId, value)
})

const pageSize = computed({
  get: () => tableState.value.pageSize,
  set: (value) => dataTableStore.updatePagination(props.tableId, tableState.value.currentPage, value)
})

// Methods
const refreshData = async () => {
  dataTableStore.setLoading(props.tableId, true)
  
  try {
    const data = await props.dataLoader()
    dataTableStore.setTableData(props.tableId, data)
    emit('refresh')
  } catch (error) {
    console.error('Failed to load data:', error)
  }
}

const handleSearch = (query: string) => {
  dataTableStore.updateSearch(props.tableId, query)
}

const handleSortChange = ({ prop, order }: { prop: string; order: string | null }) => {
  const sortOrder = order === 'ascending' ? 'ascending' : order === 'descending' ? 'descending' : null
  dataTableStore.updateSort(props.tableId, prop, sortOrder)
}

const handleSelectionChange = (rows: any[]) => {
  dataTableStore.setSelectedRows(props.tableId, rows)
  emit('selectionChange', rows)
}

const handleFilterChange = (filters: Record<string, any>) => {
  Object.entries(filters).forEach(([prop, value]) => {
    dataTableStore.updateFilter(props.tableId, prop, value)
  })
}

const handleSizeChange = (size: number) => {
  pageSize.value = size
}

const handleCurrentChange = (page: number) => {
  currentPage.value = page
}

const handleExport = (format: 'csv' | 'excel') => {
  dataTableStore.exportTableData(props.tableId, format)
}

const getColumnFilters = (column: TableColumn) => {
  if (!column.filterable) return undefined
  
  // Generate filter options based on unique values in the column
  const data = dataTableStore.getTableState(props.tableId).data
  const uniqueValues = [...new Set(data.map(row => row[column.prop]))]
    .filter(value => value !== null && value !== undefined)
    .map(value => ({ text: String(value), value }))
  
  return uniqueValues.length > 0 ? uniqueValues : undefined
}

const applyColumnSettings = () => {
  allColumns.value.forEach(column => {
    const visible = selectedColumns.value.includes(column.prop)
    dataTableStore.updateColumnVisibility(props.tableId, column.prop, visible)
  })
  
  showColumnSettings.value = false
}

// Lifecycle
onMounted(() => {
  // Initialize table
  dataTableStore.initializeTable(props.tableId, props.columns)
  
  // Set initial selected columns
  selectedColumns.value = visibleColumns.value.map(col => col.prop)
  
  // Load initial data
  refreshData()
})

// Watch for column changes
watch(visibleColumns, (newColumns) => {
  selectedColumns.value = newColumns.map(col => col.prop)
}, { deep: true })
</script>

<style scoped>
.data-table-manager {
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.table-controls {
  margin-bottom: 20px;
}

.table-pagination {
  margin-top: 20px;
  display: flex;
  justify-content: center;
}

.column-settings {
  max-height: 400px;
  overflow-y: auto;
}

.column-item {
  padding: 8px 0;
  border-bottom: 1px solid #f0f0f0;
}

.column-item:last-child {
  border-bottom: none;
}

.text-right {
  text-align: right;
}
</style>

This comprehensive guide demonstrates advanced Pinia integration with Element Plus, covering authentication, UI state management, data table management, and practical component implementation patterns.

Element Plus Study Guide