电商后台管理系统
电商后台管理系统是一个典型的企业级应用,它需要处理商品管理、订单管理、用户管理、数据分析等复杂功能。本章将介绍如何使用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 # 项目说明架构设计原则
模块化设计:
- 按功能模块划分代码
- 每个模块职责单一
- 模块间低耦合高内聚
分层架构:
- 表现层(Views/Components)
- 业务逻辑层(Composables/Stores)
- 数据访问层(Utils/API)
响应式设计:
- 适配不同屏幕尺寸
- 移动端友好
可扩展性:
- 易于添加新功能
- 支持插件机制
可维护性:
- 代码规范统一
- 完善的文档
- 单元测试
核心模块设计
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 router14.36.2 权限系统实现
权限设计
基于角色的访问控制(RBAC):
- 角色:管理员、编辑、访客
- 权限:页面访问权限、按钮操作权限
- 资源:菜单、按钮、API接口
权限存储:
- 前端:路由配置、按钮权限标识
- 后端:数据库存储角色-权限映射关系
权限实现
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 request14.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)
}项目实战总结
架构设计:
- 采用模块化、分层架构
- 清晰的目录结构
- 良好的代码组织
权限管理:
- 基于RBAC的权限模型
- 路由权限控制
- 按钮权限控制
- API权限控制
数据可视化:
- 使用ECharts实现各类图表
- 响应式设计
- 交互式图表
部署监控:
- 多种部署方案(Nginx、Docker)
- 前端错误监控
- 性能监控
- 日志管理
通过本项目,我们学习了如何使用Vue.js 3构建一个完整的电商后台管理系统,包括架构设计、权限管理、数据可视化和部署监控等方面。在实际开发中,我们应该根据项目需求选择合适的技术栈和架构方案,并注重代码质量和性能优化。