引言

在现代Web开发中,前端框架和UI库的选择对项目的成功至关重要。Element Plus作为基于Vue 3的优秀UI组件库,凭借其丰富的组件、良好的文档和活跃的社区,成为许多企业级项目的首选。本文将从零开始,详细解析如何使用Element Plus构建一个完整的前端项目,并涵盖从项目搭建、开发实践到性能优化的完整流程。

一、项目初始化与环境搭建

1.1 技术栈选择

在开始之前,我们需要明确技术栈:

  • 前端框架: Vue 3 (Composition API)
  • UI库: Element Plus
  • 构建工具: Vite (推荐) 或 Vue CLI
  • 路由: Vue Router 4
  • 状态管理: Pinia (推荐) 或 Vuex
  • 样式预处理: Sass/SCSS
  • 代码规范: ESLint + Prettier
  • 版本控制: Git

1.2 使用Vite创建项目

Vite是新一代前端构建工具,具有极快的启动速度和热更新效率。

# 使用npm创建项目
npm create vite@latest my-element-project -- --template vue

# 进入项目目录
cd my-element-project

# 安装依赖
npm install

1.3 安装Element Plus及相关依赖

# 安装Element Plus
npm install element-plus --save

# 安装Vue Router
npm install vue-router@4

# 安装Pinia
npm install pinia

# 安装Sass支持
npm install -D sass

# 安装图标库(可选)
npm install @element-plus/icons-vue

1.4 项目结构规划

推荐的项目结构如下:

my-element-project/
├── public/                 # 静态资源
├── src/
│   ├── assets/            # 静态资源(图片、字体等)
│   ├── components/        # 公共组件
│   ├── views/             # 页面视图
│   ├── router/            # 路由配置
│   ├── store/             # 状态管理(Pinia)
│   ├── utils/             # 工具函数
│   ├── services/          # API服务
│   ├── App.vue            # 根组件
│   └── main.ts            # 入口文件
├── .env.development       # 开发环境变量
├── .env.production        # 生产环境变量
├── .eslintrc.cjs          # ESLint配置
├── .prettierrc            # Prettier配置
├── vite.config.ts         # Vite配置
└── package.json

1.5 配置Vite和Element Plus

vite.config.ts 配置:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          'element-plus': ['element-plus'],
          'vue': ['vue', 'vue-router', 'pinia']
        }
      }
    }
  }
})

main.ts 配置Element Plus:

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'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)

// 全局注册Element Plus
app.use(ElementPlus)

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

// 注册路由和状态管理
app.use(router)
app.use(createPinia())

app.mount('#app')

二、路由与状态管理配置

2.1 Vue Router 4配置

src/router/index.ts:

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

// 路由懒加载
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页' }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '仪表盘', requiresAuth: true }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录' }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// 路由守卫 - 权限控制
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  const requiresAuth = to.meta.requiresAuth as boolean
  
  if (requiresAuth && !token) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else if (to.name === 'Login' && token) {
    next({ name: 'Dashboard' })
  } else {
    next()
  }
})

export default router

2.2 Pinia状态管理配置

src/store/user.ts (用户状态管理):

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(localStorage.getItem('token'))
  
  const isAuthenticated = computed(() => !!token.value)
  const username = computed(() => user.value?.username || '')
  
  const login = async (credentials: { username: string; password: string }) => {
    try {
      // 模拟API调用
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      
      const data = await response.json()
      
      if (data.success) {
        token.value = data.token
        user.value = data.user
        localStorage.setItem('token', data.token)
        return true
      }
      return false
    } catch (error) {
      console.error('登录失败:', error)
      return false
    }
  }
  
  const logout = () => {
    token.value = null
    user.value = null
    localStorage.removeItem('token')
  }
  
  const fetchUserInfo = async () => {
    if (!token.value) return
    
    try {
      const response = await fetch('/api/user/info', {
        headers: { 'Authorization': `Bearer ${token.value}` }
      })
      
      const data = await response.json()
      if (data.success) {
        user.value = data.user
      }
    } catch (error) {
      console.error('获取用户信息失败:', error)
    }
  }
  
  return {
    user,
    token,
    isAuthenticated,
    username,
    login,
    logout,
    fetchUserInfo
  }
})

