综合实践
本节提供了在实际应用中使用 Element Plus 的综合实践指南。我们将构建一个完整的管理仪表板,整合多个 Element Plus 组件协同工作。
项目概述
在这个综合实践中,我们将创建一个具有以下功能的管理仪表板:
- 用户认证系统
- 带侧边栏导航的响应式布局
- 具有排序、过滤和分页功能的数据表格
- 带验证的表单处理
- 图表和数据可视化
- 主题定制
项目设置
首先,让我们创建一个新的 Vue 3 项目并安装 Element Plus:
bash
# 创建一个新的 Vue 3 项目
npm create vue@latest admin-dashboard
# 进入项目目录
cd admin-dashboard
# 安装 Element Plus
npm install element-plus
# 安装其他依赖
npm install vue-router pinia axios echarts
项目结构
按照以下结构组织您的项目:
src/
├── assets/
│ └── styles/
│ ├── variables.scss
│ └── main.scss
├── components/
│ ├── layout/
│ │ ├── AppHeader.vue
│ │ ├── AppSidebar.vue
│ │ └── AppFooter.vue
│ └── common/
│ ├── DataTable.vue
│ └── StatCard.vue
├── views/
│ ├── Dashboard.vue
│ ├── Login.vue
│ ├── UserManagement.vue
│ └── Settings.vue
├── router/
│ └── index.js
├── stores/
│ ├── auth.js
│ └── settings.js
├── services/
│ └── api.js
├── utils/
│ └── validators.js
├── App.vue
└── main.js
主应用设置
main.js
javascript
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import './assets/styles/main.scss'
const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
app.mount('#app')
App.vue
vue
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import en from 'element-plus/lib/locale/lang/en'
const locale = ref(en)
const route = useRoute()
</script>
创建布局
AppHeader.vue
vue
<template>
<el-header class="app-header">
<div class="logo-container">
<img src="/logo.png" alt="Logo" class="logo" />
<h1>管理仪表板</h1>
</div>
<div class="header-controls">
<el-dropdown>
<el-avatar :size="32" :src="userAvatar" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="navigateTo('/profile')">个人资料</el-dropdown-item>
<el-dropdown-item @click="navigateTo('/settings')">设置</el-dropdown-item>
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const userAvatar = ref('https://placeholder.com/32x32')
const navigateTo = (path) => {
router.push(path)
}
const logout = () => {
authStore.logout()
router.push('/login')
}
</script>
<style scoped>
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.logo-container {
display: flex;
align-items: center;
}
.logo {
height: 32px;
margin-right: 10px;
}
.header-controls {
display: flex;
align-items: center;
}
</style>
AppSidebar.vue
vue
<template>
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
:collapse="isCollapsed"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
router
>
<el-menu-item index="/dashboard">
<el-icon><el-icon-menu /></el-icon>
<template #title>仪表板</template>
</el-menu-item>
<el-sub-menu index="users">
<template #title>
<el-icon><el-icon-user /></el-icon>
<span>用户管理</span>
</template>
<el-menu-item index="/users/list">用户列表</el-menu-item>
<el-menu-item index="/users/roles">角色与权限</el-menu-item>
</el-sub-menu>
<el-menu-item index="/analytics">
<el-icon><el-icon-data-line /></el-icon>
<template #title>数据分析</template>
</el-menu-item>
<el-menu-item index="/settings">
<el-icon><el-icon-setting /></el-icon>
<template #title>设置</template>
</el-menu-item>
</el-menu>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import {
Menu as ElIconMenu,
User as ElIconUser,
DataLine as ElIconDataLine,
Setting as ElIconSetting
} from '@element-plus/icons-vue'
const route = useRoute()
const isCollapsed = ref(false)
const activeMenu = computed(() => {
return route.path
})
</script>
<style scoped>
.sidebar-menu {
height: 100%;
border-right: none;
}
.sidebar-menu:not(.el-menu--collapse) {
width: 250px;
}
</style>
仪表板视图
vue
<template>
<div class="dashboard">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6" v-for="(stat, index) in stats" :key="index">
<stat-card :title="stat.title" :value="stat.value" :icon="stat.icon" :color="stat.color" />
</el-col>
</el-row>
<el-row :gutter="20" class="mt-4">
<el-col :xs="24" :lg="16">
<el-card>
<template #header>
<div class="card-header">
<span>月度收入</span>
<el-dropdown>
<el-button type="primary" size="small">
导出 <el-icon class="el-icon--right"><el-icon-arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>导出为 CSV</el-dropdown-item>
<el-dropdown-item>导出为 Excel</el-dropdown-item>
<el-dropdown-item>导出为 PDF</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div ref="chartContainer" style="height: 350px;"></div>
</el-card>
</el-col>
<el-col :xs="24" :lg="8">
<el-card>
<template #header>
<div class="card-header">
<span>最近活动</span>
<el-button type="text">查看全部</el-button>
</div>
</template>
<el-timeline>
<el-timeline-item
v-for="(activity, index) in recentActivities"
:key="index"
:timestamp="activity.time"
:type="activity.type"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
</el-row>
<el-row class="mt-4">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>最近订单</span>
<el-button type="primary" size="small">查看所有订单</el-button>
</div>
</template>
<el-table :data="recentOrders" style="width: 100%">
<el-table-column prop="id" label="订单 ID" width="120" />
<el-table-column prop="customer" label="客户" />
<el-table-column prop="date" label="日期" width="180" />
<el-table-column prop="amount" label="金额" width="150" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button type="text" size="small" @click="viewOrder(scope.row.id)">查看</el-button>
<el-button type="text" size="small" @click="editOrder(scope.row.id)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ArrowDown as ElIconArrowDown } from '@element-plus/icons-vue'
import StatCard from '@/components/common/StatCard.vue'
import * as echarts from 'echarts'
// 统计数据
const stats = ref([
{ title: '总用户数', value: '8,942', icon: 'user', color: '#409EFF' },
{ title: '总收入', value: '¥23,446', icon: 'money', color: '#67C23A' },
{ title: '新订单', value: '128', icon: 'shopping-cart', color: '#E6A23C' },
{ title: '待处理问题', value: '12', icon: 'warning', color: '#F56C6C' }
])
// 最近活动
const recentActivities = ref([
{ content: '新用户注册', time: '10 分钟前', type: 'primary' },
{ content: '系统更新完成', time: '1 小时前', type: 'success' },
{ content: '数据库备份已创建', time: '3 小时前', type: 'info' },
{ content: '收到新订单', time: '5 小时前', type: 'warning' }
])
// 最近订单
const recentOrders = ref([
{ id: 'ORD-001', customer: '张三', date: '2023-06-15 14:23', amount: '¥120.00', status: '已完成' },
{ id: 'ORD-002', customer: '李四', date: '2023-06-15 13:45', amount: '¥75.50', status: '处理中' },
{ id: 'ORD-003', customer: '王五', date: '2023-06-15 11:30', amount: '¥237.75', status: '待处理' },
{ id: 'ORD-004', customer: '赵六', date: '2023-06-15 10:15', amount: '¥54.25', status: '已取消' },
{ id: 'ORD-005', customer: '钱七', date: '2023-06-15 09:20', amount: '¥189.00', status: '已完成' }
])
const chartContainer = ref(null)
let chart = null
// 状态标签类型映射
const getStatusType = (status) => {
const types = {
'已完成': 'success',
'处理中': 'primary',
'待处理': 'warning',
'已取消': 'danger'
}
return types[status] || 'info'
}
// 订单操作
const viewOrder = (id) => {
console.log(`查看订单 ${id}`)
}
const editOrder = (id) => {
console.log(`编辑订单 ${id}`)
}
// 初始化图表
onMounted(() => {
if (chartContainer.value) {
chart = echarts.init(chartContainer.value)
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['收入', '支出', '利润']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['一月', '二月', '三月', '四月', '五月', '六月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '收入',
type: 'line',
data: [12000, 13200, 14500, 16000, 18000, 21000],
areaStyle: { opacity: 0.1 },
smooth: true
},
{
name: '支出',
type: 'line',
data: [8000, 8500, 9000, 10500, 11000, 12500],
areaStyle: { opacity: 0.1 },
smooth: true
},
{
name: '利润',
type: 'line',
data: [4000, 4700, 5500, 5500, 7000, 8500],
areaStyle: { opacity: 0.1 },
smooth: true
}
]
}
chart.setOption(option)
window.addEventListener('resize', () => {
chart.resize()
})
}
})
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.mt-4 {
margin-top: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
用户管理视图
vue
<template>
<div class="user-management">
<el-card>
<template #header>
<div class="card-header">
<h2>用户管理</h2>
<div class="header-actions">
<el-input
v-model="searchQuery"
placeholder="搜索用户"
class="search-input"
clearable
@clear="handleSearch"
@input="handleSearch"
>
<template #prefix>
<el-icon><el-icon-search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="showAddUserDialog">
<el-icon><el-icon-plus /></el-icon> 添加用户
</el-button>
</div>
</div>
</template>
<el-table
:data="filteredUsers"
style="width: 100%"
v-loading="loading"
border
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户" width="220">
<template #default="scope">
<div class="user-info">
<el-avatar :size="32" :src="scope.row.avatar" />
<div class="user-details">
<div class="user-name">{{ scope.row.name }}</div>
<div class="user-email">{{ scope.row.email }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="role" label="角色" width="120" />
<el-table-column prop="department" label="部门" />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.status === '活跃' ? 'success' : 'danger'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastLogin" label="最后登录" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="editUser(scope.row)">编辑</el-button>
<el-button
size="small"
type="danger"
@click="confirmDeleteUser(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalUsers"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 添加/编辑用户对话框 -->
<el-dialog
v-model="userDialogVisible"
:title="isEditing ? '编辑用户' : '添加新用户'"
width="500px"
>
<el-form
:model="userForm"
:rules="userFormRules"
ref="userFormRef"
label-width="120px"
label-position="top"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="userForm.name" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="userForm.role" placeholder="选择角色" style="width: 100%">
<el-option label="管理员" value="管理员" />
<el-option label="经理" value="经理" />
<el-option label="用户" value="用户" />
<el-option label="访客" value="访客" />
</el-select>
</el-form-item>
<el-form-item label="部门" prop="department">
<el-select v-model="userForm.department" placeholder="选择部门" style="width: 100%">
<el-option label="IT" value="IT" />
<el-option label="人力资源" value="人力资源" />
<el-option label="财务" value="财务" />
<el-option label="市场营销" value="市场营销" />
<el-option label="运营" value="运营" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="userForm.statusSwitch"
active-text="活跃"
inactive-text="非活跃"
/>
</el-form-item>
<el-form-item v-if="!isEditing" label="密码" prop="password">
<el-input v-model="userForm.password" type="password" show-password />
</el-form-item>
<el-form-item v-if="!isEditing" label="确认密码" prop="confirmPassword">
<el-input v-model="userForm.confirmPassword" type="password" show-password />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUserForm">
{{ isEditing ? '更新' : '创建' }}
</el-button>
</span>
</template>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog
v-model="deleteDialogVisible"
title="确认删除"
width="400px"
>
<p>您确定要删除用户"{{ userToDelete?.name }}"吗?此操作无法撤销。</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialogVisible = false">取消</el-button>
<el-button type="danger" @click="deleteUser">删除</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search as ElIconSearch, Plus as ElIconPlus } from '@element-plus/icons-vue'
// 模拟数据
const users = ref([
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
role: '管理员',
department: 'IT',
status: '活跃',
lastLogin: '2023-06-15 08:30',
avatar: 'https://randomuser.me/api/portraits/men/1.jpg'
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
role: '经理',
department: '人力资源',
status: '活跃',
lastLogin: '2023-06-14 17:45',
avatar: 'https://randomuser.me/api/portraits/women/2.jpg'
},
{
id: 3,
name: '王五',
email: 'wangwu@example.com',
role: '用户',
department: '财务',
status: '非活跃',
lastLogin: '2023-06-10 11:20',
avatar: 'https://randomuser.me/api/portraits/men/3.jpg'
},
{
id: 4,
name: '赵六',
email: 'zhaoliu@example.com',
role: '用户',
department: '市场营销',
status: '活跃',
lastLogin: '2023-06-15 09:15',
avatar: 'https://randomuser.me/api/portraits/women/4.jpg'
},
{
id: 5,
name: '钱七',
email: 'qianqi@example.com',
role: '经理',
department: '运营',
status: '活跃',
lastLogin: '2023-06-14 14:30',
avatar: 'https://randomuser.me/api/portraits/men/5.jpg'
}
])
// 状态变量
const loading = ref(false)
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalUsers = ref(100)
const userDialogVisible = ref(false)
const deleteDialogVisible = ref(false)
const isEditing = ref(false)
const userToDelete = ref(null)
const userFormRef = ref(null)
// 用户表单
const userForm = reactive({
id: null,
name: '',
email: '',
role: '',
department: '',
statusSwitch: true,
password: '',
confirmPassword: ''
})
// 表单验证规则
const userFormRules = {
name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, message: '名称必须至少包含 2 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' }
],
department: [
{ required: true, message: '请选择部门', trigger: 'change' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码必须至少包含 6 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== userForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
// 计算属性
const filteredUsers = computed(() => {
return users.value.filter(user => {
const query = searchQuery.value.toLowerCase()
return (
user.name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query) ||
user.role.toLowerCase().includes(query) ||
user.department.toLowerCase().includes(query)
)
})
})
// 方法
const handleSearch = () => {
currentPage.value = 1
}
const handleSizeChange = (size) => {
pageSize.value = size
fetchUsers()
}
const handleCurrentChange = (page) => {
currentPage.value = page
fetchUsers()
}
const fetchUsers = () => {
loading.value = true
// 在实际应用中,您会从 API 获取用户
setTimeout(() => {
loading.value = false
}, 500)
}
const showAddUserDialog = () => {
isEditing.value = false
resetUserForm()
userDialogVisible.value = true
}
const editUser = (user) => {
isEditing.value = true
Object.assign(userForm, {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
department: user.department,
statusSwitch: user.status === '活跃'
})
userDialogVisible.value = true
}
const confirmDeleteUser = (user) => {
userToDelete.value = user
deleteDialogVisible.value = true
}
const deleteUser = () => {
// 在实际应用中,您会调用 API 删除用户
users.value = users.value.filter(user => user.id !== userToDelete.value.id)
ElMessage({
type: 'success',
message: `用户 ${userToDelete.value.name} 已被删除`
})
deleteDialogVisible.value = false
}
const resetUserForm = () => {
userForm.id = null
userForm.name = ''
userForm.email = ''
userForm.role = ''
userForm.department = ''
userForm.statusSwitch = true
userForm.password = ''
userForm.confirmPassword = ''
if (userFormRef.value) {
userFormRef.value.resetFields()
}
}
const submitUserForm = () => {
userFormRef.value.validate((valid) => {
if (valid) {
if (isEditing.value) {
// 更新现有用户
const index = users.value.findIndex(u => u.id === userForm.id)
if (index !== -1) {
users.value[index] = {
...users.value[index],
name: userForm.name,
email: userForm.email,
role: userForm.role,
department: userForm.department,
status: userForm.statusSwitch ? '活跃' : '非活跃'
}
ElMessage({
type: 'success',
message: '用户更新成功'
})
}
} else {
// 添加新用户
const newUser = {
id: users.value.length + 1,
name: userForm.name,
email: userForm.email,
role: userForm.role,
department: userForm.department,
status: userForm.statusSwitch ? '活跃' : '非活跃',
lastLogin: '从未',
avatar: `https://randomuser.me/api/portraits/${Math.random() > 0.5 ? 'men' : 'women'}/${Math.floor(Math.random() * 100)}.jpg`
}
users.value.push(newUser)
ElMessage({
type: 'success',
message: '用户添加成功'
})
}
userDialogVisible.value = false
} else {
return false
}
})
}
// 初始化
fetchUsers()
</script>
<style scoped>
.user-management {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
gap: 10px;
}
.search-input {
width: 250px;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: bold;
}
.user-email {
font-size: 12px;
color: #999;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>
结论
这个综合实践展示了如何使用 Element Plus 组件构建一个完整的管理仪表板。通过这个实践,我们学习了:
布局结构:使用
el-header
、el-menu
和其他布局组件创建响应式仪表板。数据可视化:通过 ECharts 集成实现图表展示。
数据表格:使用
el-table
组件展示数据,并实现排序、过滤和分页功能。表单处理:使用
el-form
和相关组件创建表单,并实现表单验证。对话框和消息提示:使用
el-dialog
和ElMessage
实现用户交互反馈。组件组合:将多个 Element Plus 组件组合在一起,构建复杂的用户界面。
通过这个实践,您可以了解如何在实际项目中有效地使用 Element Plus,以及如何将各种组件组合起来构建功能完整的应用程序。这些知识和技能可以应用到您自己的项目中,帮助您更高效地开发 Vue 3 应用程序。
在实际开发中,您可能还需要考虑以下方面:
- 状态管理:使用 Pinia 或 Vuex 管理应用程序状态
- 路由配置:使用 Vue Router 实现页面导航
- API 集成:使用 Axios 或其他 HTTP 客户端与后端 API 交互
- 国际化:使用 Element Plus 的国际化功能支持多语言
- 主题定制:根据品牌需求自定义 Element Plus 的主题样式
通过这些实践,您将能够充分利用 Element Plus 提供的强大功能,构建出专业、高效的 Web 应用程序。