Element Plus 实战指南

🎨 Element Plus 实战指南

作为后端开发者,您需要一个能够快速构建企业级界面的UI框架。Element Plus 是 Vue 3 的最佳UI框架之一,专为中后台系统设计。

🚀 快速开始

1. 安装与配置

bash 复制代码
# 完整安装
npm install element-plus

# 按需导入(推荐)
npm install element-plus @element-plus/icons-vue
npm install unplugin-vue-components unplugin-auto-import -D

2. 完整引入(适合小型项目)

javascript 复制代码
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'

const app = createApp(App)

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.use(ElementPlus)
app.mount('#app')

3. 按需导入(推荐,减小打包体积)

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    // 自动导入 Element Plus 组件
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})
javascript 复制代码
// 现在可以直接在组件中使用,无需导入
<template>
  <el-button>按钮</el-button>
  <el-input v-model="input" />
</template>

<script setup>
// 无需 import { ElButton, ElInput } from 'element-plus'
const input = ref('')
</script>

📦 核心组件实战

1. 布局组件 Layout

vue 复制代码
<template>
  <!-- 经典后台管理系统布局 -->
  <el-container class="layout-container">
    <!-- 侧边栏 -->
    <el-aside width="200px" class="sidebar">
      <!-- Logo -->
      <div class="logo">
        <img src="@/assets/logo.png" alt="Logo" />
        <h2 v-show="!isCollapse">后台管理系统</h2>
      </div>
      
      <!-- 导航菜单 -->
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :router="true"
        :unique-opened="true"
        background-color="#304156"
        text-color="#bfcbd9"
        active-text-color="#409eff"
        @select="handleMenuSelect"
      >
        <template v-for="item in menuItems" :key="item.path">
          <!-- 有子菜单 -->
          <el-sub-menu
            v-if="item.children"
            :index="item.path"
          >
            <template #title>
              <el-icon>
                <component :is="item.icon || 'Menu'" />
              </el-icon>
              <span>{{ item.title }}</span>
            </template>
            
            <el-menu-item
              v-for="child in item.children"
              :key="child.path"
              :index="child.path"
              :route="child.path"
            >
              <el-icon>
                <component :is="child.icon || 'Menu'" />
              </el-icon>
              <span>{{ child.title }}</span>
            </el-menu-item>
          </el-sub-menu>
          
          <!-- 无子菜单 -->
          <el-menu-item
            v-else
            :index="item.path"
            :route="item.path"
          >
            <el-icon>
              <component :is="item.icon || 'Menu'" />
            </el-icon>
            <span>{{ item.title }}</span>
          </el-menu-item>
        </template>
      </el-menu>
    </el-aside>
    
    <!-- 主容器 -->
    <el-container>
      <!-- 顶部导航栏 -->
      <el-header class="header">
        <div class="header-left">
          <!-- 折叠按钮 -->
          <el-button
            type="text"
            :icon="isCollapse ? 'Expand' : 'Fold'"
            @click="toggleSidebar"
          />
          
          <!-- 面包屑 -->
          <el-breadcrumb separator="/">
            <el-breadcrumb-item
              v-for="item in breadcrumb"
              :key="item.path"
              :to="item.path"
            >
              {{ item.title }}
            </el-breadcrumb-item>
          </el-breadcrumb>
        </div>
        
        <div class="header-right">
          <!-- 全屏 -->
          <el-tooltip content="全屏">
            <el-button
              type="text"
              :icon="isFullscreen ? 'FullScreen' : 'Crop'"
              @click="toggleFullscreen"
            />
          </el-tooltip>
          
          <!-- 消息通知 -->
          <el-dropdown @command="handleMessageCommand">
            <el-badge :value="unreadCount" :max="99">
              <el-button type="text" :icon="Bell" />
            </el-badge>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item
                  v-for="message in messages"
                  :key="message.id"
                  :command="message.id"
                  divided
                >
                  <div class="message-item">
                    <div class="message-title">{{ message.title }}</div>
                    <div class="message-time">{{ message.time }}</div>
                  </div>
                </el-dropdown-item>
                <el-dropdown-item divided>
                  <el-button type="text" @click="viewAllMessages">
                    查看全部
                  </el-button>
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
          
          <!-- 用户菜单 -->
          <el-dropdown @command="handleUserCommand">
            <span class="user-info">
              <el-avatar :size="32" :src="user.avatar" />
              <span class="username">{{ user.name }}</span>
              <el-icon><arrow-down /></el-icon>
            </span>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="profile">
                  <el-icon><User /></el-icon>
                  个人中心
                </el-dropdown-item>
                <el-dropdown-item command="settings">
                  <el-icon><setting /></el-icon>
                  设置
                </el-dropdown-item>
                <el-dropdown-item divided command="logout">
                  <el-icon><SwitchButton /></el-icon>
                  退出登录
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
      </el-header>
      
      <!-- 标签页 -->
      <el-main class="main-content">
        <!-- 页面标签 -->
        <div v-if="showTabs" class="page-tabs">
          <el-tabs
            v-model="activeTab"
            type="card"
            closable
            @tab-click="handleTabClick"
            @tab-remove="handleTabRemove"
          >
            <el-tab-pane
              v-for="tab in visitedTabs"
              :key="tab.path"
              :label="tab.title"
              :name="tab.path"
            />
          </el-tabs>
        </div>
        
        <!-- 内容区域 -->
        <div class="content-wrapper">
          <router-view v-slot="{ Component }">
            <transition name="fade-transform" mode="out-in">
              <keep-alive :include="cachedViews">
                <component :is="Component" />
              </keep-alive>
            </transition>
          </router-view>
        </div>
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useFullscreen } from '@vueuse/core'
import { Bell, User, Setting, SwitchButton } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { useTagsViewStore } from '@/stores/tagsView'

const route = useRoute()
const router = useRouter()
const userStore = useUserStore()

// 侧边栏折叠状态
const isCollapse = ref(false)
const toggleSidebar = () => {
  isCollapse.value = !isCollapse.value
}

// 全屏
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()

// 用户信息
const user = computed(() => userStore.userInfo || { name: '用户', avatar: '' })

// 面包屑导航
const breadcrumb = computed(() => {
  const matched = route.matched.filter(item => item.meta?.title)
  return matched.map(item => ({
    path: item.path,
    title: item.meta.title
  }))
})

// 动态菜单(从路由生成)
const menuItems = computed(() => {
  const routes = router.getRoutes()
  return routes
    .filter(route => route.meta?.showInMenu)
    .map(route => ({
      path: route.path,
      title: route.meta.title,
      icon: route.meta.icon,
      children: route.children?.filter(child => child.meta?.showInMenu)
    }))
})