src/store/app.ts (应用状态管理):

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAppStore = defineStore('app', () => {
  const sidebarCollapsed = ref(false)
  const theme = ref<'light' | 'dark'>('light')
  const loading = ref(false)
  
  const toggleSidebar = () => {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }
  
  const setTheme = (newTheme: 'light' | 'dark') => {
    theme.value = newTheme
    document.documentElement.setAttribute('data-theme', newTheme)
  }
  
  const setLoading = (value: boolean) => {
    loading.value = value
  }
  
  return {
    sidebarCollapsed,
    theme,
    loading,
    toggleSidebar,
    setTheme,
    setLoading
  }
})

三、核心组件开发实践

3.1 登录页面实现

src/views/Login.vue:

<template>
  <div class="login-container">
    <el-card class="login-card">
      <template #header>
        <div class="card-header">
          <h2>系统登录</h2>
        </div>
      </template>
      
      <el-form 
        ref="loginFormRef"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        @submit.prevent="handleLogin"
      >
        <el-form-item label="用户名" prop="username">
          <el-input 
            v-model="loginForm.username"
            placeholder="请输入用户名"
            :prefix-icon="User"
            clearable
          />
        </el-form-item>
        
        <el-form-item label="密码" prop="password">
          <el-input 
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            :prefix-icon="Lock"
            show-password
            clearable
          />
        </el-form-item>
        
        <el-form-item label="记住我">
          <el-switch v-model="loginForm.remember" />
        </el-form-item>
        
        <el-form-item>
          <el-button 
            type="primary" 
            :loading="loading"
            native-type="submit"
            style="width: 100%"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
      
      <div class="login-footer">
        <el-link type="primary">忘记密码?</el-link>
        <el-link type="success" @click="$router.push('/register')">注册账号</el-link>
      </div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/user'

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

const loginFormRef = ref<FormInstance>()
const loading = ref(false)

const loginForm = reactive({
  username: '',
  password: '',
  remember: false
})

const loginRules: FormRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '用户名长度在3到20个字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  ]
}

const handleLogin = async () => {
  if (!loginFormRef.value) return
  
  try {
    const valid = await loginFormRef.value.validate()
    if (!valid) return
    
    loading.value = true
    
    const success = await userStore.login({
      username: loginForm.username,
      password: loginForm.password
    })
    
    if (success) {
      ElMessage.success('登录成功')
      
      // 记住登录状态
      if (loginForm.remember) {
        localStorage.setItem('rememberedUsername', loginForm.username)
      } else {
        localStorage.removeItem('rememberedUsername')
      }
      
      // 跳转到原页面或首页
      const redirect = (route.query.redirect as string) || '/dashboard'
      router.push(redirect)
    } else {
      ElMessage.error('用户名或密码错误')
    }
  } catch (error) {
    ElMessage.error('登录失败,请稍后重试')
  } finally {
    loading.value = false
  }
}

// 页面加载时检查记住的用户名
const rememberedUsername = localStorage.getItem('rememberedUsername')
if (rememberedUsername) {
  loginForm.username = rememberedUsername
  loginForm.remember = true
}
</script>

<style scoped lang="scss">
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-card {
  width: 400px;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}

.card-header {
  text-align: center;
  color: #409EFF;
}

.login-footer {
  display: flex;
  justify-content: space-between;
  margin-top: 16px;
}
</style>

3.2 仪表盘页面实现

src/views/Dashboard.vue:

