编辑
2025-10-28
代码之道
00

目录

1. 环境设置
1.1 NVM
1.2 PNPM
1.3 创建项目
1.4 启动项目
1.5 安装所需的组件
1.6 SRC文件结构
2. 启用组件
2.1 路由
2.2 启用Element Plus
3.
3.1 布局
3.2 侧边栏组件
3.3 Header组件
4. Pinia
4.1 安装和设置Pinia
4.2 侧边栏折叠功能
4.3 Home视图和用户卡片
4.4 Home视图用户表格
5. Axios
5.1 安装Axios
5.1 安装Mock.js
5.3 Axios的二次封装
5.4 Count数据

1. 环境设置

1.1 NVM

Node Version Manager的缩写,是Node的版本管理器。

  1. 列出所有可用的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 |
  1. 安装和卸载Node
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
  1. 使用特定的Node版本
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

1.2 PNPM

NPM是Node Package Manager的缩写是Node的包管理器,而PNPM则是NPM的性能版。NPM是Node自带的,PNPM可以通过NPM进行下载。

cmd
> npm install -g pnpm

1.3 创建项目

搭建项目的脚手架。

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

1.4 启动项目

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>

1.5 安装所需的组件

安装下面的组件

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里已经有了@的别名。

js
export default defineConfig({ plugins: [ vue(), vueDevTools(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, })

1.6 SRC文件结构

cmd
. ├── App.vue ├── assets │   ├── images │   └── less │   ├── index.less │   └── reset.less ├── components ├── main.js └── router └── index.js

2. 启用组件

2.1 路由

src/router/index.js内容

js
import {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.

js
import 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>

2.2 启用Element Plus

全部导入:

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) }

3.

3.1 布局

先搭一个界面的布局,包含Header, Sidebar和主页面的容器。

vue101.jpeg

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>

3.2 侧边栏组件

  • el-aside是侧边栏,设置宽度180.
  • el-menu是菜单选项,直接在标签内设置了样式。
  • el-menu-item,循环了无子菜单的选项。
  • el-sub-menu,循环了有子菜单的选项。
    • el-sub-menu-group循环父菜单下面的子选项。
    • 父菜单栏使用#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>

完成后的样式为:

vue102.jpeg

3.3 Header组件

  • 一个header div包含左和右两边的内容。
  • 左侧内容为一个菜单的icon和面包屑导航,Item是首页。
  • 右侧内容为用户的照片带下拉菜单,包括个人中心和退出两个功能。
  • getImageUrl函数是拿到用户名.png的图片。
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>

设置完的主页如下:

vue103.jpeg

4. Pinia

4.1 安装和设置Pinia

通过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 }; });

4.2 侧边栏折叠功能

在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就将折叠的状态从一个组件传到了另一个组件。

vue104.jpeg

4.3 Home视图和用户卡片

在路由设置张设置/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。主界面是使用栅格布局。

  • 主界面分为20列,左侧占8列。
  • 左上部分包括一个卡片,包括用户头像和信息,下面是登录信息。
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>

vue105.jpeg

4.4 Home视图用户表格

添加示例数据,包括表头和内容数据。

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; }

vue106.jpeg

5. Axios

5.1 安装Axios

安装Axios pnpm install axios -D

js
// /src/views/Home.vue import axios from "axios";

5.1 安装Mock.js

安装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; } });

文件结构:

plain
api/ ├── 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);

5.3 Axios的二次封装

  • 使用axios的一个实例service。
  • 请求前的拦截的例子是官网的,没有包含逻辑。
  • 响应前的拦截,把响应里的状态码,数据和错误消息拿出来。如果成功则返回数据,否则进行错误处理。
  • 封装一个request的函数,将原来的请求的参数,通过这个拦截器过一遍。最后export这个函数。
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换成封装之后的函数。

  • 从vue里导入getCurrentInstance和onMounted。删除axios的导入。
  • 从getCurrentInstance取得proxy。
  • 异步函数从api处取得列表数据并赋值到tableData,覆盖了原来手工指定的值。
  • 在onMounted的时候,加载这个列表数据。
  • 删除原来通过axios通过api节点去的数据并进行判断的逻辑。
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; // } // });

5.4 Count数据

添加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 许可协议。转载请注明出处!