// 当前激活的菜单
const activeMenu = computed(() => route.path)

// 标签页管理
const tagsViewStore = useTagsViewStore()
const visitedTabs = computed(() => tagsViewStore.visitedViews)
const activeTab = computed({
  get: () => route.path,
  set: (path) => router.push(path)
})
const cachedViews = computed(() => tagsViewStore.cachedViews)
const showTabs = ref(true)

// 消息通知
const unreadCount = ref(3)
const messages = ref([
  { id: 1, title: '您有一条新的订单', time: '2分钟前' },
  { id: 2, title: '系统将于今晚升级', time: '1小时前' }
])

// 事件处理
const handleMenuSelect = (path) => {
  tagsViewStore.addView({ path, title: route.meta?.title || '未命名' })
}

const handleTabClick = (tab) => {
  router.push(tab.props.name)
}

const handleTabRemove = (path) => {
  tagsViewStore.delView(path)
}

const handleMessageCommand = (messageId) => {
  console.log('查看消息:', messageId)
}

const handleUserCommand = (command) => {
  switch (command) {
    case 'profile':
      router.push('/profile')
      break
    case 'settings':
      router.push('/settings')
      break
    case 'logout':
      userStore.logout()
      router.push('/login')
      break
  }
}

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

<style scoped>
.layout-container {
  height: 100vh;
}

.sidebar {
  background-color: #304156;
  transition: width 0.3s;
  overflow: hidden;
}

.sidebar:not(.el-aside--collapse) {
  width: 200px;
}

.logo {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 60px;
  border-bottom: 1px solid #263445;
}

.logo img {
  width: 32px;
  height: 32px;
  margin-right: 12px;
}

.logo h2 {
  color: white;
  font-size: 18px;
  margin: 0;
  white-space: nowrap;
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: white;
  border-bottom: 1px solid #e4e7ed;
  padding: 0 20px;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 20px;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 20px;
}

.user-info {
  display: flex;
  align-items: center;
  cursor: pointer;
  padding: 5px 10px;
  border-radius: 4px;
  transition: background-color 0.3s;
}

.user-info:hover {
  background-color: #f5f7fa;
}

.username {
  margin: 0 8px;
  font-size: 14px;
}

.main-content {
  padding: 0;
  background-color: #f0f2f5;
  overflow: hidden;
}

.page-tabs {
  background: white;
  padding: 8px 20px 0;
  border-bottom: 1px solid #e4e7ed;
}

.content-wrapper {
  padding: 20px;
  height: calc(100% - 40px);
  overflow: auto;
}

.message-item {
  min-width: 200px;
  padding: 8px 0;
}

.message-title {
  font-size: 13px;
  color: #303133;
  margin-bottom: 4px;
}

.message-time {
  font-size: 12px;
  color: #909399;
}

.fade-transform-enter-active,
.fade-transform-leave-active {
  transition: all 0.5s;
}

.fade-transform-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

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

2. 表单组件 Form(高级用法)

vue 复制代码
<template>
  <div class="form-container">
    <!-- 搜索表单 -->
    <el-form
      ref="searchFormRef"
      :model="searchForm"
      :inline="true"
      label-width="80px"
      class="search-form"
    >
      <el-form-item label="用户名" prop="username">
        <el-input
          v-model="searchForm.username"
          placeholder="请输入用户名"
          clearable
          @clear="handleSearch"
        />
      </el-form-item>
      
      <el-form-item label="状态" prop="status">
        <el-select
          v-model="searchForm.status"
          placeholder="请选择状态"
          clearable
          @clear="handleSearch"
        >
          <el-option label="启用" value="active" />
          <el-option label="禁用" value="inactive" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="创建时间" prop="createTime">
        <el-date-picker
          v-model="searchForm.createTime"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
        />
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="handleSearch" :icon="Search">
          搜索
        </el-button>
        <el-button @click="handleReset" :icon="Refresh">
          重置
        </el-button>
        <el-button type="info" @click="toggleAdvancedSearch" plain>
          {{ showAdvanced ? '简化搜索' : '高级搜索' }}
          <el-icon><arrow-down /></el-icon>
        </el-button>
      </el-form-item>
    </el-form>
    
    <!-- 高级搜索 -->
    <el-collapse-transition>
      <div v-show="showAdvanced" class="advanced-search">
        <el-form
          :model="advancedForm"
          :inline="true"
          label-width="100px"
        >
          <el-form-item label="邮箱" prop="email">
            <el-input
              v-model="advancedForm.email"
              placeholder="请输入邮箱"
              clearable
            />
          </el-form-item>
          
          <el-form-item label="手机号" prop="phone">
            <el-input
              v-model="advancedForm.phone"
              placeholder="请输入手机号"
              clearable
            />
          </el-form-item>
          
          <el-form-item label="角色" prop="role">
            <el-select
              v-model="advancedForm.role"
              placeholder="请选择角色"
              clearable
              multiple
              collapse-tags
              collapse-tags-tooltip
            >
              <el-option
                v-for="role in roleOptions"
                :key="role.value"
                :label="role.label"
                :value="role.value"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
    </el-collapse-transition>
    
    <!-- 表单操作 -->
    <div class="form-actions">
      <el-button
        type="primary"
        :icon="Plus"
        @click="handleCreate"
      >
        新增用户
      </el-button>
      
      <el-button
        type="danger"
        :icon="Delete"
        :disabled="!selectedRows.length"
        @click="handleBatchDelete"
      >
        批量删除
      </el-button>
      
      <el-button
        type="success"
        :icon="Download"
        @click="handleExport"
      >
        导出数据
      </el-button>
      
      <el-button
        type="info"
        :icon="Upload"
        @click="handleImport"
      >
        导入数据
      </el-button>
    </div>
    
    <!-- 弹窗表单 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="600px"
      :close-on-click-modal="false"
      @closed="handleDialogClosed"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
        label-position="left"
        status-icon
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="form.username"
            placeholder="请输入用户名"
            clearable
            :maxlength="20"
            show-word-limit
          />
        </el-form-item>
        
        <el-form-item label="密码" prop="password" v-if="!form.id">
          <el-input
            v-model="form.password"
            type="password"
            placeholder="请输入密码"
            show-password
          />
        </el-form-item>
        
        <el-form-item label="确认密码" prop="confirmPassword" v-if="!form.id">
          <el-input
            v-model="form.confirmPassword"
            type="password"
            placeholder="请再次输入密码"
            show-password
          />
        </el-form-item>
        
        <el-form-item label="邮箱" prop="email">
          <el-input
            v-model="form.email"
            placeholder="请输入邮箱"
            clearable
          />
        </el-form-item>
        
        <el-form-item label="手机号" prop="phone">
          <el-input
            v-model="form.phone"
            placeholder="请输入手机号"
            clearable
          />
        </el-form-item>
        
        <el-form-item label="角色" prop="roles">
          <el-select
            v-model="form.roles"
            placeholder="请选择角色"
            multiple
            clearable
            style="width: 100%;"
          >
            <el-option
              v-for="role in roleOptions"
              :key="role.value"
              :label="role.label"
              :value="role.value"
            />
          </el-select>
        </el-form-item>
        
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="active">启用</el-radio>
            <el-radio label="inactive">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="form.remark"
            type="textarea"
            placeholder="请输入备注"
            :rows="3"
            :maxlength="200"
            show-word-limit
          />
        </el-form-item>
      </el-form>
      
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button
            type="primary"
            :loading="submitLoading"
            @click="handleSubmit"
          >
            {{ submitLoading ? '提交中...' : '确定' }}
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, nextTick } from 'vue'
import { Search, Refresh, Plus, Delete, Download, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'

// 搜索表单
const searchForm = reactive({
  username: '',
  status: '',
  createTime: []
})

const advancedForm = reactive({
  email: '',
  phone: '',
  role: []
})

const showAdvanced = ref(false)
const searchFormRef = ref()

// 角色选项
const roleOptions = [
  { label: '管理员', value: 'admin' },
  { label: '编辑', value: 'editor' },
  { label: '用户', value: 'user' },
  { label: '访客', value: 'guest' }
]

// 弹窗表单
const dialogVisible = ref(false)
const dialogTitle = ref('')
const submitLoading = ref(false)
const formRef = ref()

const form = reactive({
  id: '',
  username: '',
  password: '',
  confirmPassword: '',
  email: '',
  phone: '',
  roles: [],
  status: 'active',
  remark: ''
})

// 验证规则
const validateUsername = (rule, value, callback) => {
  if (!value) {
    callback(new Error('请输入用户名'))
  } else if (value.length < 3) {
    callback(new Error('用户名至少3个字符'))
  } else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
    callback(new Error('用户名只能包含字母、数字和下划线'))
  } else {
    callback()
  }
}