<template>
  <div class="dashboard">
    <!-- 顶部统计卡片 -->
    <el-row :gutter="20" class="stats-row">
      <el-col :xs="24" :sm="12" :md="6" v-for="stat in stats" :key="stat.title">
        <el-card class="stat-card" shadow="hover">
          <div class="stat-content">
            <div class="stat-icon" :style="{ background: stat.color }">
              <el-icon :size="24"><component :is="stat.icon" /></el-icon>
            </div>
            <div class="stat-info">
              <div class="stat-value">{{ stat.value }}</div>
              <div class="stat-title">{{ stat.title }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 图表区域 -->
    <el-row :gutter="20" class="chart-row">
      <el-col :xs="24" :md="12">
        <el-card class="chart-card">
          <template #header>
            <div class="card-header">
              <span>月度销售趋势</span>
              <el-button type="primary" size="small" @click="refreshChart">
                刷新
              </el-button>
            </div>
          </template>
          <div ref="lineChartRef" class="chart-container"></div>
        </el-card>
      </el-col>
      
      <el-col :xs="24" :md="12">
        <el-card class="chart-card">
          <template #header>
            <div class="card-header">
              <span>产品分布</span>
              <el-button type="primary" size="small" @click="refreshPieChart">
                刷新
              </el-button>
            </div>
          </template>
          <div ref="pieChartRef" class="chart-container"></div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 数据表格 -->
    <el-card class="table-card">
      <template #header>
        <div class="card-header">
          <span>最近订单</span>
          <el-button type="primary" size="small" @click="handleAdd">
            新增订单
          </el-button>
        </div>
      </template>
      
      <el-table 
        v-loading="tableLoading"
        :data="tableData"
        stripe
        border
        style="width: 100%"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" />
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="orderNo" label="订单号" width="180" />
        <el-table-column prop="customer" label="客户" />
        <el-table-column prop="amount" label="金额">
          <template #default="{ row }">
            <span style="color: #F56C6C; font-weight: bold;">
              ¥{{ row.amount.toFixed(2) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="120">
          <template #default="{ row }">
            <el-tag :type="getStatusType(row.status)">
              {{ getStatusText(row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createdAt" label="创建时间" width="160">
          <template #default="{ row }">
            {{ formatDate(row.createdAt) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" size="small" @click="handleEdit(row)">
              编辑
            </el-button>
            <el-button type="danger" size="small" @click="handleDelete(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"
          :total="total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>

    <!-- 编辑/新增对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="500px"
      @close="resetForm"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="订单号" prop="orderNo">
          <el-input v-model="form.orderNo" placeholder="自动生成" disabled />
        </el-form-item>
        <el-form-item label="客户" prop="customer">
          <el-input v-model="form.customer" placeholder="请输入客户名称" />
        </el-form-item>
        <el-form-item label="金额" prop="amount">
          <el-input-number 
            v-model="form.amount" 
            :precision="2" 
            :step="100"
            :min="0"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
            <el-option label="待处理" value="pending" />
            <el-option label="处理中" value="processing" />
            <el-option label="已完成" value="completed" />
            <el-option label="已取消" value="cancelled" />
          </el-select>
        </el-form-item>
      </el-form>
      
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit" :loading="submitLoading">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import * as echarts from 'echarts'
import { useAppStore } from '@/store/app'

// 图表引用
const lineChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
let lineChart: echarts.ECharts | null = null
let pieChart: echarts.ECharts | null = null

// 状态管理
const appStore = useAppStore()

// 统计数据
const stats = ref([
  { title: '总订单', value: '1,234', icon: 'Document', color: '#409EFF' },
  { title: '总金额', value: '¥125,680', icon: 'Money', color: '#67C23A' },
  { title: '活跃客户', value: '89', icon: 'User', color: '#E6A23C' },
  { title: '今日新增', value: '12', icon: 'Plus', color: '#F56C6C' }
])

// 表格数据
const tableLoading = ref(false)
const tableData = ref([
  { id: 1, orderNo: 'ORD202311001', customer: '张三', amount: 1250.00, status: 'completed', createdAt: '2023-11-01 10:30:00' },
  { id: 2, orderNo: 'ORD202311002', customer: '李四', amount: 890.50, status: 'processing', createdAt: '2023-11-02 14:20:00' },
  { id: 3, orderNo: 'ORD202311003', customer: '王五', amount: 2300.00, status: 'pending', createdAt: '2023-11-03 09:15:00' },
  { id: 4, orderNo: 'ORD202311004', customer: '赵六', amount: 560.00, status: 'completed', createdAt: '2023-11-04 16:45:00' },
  { id: 5, orderNo: 'ORD202311005', customer: '钱七', amount: 1800.00, status: 'cancelled', createdAt: '2023-11-05 11:30:00' }
])

// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(50)

// 对话框
const dialogVisible = ref(false)
const dialogTitle = ref('新增订单')
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive({
  id: null as number | null,
  orderNo: '',
  customer: '',
  amount: 0,
  status: 'pending'
})

const rules: FormRules = {
  customer: [
    { required: true, message: '请输入客户名称', trigger: 'blur' }
  ],
  amount: [
    { required: true, message: '请输入金额', trigger: 'blur' },
    { type: 'number', min: 0, message: '金额必须大于0', trigger: 'blur' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ]
}

// 方法
const getStatusType = (status: string) => {
  const map: Record<string, string> = {
    pending: 'info',
    processing: 'warning',
    completed: 'success',
    cancelled: 'danger'
  }
  return map[status] || 'info'
}

const getStatusText = (status: string) => {
  const map: Record<string, string> = {
    pending: '待处理',
    processing: '处理中',
    completed: '已完成',
    cancelled: '已取消'
  }
  return map[status] || status
}

const formatDate = (dateStr: string) => {
  return new Date(dateStr).toLocaleString('zh-CN')
}

const handleSelectionChange = (selection: any[]) => {
  console.log('选中的行:', selection)
}

const handleAdd = () => {
  dialogTitle.value = '新增订单'
  form.id = null
  form.orderNo = `ORD${Date.now()}`
  form.customer = ''
  form.amount = 0
  form.status = 'pending'
  dialogVisible.value = true
}

const handleEdit = (row: any) => {
  dialogTitle.value = '编辑订单'
  Object.assign(form, row)
  dialogVisible.value = true
}

const handleDelete = async (row: any) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除订单 ${row.orderNo} 吗?`,
      '警告',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    
    // 模拟删除
    tableData.value = tableData.value.filter(item => item.id !== row.id)
    ElMessage.success('删除成功')
  } catch (error) {
    // 用户取消
  }
}

const resetForm = () => {
  if (formRef.value) {
    formRef.value.resetFields()
  }
}

const handleSubmit = async () => {
  if (!formRef.value) return
  
  try {
    const valid = await formRef.value.validate()
    if (!valid) return
    
    submitLoading.value = true
    
    // 模拟API调用
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    if (form.id) {
      // 编辑
      const index = tableData.value.findIndex(item => item.id === form.id)
      if (index !== -1) {
        tableData.value[index] = { ...form }
      }
      ElMessage.success('编辑成功')
    } else {
      // 新增
      const newOrder = {
        ...form,
        id: Date.now(),
        createdAt: new Date().toLocaleString('zh-CN')
      }
      tableData.value.unshift(newOrder)
      ElMessage.success('新增成功')
    }
    
    dialogVisible.value = false
  } catch (error) {
    ElMessage.error('操作失败')
  } finally {
    submitLoading.value = false
  }
}

const handleSizeChange = (val: number) => {
  pageSize.value = val
  // 重新加载数据
  loadTableData()
}

const handleCurrentChange = (val: number) => {
  currentPage.value = val
  // 重新加载数据
  loadTableData()
}

const loadTableData = async () => {
  tableLoading.value = true
  try {
    // 模拟API调用
    await new Promise(resolve => setTimeout(resolve, 500))
    // 这里可以调用API获取数据
    console.log(`加载第 ${currentPage.value} 页,每页 ${pageSize.value} 条`)
  } finally {
    tableLoading.value = false
  }
}

// 图表初始化
const initLineChart = () => {
  if (!lineChartRef.value) return
  
  lineChart = echarts.init(lineChartRef.value)
  
  const option = {
    tooltip: {
      trigger: 'axis'
    },
    legend: {
      data: ['销售额', '订单数']
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true
    },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: ['1月', '2月', '3月', '4月', '5月', '6月']
    },
    yAxis: [
      {
        type: 'value',
        name: '销售额',
        position: 'left'
      },
      {
        type: 'value',
        name: '订单数',
        position: 'right'
      }
    ],
    series: [
      {
        name: '销售额',
        type: 'line',
        smooth: true,
        data: [120, 132, 101, 134, 90, 230],
        itemStyle: { color: '#409EFF' }
      },
      {
        name: '订单数',
        type: 'line',
        yAxisIndex: 1,
        smooth: true,
        data: [220, 182, 191, 234, 290, 330],
        itemStyle: { color: '#67C23A' }
      }
    ]
  }
  
  lineChart.setOption(option)
}

const initPieChart = () => {
  if (!pieChartRef.value) return
  
  pieChart = echarts.init(pieChartRef.value)
  
  const option = {
    tooltip: {
      trigger: 'item'
    },
    legend: {
      orient: 'vertical',
      left: 'left'
    },
    series: [
      {
        name: '产品分布',
        type: 'pie',
        radius: '50%',
        data: [
          { value: 1048, name: '电子产品' },
          { value: 735, name: '家居用品' },
          { value: 580, name: '服装' },
          { value: 484, name: '食品' },
          { value: 300, name: '其他' }
        ],
        emphasis: {
          itemStyle: {
            shadowBlur: 10,
            shadowOffsetX: 0,
            shadowColor: 'rgba(0, 0, 0, 0.5)'
          }
        }
      }
    ]
  }
  
  pieChart.setOption(option)
}

const refreshChart = () => {
  if (lineChart) {
    const newData = Array.from({ length: 6 }, () => Math.floor(Math.random() * 300) + 100)
    lineChart.setOption({
      series: [{ data: newData }]
    })
  }
}

const refreshPieChart = () => {
  if (pieChart) {
    const newData = [
      { value: Math.floor(Math.random() * 1000) + 500, name: '电子产品' },
      { value: Math.floor(Math.random() * 800) + 400, name: '家居用品' },
      { value: Math.floor(Math.random() * 600) + 300, name: '服装' },
      { value: Math.floor(Math.random() * 500) + 200, name: '食品' },
      { value: Math.floor(Math.random() * 400) + 100, name: '其他' }
    ]
    pieChart.setOption({
      series: [{ data: newData }]
    })
  }
}

// 响应式图表
const handleResize = () => {
  if (lineChart) lineChart.resize()
  if (pieChart) pieChart.resize()
}

onMounted(() => {
  // 初始化图表
  setTimeout(() => {
    initLineChart()
    initPieChart()
  }, 100)
  
  // 监听窗口大小变化
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  // 销毁图表实例
  if (lineChart) {
    lineChart.dispose()
    lineChart = null
  }
  if (pieChart) {
    pieChart.dispose()
    pieChart = null
  }
  
  // 移除事件监听
  window.removeEventListener('resize', handleResize)
})
</script>

<style scoped lang="scss">
.dashboard {
  padding: 20px;
}

.stats-row {
  margin-bottom: 20px;
}

.stat-card {
  height: 100%;
  
  .stat-content {
    display: flex;
    align-items: center;
    gap: 16px;
  }
  
  .stat-icon {
    width: 48px;
    height: 48px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
  }
  
  .stat-info {
    flex: 1;
  }
  
  .stat-value {
    font-size: 24px;
    font-weight: bold;
    color: #303133;
  }
  
  .stat-title {
    font-size: 14px;
    color: #909399;
  }
}

.chart-row {
  margin-bottom: 20px;
}

.chart-card {
  min-height: 350px;
  
  .chart-container {
    width: 100%;
    height: 300px;
  }
}

.table-card {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .pagination-container {
    margin-top: 20px;
    display: flex;
    justify-content: flex-end;
  }
}

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

四、API服务封装与请求管理

4.1 Axios配置与拦截器

src/services/axios.ts:

import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { useUserStore } from '@/store/user'

// 创建Axios实例
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const userStore = useUserStore()
    
    // 添加认证Token
    if (userStore.token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${userStore.token}`
      }
    }
    
    // 添加请求ID(用于日志追踪)
    config.headers['X-Request-ID'] = Date.now().toString()
    
    // 显示全局加载状态(可选)
    if (config.showLoading !== false) {
      // 可以在这里添加全局loading逻辑
    }
    
    return config
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { data, status, config } = response
    
    // 处理成功响应
    if (status === 200) {
      // 如果后端返回统一格式,可以在这里处理
      if (data.code !== undefined && data.code !== 200) {
        // 业务错误
        ElMessage.error(data.message || '请求失败')
        return Promise.reject(new Error(data.message || '请求失败'))
      }
      
      // 显示成功消息(可选)
      if (config.showSuccess) {
        ElMessage.success(data.message || '操作成功')
      }
      
      return data.data || data
    }
    
    return response
  },
  (error: AxiosError) => {
    const { response, config } = error
    
    // 处理HTTP错误
    if (response) {
      const { status, data } = response
      
      switch (status) {
        case 400:
          ElMessage.error('请求参数错误')
          break
        case 401:
          // 未授权,清除token并跳转登录页
          const userStore = useUserStore()
          userStore.logout()
          ElMessage.warning('登录已过期,请重新登录')
          // 可以在这里跳转到登录页
          break
        case 403:
          ElMessage.error('没有权限访问该资源')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器内部错误')
          break
        case 502:
          ElMessage.error('网关错误')
          break
        case 503:
          ElMessage.error('服务不可用')
          break
        case 504:
          ElMessage.error('网关超时')
          break
        default:
          ElMessage.error(`请求失败: ${status}`)
      }
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      ElMessage.error('网络连接失败,请检查网络')
    } else {
      // 发送请求时出错
      ElMessage.error('请求配置错误')
    }
    
    return Promise.reject(error)
  }
)

// 扩展Axios配置类型
declare module 'axios' {
  interface AxiosRequestConfig {
    showLoading?: boolean
    showSuccess?: boolean
  }
}

export default service

4.2 API服务封装

src/services/api/user.ts:

import service from '@/services/axios'
import type { User, LoginParams, LoginResponse } from '@/types'

export const login = (params: LoginParams): Promise<LoginResponse> => {
  return service.post('/auth/login', params, { showSuccess: true })
}

export const getUserInfo = (): Promise<User> => {
  return service.get('/user/info')
}

export const logout = (): Promise<void> => {
  return service.post('/auth/logout')
}

export const updateUser = (data: Partial<User>): Promise<User> => {
  return service.put('/user/update', data, { showSuccess: true })
}

src/services/api/order.ts:

import service from '@/services/axios'
import type { Order, OrderListParams, OrderListResponse } from '@/types'

export const getOrders = (params: OrderListParams): Promise<OrderListResponse> => {
  return service.get('/orders', { params })
}

export const getOrderById = (id: number): Promise<Order> => {
  return service.get(`/orders/${id}`)
}

export const createOrder = (data: Partial<Order>): Promise<Order> => {
  return service.post('/orders', data, { showSuccess: true })
}

export const updateOrder = (id: number, data: Partial<Order>): Promise<Order> => {
  return service.put(`/orders/${id}`, data, { showSuccess: true })
}

export const deleteOrder = (id: number): Promise<void> => {
  return service.delete(`/orders/${id}`, { showSuccess: true })
}

4.3 类型定义

src/types/index.ts:

// 用户相关类型
export interface User {
  id: number
  username: string
  email: string
  role: string
  avatar?: string
  createdAt: string
}

export interface LoginParams {
  username: string
  password: string
}

export interface LoginResponse {
  token: string
  user: User
}

// 订单相关类型
export interface Order {
  id: number
  orderNo: string
  customer: string
  amount: number
  status: 'pending' | 'processing' | 'completed' | 'cancelled'
  createdAt: string
  updatedAt?: string
}

export interface OrderListParams {
  page?: number
  pageSize?: number
  status?: string
  customer?: string
  startDate?: string
  endDate?: string
}

export interface OrderListResponse {
  list: Order[]
  total: number
  page: number
  pageSize: number
}

// API响应类型
export interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
  timestamp: number
}

五、性能优化策略

5.1 代码分割与懒加载

路由懒加载(已在路由配置中实现):

// 路由懒加载示例
const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '仪表盘', requiresAuth: true }
  },
  {
    path: '/report',
    name: 'Report',
    component: () => import('@/views/Report.vue'),
    meta: { title: '报表' }
  }
]

组件懒加载

<template>
  <div>
    <!-- 懒加载图表组件 -->
    <Suspense>
      <template #default>
        <LazyChartComponent v-if="showChart" />
      </template>
      <template #fallback>
        <el-skeleton :rows="5" animated />
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'

// 异步组件
const LazyChartComponent = defineAsyncComponent(() => 
  import('@/components/ChartComponent.vue')
)

const showChart = ref(false)

// 延迟加载
setTimeout(() => {
  showChart.value = true
}, 1000)
</script>

5.2 组件按需引入

vite.config.ts 配置自动按需引入:

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(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      imports: ['vue', 'vue-router', 'pinia']
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dirs: ['src/components']
    })
  ]
})

