电商后台管理系统

电商后台管理系统是一个典型的企业级应用,它需要处理商品管理、订单管理、用户管理、数据分析等复杂功能。本章将介绍如何使用Vue.js 3构建一个完整的电商后台管理系统。

14.36.1 项目架构设计

技术栈选择

技术 版本 用途
Vue.js 3.3.x 前端框架
Vue Router 4.2.x 路由管理
Pinia 2.1.x 状态管理
TypeScript 5.2.x 类型检查
Element Plus 2.4.x UI组件库
Vite 5.0.x 构建工具
Axios 1.6.x HTTP客户端
ECharts 5.4.x 数据可视化
Day.js 1.11.x 日期处理

项目目录结构

ecommerce-admin/
├── public/              # 静态资源
├── src/
│   ├── assets/          # 模块资源
│   ├── components/      # 通用组件
│   │   ├── common/      # 基础组件
│   │   ├── layout/      # 布局组件
│   │   └── business/    # 业务组件
│   ├── composables/     # 组合式函数
│   ├── directives/      # 自定义指令
│   ├── enums/           # 枚举类型
│   ├── hooks/           # 自定义钩子
│   ├── locales/         # 国际化配置
│   ├── router/          # 路由配置
│   │   ├── modules/     # 路由模块
│   │   ├── guards/      # 路由守卫
│   │   └── index.ts     # 路由入口
│   ├── stores/          # 状态管理
│   │   ├── modules/     # Store模块
│   │   └── index.ts     # Store入口
│   ├── styles/          # 样式文件
│   │   ├── variables.scss # 变量定义
│   │   ├── mixins.scss    # 混合样式
│   │   └── index.scss     # 样式入口
│   ├── types/           # 类型定义
│   ├── utils/           # 工具函数
│   │   ├── request.ts   # 请求封装
│   │   ├── auth.ts      # 认证工具
│   │   └── index.ts     # 工具入口
│   ├── views/           # 页面组件
│   │   ├── dashboard/   # 仪表盘
│   │   ├── product/     # 商品管理
│   │   ├── order/       # 订单管理
│   │   ├── user/        # 用户管理
│   │   └── system/      # 系统设置
│   ├── App.vue          # 根组件
│   └── main.ts          # 入口文件
├── .env.development     # 开发环境配置
├── .env.production      # 生产环境配置
├── index.html           # 页面模板
├── package.json         # 项目配置
├── tsconfig.json        # TypeScript配置
├── vite.config.ts       # Vite配置
└── README.md            # 项目说明

架构设计原则

  1. 模块化设计

    • 按功能模块划分代码
    • 每个模块职责单一
    • 模块间低耦合高内聚
  2. 分层架构

    • 表现层(Views/Components)
    • 业务逻辑层(Composables/Stores)
    • 数据访问层(Utils/API)
  3. 响应式设计

    • 适配不同屏幕尺寸
    • 移动端友好
  4. 可扩展性

    • 易于添加新功能
    • 支持插件机制
  5. 可维护性

    • 代码规范统一
    • 完善的文档
    • 单元测试

核心模块设计

1. 布局模块

<!-- src/components/layout/MainLayout.vue -->
<template>
  <div class="main-layout">
    <!-- 侧边栏 -->
    <Sidebar v-if="!isMobile" />
    <!-- 移动端侧边栏 -->
    <MobileSidebar v-else />
    <!-- 主内容区 -->
    <div class="main-content">
      <!-- 顶部导航 -->
      <Topbar @toggle-sidebar="toggleSidebar" />
      <!-- 内容区域 -->
      <div class="content-wrapper">
        <router-view v-slot="{ Component }">
          <Transition name="fade" mode="out-in">
            <component :is="Component" />
          </Transition>
        </router-view>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import Topbar from './Topbar.vue'
import Sidebar from './Sidebar.vue'
import MobileSidebar from './MobileSidebar.vue'
import { useAppStore } from '@/stores/modules/app'