const validatePassword = (rule, value, callback) => {
  if (!form.id && !value) {
    callback(new Error('请输入密码'))
  } else if (value && value.length < 6) {
    callback(new Error('密码至少6位'))
  } else {
    callback()
  }
}

const validateConfirmPassword = (rule, value, callback) => {
  if (!form.id && value !== form.password) {
    callback(new Error('两次密码输入不一致'))
  } else {
    callback()
  }
}

const validateEmail = (rule, value, callback) => {
  if (value && !/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value)) {
    callback(new Error('请输入正确的邮箱格式'))
  } else {
    callback()
  }
}

const validatePhone = (rule, value, callback) => {
  if (value && !/^1[3-9]\d{9}$/.test(value)) {
    callback(new Error('请输入正确的手机号'))
  } else {
    callback()
  }
}

const rules = {
  username: [
    { required: true, validator: validateUsername, trigger: 'blur' }
  ],
  password: [
    { required: true, validator: validatePassword, trigger: 'blur' }
  ],
  confirmPassword: [
    { required: true, validator: validateConfirmPassword, trigger: 'blur' }
  ],
  email: [
    { validator: validateEmail, trigger: 'blur' }
  ],
  phone: [
    { validator: validatePhone, trigger: 'blur' }
  ],
  roles: [
    { required: true, message: '请选择角色', trigger: 'change' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ]
}

// 表格选择的行
const selectedRows = ref([])

// 方法
const handleSearch = () => {
  // 合并搜索条件
  const searchParams = {
    ...searchForm,
    ...advancedForm,
    startTime: searchForm.createTime?.[0],
    endTime: searchForm.createTime?.[1]
  }
  delete searchParams.createTime
  
  console.log('搜索参数:', searchParams)
  // 这里调用搜索接口
}

const handleReset = () => {
  searchFormRef.value?.resetFields()
  Object.keys(advancedForm).forEach(key => {
    advancedForm[key] = Array.isArray(advancedForm[key]) ? [] : ''
  })
  handleSearch()
}

const toggleAdvancedSearch = () => {
  showAdvanced.value = !showAdvanced.value
}

const handleCreate = () => {
  dialogTitle.value = '新增用户'
  resetForm()
  dialogVisible.value = true
}

const handleEdit = (row) => {
  dialogTitle.value = '编辑用户'
  Object.assign(form, row)
  form.confirmPassword = '' // 编辑时不显示密码
  dialogVisible.value = true
}

const handleSubmit = async () => {
  if (!formRef.value) return
  
  try {
    // 表单验证
    await formRef.value.validate()
    
    submitLoading.value = true
    
    // 准备提交数据
    const submitData = { ...form }
    delete submitData.confirmPassword
    
    if (form.id) {
      // 编辑
      console.log('编辑用户:', submitData)
      // await updateUser(submitData)
      ElMessage.success('修改成功')
    } else {
      // 新增
      console.log('新增用户:', submitData)
      // await createUser(submitData)
      ElMessage.success('新增成功')
    }
    
    dialogVisible.value = false
    handleSearch() // 刷新列表
  } catch (error) {
    console.error('表单验证失败:', error)
  } finally {
    submitLoading.value = false
  }
}

const handleBatchDelete = async () => {
  try {
    await ElMessageBox.confirm(
      `确定删除选中的 ${selectedRows.value.length} 条数据吗?`,
      '提示',
      {
        type: 'warning',
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        confirmButtonClass: 'el-button--danger'
      }
    )
    
    const ids = selectedRows.value.map(row => row.id)
    // await batchDeleteUsers(ids)
    ElMessage.success('删除成功')
    handleSearch() // 刷新列表
    selectedRows.value = []
  } catch (error) {
    // 用户取消
  }
}

const handleExport = () => {
  // 导出数据
  ElMessage.info('导出功能开发中')
}

const handleImport = () => {
  // 导入数据
  ElMessage.info('导入功能开发中')
}

const handleDialogClosed = () => {
  resetForm()
}

const resetForm = () => {
  if (formRef.value) {
    formRef.value.resetFields()
  }
  Object.keys(form).forEach(key => {
    form[key] = Array.isArray(form[key]) ? [] : ''
  })
  form.status = 'active'
}
</script>

<style scoped>
.form-container {
  padding: 20px;
  background: white;
  border-radius: 4px;
  margin-bottom: 20px;
}

.search-form {
  margin-bottom: 20px;
}

.advanced-search {
  padding: 20px;
  background: #f8f9fa;
  border-radius: 4px;
  margin-bottom: 20px;
  border: 1px solid #ebeef5;
}

.form-actions {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>

3. 表格组件 Table(高级用法)

vue 复制代码
<template>
  <div class="table-container">
    <!-- 表格 -->
    <el-table
      ref="tableRef"
      v-loading="loading"
      :data="tableData"
      :border="true"
      :stripe="true"
      :highlight-current-row="true"
      :row-key="rowKey"
      @selection-change="handleSelectionChange"
      @sort-change="handleSortChange"
      @row-click="handleRowClick"
      @row-dblclick="handleRowDblClick"
    >
      <!-- 选择列 -->
      <el-table-column
        v-if="showSelection"
        type="selection"
        width="55"
        align="center"
        fixed
      />
      
      <!-- 序号列 -->
      <el-table-column
        v-if="showIndex"
        type="index"
        label="序号"
        width="60"
        align="center"
        fixed
      >
        <template #default="{ $index }">
          {{ (pagination.page - 1) * pagination.pageSize + $index + 1 }}
        </template>
      </el-table-column>
      
      <!-- 数据列 -->
      <el-table-column
        v-for="column in columns"
        :key="column.prop"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        :min-width="column.minWidth"
        :align="column.align || 'center'"
        :sortable="column.sortable"
        :sort-orders="['ascending', 'descending']"
        :fixed="column.fixed"
        :show-overflow-tooltip="column.tooltip !== false"
      >
        <template #default="{ row, $index }">
          <!-- 自定义列渲染 -->
          <template v-if="column.render">
            <component
              :is="column.render"
              :row="row"
              :index="$index"
              :value="row[column.prop]"
            />
          </template>
          
          <!-- 状态列 -->
          <template v-else-if="column.type === 'status'">
            <el-tag
              :type="getStatusType(row[column.prop])"
              size="small"
            >
              {{ getStatusText(row[column.prop]) }}
            </el-tag>
          </template>
          
          <!-- 开关列 -->
          <template v-else-if="column.type === 'switch'">
            <el-switch
              v-model="row[column.prop]"
              :active-value="1"
              :inactive-value="0"
              :loading="row.switchLoading"
              @change="(value) => handleSwitchChange(row, column.prop, value)"
            />
          </template>
          
          <!-- 图片列 -->
          <template v-else-if="column.type === 'image'">
            <el-image
              v-if="row[column.prop]"
              style="width: 50px; height: 50px; border-radius: 4px;"
              :src="row[column.prop]"
              :preview-src-list="[row[column.prop]]"
              :preview-teleported="true"
              fit="cover"
              hide-on-click-modal
            />
            <span v-else>暂无图片</span>
          </template>
          
          <!-- 链接列 -->
          <template v-else-if="column.type === 'link'">
            <el-link
              type="primary"
              :underline="false"
              @click="handleLinkClick(row, column)"
            >
              {{ row[column.prop] }}
            </el-link>
          </template>
          
          <!-- 操作列 -->
          <template v-else-if="column.type === 'action'">
            <div class="action-buttons">
              <el-button
                v-if="!column.hideView"
                type="primary"
                link
                size="small"
                :icon="View"
                @click="handleView(row)"
              >
                查看
              </el-button>
              
              <el-button
                v-if="!column.hideEdit"
                type="primary"
                link
                size="small"
                :icon="Edit"
                @click="handleEdit(row)"
              >
                编辑
              </el-button>
              
              <el-button
                v-if="!column.hideDelete"
                type="danger"
                link
                size="small"
                :icon="Delete"
                @click="handleDelete(row)"
              >
                删除
              </el-button>
              
              <!-- 自定义操作按钮 -->
              <template v-for="action in column.actions" :key="action.name">
                <el-button
                  :type="action.type || 'primary' icon"
                  link
                  size="small"
                  :icon="action.icon"
                  @click="action.handler(row)"
                >
                  {{ action.label }}
                </el-button>
              </template>
            </div>
          </template>
          
          <!-- 默认文本 -->
          <template v-else>
            {{ formatCellValue(row[column.prop], column) }}
          </template>
        </template>
      </el-table-column>
      
      <!-- 操作列(固定右侧) -->
      <el-table-column
        v-if="showActionColumn"
        label="操作"
        width="200"
        fixed="right"
        align="center"
      >
        <template #default="{ row }">
          <div class="action-buttons">
            <el-button
              type="primary"
              link
              size="small"
              :icon="View"
              @click="handleView(row)"
            >
              详情
            </el-button>
            
            <el-button
              type="warning"
              link
              size="small"
              :icon="Edit"
              @click="handleEdit(row)"
            >
              编辑
            </el-button>
            
            <el-button
              type="danger"
              link
              size="small"
              :icon="Delete"
              @click="handleDelete(row)"
            >
              删除
            </el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页 -->
    <div class="pagination-container">
      <el-pagination
        v-model:current-page="pagination.page"
        v-model:page-size="pagination.pageSize"
        :total="pagination.total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        background
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
      />
    </div>
    
    <!-- 详情抽屉 -->
    <el-drawer
      v-model="detailVisible"
      title="详情"
      :size="600"
      direction="rtl"
      :destroy-on-close="true"
    >
      <template #header>
        <h4>用户详情</h4>
      </template>
      
      <el-descriptions
        v-if="currentRow"
        :column="1"
        border
      >
        <el-descriptions-item label="ID">
          {{ currentRow.id }}
        </el-descriptions-item>
        
        <el-descriptions-item label="用户名">
          {{ currentRow.username }}
        </el-descriptions-item>
        
        <el-descriptions-item label="邮箱">
          {{ currentRow.email }}
        </el-descriptions-item>
        
        <el-descriptions-item label="手机号">
          {{ currentRow.phone }}
        </el-descriptions-item>
        
        <el-descriptions-item label="状态">
          <el-tag :type="currentRow.status === 1 ? 'success' : 'danger'">
            {{ currentRow.status === 1 ? '启用' : '禁用' }}
          </el-tag>
        </el-descriptions-item>
        
        <el-descriptions-item label="创建时间">
          {{ formatDate(currentRow.createTime) }}
        </el-descriptions-item>
        
        <el-descriptions-item label="备注">
          {{ currentRow.remark }}
        </el-descriptions-item>
      </el-descriptions>
      
      <div v-else class="empty-detail">
        暂无数据
      </div>
      
      <template #footer>
        <div style="flex: auto">
          <el-button @click="detailVisible = false">关闭</el-button>
        </div>
      </template>
    </el-drawer>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { View, Edit, Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'

// 表格配置
const tableRef = ref()
const loading = ref(false)
const tableData = ref([])
const selectedRows = ref([])

// 分页配置
const pagination = reactive({
  page: 1,
  pageSize: 10,
  total: 0
})

// 列配置
const columns = ref([
  {
    prop: 'username',
    label: '用户名',
    width: 120,
    sortable: true
  },
  {
    prop: 'nickname',
    label: '昵称',
    width: 120
  },
  {
    prop: 'email',
    label: '邮箱',
    minWidth: 180
  },
  {
    prop: 'phone',
    label: '手机号',
    width: 130
  },
  {
    prop: 'role',
    label: '角色',
    width: 100,
    render: {
      template: `
        <div>
          <el-tag
            v-for="role in value"
            :key="role"
            type="info"
            size="small"
            style="margin-right: 5px;"
          >
            {{ role }}
          </el-tag>
        </div>
      `
    }
  },
  {
    prop: 'status',
    label: '状态',
    width: 100,
    type: 'status'
  },
  {
    prop: 'avatar',
    label: '头像',
    width: 100,
    type: 'image'
  },
  {
    prop: 'createTime',
    label: '创建时间',
    width: 180,
    sortable: 'custom',
    render: {
      template: '<span>{{ formatDate(value) }}</span>'
    }
  }
])

// 详情抽屉
const detailVisible = ref(false)
const currentRow = ref(null)

// 配置
const showSelection = ref(true)
const showIndex = ref(true)
const showActionColumn = ref(true)
const rowKey = 'id'

// 方法
const fetchData = async () => {
  loading.value = true
  
  try {
    // 模拟API调用
    const params = {
      page: pagination.page,
      pageSize: pagination.pageSize,
      ...searchForm // 搜索条件
    }
    
    // const response = await getUserList(params)
    // tableData.value = response.data.list
    // pagination.total = response.data.total
    
    // 模拟数据
    setTimeout(() => {
      tableData.value = Array.from({ length: pagination.pageSize }, (_, i) => ({
        id: (pagination.page - 1) * pagination.pageSize + i + 1,
        username: `user${i + 1}`,
        nickname: `用户${i + 1}`,
        email: `user${i + 1}@example.com`,
        phone: `1380013800${i.toString().padStart(2, '0')}`,
        role: i % 3 === 0 ? ['admin'] : i % 3 === 1 ? ['editor'] : ['user'],
        status: i % 4 === 0 ? 0 : 1, // 0: 禁用, 1: 启用
        avatar: i % 2 === 0 ? 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png' : '',
        createTime: new Date(Date.now() - i * 86400000).toISOString(),
        remark: '这是一个备注信息'
      }))
      pagination.total = 100
      loading.value = false
    }, 500)
  } catch (error) {
    console.error('获取数据失败:', error)
    loading.value = false
  }
}

const handleSelectionChange = (selection) => {
  selectedRows.value = selection
  console.log('选中的行:', selection)
}

const handleSortChange = ({ column, prop, order }) => {
  console.log('排序:', { prop, order })
  // 调用排序接口
  fetchData()
}

const handleRowClick = (row, column, event) => {
  console.log('点击行:', row)
}

const handleRowDblClick = (row, column, event) => {
  handleView(row)
}

const handleView = (row) => {
  currentRow.value = row
  detailVisible.value = true
}

const handleEdit = (row) => {
  console.log('编辑:', row)
  // 触发父组件的编辑事件
  emit('edit', row)
}

const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm(
      `确定删除用户 "${row.username}" 吗?`,
      '提示',
      {
        type: 'warning',
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        confirmButtonClass: 'el-button--danger'
      }
    )
    
    // await deleteUser(row.id)
    ElMessage.success('删除成功')
    fetchData() // 刷新数据
  } catch (error) {
    // 用户取消
  }
}

const handleSwitchChange = async (row, prop, value) => {
  try {
    row.switchLoading = true
    // await updateUserStatus(row.id, value)
    ElMessage.success('状态更新成功')
  } catch (error) {
    // 失败时恢复原状态
    row[prop] = value === 1 ? 0 : 1
    ElMessage.error('状态更新失败')
  } finally {
    row.switchLoading = false
  }
}

const handleLinkClick = (row, column) => {
  console.log('点击链接:', row[column.prop])
}

const handleSizeChange = (pageSize) => {
  pagination.pageSize = pageSize
  pagination.page = 1
  fetchData()
}

const handlePageChange = (page) => {
  pagination.page = page
  fetchData()
}

// 工具函数
const getStatusType = (status) => {
  return status === 1 ? 'success' : 'danger'
}

const getStatusText = (status) => {
  return status === 1 ? '启用' : '禁用'
}

const formatCellValue = (value, column) => {
  if (value === null || value === undefined || value === '') {
    return '-'
  }
  
  // 日期格式化
  if (column.type === 'date' || column.prop.includes('Time')) {
    return formatDate(value)
  }
  
  // 金额格式化
  if (column.type === 'currency') {
    return `¥${Number(value).toFixed(2)}`
  }
  
  return value
}

const formatDate = (date) => {
  if (!date) return '-'
  return new Date(date).toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  }).replace(/\//g, '-')
}