安装依赖

npm install -D unplugin-auto-import unplugin-vue-components

修改main.ts(移除全局引入):

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

// 移除全局引入Element Plus
// import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css'

const app = createApp(App)

// app.use(ElementPlus) // 移除这行

app.use(router)
app.use(createPinia())

app.mount('#app')

5.3 图片与静态资源优化

vite.config.ts 配置图片优化:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteImagemin from 'vite-plugin-imagemin'

export default defineConfig({
  plugins: [
    vue(),
    viteImagemin({
      mozjpeg: {
        quality: 80
      },
      png: {
        quality: 80
      },
      webp: {
        quality: 80
      },
      gif: {
        optimizationLevel: 3
      }
    })
  ],
  build: {
    assetsDir: 'assets',
    rollupOptions: {
      output: {
        manualChunks: {
          'element-plus': ['element-plus'],
          'vue': ['vue', 'vue-router', 'pinia'],
          'echarts': ['echarts']
        }
      }
    }
  }
})

安装图片优化插件

npm install -D vite-plugin-imagemin

5.4 缓存策略

Service Worker缓存(PWA支持):

src/sw.js:

const CACHE_NAME = 'element-app-v1'
const urlsToCache = [
  '/',
  '/index.html',
  '/assets/logo.png',
  '/assets/icon-192.png',
  '/assets/icon-512.png'
]

