)
Vue3Element Plus实战从零搭建微商城后台管理系统含完整代码在当今快速发展的电商领域一个高效、易用的后台管理系统对于微商城的运营至关重要。本文将带你从零开始使用Vue3、Element Plus和Pinia等现代前端技术栈构建一个功能完善的微商城后台管理系统。无论你是刚接触Vue3的开发者还是希望提升项目实战经验的前端工程师本教程都将为你提供清晰的实现路径和可直接复用的代码方案。1. 项目架构与初始化1.1 技术选型与项目结构现代前端开发中合理的技术选型是项目成功的基础。我们选择以下技术栈Vue3最新版本的Vue框架提供更好的性能和组合式APIElement Plus基于Vue3的UI组件库提供丰富的现成组件PiniaVue官方推荐的状态管理库比Vuex更简洁高效Vue Router实现单页应用的路由管理Axios处理HTTP请求ECharts用于数据可视化展示项目目录结构设计如下shop-system/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API接口封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── pages/ # 页面组件 │ │ ├── subpages/ # 子页面 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia状态管理 │ ├── utils/ # 工具函数 │ ├── App.vue # 根组件 │ └── main.js # 应用入口 ├── package.json └── vite.config.js # Vite配置1.2 环境准备与依赖安装首先确保你的开发环境已安装Node.js建议版本16和Yarn。然后初始化项目并安装所需依赖# 创建项目目录 mkdir shop-system cd shop-system # 初始化项目 yarn init -y # 安装核心依赖 yarn add vue3 element-plus2 axios1 pinia2 vue-router4 # 安装开发依赖 yarn add -D vite4 vitejs/plugin-vue创建vite.config.js配置文件import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, open: true } })1.3 基础配置与入口文件在src/main.js中配置应用入口import { createApp } from vue import App from ./App.vue import ElementPlus from element-plus import element-plus/dist/index.css import { createPinia } from pinia import router from ./router const app createApp(App) app.use(ElementPlus) app.use(createPinia()) app.use(router) app.mount(#app)配置Element Plus中文语言包import zhCn from element-plus/lib/locale/lang/zh-cn app.use(ElementPlus, { locale: zhCn })2. 核心功能模块实现2.1 路由与权限控制2.1.1 路由配置在src/router/index.js中定义应用路由import { createWebHistory, createRouter } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, redirect: /index, component: () import(/pages/Index.vue), meta: { title: 首页 }, children: [ { path: /index, name: index, component: () import(/pages/subpages/Index.vue) }, { path: /category, name: category, component: () import(/pages/subpages/Category.vue) }, { path: /goods, name: goods, component: () import(/pages/subpages/Goods.vue) }, { path: /setting, name: setting, component: () import(/pages/subpages/Setting.vue) } ] }, { path: /login, name: login, component: () import(/pages/Login.vue) } ] }) export default router2.1.2 权限控制中间件创建src/router/permission.js实现路由守卫import router from ./index import useToken from /stores/token import notification from /utils/notification const whiteList [/login] router.beforeEach((to, from, next) { document.title ${to.meta.title} - 后台管理系统 const { token } useToken() if (token) { next() } else { if (whiteList.includes(to.path)) { next() } else { notification({ message: 请先登录, type: error }) next(/login) } } })2.2 登录与用户认证2.2.1 登录页面实现创建src/pages/Login.vuetemplate el-card classlogin-container el-form refloginForm :modelform :rulesrules h3 classtitle微商城后台管理系统/h3 el-form-item propusername el-input v-modelform.username placeholder请输入用户名 / /el-form-item el-form-item proppassword el-input v-modelform.password typepassword show-password placeholder请输入密码 / /el-form-item el-form-item el-button typeprimary clicksubmitForm登录/el-button el-button clickresetForm重置/el-button /el-form-item /el-form /el-card /template script setup import { reactive, ref } from vue import { useRouter } from vue-router import { login } from /api import useToken from /stores/token import notification from /utils/notification const router useRouter() const loginForm ref() const { updateToken } useToken() const form reactive({ username: , password: }) const rules reactive({ username: [ { required: true, message: 请输入用户名, trigger: blur }, { min: 3, max: 12, message: 用户名长度为3-12个字符, trigger: blur } ], password: [ { required: true, message: 请输入密码, trigger: blur }, { min: 6, max: 24, message: 密码长度为6-24个字符, trigger: blur } ] }) const submitForm async () { try { await loginForm.value.validate() const data await login(form) if (data) { updateToken(data.token) router.push({ name: index }) } } catch (error) { notification({ message: 表单验证失败, type: error }) } } const resetForm () { loginForm.value.resetFields() } /script style scoped .login-container { width: 400px; margin: 100px auto; padding: 20px; .title { text-align: center; margin-bottom: 20px; } .el-button { width: 100%; } } /style2.2.2 Token管理创建src/stores/token.js管理用户tokenimport { defineStore } from pinia import { ref } from vue export const useToken defineStore(token, () { const token ref(localStorage.getItem(token) || null) const updateToken (newToken) { token.value newToken localStorage.setItem(token, newToken) } const removeToken () { token.value null localStorage.removeItem(token) } return { token, updateToken, removeToken } }, { persist: { enabled: true, strategies: [ { storage: localStorage, key: token } ] } })2.3 后台布局与导航2.3.1 主布局组件创建src/pages/Index.vue作为主布局template div classcommon-layout el-container el-headerHeader //el-header el-container el-aside width200pxAside //el-aside el-main el-card classmain-content router-view / /el-card /el-main /el-container /el-container /div /template script setup import Header from /components/Header.vue import Aside from /components/Aside.vue /script style scoped .common-layout { height: 100vh; .el-header { background: linear-gradient(90deg, #1493fa, #01c6fa); color: white; line-height: 60px; } .el-aside { background-color: #fff; border-right: 1px solid #e6e6e6; } .el-main { background-color: #f0f2f5; padding: 20px; .main-content { min-height: calc(100vh - 120px); } } } /style2.3.2 顶部导航组件创建src/components/Header.vuetemplate div classheader-container div classlogo微商城后台管理系统/div div classuser-info el-dropdown span classel-dropdown-link el-avatar :srcadmin.avatar / {{ admin.username }} /span template #dropdown el-dropdown-menu el-dropdown-item clickgoToSetting个人中心/el-dropdown-item el-dropdown-item clicklogout退出登录/el-dropdown-item /el-dropdown-menu /template /el-dropdown /div /div /template script setup import { useRouter } from vue-router import useAdmin from /stores/admin import useToken from /stores/token import notification from /utils/notification const router useRouter() const { admin } useAdmin() const { removeToken } useToken() const logout () { removeToken() router.push({ name: login }) notification({ message: 退出成功, type: success }) } const goToSetting () { router.push({ name: setting }) } /script style scoped .header-container { display: flex; justify-content: space-between; align-items: center; height: 100%; .logo { font-size: 20px; font-weight: bold; } .user-info { display: flex; align-items: center; .el-dropdown-link { display: flex; align-items: center; cursor: pointer; .el-avatar { margin-right: 10px; } } } } /style2.3.3 侧边栏组件创建src/components/Aside.vuetemplate el-menu :default-activeactiveMenu router background-color#fff text-color#333 active-text-color#1493fa el-menu-item index/index el-iconHomeFilled //el-icon span首页/span /el-menu-item el-menu-item index/category el-iconList //el-icon span分类管理/span /el-menu-item el-menu-item index/goods el-iconGoods //el-icon span商品管理/span /el-menu-item el-menu-item index/setting el-iconSetting //el-icon span个人中心/span /el-menu-item /el-menu /template script setup import { computed } from vue import { useRoute } from vue-router import { HomeFilled, List, Goods, Setting } from element-plus/icons-vue const route useRoute() const activeMenu computed(() route.path) /script style scoped .el-menu { border-right: none; .el-menu-item { :hover { background-color: #ecf5ff !important; } .is-active { background-color: #ecf5ff !important; } } } /style3. 业务模块开发3.1 首页仪表盘3.1.1 数据统计卡片创建src/pages/subpages/Index.vuetemplate div classdashboard-container el-row :gutter20 el-col :span6 v-foritem in stats :keyitem.title el-card shadowhover classstat-card div classcard-content div classicon :style{ backgroundColor: item.color } el-icon :size30component :isitem.icon //el-icon /div div classinfo div classvalue{{ item.value }}/div div classtitle{{ item.title }}/div /div /div /el-card /el-col /el-row el-row :gutter20 stylemargin-top: 20px; el-col :span12 el-card shadowhover div refsalesChart styleheight: 400px;/div /el-card /el-col el-col :span12 el-card shadowhover div reforderChart styleheight: 400px;/div /el-card /el-col /el-row /div /template script setup import { ref, onMounted } from vue import * as echarts from echarts import { Goods, List, User, ShoppingCart } from element-plus/icons-vue const stats ref([ { title: 商品总数, value: 356, icon: Goods, color: #EEAD0E }, { title: 分类数量, value: 24, icon: List, color: #AB82FF }, { title: 今日订单, value: 89, icon: ShoppingCart, color: #63B8FF }, { title: 会员数量, value: 1562, icon: User, color: #EE6363 } ]) const salesChart ref() const orderChart ref() onMounted(() { initCharts() }) const initCharts () { // 销售额图表 const salesChartInstance echarts.init(salesChart.value) salesChartInstance.setOption({ title: { text: 近12个月销售额 }, tooltip: { trigger: axis }, xAxis: { type: category, data: [1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] }, yAxis: { type: value }, series: [{ data: [120, 200, 150, 80, 70, 110, 130, 180, 210, 240, 200, 250], type: line, smooth: true, areaStyle: {} }] }) // 订单图表 const orderChartInstance echarts.init(orderChart.value) orderChartInstance.setOption({ title: { text: 近12个月订单量 }, tooltip: { trigger: axis }, xAxis: { type: category, data: [1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] }, yAxis: { type: value }, series: [{ data: [320, 302, 341, 374, 390, 450, 420, 380, 400, 420, 410, 450], type: bar, barWidth: 60% }] }) window.addEventListener(resize, () { salesChartInstance.resize() orderChartInstance.resize() }) } /script style scoped .dashboard-container { padding: 20px; .stat-card { .card-content { display: flex; align-items: center; .icon { width: 60px; height: 60px; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; margin-right: 15px; } .info { .value { font-size: 24px; font-weight: bold; margin-bottom: 5px; } .title { font-size: 14px; color: #999; } } } } } /style3.2 分类管理模块3.2.1 分类列表与树形展示创建src/pages/subpages/Category.vuetemplate div classcategory-container el-card shadowhover template #header div classcard-header span分类管理/span el-button typeprimary clickhandleAdd新增分类/el-button /div /template el-table :datacategoryList row-keyid border default-expand-all :tree-props{ children: children } el-table-column propname label分类名称 width200 / el-table-column label分类级别 template #default{ row } el-tag :typerow.pid 0 ? primary : success {{ row.pid 0 ? 一级分类 : 二级分类 }} /el-tag /template /el-table-column el-table-column propid label分类ID width100 / el-table-column label分类图片 width120 template #default{ row } el-image v-ifrow.picture :srcrow.picture fitcover stylewidth: 60px; height: 60px; :preview-src-list[row.picture] / /template /el-table-column el-table-column label操作 width180 fixedright template #default{ row } el-button typewarning sizesmall clickhandleEdit(row)编辑/el-button el-button typedanger sizesmall clickhandleDelete(row)删除/el-button /template /el-table-column /el-table /el-card !-- 新增/编辑分类弹窗 -- el-dialog v-modeldialogVisible :titlecurrentCategory.id ? 编辑分类 : 新增分类 width500px el-form refformRef :modelcurrentCategory :rulesrules label-width80px el-form-item label分类名称 propname el-input v-modelcurrentCategory.name placeholder请输入分类名称 / /el-form-item el-form-item label上级分类 proppid v-if!currentCategory.id el-select v-modelcurrentCategory.pid placeholder请选择上级分类 el-option label一级分类 :value0 / el-option v-foritem in topCategories :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item label分类图片 v-ifcurrentCategory.pid ! 0 el-upload classavatar-uploader :actionuploadUrl :show-file-listfalse :on-successhandleUploadSuccess :headersheaders el-image v-ifcurrentCategory.picture :srccurrentCategory.picture classavatar fitcover / el-icon v-else classavatar-uploader-iconPlus //el-icon /el-upload /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确定/el-button /template /el-dialog /div /template script setup import { ref, reactive, onMounted } from vue import { Plus } from element-plus/icons-vue import { getCategoryList, addCategory, updateCategory, deleteCategory } from /api import useToken from /stores/token import notification from /utils/notification const { token } useToken() const headers { Authorization: Bearer ${token.value} } const uploadUrl ${import.meta.env.VITE_API_BASE_URL}/upload const categoryList ref([]) const topCategories ref([]) const dialogVisible ref(false) const formRef ref() const currentCategory reactive({ id: null, name: , pid: 0, picture: }) const rules { name: [{ required: true, message: 请输入分类名称, trigger: blur }] } onMounted(() { fetchCategoryList() }) const fetchCategoryList async () { const data await getCategoryList() categoryList.value buildTree(data) topCategories.value data.filter(item item.pid 0) } const buildTree (items) { const map {} const tree [] items.forEach(item { map[item.id] { ...item, children: [] } }) items.forEach(item { if (item.pid 0) { tree.push(map[item.id]) } else { map[item.pid]?.children.push(map[item.id]) } }) return tree } const handleAdd () { Object.assign(currentCategory, { id: null, name: , pid: 0, picture: }) dialogVisible.value true } const handleEdit (row) { Object.assign(currentCategory, { id: row.id, name: row.name, pid: row.pid, picture: row.picture }) dialogVisible.value true } const handleDelete async (row) { try { await ElMessageBox.confirm(确定删除该分类吗?, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning }) await deleteCategory({ id: row.id }) notification({ message: 删除成功, type: success }) fetchCategoryList() } catch (error) { console.log(取消删除) } } const handleUploadSuccess (response) { if (response.code 0) { currentCategory.picture response.data.url notification({ message: 上传成功, type: success }) } else { notification({ message: response.message || 上传失败, type: error }) } } const submitForm async () { try { await formRef.value.validate() if (currentCategory.id) { await updateCategory(currentCategory) notification({ message: 更新成功, type: success }) } else { await addCategory(currentCategory) notification({ message: 新增成功, type: success }) } dialogVisible.value false fetchCategoryList() } catch (error) { console.error(表单验证失败, error) } } /script style scoped .category-container { padding: 20px; .card-header { display: flex; justify-content: space-between; align-items: center; } } .avatar-uploader { :deep(.el-upload) { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: var(--el-transition-duration-fast); :hover { border-color: var(--el-color-primary); } } .avatar { width: 120px; height: 120px; display: block; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 120px; height: 120px; text-align: center; line-height: 120px; } } /style3.2.2 分类API封装在src/api/index.js中添加分类相关接口import request from /utils/request export const getCategoryList () request.get(/admin/category/list) export const addCategory (data) request.post(/admin/category/add, data) export const updateCategory (data) request.post(/admin/category/update, data) export const deleteCategory (data) request.post(/admin/category/delete, data)3.3 商品管理模块3.3.1 商品列表与分页创建src/pages/subpages/Goods.vuetemplate div classgoods-container el-card shadowhover template #header div classcard-header span商品管理/span div el-input v-modelsearchParams.keyword placeholder请输入商品名称 stylewidth: 200px; margin-right: 10px; clearable clearhandleSearch keyup.enterhandleSearch / el-button typeprimary clickhandleAdd新增商品/el-button /div /div /template el-table :datagoodsList border stylewidth: 100% el-table-column propid labelID width80 / el-table-column propname label商品名称 width200 / el-table-column propcategory_name label分类 width120 / el-table-column propprice label价格 width120 template #default{ row } ¥{{ row.price.toFixed(2) }} /template /el-table-column el-table-column propstock label库存 width100 / el-table-column label主图 width120 template #default{ row } el-image v-ifrow.picture :srcrow.picture fitcover stylewidth: 60px; height: 60px; :preview-src-list[row.picture] preview-teleported / /template /el-table-column el-table-column propstatus label状态 width100 template #default{ row } el-tag :typerow.status ? success : danger {{ row.status ? 上架 : 下架 }} /el-tag /template /el-table-column el-table-column label操作 width180 fixedright template #default{ row } el-button typewarning sizesmall clickhandleEdit(row)编辑/el-button el-button typedanger sizesmall clickhandleDelete(row)删除/el-button /template /el-table-column /el-table div classpagination el-pagination v-model:current-pagesearchParams.page v-model:page-sizesearchParams.pageSize :totaltotal layouttotal, sizes, prev, pager, next, jumper :page-sizes[10, 20, 50, 100] size-changehandleSizeChange current-changehandleCurrentChange / /div /el-card !-- 商品编辑弹窗 -- el-dialog v-modeldialogVisible :titlecurrentGoods.id ? 编辑商品 : 新增商品 width800px el-form refformRef :modelcurrentGoods :rulesrules label-width100px el-row :gutter20 el-col :span12 el-form-item label商品名称 propname el-input v-modelcurrentGoods.name placeholder请输入商品名称 / /el-form-item /el-col el-col :span12 el-form-item label商品分类 propcategory_id el-select v-modelcurrentGoods.category_id placeholder请选择商品分类 stylewidth: 100% el-option v-foritem in categoryOptions :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item /el-col /el-row el-row :gutter20 el-col :span12 el-form-item label商品价格 propprice el-input-number v-modelcurrentGoods.price :min0 :precision2 :step0.1 controls-positionright stylewidth: 100% / /el-form-item /el-col el-col :span12 el-form-item label商品库存 propstock el-input-number v-modelcurrentGoods.stock :min0 controls-positionright stylewidth: 100% / /el-form-item /el-col /el-row el-form-item label商品主图 el-upload classavatar-uploader :actionuploadUrl :show-file-listfalse :on-successhandleMainImageSuccess :headersheaders el-image v-ifcurrentGoods.picture :srccurrentGoods.picture classavatar fitcover / el-icon v-else classavatar-uploader-iconPlus //el-icon /el-upload /el-form-item el-form-item label商品相册 el-upload v-model:file-listalbumFiles :actionuploadUrl list-typepicture-card :headersheaders :on-previewhandlePictureCardPreview :on-removehandleRemove :on-successhandleAlbumSuccess multiple el-iconPlus //el-icon /el-upload /el-form-item el-form-item label商品状态 propstatus el-radio-group v-modelcurrentGoods.status el-radio :label1上架/el-radio el-radio :label0下架/el-radio /el-radio-group /el-form-item el-form-item label商品描述 propdescription el-input v-modelcurrentGoods.description typetextarea :rows4 placeholder请输入商品描述 / /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确定/el-button /template /el-dialog !-- 图片预览 -- el-dialog v-modelpreviewVisible width60% el-image :srcpreviewImage fitcontain stylewidth: 100%; / /el-dialog /div /template script setup import { ref, reactive, onMounted } from vue import { Plus } from element-plus/icons-vue import { getGoodsList, addGoods, updateGoods, deleteGoods } from /api import { getCategoryList } from /api import useToken from /stores/token import notification from /utils/notification const { token } useToken() const headers { Authorization: Bearer ${token.value} } const uploadUrl ${import.meta.env.VITE_API_BASE_URL}/upload const goodsList ref([]) const total ref(0) const dialogVisible ref(false) const previewVisible ref(false) const previewImage ref() const formRef ref() const albumFiles ref([]) const searchParams reactive({ page: 1, pageSize: 10, keyword: }) const currentGoods reactive({ id: null, name: , category_id: null, price: 0, stock: 0, picture: , album: [], status: 1, description: }) const categoryOptions ref([]) const rules { name: [{ required: true, message: 请输入商品名称, trigger: blur }], category_id: [{ required: true, message: 请选择商品分类, trigger: change }], price: [{ required: true, message: 请输入商品价格, trigger: blur }], stock: [{ required: true, message: 请输入商品库存, trigger: blur }], status: [{ required: true, message: 请选择商品状态, trigger: change }] } onMounted(() { fetchGoodsList() fetchCategoryOptions() }) const fetchGoodsList async () { const params { page