const appStore = useAppStore()
const route = useRoute()
const isMobile = ref(false)

// 切换侧边栏
const toggleSidebar = () => {
  appStore.toggleSidebar()
}

// 监听窗口大小变化
const handleResize = () => {
  isMobile.value = window.innerWidth < 768
}

onMounted(() => {
  handleResize()
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

2. 路由模块

// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import MainLayout from '@/components/layout/MainLayout.vue'
import { useAuthStore } from '@/stores/modules/auth'

// 路由白名单
const whiteList = ['/login', '/register', '/404', '/500']

// 动态路由
const asyncRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    component: MainLayout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        meta: {
          title: '仪表盘',
          icon: 'dashboard',
          roles: ['admin', 'editor']
        }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/login/index.vue'),
      meta: {
        title: '登录',
        hidden: true
      }
    },
    ...asyncRoutes
  ]
})

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  const token = authStore.token

  // 存在token
  if (token) {
    if (to.path === '/login') {
      next('/')
    } else {
      // 检查是否已获取用户信息
      if (!authStore.userInfo) {
        try {
          await authStore.getUserInfo()
          // 动态生成路由
          const accessRoutes = await authStore.generateRoutes()
          accessRoutes.forEach(route => {
            router.addRoute(route)
          })
          next({ ...to, replace: true })
        } catch (error) {
          // 清除token并跳转到登录页
          await authStore.logout()
          next(`/login?redirect=${to.path}`)
        }
      } else {
        next()
      }
    }
  } else {
    // 不存在token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

export default router

14.36.2 权限系统实现

权限设计

  1. 基于角色的访问控制(RBAC)

    • 角色:管理员、编辑、访客
    • 权限:页面访问权限、按钮操作权限
    • 资源:菜单、按钮、API接口
  2. 权限存储

    • 前端:路由配置、按钮权限标识
    • 后端:数据库存储角色-权限映射关系

权限实现

1. 路由权限控制

// src/stores/modules/auth.ts
import { defineStore } from 'pinia'
import { RouteRecordRaw } from 'vue-router'
import { getMenuList } from '@/api/system/menu'
import { generateRoutes } from '@/utils/router'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: localStorage.getItem('token') || '',
    userInfo: null,
    roles: [],
    permissions: [],
    menuList: []
  }),
  actions: {
    // 登录
    async login(params: LoginParams) {
      const { data } = await loginApi(params)
      this.token = data.token
      localStorage.setItem('token', data.token)
    },
    // 获取用户信息
    async getUserInfo() {
      const { data } = await getUserInfoApi()
      this.userInfo = data.user
      this.roles = data.roles
      this.permissions = data.permissions
      this.menuList = data.menuList
    },
    // 生成路由
    async generateRoutes() {
      let accessedRoutes: RouteRecordRaw[]
      if (this.roles.includes('admin')) {
        // 管理员拥有所有权限
        accessedRoutes = generateRoutes(this.menuList)
      } else {
        // 根据角色过滤路由
        accessedRoutes = generateRoutes(this.menuList, this.roles)
      }
      return accessedRoutes
    },
    // 登出
    async logout() {
      await logoutApi()
      this.token = ''
      this.userInfo = null
      this.roles = []
      this.permissions = []
      this.menuList = []
      localStorage.removeItem('token')
    }
  }
})

2. 按钮权限控制

// src/directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue'
import { useAuthStore } from '@/stores/modules/auth'

const permission: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const { value } = binding
    const authStore = useAuthStore()
    const permissions = authStore.permissions

    if (value && Array.isArray(value)) {
      const hasPermission = value.some(permission => permissions.includes(permission))
      if (!hasPermission) {
        el.style.display = 'none'
        el.parentNode?.removeChild(el)
      }
    } else {
      throw new Error('权限指令值必须是数组')
    }
  }
}