// 安装Service Worker
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        console.log('Opened cache')
        return cache.addAll(urlsToCache)
      })
  )
})

// 拦截请求并返回缓存
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // 缓存命中,返回缓存
        if (response) {
          return response
        }
        
        // 缓存未命中,从网络获取
        return fetch(event.request).then((response) => {
          // 检查响应是否有效
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response
          }
          
          // 克隆响应
          const responseToCache = response.clone()
          
          caches.open(CACHE_NAME)
            .then((cache) => {
              cache.put(event.request, responseToCache)
            })
          
          return response
        })
      })
  )
})

// 清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})

注册Service Worker(main.ts):

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then((registration) => {
        console.log('SW registered: ', registration)
      })
      .catch((registrationError) => {
        console.log('SW registration failed: ', registrationError)
      })
  })
}

5.5 性能监控

性能监控工具

// src/utils/performance.ts
export class PerformanceMonitor {
  private static instance: PerformanceMonitor
  private metrics: Map<string, number[]> = new Map()
  
  static getInstance(): PerformanceMonitor {
    if (!PerformanceMonitor.instance) {
      PerformanceMonitor.instance = new PerformanceMonitor()
    }
    return PerformanceMonitor.instance
  }
  
  // 记录性能指标
  record(metricName: string, value: number): void {
    if (!this.metrics.has(metricName)) {
      this.metrics.set(metricName, [])
    }
    this.metrics.get(metricName)!.push(value)
    
