element-plus+vite+vue3+pinpa+mock+typeScript搭建经典的电商管理后台

一、项目介绍

源码下载https://download.csdn.net/download/lucky_fang/93523305

项目使用vue3+vite+element-plus+pinia+router+axios+echarts等搭建,每个页面已经完成了对后端api请求的编码,各种交互已经写好,同时通过pinia实现数据同步与通讯,数据采用mock方式模拟后端进行api请求动态查询数据,同时也支持通过真实后端api请求获取真实数据,在后面会详细介绍如何使用。

其中首页作为数据统计页面,其他页面包含登录页、用户管理、商品管理、订单管理、评论管理、轮播图管理和个人中心等页面。

项目同时采用组件来布局整个框架,顶部菜单栏+具体页面功能展示容器+底部,每个页面是独立,菜单布局作为共用的结构来设计。

element-plus官方文档:Overview 组件总览 | Element Plus

二、项目实现

1.项目截图

项目目录主要分为api、components、mock、router、stores、views,其中api存放是各个页面的请求文件;components是组件文件存放目录,比如页面共用底部文件;mock存放的是各个页面模拟后端api请求后查询获取的返回数据;router是项目各个路由配置,在这里可以控制指定哪些页面需要登录获取到token后才能访问,否则跳到登录页面;stores是对登录信息进行整理的文件,比如当前登录用户信息、token管理、退出登录等;views是各个功能具体页面。

vscode截图:

2.api+mock模拟实现数据

通过前缀/api来指定mock请求,统一转发请求到mock。

商品的api:

javascript 复制代码
// 获取商品列表
export const getProductList = (params: ProductQuery) => {
  return axios.get<ProductResponse>('/api/products', { params })
}

// 新增商品
export const addProduct = (data: Omit<Product, 'id' | 'createTime'>) => {
  return axios.post<ProductResponse>('/api/products', data)
}

// 更新商品
export const updateProduct = (data: Partial<Product>) => {
  return axios.put<ProductResponse>(`/api/products`, data)
}

// 删除商品
export const deleteProduct = (id: number) => {
  return axios.delete<ProductResponse>(`/api/products/${id}`)
}

商品的mock:

javascript 复制代码
export default [
  // 获取商品列表
  {
    url: '/api/products',
    method: 'get',
    response: ({ query }) => {
      const { page = 1, pageSize = 10, name, category, status } = query
      
      let filteredList = [...productList]
      
      // 筛选
      if (name) {
        filteredList = filteredList.filter(item => item.name.includes(name as string))
      }
      if (category) {
        filteredList = filteredList.filter(item => item.category === category)
      }
      if (status !== undefined && status !== '') {
        filteredList = filteredList.filter(item => item.status === Number(status))
      }
      
      // 排序(按ID倒序)
      filteredList.sort((a, b) => b.id - a.id)
      
      // 分页
      const total = filteredList.length
      const start = (Number(page) - 1) * Number(pageSize)
      const end = start + Number(pageSize)
      const list = filteredList.slice(start, end)
      
      return {
        code: 200,
        message: 'success',
        data: {
          list,
          total
        }
      }
    }
  },
  
  // 新增商品
  {
    url: '/api/products',
    method: 'post',
    response: ({ body }) => {
      const newProduct: Product = {
        id: productList.length > 0 ? Math.max(...productList.map(p => p.id)) + 1 : 1,
        name: body.name,
        category: body.category,
        price: body.price,
        stock: body.stock,
        status: body.status || 1,
        createTime: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
        description: body.description || ''
      }
      productList.unshift(newProduct)
      return {
        code: 200,
        message: '新增成功',
        data: null
      }
    }
  },
  
  // 更新商品
  {
    url: '/api/products',
    method: 'put',
    response: ({ body }) => {
      const id = Number(body.id)
      const index = productList.findIndex(p => p.id === id)
      if (index !== -1) {
        productList[index] = { ...productList[index], ...body }
        return {
          code: 200,
          message: '更新成功',
          data: null
        }
      }
      return {
        code: 404,
        message: '商品不存在',
        data: null
      }
    }
  },
  
  // 删除商品
  {
    url: '/api/products/(\\d+)',
    method: 'delete',
    response: (req) => {
      const id = Number(req.query[0])
      const index = productList.findIndex(p => p.id === id)
      if (index !== -1) {
        productList.splice(index, 1)
        return {
          code: 200,
          message: '删除成功',
          data: null
        }
      }
      return {
        code: 404,
        message: '商品不存在',
        data: null
      }
    }
  }
] as MockMethod[]

特别注意的是,为了避免mock与真实请求到后端的api冲突,最好用不同的前缀区分mock与真实api请求,比如我这里是/api代表mock的请求,/real-api代表测试真实后端api,如下图配置。

3.按需导入插件

项目使用按需加载依赖的方式,采用插件unplugin-auto-import,比如页面不用每次import,可以直接使用ElMessage、ElMessageBox等。

