Node Version Manager的缩写,是Node的版本管理器。
cmd> nvm list available | CURRENT | LTS | OLD STABLE | OLD UNSTABLE | |--------------|--------------|--------------|--------------| | 25.0.0 | 22.21.0 | 0.12.18 | 0.11.16 | | 24.10.0 | 22.20.0 | 0.12.17 | 0.11.15 | | 24.9.0 | 22.19.0 | 0.12.16 | 0.11.14 | | 24.8.0 | 22.18.0 | 0.12.15 | 0.11.13 | | 24.7.0 | 22.17.1 | 0.12.14 | 0.11.12 | | 24.6.0 | 22.17.0 | 0.12.13 | 0.11.11 | | 24.5.0 | 22.16.0 | 0.12.12 | 0.11.10 | | 24.4.1 | 22.15.1 | 0.12.11 | 0.11.9 | | 24.4.0 | 22.15.0 | 0.12.10 | 0.11.8 | | 24.3.0 | 22.14.0 | 0.12.9 | 0.11.7 | | 24.2.0 | 22.13.1 | 0.12.8 | 0.11.6 | | 24.1.0 | 22.13.0 | 0.12.7 | 0.11.5 | | 24.0.2 | 22.12.0 | 0.12.6 | 0.11.4 | | 24.0.1 | 22.11.0 | 0.12.5 | 0.11.3 | | 24.0.0 | 20.19.5 | 0.12.4 | 0.11.2 | | 23.11.1 | 20.19.4 | 0.12.3 | 0.11.1 | | 23.11.0 | 20.19.3 | 0.12.2 | 0.11.0 | | 23.10.0 | 20.19.2 | 0.12.1 | 0.9.12 | | 23.9.0 | 20.19.1 | 0.12.0 | 0.9.11 | | 23.8.0 | 20.19.0 | 0.10.48 | 0.9.10 |
cmd> nvm install 20.16.0 > nvm ls * 20.16.0 (Currently using 64-bit executable) > nvm uninstall 20.16.0 Uninstalling node v20.16.0... done
cmd> nvm ls 22.21.0 > nvm use 22.21.0 Now using node v22.21.0 (64-bit) > nvm ls * 22.21.0 (Currently using 64-bit executable) > node -v v22.21.0
NPM是Node Package Manager的缩写是Node的包管理器,而PNPM则是NPM的性能版。NPM是Node自带的,PNPM可以通过NPM进行下载。
cmd> npm install -g pnpm
搭建项目的脚手架。
cmd> pnpm create vite | o Project name: | hsbcfront | o Select a framework: | Vue | o Select a variant: | Official Vue Starter ↗ | o Use rolldown-vite (Experimental)?: | No | o Install with pnpm and start now? | Yes T Vue.js - The Progressive JavaScript Framework | o 请选择要包含的功能: (↑/↓ 切换,空格选择,a 全选,回车确认) | none | o 选择要包含的试验特性: (↑/↓ 切换,空格选择,a 全选,回车确认) | none | o 跳过所有示例代码,创建一个空白的 Vue 项目? | Yes 正在初始化项目 C:\devops\hsbcfront... | — 项目初始化完成,可执行以下命令: cd hsbcfront pnpm install pnpm dev | 可选:使用以下命令在项目目录中初始化 Git: git init && git add -A && git commit -m "initial commit" > cd hsbcfront > pnpm install
cmd> pnpm run dev > hsbcfront@0.0.0 dev C:\devops\hsbcfront > vite VITE v7.1.12 ready in 1040 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window ➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools ➜ press h + enter to show help
将App.Vue只留下格式。
js<script setup>
</script>
<template>
</template>
<style scoped>
</style>
安装下面的组件
cmd> pnpm install less vue-router element-plus -S > pnpm install @element-plus/icons-vue -S
在package.json里可以看到下面的依赖了。
js "dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.11.5",
"less": "^4.4.2",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
}
在vite.config.js里已经有了@的别名。
jsexport default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
cmd. ├── App.vue ├── assets │ ├── images │ └── less │ ├── index.less │ └── reset.less ├── components ├── main.js └── router └── index.js
src/router/index.js内容
jsimport {createRouter, createWebHashHistory} from "vue-router";
// 制定路由规则
const routes = [
{
path: '/',
name: 'main',
component: () => import('@/views/Main.vue')
}
]
const router = createRouter({
// 设置路由模式
history: createWebHashHistory(),
routes
})
export default router;
根路径的Main.vue的只有这些。
js<script setup></script>
<template>
<div>Hello World</div>
</template>
<style scoped></style>
将Router注册到src/main.js.
jsimport router from "@/router";
const app = createApp(App);
app.use(router);
app.mount('#app');
App.Vue调用Router View。
js<script setup></script>
<template>
<router-view></router-view>
</template>
<style scoped></style>
全部导入:
js// main.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
按需导入:
自动导入需安装下面的插件,
pnpm install -D unplugin-vue-components unplugin-auto-import。
设置Vite的配置文件
js// vite.config.js
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: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
导入图标:
js// main.js
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
先搭一个界面的布局,包含Header, Sidebar和主页面的容器。