    // 限制存储数量
    if (this.metrics.get(metricName)!.length > 100) {
      this.metrics.get(metricName)!.shift()
    }
  }
  
  // 获取平均值
  getAverage(metricName: string): number {
    const values = this.metrics.get(metricName)
    if (!values || values.length === 0) return 0
    
    const sum = values.reduce((a, b) => a + b, 0)
    return sum / values.length
  }
  
  // 获取所有指标
  getAllMetrics(): Record<string, number[]> {
    const result: Record<string, number[]> = {}
    this.metrics.forEach((values, key) => {
      result[key] = [...values]
    })
    return result
  }
  
  // 清空指标
  clear(): void {
    this.metrics.clear()
  }
}

// 使用Performance API监控
export const measurePerformance = (name: string, fn: () => Promise<any>) => {
  const start = performance.now()
  
  return fn().finally(() => {
    const end = performance.now()
    const duration = end - start
    
    // 记录到监控器
    PerformanceMonitor.getInstance().record(name, duration)
    
    // 控制台输出
    console.log(`${name} 耗时: ${duration.toFixed(2)}ms`)
    
    // 如果耗时过长,发出警告
    if (duration > 1000) {
      console.warn(`${name} 执行时间过长: ${duration.toFixed(2)}ms`)
    }
  })
}