unplugin-auto-import 是一款强大的前端构建插件,旨在解决现代开发中频繁手动编写 import 语句的痛点。它能在编译时自动分析代码,按需插入导入语句,让你直接使用如 ref、computed、useState 等 API 而无需显式引入。

该插件基于 unplugin 架构,支持 ‌Vite ‌、‌Webpack ‌、‌Rollup ‌、‌esbuild ‌、‌Rspack ‌ 等多种构建工具,并完美兼容 ‌Vue ‌、‌React ‌、‌Svelte ‌ 等主流框架以及 ‌TypeScript‌。

以下是核心功能、配置方法及常见问题的完整指南:

1. 核心优势

  • ‌简化代码 ‌:移除文件顶部冗长的 import { ref, reactive, onMounted } from 'vue' 语句。
  • ‌按需导入‌:仅在代码中实际使用到某个 API 时才生成导入语句,保持 bundle 体积最小化(Tree-shakable)。
  • ‌类型安全 ‌:自动生成 TypeScript 类型声明文件(.d.ts),确保 IDE 智能提示和类型检查正常工作。
  • ‌多框架支持‌:内置 Vue、Vue Router、Pinia、React、React Router、VueUse 等常用库的预设配置。
javascript 复制代码
import { ElMessage, ElMessageBox} from 'element-plus'

4.项目依赖package.json

javascript 复制代码
{
  "name": "my-shop-vue",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.2",
    "element-plus": "^2.14.6",
    "vue": "^3.5.42",
    "axios": "^1.6.0",
    "pinia": "^2.1.0",
    "vue-router": "^4.3.0",
    "echarts": "^5.4.3"
  },
  "devDependencies": {
    "@types/node": "^24.13.3",
    "@vitejs/plugin-vue": "^6.0.8",
    "@vue/tsconfig": "^0.9.1",
    "typescript": "~6.0.2",
    "unplugin-auto-import": "^21.1.0",
    "unplugin-vue-components": "^32.1.0",
    "vite": "^8.3.0",
    "vue-tsc": "^3.3.11",
    "sass": "^1.70.0",
    "mockjs": "^1.1.0",
    "vite-plugin-mock": "^2.9.6",
    "@types/mockjs": "^1.0.10"
  }
}

三、项目概述

1、登录页

2.忘记密码页

3.首页

4.用户管理

5.商品管理

6.订单管理

7.评论管理

8.轮播图管理

9.个人中心

四、部分源码

源码较多,如需要可以前往下载

源码下载https://download.csdn.net/download/lucky_fang/93523305

1.登录页

html 复制代码
<template>
  <div class="login-container">
    <el-card class="login-card">
      <template #header>
        <div class="card-header">
          <h2>商品管理后台</h2>
          <p>欢迎回来,请登录您的账户</p>
        </div>
      </template>
      
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        :rules="rules"
        label-width="0"
        class="login-form"
      >
        <el-form-item prop="username">
          <el-input
            v-model="loginForm.username"
            placeholder="用户名"
            :prefix-icon="User"
            size="large"
          />
        </el-form-item>
        
        <el-form-item prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="密码"
            :prefix-icon="Lock"
            show-password
            size="large"
            @keyup.enter="handleLogin"
          />
        </el-form-item>
        
        <div class="form-options">
          <el-checkbox v-model="rememberMe">记住我</el-checkbox>
          <el-link type="primary" :underline="false" @click="$router.push('/forgot-password')">
            忘记密码?
          </el-link>
        </div>
        
        <el-form-item>
          <el-button
            type="primary"
            size="large"
            class="login-btn"
            :loading="loading"
            @click="handleLogin"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

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

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

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

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

const rules = reactive<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
  
  await loginFormRef.value.validate(async (valid) => {
    if (valid) {
      loading.value = true
      try {
        const success = await userStore.login(loginForm.username, loginForm.password)
        if (success) {
          // 登录成功后跳转到之前尝试访问的页面或首页
          const redirect = route.query.redirect as string
          router.push(redirect || '/')
        }
      } catch (error) {
        console.error(error)
      } finally {
        loading.value = false
      }
    }
  })
}
</script>

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

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

.card-header {
  text-align: center;
}

.card-header h2 {
  margin: 0 0 10px 0;
  color: #303133;
}

.card-header p {
  margin: 0;
  color: #909399;
  font-size: 14px;
}

.login-form {
  margin-top: 20px;
}

.form-options {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.login-btn {
  width: 100%;
}
</style>

五、总结

项目页面完整,后续可能将不断升级,完善其他页面。

关注作者,及时了解更多好项目!

更多优质项目请看作者主页!

获取源码或如需帮助,可通过博客后面名片+作者即可!

源码下载https://download.csdn.net/download/lucky_fang/93523305

相关推荐
jay神4 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat8 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能