// 生命周期
onMounted(() => {
  fetchData()
})

// 暴露方法给父组件
defineExpose({
  refresh: fetchData,
  clearSelection: () => {
    tableRef.value?.clearSelection()
  },
  getSelectedRows: () => selectedRows.value
})

// 搜索条件(从父组件传入)
const searchForm = defineProps(['searchForm'])
</script>

<style scoped>
.table-container {
  background: white;
  border-radius: 4px;
  overflow: hidden;
}

.pagination-container {
  display: flex;
  justify-content: flex-end;
  padding: 20px;
  background: white;
  border-top: 1px solid #ebeef5;
}

.action-buttons {
  display: flex;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

.empty-detail {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  color: #909399;
  font-size: 14px;
}

/* 自定义表格样式 */
:deep(.el-table) {
  .el-table__header-wrapper th {
    background-color: #f5f7fa;
    color: #303133;
    font-weight: 600;
  }
  
  .el-table__body tr:hover > td {
    background-color: #f5f7fa;
  }
  
  .el-table__body tr.current-row > td {
    background-color: #ecf5ff;
  }
}

/* 响应式调整 */
@media (max-width: 768px) {
  .action-buttons {
    flex-direction: column;
    align-items: center;
  }
  
  .pagination-container {
    flex-direction: column;
    align-items: stretch;
  }
}
</style>

4. 弹窗和对话框

vue 复制代码
<template>
  <div class="dialog-example">
    <!-- 各种弹窗示例 -->
    <div class="button-group">
      <el-button @click="showAlert">提示框</el-button>
      <el-button @click="showConfirm">确认框</el-button>
      <el-button @click="showPrompt">输入框</el-button>
      <el-button @click="showMessage">消息提示</el-button>
      <el-button @click="showNotify">通知</el-button>
      <el-button @click="showDrawer">抽屉</el-button>
    </div>
    
    <!-- 自定义弹窗 -->
    <el-button type="primary" @click="showCustomDialog">
      自定义弹窗
    </el-button>
    
    <!-- 步骤弹窗 -->
    <el-button @click="showStepDialog">步骤弹窗</el-button>
    
    <!-- 全屏弹窗 -->
    <el-button @click="showFullscreenDialog">全屏弹窗</el-button>
    
    <!-- 嵌套弹窗 -->
    <el-button @click="showNestedDialog">嵌套弹窗</el-button>
    
    <!-- 弹窗容器 -->
    <el-dialog
      v-model="dialogVisible"
      title="自定义弹窗"
      width="600px"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :show-close="true"
      :draggable="true"
      destroy-on-close
      @open="handleDialogOpen"
      @close="handleDialogClose"
      @closed="handleDialogClosed"
    >
      <div class="dialog-content">
        <!-- 富文本编辑器 -->
        <el-form
          ref="dialogFormRef"
          :model="dialogForm"
          :rules="dialogRules"
          label-width="100px"
        >
          <el-form-item label="标题" prop="title">
            <el-input
              v-model="dialogForm.title"
              placeholder="请输入标题"
              clearable
            />
          </el-form-item>
          
          <el-form-item label="内容" prop="content">
            <el-input
              v-model="dialogForm.content"
              type="textarea"
              :rows="4"
              placeholder="请输入内容"
              maxlength="500"
              show-word-limit
            />
          </el-form-item>
          
          <el-form-item label="类型" prop="type">
            <el-select
              v-model="dialogForm.type"
              placeholder="请选择类型"
              clearable
            >
              <el-option label="普通" value="normal" />
              <el-option label="重要" value="important" />
              <el-option label="紧急" value="urgent" />
            </el-select>
          </el-form-item>
          
          <el-form-item label="状态" prop="status">
            <el-radio-group v-model="dialogForm.status">
              <el-radio label="draft">草稿</el-radio>
              <el-radio label="published">已发布</el-radio>
            </el-radio-group>
          </el-form-item>
          
          <el-form-item label="日期" prop="date">
            <el-date-picker
              v-model="dialogForm.date"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="YYYY-MM-DD"
            />
          </el-form-item>
          
          <el-form-item label="上传文件">
            <el-upload
              v-model:file-list="fileList"
              class="upload-demo"
              action="/api/upload"
              multiple
              :limit="3"
              :on-exceed="handleExceed"
              :before-upload="beforeUpload"
              :on-success="handleUploadSuccess"
              :on-error="handleUploadError"
              :on-remove="handleRemove"
              list-type="picture-card"
            >
              <el-icon><Plus /></el-icon>
              
              <template #file="{ file }">
                <div>
                  <img
                    class="el-upload-list__item-thumbnail"
                    :src="file.url"
                    alt=""
                  />
                  <span class="el-upload-list__item-actions">
                    <span
                      class="el-upload-list__item-preview"
                      @click="handlePictureCardPreview(file)"
                    >
                      <el-icon><zoom-in /></el-icon>
                    </span>
                    <span
                      v-if="!disabled"
                      class="el-upload-list__item-delete"
                      @click="handleRemove(file)"
                    >
                      <el-icon><Delete /></el-icon>
                    </span>
                  </span>
                </div>
              </template>
            </el-upload>
          </el-form-item>
        </el-form>
      </div>
      
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button
            type="primary"
            :loading="dialogLoading"
            @click="handleDialogSubmit"
          >
            确定
          </el-button>
        </div>
      </template>
    </el-dialog>
    
    <!-- 步骤弹窗 -->
    <el-dialog
      v-model="stepDialogVisible"
      title="步骤弹窗"
      width="800px"
      :close-on-click-modal="false"
    >
      <el-steps
        :active="stepActive"
        finish-status="success"
        align-center
      >
        <el-step title="步骤1" description="填写基本信息" />
        <el-step title="步骤2" description="配置参数" />
        <el-step title="步骤3" description="完成" />
      </el-steps>
      
      <div class="step-content">
        <div v-show="stepActive === 0" class="step-1">
          <el-form :model="stepForm1" label-width="100px">
            <el-form-item label="名称">
              <el-input v-model="stepForm1.name" />
            </el-form-item>
            <el-form-item label="描述">
              <el-input v-model="stepForm1.description" type="textarea" />
            </el-form-item>
          </el-form>
        </div>
        
        <div v-show="stepActive === 1" class="step-2">
          <el-form :model="stepForm2" label-width="100px">
            <el-form-item label="参数1">
              <el-input v-model="stepForm2.param1" />
            </el-form-item>
            <el-form-item label="参数2">
              <el-input v-model="stepForm2.param2" />
            </el-form-item>
          </el-form>
        </div>
        
        <div v-show="stepActive === 2" class="step-3">
          <el-result
            icon="success"
            title="创建成功"
            sub-title="您的配置已保存"
          >
            <template #extra>
              <el-button type="primary" @click="handleFinish">完成</el-button>
            </template>
          </el-result>
        </div>
      </div>
      
      <template #footer>
        <div class="step-footer">
          <el-button
            v-show="stepActive > 0"
            @click="handleStepPrev"
          >
            上一步
          </el-button>
          <el-button
            v-show="stepActive < 2"
            type="primary"
            @click="handleStepNext"
          >
            {{ stepActive === 1 ? '完成' : '下一步' }}
          </el-button>
          <el-button
            v-show="stepActive === 2"
            @click="stepDialogVisible = false"
          >
            关闭
          </el-button>
        </div>
      </template>
    </el-dialog>
    
    <!-- 全屏弹窗 -->
    <el-dialog
      v-model="fullscreenDialogVisible"
      title="全屏弹窗"
      fullscreen
      :modal="false"
    >
      <div class="fullscreen-content">
        <h3>全屏弹窗内容</h3>
        <p>这是一个全屏弹窗,适合展示大量内容</p>
        <!-- 可以放置表格、表单等复杂内容 -->
      </div>
      
      <template #footer>
        <el-button @click="fullscreenDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    
    <!-- 图片预览 -->
    <el-dialog v-model="previewVisible" title="图片预览">
      <img :src="previewImage" alt="预览" style="width: 100%" />
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { Plus, ZoomIn, Delete } from '@element-plus/icons-vue'

// 弹窗状态
const dialogVisible = ref(false)
const dialogLoading = ref(false)
const dialogFormRef = ref()

// 弹窗表单
const dialogForm = reactive({
  title: '',
  content: '',
  type: '',
  status: 'draft',
  date: []
})

const dialogRules = {
  title: [
    { required: true, message: '请输入标题', trigger: 'blur' },
    { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
  ],
  content: [
    { required: true, message: '请输入内容', trigger: 'blur' }
  ],
  type: [
    { required: true, message: '请选择类型', trigger: 'change' }
  ]
}

// 文件上传
const fileList = ref([])
const previewImage = ref('')
const previewVisible = ref(false)
const disabled = ref(false)

// 步骤弹窗
const stepDialogVisible = ref(false)
const stepActive = ref(0)
const stepForm1 = reactive({ name: '', description: '' })
const stepForm2 = reactive({ param1: '', param2: '' })

// 全屏弹窗
const fullscreenDialogVisible = ref(false)

// 方法
const showAlert = () => {
  ElMessageBox.alert('这是一个提示消息', '提示', {
    confirmButtonText: '确定',
    callback: (action) => {
      ElMessage.success('用户点击了确定')
    }
  })
}

const showConfirm = async () => {
  try {
    await ElMessageBox.confirm('确定要执行此操作吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      beforeClose: (action, instance, done) => {
        if (action === 'confirm') {
          instance.confirmButtonLoading = true
          instance.confirmButtonText = '执行中...'
          
          // 模拟异步操作
          setTimeout(() => {
            done()
            setTimeout(() => {
              instance.confirmButtonLoading = false
            }, 300)
          }, 2000)
        } else {
          done()
        }
      }
    })
    ElMessage.success('操作成功')
  } catch (error) {
    ElMessage.info('已取消操作')
  }
}