export default permission
<!-- 使用权限指令 -->
<template>
  <el-button v-permission="['product:add']" type="primary">添加商品</el-button>
  <el-button v-permission="['product:edit']" type="success">编辑商品</el-button>
  <el-button v-permission="['product:delete']" type="danger">删除商品</el-button>
</template>

3. API权限控制

// src/utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useAuthStore } from '@/stores/modules/auth'
import router from '@/router'

const request: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
request.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const authStore = useAuthStore()
    const token = authStore.token
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, message } = response.data
    if (code === 200) {
      return response.data
    } else {
      // 处理错误
      ElMessage.error(message || '请求失败')
      return Promise.reject(new Error(message || '请求失败'))
    }
  },
  (error) => {
    if (error.response?.status === 401) {
      // 未授权,跳转到登录页
      const authStore = useAuthStore()
      authStore.logout()
      router.push(`/login?redirect=${router.currentRoute.value.path}`)
    } else if (error.response?.status === 403) {
      // 没有权限
      ElMessage.error('没有操作权限')
    } else {
      ElMessage.error(error.message || '网络错误')
    }
    return Promise.reject(error)
  }
)

export default request

14.36.3 数据可视化集成

仪表盘设计

1. 核心指标卡片

<!-- src/views/dashboard/components/MetricCard.vue -->
<template>
  <el-card class="metric-card" shadow="hover">
    <div class="metric-content">
      <div class="metric-info">
        <h3 class="metric-title">{{ title }}</h3>
        <div class="metric-value">{{ value }}</div>
        <div class="metric-change" :class="changeType">
          <el-icon :size="16">{{ changeType === 'increase' ? ArrowUp : ArrowDown }}</el-icon>
          <span>{{ change }}%</span>
        </div>
      </div>
      <div class="metric-icon" :style="{ backgroundColor: iconColor + '20' }">
        <component :is="icon" :size="32" :color="iconColor" />
      </div>
    </div>
  </el-card>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'

interface Props {
  title: string
  value: string | number
  change: number
  icon: any
  iconColor: string
}

const props = defineProps<Props>()

const changeType = computed(() => {
  return props.change > 0 ? 'increase' : 'decrease'
})
</script>

2. 销售趋势图

<!-- src/views/dashboard/components/SalesTrend.vue -->
<template>
  <el-card class="chart-card" shadow="hover">
    <template #header>
      <div class="card-header">
        <h3>销售趋势</h3>
        <el-select v-model="timeRange" size="small" @change="handleTimeRangeChange">
          <el-option label="最近7天" value="7d" />
          <el-option label="最近30天" value="30d" />
          <el-option label="最近90天" value="90d" />
          <el-option label="最近1年" value="1y" />
        </el-select>
      </div>
    </template>
    <div ref="chartRef" class="chart-container"></div>
  </el-card>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import { useDashboardStore } from '@/stores/modules/dashboard'

const chartRef = ref<HTMLElement>()
const timeRange = ref('7d')
let chartInstance: echarts.ECharts | null = null
const dashboardStore = useDashboardStore()

// 初始化图表
const initChart = () => {
  if (chartRef.value) {
    chartInstance = echarts.init(chartRef.value)
    const option = {
      tooltip: {
        trigger: 'axis',
        formatter: '{b}: {c}元'
      },
      xAxis: {
        type: 'category',
        data: dashboardStore.salesTrend.data.map(item => item.date)
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: '{value}元'
        }
      },
      series: [
        {
          data: dashboardStore.salesTrend.data.map(item => item.amount),
          type: 'line',
          smooth: true,
          itemStyle: {
            color: '#67c23a'
          },
          areaStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              {
                offset: 0,
                color: 'rgba(103, 194, 58, 0.5)'
              },
              {
                offset: 1,
                color: 'rgba(103, 194, 58, 0.1)'
              }
            ])
          }
        }
      ]
    }
    chartInstance.setOption(option)
  }
}

