VueUse Utilities Integration with Element Plus
Overview
VueUse is a collection of essential Vue composition utilities that work seamlessly with Element Plus components. This integration guide covers how to leverage VueUse's powerful composables to enhance Element Plus applications with reactive utilities, browser APIs, and advanced component behaviors.
Installation and Setup
Basic Installation
bash
# Install VueUse
npm install @vueuse/core
# Additional packages for specific features
npm install @vueuse/components # Vue components
npm install @vueuse/integrations # Third-party integrations
npm install @vueuse/router # Vue Router utilities
npm install @vueuse/head # Document head management
TypeScript Configuration
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
include: [
'@vueuse/core',
'@vueuse/components',
'@vueuse/integrations'
]
}
})
Core Utilities Integration
Reactive State Management
vue
<!-- components/UserPreferences.vue -->
<template>
<el-card class="preferences-card">
<template #header>
<h3>User Preferences</h3>
</template>
<!-- Theme Toggle -->
<el-form-item label="Theme">
<el-switch
v-model="isDark"
active-text="Dark"
inactive-text="Light"
:active-icon="Moon"
:inactive-icon="Sunny"
/>
</el-form-item>
<!-- Language Selection -->
<el-form-item label="Language">
<el-select v-model="locale" style="width: 200px">
<el-option
v-for="lang in availableLocales"
:key="lang.value"
:label="lang.label"
:value="lang.value"
/>
</el-select>
</el-form-item>
<!-- Auto-save Status -->
<el-form-item label="Auto-save">
<el-tag :type="isAutoSaving ? 'success' : 'info'">
{{ isAutoSaving ? 'Saving...' : 'Saved' }}
</el-tag>
</el-form-item>
<!-- Network Status -->
<el-form-item label="Connection">
<el-tag :type="isOnline ? 'success' : 'danger'">
{{ isOnline ? 'Online' : 'Offline' }}
</el-tag>
</el-form-item>
<!-- Battery Status -->
<el-form-item label="Battery" v-if="isSupported">
<el-progress
:percentage="Math.round((battery?.level || 0) * 100)"
:status="getBatteryStatus()"
:show-text="true"
/>
<el-text size="small" class="battery-info">
{{ battery?.charging ? 'Charging' : 'Not charging' }}
</el-text>
</el-form-item>
</el-card>
</template>
<script setup>
import { computed } from 'vue'
import {
useDark,
useToggle,
useStorage,
useOnline,
useBattery,
useLocalStorage,
watchDebounced
} from '@vueuse/core'
import { Moon, Sunny } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// Theme management
const isDark = useDark({
selector: 'html',
attribute: 'data-theme',
valueDark: 'dark',
valueLight: 'light'
})
// Language management with persistence
const locale = useLocalStorage('user-locale', 'en')
const availableLocales = [
{ label: 'English', value: 'en' },
{ label: '中文', value: 'zh' },
{ label: 'Español', value: 'es' },
{ label: 'Français', value: 'fr' }
]
// Auto-save functionality
const preferences = useStorage('user-preferences', {
theme: isDark,
language: locale,
notifications: true,
autoSave: true
})
const isAutoSaving = ref(false)
// Watch for changes and auto-save
watchDebounced(
[isDark, locale],
async () => {
isAutoSaving.value = true
try {
// Simulate API call to save preferences
await new Promise(resolve => setTimeout(resolve, 1000))
preferences.value = {
theme: isDark.value,
language: locale.value,
notifications: preferences.value.notifications,
autoSave: preferences.value.autoSave
}
ElMessage.success('Preferences saved')
} catch (error) {
ElMessage.error('Failed to save preferences')
} finally {
isAutoSaving.value = false
}
},
{ debounce: 500 }
)
// Network status
const isOnline = useOnline()
// Battery status
const { isSupported, battery } = useBattery()
const getBatteryStatus = () => {
if (!battery.value) return 'info'
const level = battery.value.level * 100
if (level > 50) return 'success'
if (level > 20) return 'warning'
return 'exception'
}
</script>
<style scoped>
.preferences-card {
max-width: 400px;
margin: 20px auto;
}
.battery-info {
margin-left: 10px;
color: var(--el-text-color-secondary);
}
</style>
Mouse and Touch Interactions
vue
<!-- components/InteractiveCanvas.vue -->
<template>
<el-card class="canvas-card">
<template #header>
<el-row justify="space-between" align="middle">
<el-col>
<h3>Interactive Canvas</h3>
</el-col>
<el-col :span="8">
<el-button-group>
<el-button
:type="isDrawing ? 'primary' : 'default'"
:icon="Edit"
@click="toggleDrawing"
>
{{ isDrawing ? 'Stop' : 'Draw' }}
</el-button>
<el-button :icon="Delete" @click="clearCanvas">
Clear
</el-button>
</el-button-group>
</el-col>
</el-row>
</template>
<div class="canvas-container">
<canvas
ref="canvasRef"
:width="canvasSize.width"
:height="canvasSize.height"
class="drawing-canvas"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
/>
<!-- Mouse position indicator -->
<div class="mouse-info">
<el-tag size="small">
Mouse: {{ Math.round(mouse.x) }}, {{ Math.round(mouse.y) }}
</el-tag>
<el-tag size="small" type="info">
{{ isPressed ? 'Pressed' : 'Released' }}
</el-tag>
</div>
<!-- Touch support indicator -->
<div class="touch-info" v-if="isTouchDevice">
<el-tag size="small" type="success">
Touch Device Detected
</el-tag>
</div>
</div>
<!-- Canvas controls -->
<div class="canvas-controls">
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="Brush Size">
<el-slider
v-model="brushSize"
:min="1"
:max="20"
show-input
size="small"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Color">
<el-color-picker v-model="brushColor" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Opacity">
<el-slider
v-model="brushOpacity"
:min="0.1"
:max="1"
:step="0.1"
show-input
size="small"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import {
useMouse,
useMousePressed,
useElementSize,
useDevicesList,
useRafFn,
useToggle
} from '@vueuse/core'
import { Edit, Delete } from '@element-plus/icons-vue'
const canvasRef = ref<HTMLCanvasElement>()
const { width: canvasWidth, height: canvasHeight } = useElementSize(canvasRef)
// Mouse tracking
const { x: mouseX, y: mouseY } = useMouse()
const { pressed: isPressed } = useMousePressed()
// Canvas-relative mouse position
const mouse = computed(() => {
if (!canvasRef.value) return { x: 0, y: 0 }
const rect = canvasRef.value.getBoundingClientRect()
return {
x: mouseX.value - rect.left,
y: mouseY.value - rect.top
}
})
// Touch device detection
const { devices } = useDevicesList()
const isTouchDevice = computed(() =>
devices.value.some(device => device.kind === 'touchinput')
)
// Drawing state
const [isDrawing, toggleDrawing] = useToggle(false)
const isCurrentlyDrawing = ref(false)
const lastPoint = ref({ x: 0, y: 0 })
// Canvas settings
const canvasSize = computed(() => ({
width: 800,
height: 400
}))
const brushSize = ref(5)
const brushColor = ref('#409EFF')
const brushOpacity = ref(0.8)
// Drawing functions
const getContext = () => {
const canvas = canvasRef.value
if (!canvas) return null
const ctx = canvas.getContext('2d')
if (!ctx) return null
// Set drawing properties
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
ctx.lineWidth = brushSize.value
ctx.strokeStyle = brushColor.value
ctx.globalAlpha = brushOpacity.value
return ctx
}
const startDrawing = (event: MouseEvent) => {
if (!isDrawing.value) return
isCurrentlyDrawing.value = true
lastPoint.value = { x: mouse.value.x, y: mouse.value.y }
}
const draw = (event: MouseEvent) => {
if (!isDrawing.value || !isCurrentlyDrawing.value) return
const ctx = getContext()
if (!ctx) return
ctx.beginPath()
ctx.moveTo(lastPoint.value.x, lastPoint.value.y)
ctx.lineTo(mouse.value.x, mouse.value.y)
ctx.stroke()
lastPoint.value = { x: mouse.value.x, y: mouse.value.y }
}
const stopDrawing = () => {
isCurrentlyDrawing.value = false
}
const clearCanvas = () => {
const ctx = getContext()
if (!ctx) return
ctx.clearRect(0, 0, canvasSize.value.width, canvasSize.value.height)
}
// Smooth drawing with RAF
const { pause: pauseRaf, resume: resumeRaf } = useRafFn(() => {
// Update canvas properties if they changed
const ctx = getContext()
if (ctx) {
ctx.lineWidth = brushSize.value
ctx.strokeStyle = brushColor.value
ctx.globalAlpha = brushOpacity.value
}
})
</script>
<style scoped>
.canvas-card {
margin: 20px;
}
.canvas-container {
position: relative;
text-align: center;
margin-bottom: 20px;
}
.drawing-canvas {
border: 2px solid var(--el-border-color);
border-radius: 8px;
cursor: crosshair;
background: white;
}
.drawing-canvas:hover {
border-color: var(--el-color-primary);
}
.mouse-info,
.touch-info {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
}
.touch-info {
top: 50px;
}
.canvas-controls {
background: var(--el-bg-color-page);
padding: 16px;
border-radius: 8px;
}
</style>
Window and Viewport Utilities
vue
<!-- components/ResponsiveLayout.vue -->
<template>
<div class="responsive-layout">
<!-- Window info panel -->
<el-card class="info-panel">
<template #header>
<h3>Viewport Information</h3>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="Window Size">
{{ windowSize.width }} × {{ windowSize.height }}
</el-descriptions-item>
<el-descriptions-item label="Scroll Position">
{{ Math.round(scrollY) }}px
</el-descriptions-item>
<el-descriptions-item label="Device Type">
<el-tag :type="getDeviceType()">{{ deviceType }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Orientation">
<el-tag>{{ orientation }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Pixel Ratio">
{{ pixelRatio }}x
</el-descriptions-item>
<el-descriptions-item label="Color Scheme">
<el-tag :type="preferredDark ? 'info' : 'warning'">
{{ preferredDark ? 'Dark' : 'Light' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- Responsive grid -->
<el-row :gutter="16" class="responsive-grid">
<el-col
v-for="item in gridItems"
:key="item.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
>
<el-card class="grid-item" :body-style="{ padding: '16px' }">
<div class="item-content">
<el-icon :size="32" class="item-icon">
<component :is="item.icon" />
</el-icon>
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
</el-card>
</el-col>
</el-row>
<!-- Scroll progress -->
<div class="scroll-progress">
<el-progress
:percentage="scrollProgress"
:show-text="false"
:stroke-width="4"
color="var(--el-color-primary)"
/>
</div>
<!-- Back to top button -->
<el-backtop
:visibility-height="200"
:right="40"
:bottom="40"
>
<el-button type="primary" :icon="ArrowUp" circle />
</el-backtop>
<!-- Fullscreen toggle -->
<el-button
class="fullscreen-btn"
:type="isFullscreen ? 'danger' : 'primary'"
:icon="isFullscreen ? 'FullScreen' : 'Aim'"
@click="toggle"
circle
>
</el-button>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import {
useWindowSize,
useWindowScroll,
useDeviceOrientation,
useDevicePixelRatio,
usePreferredDark,
useFullscreen,
useBreakpoints,
breakpointsTailwind
} from '@vueuse/core'
import {
Monitor,
Phone,
Tablet,
ArrowUp,
Setting,
User,
Document,
Star
} from '@element-plus/icons-vue'
// Window and viewport utilities
const { width: windowWidth, height: windowHeight } = useWindowSize()
const { x: scrollX, y: scrollY } = useWindowScroll()
const { orientation } = useDeviceOrientation()
const { pixelRatio } = useDevicePixelRatio()
const preferredDark = usePreferredDark()
// Fullscreen functionality
const { isFullscreen, toggle } = useFullscreen()
// Responsive breakpoints
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('md')
const isTablet = breakpoints.between('md', 'lg')
const isDesktop = breakpoints.greater('lg')
// Computed properties
const windowSize = computed(() => ({
width: windowWidth.value,
height: windowHeight.value
}))
const deviceType = computed(() => {
if (isMobile.value) return 'Mobile'
if (isTablet.value) return 'Tablet'
if (isDesktop.value) return 'Desktop'
return 'Unknown'
})
const scrollProgress = computed(() => {
const documentHeight = document.documentElement.scrollHeight - windowHeight.value
return documentHeight > 0 ? Math.round((scrollY.value / documentHeight) * 100) : 0
})
const getDeviceType = () => {
if (isMobile.value) return 'warning'
if (isTablet.value) return 'info'
return 'success'
}
// Grid items data
const gridItems = ref([
{
id: 1,
title: 'Dashboard',
description: 'Main dashboard view',
icon: Monitor
},
{
id: 2,
title: 'Users',
description: 'User management',
icon: User
},
{
id: 3,
title: 'Settings',
description: 'Application settings',
icon: Setting
},
{
id: 4,
title: 'Documents',
description: 'Document library',
icon: Document
},
{
id: 5,
title: 'Favorites',
description: 'Favorite items',
icon: Star
},
{
id: 6,
title: 'Mobile',
description: 'Mobile interface',
icon: Phone
}
])
</script>
<style scoped>
.responsive-layout {
padding: 20px;
min-height: 200vh; /* For scroll testing */
}
.info-panel {
margin-bottom: 24px;
}
.responsive-grid {
margin-bottom: 24px;
}
.grid-item {
margin-bottom: 16px;
transition: transform 0.2s ease;
}
.grid-item:hover {
transform: translateY(-2px);
}
.item-content {
text-align: center;
}
.item-icon {
color: var(--el-color-primary);
margin-bottom: 8px;
}
.scroll-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.fullscreen-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
</style>
Storage and Persistence
vue
<!-- components/DataPersistence.vue -->
<template>
<el-card class="persistence-demo">
<template #header>
<h3>Data Persistence Demo</h3>
</template>
<!-- Local Storage Form -->
<el-form :model="localForm" label-width="120px">
<h4>Local Storage</h4>
<el-form-item label="Username">
<el-input v-model="localForm.username" placeholder="Enter username" />
</el-form-item>
<el-form-item label="Email">
<el-input v-model="localForm.email" type="email" placeholder="Enter email" />
</el-form-item>
<el-form-item label="Preferences">
<el-checkbox-group v-model="localForm.preferences">
<el-checkbox label="notifications">Email Notifications</el-checkbox>
<el-checkbox label="darkMode">Dark Mode</el-checkbox>
<el-checkbox label="autoSave">Auto Save</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<el-divider />
<!-- Session Storage Form -->
<el-form :model="sessionForm" label-width="120px">
<h4>Session Storage</h4>
<el-form-item label="Search Query">
<el-input v-model="sessionForm.searchQuery" placeholder="Search..." />
</el-form-item>
<el-form-item label="Filters">
<el-select v-model="sessionForm.selectedFilters" multiple placeholder="Select filters">
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
<el-option label="Pending" value="pending" />
<el-option label="Completed" value="completed" />
</el-select>
</el-form-item>
</el-form>
<el-divider />
<!-- IndexedDB Demo -->
<div class="indexeddb-demo">
<h4>IndexedDB Storage</h4>
<el-row :gutter="16">
<el-col :span="12">
<el-input
v-model="newNote.title"
placeholder="Note title"
style="margin-bottom: 8px"
/>
<el-input
v-model="newNote.content"
type="textarea"
:rows="3"
placeholder="Note content"
style="margin-bottom: 8px"
/>
<el-button type="primary" @click="addNote" :loading="isAdding">
Add Note
</el-button>
</el-col>
<el-col :span="12">
<div class="notes-list">
<el-empty v-if="notes.length === 0" description="No notes yet" />
<el-card
v-for="note in notes"
:key="note.id"
class="note-card"
:body-style="{ padding: '12px' }"
>
<h5>{{ note.title }}</h5>
<p>{{ note.content }}</p>
<el-text size="small" type="info">
{{ formatDate(note.createdAt) }}
</el-text>
<el-button
size="small"
type="danger"
text
@click="deleteNote(note.id)"
style="float: right"
>
Delete
</el-button>
</el-card>
</div>
</el-col>
</el-row>
</div>
<el-divider />
<!-- Storage Info -->
<div class="storage-info">
<h4>Storage Information</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="Local Storage Size">
{{ localStorageSize }} bytes
</el-descriptions-item>
<el-descriptions-item label="Session Storage Size">
{{ sessionStorageSize }} bytes
</el-descriptions-item>
<el-descriptions-item label="IndexedDB Supported">
<el-tag :type="isIDBSupported ? 'success' : 'danger'">
{{ isIDBSupported ? 'Yes' : 'No' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="storage-actions">
<el-button @click="clearLocalStorage" type="warning">
Clear Local Storage
</el-button>
<el-button @click="clearSessionStorage" type="warning">
Clear Session Storage
</el-button>
<el-button @click="clearAllNotes" type="danger">
Clear All Notes
</el-button>
</div>
</div>
</el-card>
</template>
<script setup>
import { ref, computed } from 'vue'
import {
useLocalStorage,
useSessionStorage,
useStorageAsync,
useIndexedDB
} from '@vueuse/core'
import { ElMessage, ElMessageBox } from 'element-plus'
// Local Storage
const localForm = useLocalStorage('user-form', {
username: '',
email: '',
preferences: []
})
// Session Storage
const sessionForm = useSessionStorage('session-form', {
searchQuery: '',
selectedFilters: []
})
// IndexedDB for notes
interface Note {
id?: number
title: string
content: string
createdAt: Date
}
const newNote = ref<Omit<Note, 'id' | 'createdAt'>>({
title: '',
content: ''
})
const isAdding = ref(false)
// IndexedDB setup
const { isSupported: isIDBSupported, open } = useIndexedDB('notes-db', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true
})
store.createIndex('title', 'title')
store.createIndex('createdAt', 'createdAt')
}
}
})
const notes = ref<Note[]>([])
// Load notes from IndexedDB
const loadNotes = async () => {
if (!isIDBSupported.value) return
try {
const db = await open()
const transaction = db.transaction(['notes'], 'readonly')
const store = transaction.objectStore('notes')
const request = store.getAll()
request.onsuccess = () => {
notes.value = request.result.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
}
} catch (error) {
console.error('Failed to load notes:', error)
ElMessage.error('Failed to load notes')
}
}
// Add note to IndexedDB
const addNote = async () => {
if (!newNote.value.title.trim() || !newNote.value.content.trim()) {
ElMessage.warning('Please fill in both title and content')
return
}
if (!isIDBSupported.value) {
ElMessage.error('IndexedDB is not supported')
return
}
try {
isAdding.value = true
const db = await open()
const transaction = db.transaction(['notes'], 'readwrite')
const store = transaction.objectStore('notes')
const noteToAdd: Omit<Note, 'id'> = {
title: newNote.value.title,
content: newNote.value.content,
createdAt: new Date()
}
const request = store.add(noteToAdd)
request.onsuccess = () => {
ElMessage.success('Note added successfully')
newNote.value = { title: '', content: '' }
loadNotes()
}
request.onerror = () => {
ElMessage.error('Failed to add note')
}
} catch (error) {
console.error('Failed to add note:', error)
ElMessage.error('Failed to add note')
} finally {
isAdding.value = false
}
}
// Delete note from IndexedDB
const deleteNote = async (id: number) => {
try {
await ElMessageBox.confirm(
'Are you sure you want to delete this note?',
'Confirm Delete',
{
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning'
}
)
const db = await open()
const transaction = db.transaction(['notes'], 'readwrite')
const store = transaction.objectStore('notes')
const request = store.delete(id)
request.onsuccess = () => {
ElMessage.success('Note deleted successfully')
loadNotes()
}
request.onerror = () => {
ElMessage.error('Failed to delete note')
}
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to delete note:', error)
ElMessage.error('Failed to delete note')
}
}
}
// Storage size calculations
const localStorageSize = computed(() => {
let total = 0
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length
}
}
return total
})
const sessionStorageSize = computed(() => {
let total = 0
for (let key in sessionStorage) {
if (sessionStorage.hasOwnProperty(key)) {
total += sessionStorage[key].length + key.length
}
}
return total
})
// Clear storage functions
const clearLocalStorage = async () => {
try {
await ElMessageBox.confirm(
'This will clear all local storage data. Continue?',
'Warning',
{
confirmButtonText: 'Clear',
cancelButtonText: 'Cancel',
type: 'warning'
}
)
localStorage.clear()
ElMessage.success('Local storage cleared')
// Reset form
localForm.value = {
username: '',
email: '',
preferences: []
}
} catch (error) {
// User cancelled
}
}
const clearSessionStorage = async () => {
try {
await ElMessageBox.confirm(
'This will clear all session storage data. Continue?',
'Warning',
{
confirmButtonText: 'Clear',
cancelButtonText: 'Cancel',
type: 'warning'
}
)
sessionStorage.clear()
ElMessage.success('Session storage cleared')
// Reset form
sessionForm.value = {
searchQuery: '',
selectedFilters: []
}
} catch (error) {
// User cancelled
}
}
const clearAllNotes = async () => {
try {
await ElMessageBox.confirm(
'This will delete all notes permanently. Continue?',
'Warning',
{
confirmButtonText: 'Delete All',
cancelButtonText: 'Cancel',
type: 'warning'
}
)
const db = await open()
const transaction = db.transaction(['notes'], 'readwrite')
const store = transaction.objectStore('notes')
const request = store.clear()
request.onsuccess = () => {
ElMessage.success('All notes deleted')
notes.value = []
}
request.onerror = () => {
ElMessage.error('Failed to delete notes')
}
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to clear notes:', error)
ElMessage.error('Failed to clear notes')
}
}
}
const formatDate = (date: Date) => {
return new Date(date).toLocaleString()
}
// Initialize
loadNotes()
</script>
<style scoped>
.persistence-demo {
max-width: 800px;
margin: 20px auto;
}
.indexeddb-demo {
margin: 20px 0;
}
.notes-list {
max-height: 300px;
overflow-y: auto;
}
.note-card {
margin-bottom: 8px;
}
.note-card h5 {
margin: 0 0 8px 0;
color: var(--el-text-color-primary);
}
.note-card p {
margin: 0 0 8px 0;
color: var(--el-text-color-regular);
font-size: 14px;
}
.storage-info {
margin-top: 20px;
}
.storage-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
</style>
Advanced Integration Patterns
Custom Composables with Element Plus
typescript
// composables/useElementPlusIntegration.ts
import { ref, computed, watch } from 'vue'
import {
useClipboard,
useShare,
usePermission,
useGeolocation,
useDeviceMotion,
useSpeechSynthesis,
useSpeechRecognition
} from '@vueuse/core'
import { ElMessage, ElNotification, ElLoading } from 'element-plus'
// Enhanced clipboard with Element Plus feedback
export const useEnhancedClipboard = () => {
const { copy, copied, isSupported } = useClipboard()
const copyWithFeedback = async (text: string) => {
if (!isSupported.value) {
ElMessage.error('Clipboard not supported')
return false
}
try {
await copy(text)
ElMessage.success('Copied to clipboard')
return true
} catch (error) {
ElMessage.error('Failed to copy to clipboard')
return false
}
}
return {
copy: copyWithFeedback,
copied,
isSupported
}
}
// Enhanced share with Element Plus integration
export const useEnhancedShare = () => {
const { share, isSupported } = useShare()
const shareWithFallback = async (shareData: ShareData) => {
if (isSupported.value) {
try {
await share(shareData)
return true
} catch (error) {
if (error.name !== 'AbortError') {
ElMessage.error('Failed to share')
}
return false
}
} else {
// Fallback to clipboard
const { copy } = useEnhancedClipboard()
const shareText = `${shareData.title}\n${shareData.text}\n${shareData.url}`
return await copy(shareText)
}
}
return {
share: shareWithFallback,
isSupported
}
}
// Enhanced geolocation with loading states
export const useEnhancedGeolocation = () => {
const {
coords,
locatedAt,
error,
resume,
pause
} = useGeolocation()
const isLoading = ref(false)
const getCurrentPosition = async () => {
const permission = await usePermission('geolocation')
if (permission.value === 'denied') {
ElMessage.error('Geolocation permission denied')
return null
}
isLoading.value = true
const loadingInstance = ElLoading.service({
lock: true,
text: 'Getting your location...',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
resume()
// Wait for location or timeout
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Location timeout'))
}, 10000)
const unwatch = watch(
[coords, error],
([newCoords, newError]) => {
if (newCoords.latitude !== Infinity) {
clearTimeout(timeout)
unwatch()
resolve(newCoords)
} else if (newError) {
clearTimeout(timeout)
unwatch()
reject(newError)
}
},
{ immediate: true }
)
})
ElMessage.success('Location obtained successfully')
return coords.value
} catch (err) {
ElMessage.error('Failed to get location')
return null
} finally {
isLoading.value = false
loadingInstance.close()
pause()
}
}
return {
coords,
locatedAt,
error,
isLoading,
getCurrentPosition
}
}
// Enhanced speech synthesis with Element Plus controls
export const useEnhancedSpeech = () => {
const {
isSupported,
isPlaying,
status,
utterance,
speak,
stop,
pause,
resume
} = useSpeechSynthesis()
const speakWithFeedback = (text: string, options?: SpeechSynthesisUtterance) => {
if (!isSupported.value) {
ElMessage.error('Speech synthesis not supported')
return
}
if (!text.trim()) {
ElMessage.warning('No text to speak')
return
}
speak(text, options)
ElNotification({
title: 'Speech Started',
message: `Speaking: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`,
type: 'info',
duration: 2000
})
}
const stopWithFeedback = () => {
stop()
ElMessage.info('Speech stopped')
}
return {
isSupported,
isPlaying,
status,
utterance,
speak: speakWithFeedback,
stop: stopWithFeedback,
pause,
resume
}
}
// Enhanced speech recognition with Element Plus integration
export const useEnhancedSpeechRecognition = () => {
const {
isSupported,
isListening,
isFinal,
result,
start,
stop
} = useSpeechRecognition({
continuous: true,
interimResults: true
})
const startWithFeedback = () => {
if (!isSupported.value) {
ElMessage.error('Speech recognition not supported')
return
}
start()
ElNotification({
title: 'Listening',
message: 'Speech recognition started. Start speaking...',
type: 'success',
duration: 3000
})
}
const stopWithFeedback = () => {
stop()
ElNotification({
title: 'Stopped',
message: 'Speech recognition stopped',
type: 'info',
duration: 2000
})
}
return {
isSupported,
isListening,
isFinal,
result,
start: startWithFeedback,
stop: stopWithFeedback
}
}
Performance Monitoring Integration
vue
<!-- components/PerformanceMonitor.vue -->
<template>
<el-card class="performance-monitor">
<template #header>
<el-row justify="space-between" align="middle">
<el-col>
<h3>Performance Monitor</h3>
</el-col>
<el-col :span="6">
<el-switch
v-model="isMonitoring"
active-text="Monitoring"
inactive-text="Stopped"
@change="toggleMonitoring"
/>
</el-col>
</el-row>
</template>
<!-- Performance Metrics -->
<el-row :gutter="16">
<el-col :span="8">
<el-statistic
title="FPS"
:value="fps"
suffix="fps"
:value-style="getFpsStyle()"
/>
</el-col>
<el-col :span="8">
<el-statistic
title="Memory Usage"
:value="memoryUsage"
suffix="MB"
:precision="1"
/>
</el-col>
<el-col :span="8">
<el-statistic
title="Network Status"
:value="networkInfo.effectiveType || 'Unknown'"
:value-style="getNetworkStyle()"
/>
</el-col>
</el-row>
<!-- Performance Charts -->
<div class="charts-container">
<el-row :gutter="16">
<el-col :span="12">
<div class="chart-wrapper">
<h4>FPS History</h4>
<div ref="fpsChartRef" class="chart"></div>
</div>
</el-col>
<el-col :span="12">
<div class="chart-wrapper">
<h4>Memory Usage</h4>
<div ref="memoryChartRef" class="chart"></div>
</div>
</el-col>
</el-row>
</div>
<!-- Performance Warnings -->
<div class="warnings" v-if="warnings.length > 0">
<h4>Performance Warnings</h4>
<el-alert
v-for="warning in warnings"
:key="warning.id"
:title="warning.title"
:description="warning.description"
:type="warning.type"
:closable="true"
@close="removeWarning(warning.id)"
style="margin-bottom: 8px"
/>
</div>
</el-card>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import {
useRafFn,
useMemory,
useNetwork,
usePerformanceObserver,
useTimestamp
} from '@vueuse/core'
import { ElMessage } from 'element-plus'
// Performance monitoring state
const isMonitoring = ref(false)
const fps = ref(0)
const fpsHistory = ref<number[]>([])
const memoryHistory = ref<number[]>([])
const warnings = ref<Array<{
id: string
title: string
description: string
type: 'warning' | 'error'
}>>([])
// Chart references
const fpsChartRef = ref<HTMLElement>()
const memoryChartRef = ref<HTMLElement>()
// VueUse composables
const { memory } = useMemory()
const networkInfo = useNetwork()
const timestamp = useTimestamp()
// FPS calculation
let frameCount = 0
let lastTime = 0
const { pause: pauseFps, resume: resumeFps } = useRafFn(() => {
frameCount++
const currentTime = timestamp.value
if (currentTime - lastTime >= 1000) {
fps.value = Math.round((frameCount * 1000) / (currentTime - lastTime))
frameCount = 0
lastTime = currentTime
// Store FPS history
fpsHistory.value.push(fps.value)
if (fpsHistory.value.length > 60) {
fpsHistory.value.shift()
}
// Check for performance issues
checkPerformanceIssues()
}
})
// Memory usage calculation
const memoryUsage = computed(() => {
if (!memory.value) return 0
const usage = memory.value.usedJSHeapSize / (1024 * 1024)
// Store memory history
memoryHistory.value.push(usage)
if (memoryHistory.value.length > 60) {
memoryHistory.value.shift()
}
return usage
})
// Performance issue detection
const checkPerformanceIssues = () => {
// Low FPS warning
if (fps.value < 30) {
addWarning(
'low-fps',
'Low FPS Detected',
`Current FPS is ${fps.value}, which may cause stuttering`,
'warning'
)
}
// High memory usage warning
if (memoryUsage.value > 100) {
addWarning(
'high-memory',
'High Memory Usage',
`Memory usage is ${memoryUsage.value.toFixed(1)}MB`,
'warning'
)
}
// Network issues
if (networkInfo.effectiveType.value === 'slow-2g' || networkInfo.effectiveType.value === '2g') {
addWarning(
'slow-network',
'Slow Network Detected',
'Network connection is slow, which may affect performance',
'warning'
)
}
}
const addWarning = (id: string, title: string, description: string, type: 'warning' | 'error') => {
// Don't add duplicate warnings
if (warnings.value.find(w => w.id === id)) return
warnings.value.push({ id, title, description, type })
// Auto-remove warnings after 10 seconds
setTimeout(() => {
removeWarning(id)
}, 10000)
}
const removeWarning = (id: string) => {
const index = warnings.value.findIndex(w => w.id === id)
if (index > -1) {
warnings.value.splice(index, 1)
}
}
// Style helpers
const getFpsStyle = () => {
if (fps.value >= 60) return { color: '#67C23A' }
if (fps.value >= 30) return { color: '#E6A23C' }
return { color: '#F56C6C' }
}
const getNetworkStyle = () => {
const type = networkInfo.effectiveType.value
if (type === '4g') return { color: '#67C23A' }
if (type === '3g') return { color: '#E6A23C' }
return { color: '#F56C6C' }
}
// Monitoring control
const toggleMonitoring = (enabled: boolean) => {
if (enabled) {
resumeFps()
ElMessage.success('Performance monitoring started')
} else {
pauseFps()
ElMessage.info('Performance monitoring stopped')
}
}
// Chart rendering (simplified - would use a real charting library)
const renderCharts = () => {
// This would typically use Chart.js, ECharts, or similar
// For demo purposes, we'll just update the chart containers
if (fpsChartRef.value) {
fpsChartRef.value.innerHTML = `
<div style="height: 200px; background: linear-gradient(to right, #67C23A, #E6A23C, #F56C6C); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white;">
FPS Chart (${fpsHistory.value.length} data points)
</div>
`
}
if (memoryChartRef.value) {
memoryChartRef.value.innerHTML = `
<div style="height: 200px; background: linear-gradient(to right, #409EFF, #67C23A); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white;">
Memory Chart (${memoryHistory.value.length} data points)
</div>
`
}
}
// Performance Observer for additional metrics
const { isSupported: isPerfObserverSupported } = usePerformanceObserver('navigation', (list) => {
const entries = list.getEntries()
entries.forEach(entry => {
console.log('Navigation timing:', entry)
})
})
// Lifecycle
onMounted(() => {
nextTick(() => {
renderCharts()
})
// Start monitoring by default
isMonitoring.value = true
toggleMonitoring(true)
})
onUnmounted(() => {
pauseFps()
})
// Watch for data changes and re-render charts
watch([fpsHistory, memoryHistory], () => {
renderCharts()
}, { deep: true })
</script>
<style scoped>
.performance-monitor {
margin: 20px;
}
.charts-container {
margin: 24px 0;
}
.chart-wrapper {
margin-bottom: 16px;
}
.chart-wrapper h4 {
margin-bottom: 8px;
color: var(--el-text-color-primary);
}
.chart {
height: 200px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
.warnings {
margin-top: 24px;
}
.warnings h4 {
margin-bottom: 12px;
color: var(--el-text-color-primary);
}
</style>
This comprehensive guide demonstrates how to effectively integrate VueUse utilities with Element Plus components, covering reactive state management, browser APIs, performance monitoring, and advanced interaction patterns. The examples provide practical implementations that enhance user experience and application functionality.