// 监控页面加载性能
export const monitorPageLoad = () => {
  if ('PerformanceObserver' in window) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'navigation') {
          const navEntry = entry as PerformanceNavigationTiming
          
          console.log('页面加载性能:')
          console.log(`DNS查询: ${navEntry.domainLookupEnd - navEntry.domainLookupStart}ms`)
          console.log(`TCP连接: ${navEntry.connectEnd - navEntry.connectStart}ms`)
          console.log(`请求响应: ${navEntry.responseEnd - navEntry.requestStart}ms`)
          console.log(`DOM解析: ${navEntry.domContentLoadedEventEnd - navEntry.domContentLoadedEventStart}ms`)
          console.log(`总加载时间: ${navEntry.loadEventEnd - navEntry.startTime}ms`)
          
          // 记录到监控器
          PerformanceMonitor.getInstance().record('pageLoad', navEntry.loadEventEnd - navEntry.startTime)
        }
      }
    })
    
    observer.observe({ entryTypes: ['navigation'] })
  }
}

六、部署与CI/CD

6.1 构建配置优化

vite.config.ts 生产环境配置:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // 移除console.log
        drop_debugger: true // 移除debugger
      }
    },
    rollupOptions: {
      output: {
        manualChunks: {
          'element-plus': ['element-plus'],
          'vue': ['vue', 'vue-router', 'pinia'],
          'echarts': ['echarts']
        }
      }
    }
  },
  // 环境变量
  define: {
    'process.env': {}
  }
})

6.2 Docker部署

Dockerfile:

# 构建阶段
FROM node:18-alpine as builder