const showPrompt = async () => {
  try {
    const { value } = await ElMessageBox.prompt('请输入内容', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      inputPattern: /^[a-zA-Z0-9]+$/,
      inputErrorMessage: '只能输入字母和数字',
      inputType: 'text',
      inputPlaceholder: '请输入...',
      inputValue: '默认值'
    })
    
    ElMessage.success(`你输入了: ${value}`)
  } catch (error) {
    ElMessage.info('已取消输入')
  }
}

const showMessage = () => {
  // 不同类型的消息提示
  ElMessage.success('成功消息')
  // ElMessage.error('错误消息')
  // ElMessage.warning('警告消息')
  // ElMessage.info('信息消息')
}

const showNotify = () => {
  // 不同类型的通知
  ElNotification.success({
    title: '成功',
    message: '这是一条成功的提示消息',
    duration: 3000,
    position: 'top-right',
    onClick: () => {
      console.log('通知被点击')
    }
  })
}

const showDrawer = () => {
  // 抽屉组件已经在表格示例中展示
  ElMessage.info('查看表格示例中的抽屉')
}

const showCustomDialog = () => {
  dialogVisible.value = true
}

const showStepDialog = () => {
  stepDialogVisible.value = true
  stepActive.value = 0
  // 重置表单
  Object.assign(stepForm1, { name: '', description: '' })
  Object.assign(stepForm2, { param1: '', param2: '' })
}