// 更新图表
const updateChart = () => {
  if (chartInstance) {
    chartInstance.setOption({
      xAxis: {
        data: dashboardStore.salesTrend.data.map(item => item.date)
      },
      series: [
        {
          data: dashboardStore.salesTrend.data.map(item => item.amount)
        }
      ]
    })
  }
}

// 处理时间范围变化
const handleTimeRangeChange = () => {
  dashboardStore.getSalesTrend(timeRange.value)
}

onMounted(() => {
  dashboardStore.getSalesTrend(timeRange.value)
  nextTick(() => {
    initChart()
  })

  // 监听窗口大小变化
  window.addEventListener('resize', () => {
    chartInstance?.resize()
  })
})

// 监听数据变化
watch(() => dashboardStore.salesTrend, () => {
  updateChart()
}, { deep: true })
</script>

3. 商品分类占比

<!-- src/views/dashboard/components/ProductCategory.vue -->
<template>
  <el-card class="chart-card" shadow="hover">
    <template #header>
      <h3>商品分类占比</h3>
    </template>
    <div ref="chartRef" class="chart-container"></div>
  </el-card>
</template>

<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { useDashboardStore } from '@/stores/modules/dashboard'

const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
const dashboardStore = useDashboardStore()

// 初始化图表
const initChart = () => {
  if (chartRef.value) {
    chartInstance = echarts.init(chartRef.value)
    const option = {
      tooltip: {
        trigger: 'item',
        formatter: '{b}: {c} ({d}%)'
      },
      legend: {
        orient: 'vertical',
        left: 'left',
        data: dashboardStore.productCategory.map(item => item.name)
      },
      series: [
        {
          name: '商品分类',
          type: 'pie',
          radius: '50%',
          data: dashboardStore.productCategory,
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)'
            }
          }
        }
      ]
    }
    chartInstance.setOption(option)
  }
}

onMounted(() => {
  dashboardStore.getProductCategory()
  nextTick(() => {
    initChart()
  })

  // 监听窗口大小变化
  window.addEventListener('resize', () => {
    chartInstance?.resize()
  })
})
</script>

14.36.4 部署与监控

部署方案

1. 前端部署

# 安装依赖
npm install

# 构建生产版本
npm run build:prod

# 构建结果位于 dist 目录

2. Nginx配置

server {
    listen 80;
    server_name admin.example.com;
    root /usr/share/nginx/html/ecommerce-admin;
    index index.html;

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

    # 单页应用配置
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 反向代理API
    location /api {
        proxy_pass http://api.example.com;
        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;
    }
}

3. Docker部署

# 基础镜像
FROM node:18-alpine as builder

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装依赖
RUN npm install --registry=https://registry.npmmirror.com

# 复制源代码
COPY . .

# 构建生产版本
RUN npm run build:prod

# 生产环境镜像
FROM nginx:alpine

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

# 复制Nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 暴露端口
EXPOSE 80

# 启动Nginx
CMD ["nginx", "-g", "daemon off;"]
# 构建镜像
docker build -t ecommerce-admin:v1.0.0 .

# 运行容器
docker run -d -p 80:80 --name ecommerce-admin ecommerce-admin:v1.0.0

# 查看容器日志
docker logs -f ecommerce-admin

监控方案

1. 前端监控

// src/utils/monitor.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
import { createApp } from 'vue'

// 初始化Sentry
export const initSentry = (app: any) => {
  Sentry.init({
    app,
    dsn: 'https://your-sentry-dsn',
    environment: import.meta.env.MODE,
    release: 'ecommerce-admin@1.0.0',
    tracesSampleRate: 1.0,
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        tracingOrigins: ['localhost', 'api.example.com', /^https:\/\/your-domain\.com\//]
      })
    ]
  })
}

// 捕获错误
export const captureError = (error: any, context?: any) => {
  Sentry.captureException(error, {
    extra: context
  })
}

