Skip to content

综合实践

本节提供了在实际应用中使用 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 组件构建一个完整的管理仪表板。通过这个实践,我们学习了:

  1. 布局结构:使用 el-headerel-menu 和其他布局组件创建响应式仪表板。

  2. 数据可视化:通过 ECharts 集成实现图表展示。

  3. 数据表格:使用 el-table 组件展示数据,并实现排序、过滤和分页功能。

  4. 表单处理:使用 el-form 和相关组件创建表单,并实现表单验证。

  5. 对话框和消息提示:使用 el-dialogElMessage 实现用户交互反馈。

  6. 组件组合:将多个 Element Plus 组件组合在一起,构建复杂的用户界面。

通过这个实践,您可以了解如何在实际项目中有效地使用 Element Plus,以及如何将各种组件组合起来构建功能完整的应用程序。这些知识和技能可以应用到您自己的项目中,帮助您更高效地开发 Vue 3 应用程序。

在实际开发中,您可能还需要考虑以下方面:

  • 状态管理:使用 Pinia 或 Vuex 管理应用程序状态
  • 路由配置:使用 Vue Router 实现页面导航
  • API 集成:使用 Axios 或其他 HTTP 客户端与后端 API 交互
  • 国际化:使用 Element Plus 的国际化功能支持多语言
  • 主题定制:根据品牌需求自定义 Element Plus 的主题样式

通过这些实践,您将能够充分利用 Element Plus 提供的强大功能,构建出专业、高效的 Web 应用程序。

Element Plus Study Guide