const showFullscreenDialog = () => {
  fullscreenDialogVisible.value = true
}

const showNestedDialog = () => {
  ElMessageBox.confirm('打开第二个弹窗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消'
  }).then(() => {
    ElMessageBox.confirm('这是第二个弹窗', '嵌套弹窗', {
      confirmButtonText: '确定',
      cancelButtonText: '取消'
    }).then(() => {
      ElMessage.success('操作完成')
    })
  })
}

const handleDialogOpen = () => {
  console.log('弹窗打开')
  // 可以在这里初始化数据
}

const handleDialogClose = () => {
  console.log('弹窗关闭中')
  // 关闭前的处理
}

const handleDialogClosed = () => {
  console.log('弹窗已关闭')
  // 重置表单
  dialogFormRef.value?.resetFields()
  fileList.value = []
}

const handleDialogSubmit = async () => {
  if (!dialogFormRef.value) return
  
  try {
    await dialogFormRef.value.validate()
    dialogLoading.value = true
    
    // 模拟提交
    setTimeout(() => {
      console.log('提交数据:', dialogForm, fileList.value)
      dialogLoading.value = false
      dialogVisible.value = false
      ElMessage.success('提交成功')
    }, 1500)
  } catch (error) {
    console.error('表单验证失败:', error)
  }
}

