前言
项目安装与启动
使用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中配置
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库
js
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 并配置基本内容
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中使用
js
//main.js
app.use(router).mount('#app')
//App.vue
<template>
<router-view>
</router-view>
</template>
创建views文件夹,创建Main.vue文件
使用element-plus组件库container布局容器
vue
<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
js
//在main.js
const pinia = createPinia()
app.use(pinia)
// store/index.js
import { defineStore } from 'pinia'
state:() => {
return {
isCollapse: true,
menuList:[],
}
},
vue
<template>
<el-aside :width="MenuWidth">
<el-menu
background-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-item
v-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
vue
<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卡片来分隔不同内容
vue
<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-column
v-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来处理网络请求
js
pnpm i axios
pnpm i mockjs
创建api文件夹 创建request.js 文件 二次封装下axios
js
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.data
if(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.mock
if(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
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作为出口
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