js// Main.vue
<script setup></script>
<template>
<div class="common-layout">
<el-container class="lay-container">
<common-aside></common-aside>
<el-container>
<el-header class="el-header">
<common-header>头部</common-header>
</el-header>
<el-main class="right-main"> 右侧主界面 </el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped lang="less">
.common-layout,
.lay-container {
height: 100%;
}
.el-header {
background-color: #333;
}
</style>
#title作为其named slot的显示。不然不显示东西。js<template>
<el-aside width="180px">
<el-menu
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<h3>通用后台管理系统</h3>
<el-menu-item
v-for="item in noChildren"
:index="item.path"
:key="item.path"
>
<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"
:index="subItem.path"
:key="subItem.path"
>
<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";
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: "页面一",
icon: "setting",
url: "Page1",
},
{
path: "/page2",
name: "page2",
label: "页面二",
icon: "setting",
url: "Page2",
},
],
},
]);
const noChildren = computed(() => list.value.filter((item) => !item.children));
const hasChildren = computed(() => list.value.filter((item) => item.children));
</script>
<style scoped lang="less">
.icons {
width: 18px;
height: 18px;
margin-right: 5px;
}
.el-menu {
border-right: none;
height: 100vh;
h3 {
line-height: 48px;
color: #fff;
text-align: center;
}
}
.el-aside {
height: 100vh;
overflow: hidden;
background-color: #545c64;
}
</style>
完成后的样式为:

js// CommonHeader.vue
<template>
<div class="header">
<div class="l-content">
<el-button size="small">
<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">
<img :src="getImageUrl('user')" class="user" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
const getImageUrl = (user) => {
return new URL(`../assets/images/${user}.png`, import.meta.url).href;
};
</script>
<style scoped lang="less">
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #333;
width: 100%;
height: 100%;
}
.icons {
width: 20px;
height: 20px;
}
.l-content {
display: flex;
align-items: center;
.el-button {
margin-right: 20px;
}
}
.r-content {
.user {
width: 50px;
height: 50px;
border-radius: 50%;
}
}
:deep(.bread span) {
color: #fff !important;
cursor: pointer !important;
}
</style>
设置完的主页如下:

