前言
项目安装与启动
使用vite作为项目脚手架
# pnpm
pnpm create vite my-vue-app --template vue
安装相应依赖
# sass
pnpm i sass
# vue-router
pnpm i vue-router
# element-plus
pnpm i element-plus
# element-plus/icon
pnpm i @element-plus/icons-vue
安装element-plus按需自动引入插件
pnpm install -D unplugin-vue-components unplugin-auto-import
并在vite.config.js中配置
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()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve:{alias: {'@': '/src'}}})
注册elementplus的icon库
import * as ElementPlusIconsVue from '@element-plus/icons-vue'const app = createApp(App)app.use(router).mount('#app')for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)}
删除helloworld组件和style.css 删除App.vue相应代码
在src目录下创建router文件夹在其中创建index.js 并配置基本内容
import {createRouter,createWebHashHistory} from 'vue-router'
const routes = [
{
path:'/',name:'main',
redirect:"/home",
component:()=>import("@/views/main.vue"),
children:[path:"/home",name:"Home",component:()=>import("@/views/Home.vue")
]}]const router = createRouter({history:createWebHashHistory() ,routes})export default router
并在main.js中注册和在App.vue中使用
//main.js
app.use(router).mount('#app')
//App.vue
<template><router-view></router-view></template>
创建views文件夹,创建Main.vue文件
使用element-plus组件库container布局容器
<template><div class="commom-layout"><el-container class="lay-container"><common-aside></common-aside><el-container><el-header class="el-header"><commonHeader></commonHeader></el-header><common-tab></common-tab><el-main class="right-main"><RouterView></RouterView></el-main></el-container></el-container></div></template><script setup>import commonAside from '../components/commonAside.vue';import commonHeader from '../components/commonHeader.vue';import commonTab from '../components/commonTab.vue';</script><style scoped lang="scss">.commom-layout, .lay-container{height:100%;}.el-header{background-color: #333;}</style>
布局已经搭好 让我们完成里面的组件
创建component文件夹创建common-aside.vue文件,也就是el-aside中套了一个el-menu,并使用vue-router中跳转路由,并用pinia
状态管理工具 来控制兄弟组件中的通信
所以需要先引入pinia,并创建store文件夹与index.js文件
pnpm i pinia
//在main.js
const pinia = createPinia()app.use(pinia)
// store/index.js
import { defineStore } from 'pinia'state:() => {return {isCollapse: true,
menuList:[],
}},
<template><el-aside :width="MenuWidth"><el-menubackground-color="#545c64"text-color="#fff":collapse="isCollapse":collapse-transition="false":default-active="activeMenu"><h3 v-show="!isCollapse">通用后台管理系统</h3><h3 v-show="isCollapse">后台</h3><el-menu-item v-for="item in noChildren" :index="item.path" :key="item.path"@click="handleMenu(item)"><component class="icons" :is="item.icon"></component><span>{{ item.label }}</span></el-menu-item><el-sub-menu v-for="item in hasChildren" :index="item.path" :key="item.path"><template #title><component class="icons" :is="item.icon"></component><span>{{ item.label }}</span></template><el-menu-item-group><el-menu-itemv-for="(subItem,subIndex) in item.children":key="subItem.path":subIndex="subItem.path"@click="handleMenu(subItem)"><component class="icons" :is="subItem.icon"></component><span>{{ subItem.label }}</span></el-menu-item></el-menu-item-group></el-sub-menu></el-menu></el-aside></template><script setup>import { ref, computed } from 'vue'import { useRouter ,useRoute} from 'vue-router';import { useAllDataStore } from '../stores/index.js'// const list = ref([// { path: '/home', name: 'home', label: '首页', icon: 'house', url: 'Home' },// { path: '/mall', name: 'mall', label: '商品管理', icon: 'video-play', url: 'Mall' },// { path: '/user', name: 'user', label: '用户管理', icon: 'user', url: 'User' },// {// path: 'other', label: '其他', icon: 'location', children: [{ path: '/page1', name: 'page1', label: '页面1', icon: 'setting', url: 'Page1' },// { path: '/page2', name: 'page2', label: '页面2', icon: 'setting', url: 'Page2' }]// }])const AllDataStore = useAllDataStore()const list = computed(()=>AllDataStore.$state.menuList)const noChildren = computed(() => list.value.filter(item => !item.children))const hasChildren = computed(() => list.value.filter(item => item.children))const clickMenu = (item) => { router.push(item.path) }const isCollapse = computed(() => AllDataStore.$state.isCollapse)const MenuWidth= computed(() => AllDataStore.$state.isCollapse ? '64px' : '180px')const router = useRouter()const route = useRoute()const activeMenu = computed(()=>route.path)const handleMenu= (item) => {router.push(item.path)AllDataStore.selectMenu(item)}</script>
<style lang="scss" scoped>
// var asideColor: #545c64;
.icons{
height:18px;
width:18px;
margin-right:5px;
}
.el-menu{
border-right:none;
h3{
line-height:48px;
color:#fff;
text-align:center;
}
}
.el-aside{
height:100%;
background-color: #545c64;
}
</style>
comonheader组件搭建 使用下拉框el-dropdown和面包屑el-breadcrumb
<template><div class="header"><div class="l-content"><el-button size="small" @click="handleCollapse"><component class="icons" is="menu"></component></el-button><el-breadcrumb separator="/" class="bread"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item></el-breadcrumb></div><div class="r-content"><el-dropdown><span class="el-dropdown-link"><span><img :src="getImageUrl('user')" class="user"></span></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item @click="handleLoginOut">退出</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></div></template><script setup>import {useAllDataStore}from '../stores/index.js'import {useRouter}from 'vue-router'import {computed} from 'vue'const AllData = useAllDataStore()const getImageUrl = (user) => {return new URL(`../assets/images/${user}.png`,import.meta.url).href}const handleCollapse = () => {AllData.$state.isCollapse = !AllData.$state.isCollapse}</script><style lang="scss" scoped>.header {display:flex;justify-content: space-between;align-items: center;height:100%;width:100%;background-color: #333;}.icons{height:20px;width:20px;}.l-content{display: flex;align-items: center;.el-button{margin-right:20px;}}.r-content{.user{width:40px;height:40px;border-radius: 50%;outline: none;}}//样式穿透:deep(.bread span){color:#fff !important;cursor:pointer !important;}</style>
接下来实现首页剩余内容
使用elementplus中的layout布局 通过基础的 24 分栏,迅速简便地创建布局,在使用el-card卡片来分隔不同内容
<template><el-row class="home" :gutter="20"><el-col :span="8" style="margin-top:20px"><el-card shadow="hover"><div class="user"><img :src="getImageUrl('user')" class="user"/><div class="user-info"><p class="user-info-admin">Admin</p><p class="user-info-p">超级管理员</p></div></div><div class="login_info"><p>上次登陆时间:<span>2024-07-24</span></p><p>上次登陆地点:<span>北京</span></p></div></el-card><el-card shadow="hover" class="user-table"><el-table :data="tableData"><el-table-columnv-for="(val,key) in tableLabel":key = "key":prop = "key":label = "val"></el-table-column></el-table></el-card></el-col><el-col :span="16" style="margin-top:20px"><div class="num"><el-card :body-style="{display:'flex', padding:0}"v-for="item in countData":key="item.name"><component :is="item.icon" class="icons" :style="{background:item.color}"></component><div class="detail"><p class="num">${{ item.value }}</p><p class="txt">${{ item.name }}</p></div></el-card></div><el-card class="top-echart"><div ref="echart" style="height:230px"></div></el-card><div class="graph"><el-card><div ref="userEchart" style="height:240px"></div></el-card><el-card><div ref="videoEchart" style="height:240px"></div></el-card></div></el-col></el-row></template><script setup>import {ref,getCurrentInstance,onMounted,reactive} from 'vue'import * as echarts from 'echarts'const {proxy} = getCurrentInstance()const getImageUrl = (user) => {return new URL(`../assets/images/${user}.png`,import.meta.url).href}const observer =ref(null)const tableData = ref([])const countData = ref([])const chartData = ref([])const tableLabel = ref({name: "课程",todayBuy: "今日购买",monthBuy: "本月购买",totalBuy: "总购买",})const getTableData = async () => {const data = await proxy.$api.getTableData()tableData.value = data.tableData}const getCountData = async () => {const data = await proxy.$api.getCountData()countData.value = data}const xOptions = reactive({// 图例文字颜色textStyle: {color: "#333",},legend: {},grid: {left: "20%",},// 提示框tooltip: {trigger: "axis",},xAxis: {type: "category", // 类目轴data: [],axisLine: {lineStyle: {color: "#17b3a3",},},axisLabel: {interval: 0,color: "#333",},},yAxis: [{type: "value",axisLine: {lineStyle: {color: "#17b3a3",},},},],color: ["#2ec7c9", "#b6a2de", "#5ab1ef", "#ffb980", "#d87a80", "#8d98b3"],series: [],})const pieOptions = reactive({tooltip: {trigger: "item",},legend: {},color: ["#0f78f4","#dd536b","#9462e5","#a6a6a6","#e1bb22","#39c362","#3ed1cf",],series: []})const getChartData = async () => {const {orderData,userData,videoData}= await proxy.$api.getChartData()xOptions.xAxis.data = orderData.date;xOptions.series = Object.keys(orderData.data[0]).map(val=>({name:val,data:orderData.data.map(item => item[val]),type:'line'}))const oneEchart = echarts.init(proxy.$refs['echart'])oneEchart.setOption(xOptions)xOptions.xAxis.data = userData.map(item => item.date)xOptions.series=[{name:'新增用户',data:userData.map(item=>item.new),type:'bar'},{name:'活跃用户',data:userData.map(item=>item.active),type:'bar'}]const twoEchart = echarts.init(proxy.$refs['userEchart'])twoEchart.setOption(xOptions)pieOptions.series = [{data:videoData,type:'pie',}]const threeEchart = echarts.init(proxy.$refs['videoEchart'])threeEchart.setOption(pieOptions)//监听页面的变化observer.value = new ResizeObserver(() => {oneEchart.resize()twoEchart.resize()threeEchart.resize()})if(proxy.$refs['echart']){observer.value.observe(proxy.$refs["echart"])}}onMounted(() => {getTableData()getCountData()getChartData()})</script><style lang="scss" scoped>.home{height:150vh;overflow:hidden;.user{display:flex;align-items:center;border-bottom:1px solid #ccc;margin-bottom:20px;img{width:150px;height:150px;border-radius: 50%;margin-right: 40px;}.user-info{p{line-height:40px;}.user-info-p{color:#999;}.user-info-admin{font-size:35px;}}}.login_info{p{line-height:30px;font-size:14px;color:#990;}span{color:#666;margin-left:60px;}}.user-table{margin-top:20px;}.num {display: flex;flex-wrap: wrap;justify-content: space-between;.el-card {width: 32%;margin-bottom: 20px;}.icons {width: 80px;height: 80px;font-size: 30px;text-align: center;line-height: 80px;color: #fff;}.detail {margin-left: 15px;display: flex;flex-direction: column;justify-content: center;.num {font-size: 30px;margin-bottom: 10px;}.txt {font-size: 14px;text-align: center;color: #999;}}}.graph {margin-top: 20px;display: flex;justify-content: space-between;.el-card {width: 48%;height: 260px;}}}</style>
没有后端 我们用mock来模拟网络请求 使用axios来处理网络请求
pnpm i axios
pnpm i mockjs
创建api文件夹 创建request.js 文件 二次封装下axios
import axios from "axios";import config from '../config/index'const service = axios.create({baseURL:config.baseApi,});const NETWORK_ERROR = '网络错误...'service.interceptors.request.use((config) => {return config},(error) => {return Promise.reject(error)})service.interceptors.response.use((res) => {const {code,data,msg} = res.dataif(code===200){return data}else{return Promise.reject(msg || NETWORK_ERROR)}})function request(options){// console.log(config.env)options.method = options.method || "get"if(options.method.toLowerCase() === "get"){options.params = options.data}//对mock的开关做一个处理let isMock = config.mockif(config.env !== "undefined"){isMock = config.env}//针对环境作处理if(config.env === "prod"){service.defaults.baseURL = config.baseAPi;}else{// console.log('isMock',isMock)service.defaults.baseURL = isMock ? config.mockApi : config.baseApi;}return service(options)}export default request
在api文件下创建api.js
import request from './request.js'export default {getTableData(){return request({url:"/home/getTable",method:'get',})},getCountData(){return request({url:"/home/getCountData",method:'get',})},getChartData(){return request({url:"/home/getChartData",method:'get',})},}
使用mock模拟请求 在api文件下创建mockData下创建home.js 并在api目录下创建mock.js作为出口
//mock.js
import Mock from "mockjs"
import homeApi from "./mockData/home.js"
Mock.mock(/api\/home\/getTableData/,"get",homeApi.getTableData)Mock.mock(/api\/home\/getCountData/,"get",homeApi.getCountData)Mock.mock(/api\/home\/getChartData/,"get",homeApi.getChartData)
// mockData/home.js
export default {getTableData: () => {return {code: 200,data: {tableData: [{name: "oppo",todayBuy: 500,monthBuy: 3500,totalBuy: 22000,},{name: "vivo",todayBuy: 300,monthBuy: 2200,totalBuy: 24000,},{name: "苹果",todayBuy: 800,monthBuy: 4500,totalBuy: 65000,},{name: "小米",todayBuy: 1200,monthBuy: 6500,totalBuy: 45000,},{name: "三星",todayBuy: 300,monthBuy: 2000,totalBuy: 34000,},{name: "魅族",todayBuy: 350,monthBuy: 3000,totalBuy: 22000,},],},}},getCountData: () => {return {code: 200,data: [{name: "今日支付订单",value: 1234,icon: "SuccessFilled",color: "#2ec7c9",},{name: "今日收藏订单",value: 210,icon: "StarFilled",color: "#ffb980",},{name: "今日未支付订单",value: 1234,icon: "GoodsFilled",color: "#5ab1ef",},{name: "本月支付订单",value: 1234,icon: "SuccessFilled",color: "#2ec7c9",},{name: "本月收藏订单",value: 210,icon: "StarFilled",color: "#ffb980",},{name: "本月未支付订单",value: 1234,icon: "GoodsFilled",color: "#5ab1ef",},],};},getChartData: () => {return {code: 200,data: {orderData: {date: ["2019-10-01","2019-10-02","2019-10-03","2019-10-04","2019-10-05","2019-10-06","2019-10-07",],data: [{苹果: 3839,小米: 1423,华为: 4965,oppo: 3334,vivo: 2820,一加: 4751,},{苹果: 3560,小米: 2099,华为: 3192,oppo: 4210,vivo: 1283,一加: 1613,},{苹果: 1864,小米: 4598,华为: 4202,oppo: 4377,vivo: 4123,一加: 4750,},{苹果: 2634,小米: 1458,华为: 4155,oppo: 2847,vivo: 2551,一加: 1733,},{苹果: 3622,小米: 3990,华为: 2860,oppo: 3870,vivo: 1852,一加: 1712,},{苹果: 2004,小米: 1864,华为: 1395,oppo: 1315,vivo: 4051,一加: 2293,},{苹果: 3797,小米: 3936,华为: 3642,oppo: 4408,vivo: 3374,一加: 3874,},],},videoData: [{ name: "小米", value: 2999 },{ name: "苹果", value: 5999 },{ name: "vivo", value: 1500 },{ name: "oppo", value: 1999 },{ name: "魅族", value: 2200 },{ name: "三星", value: 4500 },],userData: [{ date: "周一", new: 5, active: 200 },{ date: "周二", new: 10, active: 500 },{ date: "周三", new: 12, active: 550 },{ date: "周四", new: 60, active: 800 },{ date: "周五", new: 65, active: 550 },{ date: "周六", new: 53, active: 770 },{ date: "周日", new: 33, active: 170 },],},};}}
最终效果
如果对你有所帮助就点个关注吧
本文是这篇文章的笔记
https://www.bilibili.com/video/BV1LS421d7cY?p=5&vd_source=e73709c9a1618b4c6dfd58c6c40d8986