// 上传相关方法
const handleExceed = (files) => {
  ElMessage.warning(`最多上传3个文件,你选择了${files.length}个文件`)
}

const beforeUpload = (file) => {
  const isImage = file.type.startsWith('image/')
  const isLt5M = file.size / 1024 / 1024 < 5
  
  if (!isImage) {
    ElMessage.error('只能上传图片文件!')
    return false
  }
  if (!isLt5M) {
    ElMessage.error('图片大小不能超过5MB!')
    return false
  }
  
  return true
}

const handleUploadSuccess = (response, file, fileList) => {
  ElMessage.success('上传成功')
  console.log('上传成功:', file, fileList)
}

const handleUploadError = (error, file, fileList) => {
  ElMessage.error('上传失败')
  console.error('上传失败:', error)
}

const handleRemove = (file, fileList) => {
  console.log('移除文件:', file, fileList)
}

const handlePictureCardPreview = (file) => {
  previewImage.value = file.url
  previewVisible.value = true
}

// 步骤弹窗方法
const handleStepNext = () => {
  if (stepActive.value < 2) {
    stepActive.value++
  } else {
    handleFinish()
  }
}

const handleStepPrev = () => {
  if (stepActive.value > 0) {
    stepActive.value--
  }
}

const handleFinish = () => {
  console.log('完成配置:', stepForm1, stepForm2)
  stepDialogVisible.value = false
  ElMessage.success('配置完成')
}
</script>

<style scoped>
.dialog-example {
  padding: 20px;
}