// 捕获消息
export const captureMessage = (message: string, level?: Sentry.SeverityLevel) => {
  Sentry.captureMessage(message, level)
}

2. 性能监控

// src/utils/performance.ts
// 页面加载性能监控
export const monitorPageLoad = () => {
  if ('performance' in window) {
    window.addEventListener('load', () => {
      const perfData = performance.timing
      const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart
      const domReadyTime = perfData.domContentLoadedEventEnd - perfData.navigationStart
      const firstPaintTime = perfData.responseStart - perfData.navigationStart
      
      // 上报性能数据
      console.log('页面加载时间:', pageLoadTime, 'ms')
      console.log('DOM就绪时间:', domReadyTime, 'ms')
      console.log('首次渲染时间:', firstPaintTime, 'ms')
      
      // 这里可以添加上报逻辑
    })
  }
}

// API请求性能监控
export const monitorApiPerformance = (url: string, method: string, duration: number, status: number) => {
  // 上报API性能数据
  console.log(`API请求: ${method} ${url} - ${status} - ${duration}ms`)
  
  // 这里可以添加上报逻辑
}

3. 日志管理

// src/utils/logger.ts
// 日志级别
export enum LogLevel {
  DEBUG = 'debug',
  INFO = 'info',
  WARN = 'warn',
  ERROR = 'error'
}

// 日志配置
interface LogConfig {
  level: LogLevel
  enable: boolean
  enableReport: boolean
}

const config: LogConfig = {
  level: LogLevel.INFO,
  enable: true,
  enableReport: import.meta.env.MODE === 'production'
}

// 日志工具类
export class Logger {
  private name: string
  
  constructor(name: string) {
    this.name = name
  }
  
  private log(level: LogLevel, message: string, ...args: any[]) {
    if (!config.enable) return
    
    const logLevels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]
    if (logLevels.indexOf(level) < logLevels.indexOf(config.level)) return
    
    const timestamp = new Date().toISOString()
    const logMessage = `[${timestamp}] [${level.toUpperCase()}] [${this.name}] ${message}`
    
    switch (level) {
      case LogLevel.DEBUG:
        console.debug(logMessage, ...args)
        break
      case LogLevel.INFO:
        console.info(logMessage, ...args)
        break
      case LogLevel.WARN:
        console.warn(logMessage, ...args)
        break
      case LogLevel.ERROR:
        console.error(logMessage, ...args)
        // 上报错误日志
        if (config.enableReport) {
          // 这里可以添加上报逻辑
        }
        break
    }
  }
  
  debug(message: string, ...args: any[]) {
    this.log(LogLevel.DEBUG, message, ...args)
  }
  
  info(message: string, ...args: any[]) {
    this.log(LogLevel.INFO, message, ...args)
  }
  
  warn(message: string, ...args: any[]) {
    this.log(LogLevel.WARN, message, ...args)
  }
  
  error(message: string, ...args: any[]) {
    this.log(LogLevel.ERROR, message, ...args)
  }
}

// 创建日志实例
export const createLogger = (name: string) => {
  return new Logger(name)
}

项目实战总结

  1. 架构设计

    • 采用模块化、分层架构
    • 清晰的目录结构
    • 良好的代码组织
  2. 权限管理

    • 基于RBAC的权限模型
    • 路由权限控制
    • 按钮权限控制
    • API权限控制
  3. 数据可视化

    • 使用ECharts实现各类图表
    • 响应式设计
    • 交互式图表
  4. 部署监控

    • 多种部署方案(Nginx、Docker)
    • 前端错误监控
    • 性能监控
    • 日志管理

通过本项目,我们学习了如何使用Vue.js 3构建一个完整的电商后台管理系统,包括架构设计、权限管理、数据可视化和部署监控等方面。在实际开发中,我们应该根据项目需求选择合适的技术栈和架构方案,并注重代码质量和性能优化。

« 上一篇 34-build-optimization 下一篇 » 36-real-time-chat-app