WORKDIR /app

# 复制package.json和package-lock.json
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建项目
RUN npm run build

# 生产阶段
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

nginx.conf:

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

    # 缓存配置
    map $uri $static_cache {
        ~*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ 1y;
        default 0;
    }

    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires $static_cache;
            add_header Cache-Control "public, immutable";
            add_header Vary "Accept-Encoding";
        }

        # HTML文件不缓存
        location ~* \.html$ {
            expires -1;
            add_header Cache-Control "no-cache, no-store, must-revalidate";
        }

        # API代理(如果需要)
        location /api/ {
            proxy_pass http://backend:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # 单页应用路由支持
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}

6.3 GitHub Actions CI/CD

.github/workflows/deploy.yml:

name: Deploy to Production

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linting
      run: npm run lint
    
    - name: Run tests
      run: npm run test
    
    - name: Build project
      run: npm run build
    
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: dist
        path: dist/
  
  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Download build artifacts
      uses: actions/download-artifact@v3
      with:
        name: dist
        path: dist/
    
    - name: Deploy to server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USERNAME }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/element-app
          git pull origin main
          npm ci
          npm run build
          docker-compose down
          docker-compose up -d --build

七、最佳实践与常见问题

7.1 代码规范与团队协作

ESLint配置 (.eslintrc.cjs):

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    ecmaVersion: 2021,
    sourceType: 'module',
    parser: '@typescript-eslint/parser'
  },
  plugins: ['vue', '@typescript-eslint'],
  rules: {
    'vue/multi-word-component-names': 'off',
    'vue/no-v-html': 'off',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
  }
}

Prettier配置 (.prettierrc):

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 100,
  "vueIndentScriptAndStyle": false
}

7.2 常见问题解决方案

问题1:Element Plus样式覆盖

// 全局样式覆盖
:root {
  --el-color-primary: #409EFF;
  --el-color-success: #67C23A;
  --el-color-warning: #E6A23C;
  --el-color-danger: #F56C6C;
  --el-color-info: #909399;
}

// 深度选择器覆盖
:deep(.el-button) {
  border-radius: 4px;
}

// 特定组件样式
.dashboard {
  :deep(.el-card__header) {
    background: #f5f7fa;
    border-bottom: 1px solid #ebeef5;
  }
}

问题2:路由跳转报错

// 解决Vue Router 4的导航重复问题
router.beforeEach((to, from, next) => {
  // 避免重复导航
  if (to.name === from.name && to.fullPath === from.fullPath) {
    next(false)
    return
  }
  
  // 正常导航
  next()
})

问题3:内存泄漏

// 组件卸载时清理资源
import { onBeforeUnmount } from 'vue'

export default {
  setup() {
    const timer = setInterval(() => {
      console.log('定时器运行中')
    }, 1000)
    
    // 清理定时器
    onBeforeUnmount(() => {
      clearInterval(timer)
    })
    
    return {}
  }
}

7.3 安全最佳实践

XSS防护

// 使用DOMPurify进行HTML净化
import DOMPurify from 'dompurify'

export const sanitizeHTML = (html: string): string => {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'title', 'target']
  })
}

// 在Vue中使用
const safeHtml = computed(() => {
  return sanitizeHTML(userInput.value)
})

CSRF防护

// 在请求头中添加CSRF Token
service.interceptors.request.use((config) => {
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
  if (csrfToken) {
    config.headers['X-CSRF-TOKEN'] = csrfToken
  }
  return config
})

八、总结

本文详细介绍了使用Element Plus从零搭建前端项目的完整流程,涵盖了:

  1. 项目初始化:使用Vite创建项目,配置Element Plus、Vue Router和Pinia
  2. 核心组件开发:实现了登录页面和仪表盘页面,包含表单验证、图表展示和数据表格
  3. API服务封装:使用Axios进行请求管理,添加拦截器处理认证和错误
  4. 性能优化:代码分割、懒加载、按需引入、图片优化和缓存策略
  5. 部署与CI/CD:Docker部署配置和GitHub Actions自动化流程
  6. 最佳实践:代码规范、常见问题解决方案和安全防护

通过遵循这些步骤和最佳实践,您可以构建一个高性能、可维护的企业级前端应用。记住,前端开发是一个持续优化的过程,建议定期进行性能分析和代码审查,以确保应用始终保持最佳状态。

九、扩展阅读

希望这份指南能帮助您在前端开发道路上走得更远!