通过pnpm install pinia -D安装Pinia。启用Pinia。
js// main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
定义Store。
js// src/stores/index.js
import { defineStore } from "pinia";
import { ref } from "vue";
function initialState() {}
export const useAllDataStore = defineStore("allData", () => {
const state = ref(initialState());
return { state };
});
在Pinia Store里设置侧边栏的初始状态。
js// src/stores/index.js
function initialState() {
return {
isCollapse: false,
};
}
在侧边栏里,给Menu加上折叠的属性。并在Script里引用Store。设置折叠和非折叠的宽度。再加上一些CSS。
js// CommonAside.vue
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
>
<h3 v-if="!isCollapse">通用后台管理系统</h3>
<h3 v-if="isCollapse" class="collapsed-only">后台</h3>
</el-menu>
<script setup>
import { useAllDataStore } from "@/stores/index.js";
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => (store.state.isCollapse ? "64px" : "180px"));
</script>
<style scoped lang="less">
.el-aside {
height: 100vh;
overflow: hidden;
background-color: #545c64;
transition: width 160ms ease-in-out;
}
.el-menu .el-menu-item span,
.el-menu .el-submenu__title span {
transition: none !important;
}
while width animates */
.is-collapsed .el-menu-item span,
.is-collapsed .el-submenu__title span,
.is-collapsed h3 {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
.is-collapsed .collapsed-only {
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
text-align: center;
color: #fff;
}
</style>
在Header里给按钮绑定点击事件handleCollapes。在Script里添加Store,添加按钮功能来反转折叠状态。
js// CommonHeader.vue
<el-button size="small" @click="handleCollapse">
<script setup>
import { useAllDataStore } from "@/stores";
const store = useAllDataStore();
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
</script>
通过Pinia Store就将折叠的状态从一个组件传到了另一个组件。

在路由设置张设置/home路径,并使根路径默认跳转到/home。
js// /src/router/index.js
const routes = [
{
path: "/",
name: "main",
component: () => import("@/views/Main.vue"),
redirect: "/home",
children: [
{
path: "/home",
name: "home",
component: () => import("@/views/Home.vue"),
},
],
},
];
路由调用了Home.vue。主界面是使用栅格布局。
js<script setup>
const getImageUrl = (user) => {
return new URL(`../assets/images/${user}.png`, import.meta.url).href;
};
</script>
<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>最后登录时间:2024-06-01 10:00:00</p>
<p>最后登录IP: 192.168.1.1</p>
</div>
</el-card>
</el-col>
</el-row>
</template>
<style scoped lang="less">
.home {
height: 100%;
overflow: hidden;
.user {
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
.user-info {
p {
list-style: 40px;
}
.user-info-p {
color: #999;
}
.user-info-admin {
font-size: 35px;
font-weight: bold;
}
}
img {
border-radius: 50%;
width: 150px;
height: 150px;
margin-right: 40px;
}
.login-info {
p {
line-height: 30px;
font-size: 14px;
color: #999;
span {
color: #666;
margin-left: 30px;
}
}
}
}
}
</style>
回到Main.vue,在页面布局的右主页面调用路由视图。
js// Main.vue
<el-main class="right-main">
<router-view></router-view>
</el-main>

添加示例数据,包括表头和内容数据。
js// /src/views/Home.vue
<script setup>
import { ref } from "vue";
const tableData = ref([
{
name: "Java",
todayBuy: 100,
monthBuy: 200,
totalBuy: 300,
},
{
name: "Python",
todayBuy: 120,
monthBuy: 250,
totalBuy: 350,
},
{
name: "JavaScript",
todayBuy: 150,
monthBuy: 300,
totalBuy: 450,
},
]);
const tableLabel = ref([
{ prop: "name", label: "课程" },
{ prop: "todayBuy", label: "今日购买" },
{ prop: "monthBuy", label: "本月购买" },
{ prop: "totalBuy", label: "总购买" },
]);
</script>
模板添加用户表格和其样式。
js// /src/views/Home.vue
<el-card shadow="hover" class="user-table">
<el-table :data="tableData">
<el-table-column
v-for="label in tableLabel"
:key="label.prop"
:prop="label.prop"
:label="label.label"
></el-table-column>
</el-table>
.user-table {
margin-top: 20px;
}

安装Axios pnpm install axios -D。
js// /src/views/Home.vue
import axios from "axios";
安装Mockjs pnpm install mockjs -D。
加载Mock.js并从其取数据。前面的写死的数据可以只宣告一个变量。
js// main.js
import "@/api/mock.js";
// /src/views/Home.vue
const tableData = ref();
axios.get("/api/home/getTableData").then((response) => {
if (response.data.code === 200) {
tableData.value = response.data.data.tableData;
}
});
文件结构:
plainapi/ ├── mock.js └── mockData └── home.js
编造假数据。具体数据放在具体的js下,mock.js设置请求拦截的规则和返回的数据。
js// home.js
export default {
getTableData: () => {
return {
code: 200,
data: {
tableData: [
{ name: "Ruby", todayBuy: 100, monthBuy: 200, totalBuy: 300 },
{ name: "Python", todayBuy: 120, monthBuy: 250, totalBuy: 350 },
{ name: "JavaScript", todayBuy: 150, monthBuy: 300, totalBuy: 450 },
],
},
};
},
};
// mock.js
import Mock from "mockjs";
import homeApi from "./mockData/home.js";
Mock.mock(/api\/home\/getTableData/, "get", homeApi.getTableData);
js// /src/api/request.js
import axios from "axios";
import { ElMessage } from "element-plus";
const service = axios.create();
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
service.interceptors.response.use((response) => {
console.log("Response received:", response);
const { code, data, message } = response.data;
if (code === 200) {
return data;
} else {
const NETWORK_ERROR = "网络请求异常,请稍后重试";
ElMessage.error(message || NETWORK_ERROR);
return Promise.reject(message || NETWORK_ERROR);
}
});
function request(options) {
options.method = options.method || "get";
return service.request(options);
}
export default request;
在api.js里进行整个项目的API管理。
js// /src/api/api.js
import request from "./request";
export default {
getTableData() {
return request({
url: "/api/home/getTableData",
method: "get",
});
},
};
在全局注册这个API服务。
js// main.js
import api from "@/api/api.js";
app.config.globalProperties.$api = api;
将Axios换成封装之后的函数。
js// /src/views/Home.vue
<script setup>
import { ref, getCurrentInstance, onMounted } from "vue";
// import axios from "axios";
const { proxy } = getCurrentInstance();
const getTableData = async () => {
const data = await proxy.$api.getTableData();
console.log("Data fetched via global API:", data);
tableData.value = data.tableData;
};
onMounted(() => {
getTableData();
});
// axios.get("/api/home/getTableData").then((response) => {
// console.log("Data fetched:", response.data.data.tableData);
// if (response.data.code === 200) {
// tableData.value = response.data.data.tableData;
// }
// });
添加Count数据的API。添加Mock的拦截。
js// /src/api/api.js
getCountData() {
return request({
url: "/api/home/getCountData",
method: "get",
});
},
// /src/api/mock.js
Mock.mock(/api\/home\/getCountData/, "get", homeApi.getCountData);
编辑假数据。
js// /src/api/mockData/home.js
getCountData: () => {
return {
code: 200,
data: [
{
name: "今日支付订单",
value: 1234,
icon: "SuccessFilled",
color: "#2ec7c9",
},
{
name: "今日收藏订单",
value: 210,
icon: "StarFilled",
color: "#ffb980",
},
{
name: "今日未支付订单",
value: 4567,
icon: "GoodsFilled",
color: "#5ab1ef",
},
{
name: "本月支付订单",
value: 1234,
icon: "SuccessFilled",
color: "#2ec7c9",
},
{
name: "本月收藏订单",
value: 210,
icon: "StarFilled",
color: "#ffb980",
},
{
name: "本月未支付订单",
value: 4567,
icon: "GoodsFilled",
color: "#5ab1ef",
},
],
};
},
在Home.vue里拿到countData的数据,并添加右侧的Count内容
js<script setup>
const countData = ref([]);
const getCountData = async () => {
const data = await proxy.$api.getCountData();
countData.value = data.data;
};
onMounted(() => {
getCountData();
});
</script>
<template>
<el-col :span="16" style="margin-top: 20px">
<div class="num">
<el-card
:body-style="{ display: 'flex', padding: '20px' }"
shadow="hover"
v-for="item in countData"
:key="item.name"
>
<component
class="icons"
:is="item.icon"
: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-col>
</template>
<style scoped lang="less">
.num {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.el-card {
width: 32%;
margin-bottom: 20px;
}
.icons {
width: 80px;
height: 80px;
color: #fff;
font-size: 30px;
text-align: center;
line-height: 80px;
}
.detail {
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.num {
font-size: 30px;
margin-bottom: 10px;
}
.txt {
font-size: 15px;
text-align: center;
color: #999;
}
}
}
</style>
页面如下:
本文作者:潘晓可
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!