.button-group {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.dialog-content {
  padding: 20px 0;
}

.step-content {
  margin: 30px 0;
  min-height: 200px;
}

.step-footer {
  display: flex;
  justify-content: space-between;
}

.fullscreen-content {
  padding: 20px;
}

.upload-demo {
  width: 100%;
}

:deep(.el-upload--picture-card) {
  width: 100px;
  height: 100px;
  line-height: 100px;
}

:deep(.el-upload-list--picture-card .el-upload-list__item) {
  width: 100px;
  height: 100px;
}
</style>

🔧 高级功能和配置

1. 全局配置和主题定制

javascript 复制代码
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css' // 暗黑模式
import locale from 'element-plus/dist/locale/zh-cn.mjs' // 中文
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'

const app = createApp(App)

// 全局配置
app.use(ElementPlus, {
  // 全局组件大小
  size: 'default', // 'large' | 'default' | 'small'
  // 全局弹窗的初始zIndex
  zIndex: 2000,
  // 语言
  locale: locale
})

// 注册图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// 全局组件默认属性
app.config.globalProperties.$ELEMENT = {
  size: 'small',
  zIndex: 3000
}
css 复制代码
/* 主题定制 - CSS变量方式 */
:root {
  /* 主色调 */
  --el-color-primary: #409eff;
  --el-color-primary-light-3: #79bbff;
  --el-color-primary-light-5: #a0cfff;
  --el-color-primary-light-7: #c6e2ff;
  --el-color-primary-light-9: #d9ecff;
  --el-color-primary-dark-2: #337ecc;
  
  /* 成功色 */
  --el-color-success: #67c23a;
  /* 警告色 */
  --el-color-warning: #e6a23c;
  /* 危险色 */
  --el-color-danger: #f56c6c;
  /* 信息色 */
  --el-color-info: #909399;
  
  /* 文字色 */
  --el-text-color-primary: #303133;
  --el-text-color-regular: #606266;
  --el-text-color-secondary: #909399;
  --el-text-color-placeholder: #c0c4cc;
  
  /* 边框色 */
  --el-border-color: #dcdfe6;
  --el-border-color-light: #e4e7ed;
  --el-border-color-lighter: #ebeef5;
  --el-border-color-extra-light: #f2f6fc;
  
  /* 背景色 */
  --el-bg-color: #ffffff;
  --el-bg-color-page: #f2f6fc;
  --el-bg-color-overlay: #ffffff;
  
  /* 边框圆角 */
  --el-border-radius-base: 4px;
  --el-border-radius-small: 2px;
  --el-border-radius-round: 20px;
  --el-border-radius-circle: 100%;
  
  /* 阴影 */
  --el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
  --el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
  --el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
  --el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12), 0px 8px 16px -8px rgba(0, 0, 0, 0.16);
  
  /* 动画 */
  --el-transition-duration: 0.3s;
  --el-transition-duration-fast: 0.2s;
  --el-transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1);
  --el-transition-all: all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);
}

/* 暗黑模式 */
html.dark {
  --el-bg-color-page: #141414;
  --el-bg-color: #1d1e1f;
  --el-bg-color-overlay: #1d1e1f;
  --el-text-color-primary: #e5eaf3;
  --el-text-color-regular: #cfd3dc;
  --el-text-color-secondary: #a3a6ad;
  --el-text-color-placeholder: #8d9095;
  --el-border-color: #4c4d4f;
  --el-border-color-light: #414243;
  --el-border-color-lighter: #363637;
  --el-border-color-extra-light: #2b2b2b;
  --el-fill-color: #424243;
  --el-fill-color-light: #39393a;
  --el-fill-color-lighter: #262727;
  --el-fill-color-extra-light: #202121;
  --el-fill-color-blank: transparent;
  --el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
  --el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
  --el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
  --el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px rgba(0, 0, 0, 0.72), 0px 8px 16px -8px rgba(0, 0, 0, 0.72);
}

2. 自定义主题(SCSS)

scss 复制代码
// styles/element/index.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  ),
  $text-color: (
    'primary': #303133,
    'regular': #606266,
    'secondary': #909399,
  ),
  $border-radius: (
    'base': 4px,
    'small': 2px,
    'round': 20px,
    'circle': 100%,
  ),
  $box-shadow: (
    '': 0 2px 4px rgba(0, 0, 0, 0.12),
    'light': 0 2px 8px rgba(0, 0, 0, 0.12),
    'lighter': 0 1px 4px rgba(0, 0, 0, 0.12),
  ),
);

@use "element-plus/theme-chalk/src/index.scss" as *;

3. 暗黑模式切换

vue 复制代码
<!-- DarkModeToggle.vue -->
<template>
  <el-switch
    v-model="isDark"
    :active-icon="Moon"
    :inactive-icon="Sunny"
    inline-prompt
    @change="toggleDarkMode"
  />
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { Moon, Sunny } from '@element-plus/icons-vue'
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: 'light'
})

const toggleDark = useToggle(isDark)

const toggleDarkMode = (val) => {
  toggleDark()
  
  // 保存到本地存储
  localStorage.setItem('theme', val ? 'dark' : 'light')
  
  // 可以在这里通知其他组件主题变化
  document.dispatchEvent(new CustomEvent('theme-change', { detail: { isDark: val } }))
}

// 初始化时读取本地存储
onMounted(() => {
  const savedTheme = localStorage.getItem('theme')
  if (savedTheme === 'dark' && !isDark.value) {
    toggleDark()
  } else if (savedTheme === 'light' && isDark.value) {
    toggleDark()
  }
})

// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', (e) => {
  if (localStorage.getItem('theme') === null) { // 没有手动设置主题
    toggleDark(e.matches)
  }
})
</script>

4. 指令和工具函数

javascript 复制代码
// directives/index.js
import { ElMessage, ElMessageBox, ElLoading, ElNotification } from 'element-plus'

// 权限指令
export const permission = {
  mounted(el, binding) {
    const { value } = binding
    const userStore = useUserStore()
    
    if (value && value instanceof Array && value.length > 0) {
      const hasPermission = userStore.hasAnyPermission(value)
      
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error('需要权限数组,如 v-permission="[\'admin\']"')
    }
  }
}

// 防抖指令
export const debounce = {
  mounted(el, binding) {
    let timer
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer
相关推荐
Fairy要carry2 小时前
面试-Dispatch Tools
前端·chrome
IT_陈寒2 小时前
JavaScript开发者必看:3个让代码效率翻倍的隐藏技巧
前端·人工智能·后端
嘉琪0012 小时前
Day8 完整学习包(Vue 基础 & 响应式)——2026 0320
前端·vue.js·学习
FlyWIHTSKY2 小时前
Vue3 单文件中不同的组件
前端·javascript·vue.js
一字白首2 小时前
小程序组件化进阶:从复用到通信的完整指南DAY04
前端·小程序·apache
读忆2 小时前
你是否用过Tailwind CSS?你是在什么情况下使用的呢?
前端·css·经验分享·笔记·taiiwindcss
阿珊和她的猫2 小时前
探秘小程序:为何拿不到 DOM 相关 API
前端·小程序
FlyWIHTSKY2 小时前
Vue 3 onMounted 中控制同步与异步执行策略
前端·javascript·vue.js
蜗牛攻城狮2 小时前
【Vue3实战】El-Table实现“超过3行省略,悬停显示全文”的完美方案(附性能优化)
前端·vue.js·性能优化·element-plus