🎨 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