侧边栏壁纸
  • 累计撰写 8 篇文章
  • 累计创建 5 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

第一个vue3项目

池溟昌胤
2025-04-01 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

[!WARNING]

在此记录自己的第一个 vue3 项目。

因为是看的别人的教程,所以与 vue 官网快速开始项目的方法不同,不能代表实际开发也能这样来。

本文档有阅读门槛,至少需要能看得懂前端基础三剑客 html css javascript 以及对 vue3 有一些了解。如果对 vue3 不了解,那么你很有可能找不到我下面的文件到底在哪里放着,并且也不太好理解各个目录的作用,不过我会尽可能详细的在下面的文档中说明

其实这个项目主要就是教怎么使用 element-plus 组件中的内容其中只要是 el-* 的,全都是组件自带的,我们只需要去学习怎么调用即可,这也是 vue 的强大之处

创建项目

项目启动

初始化项目

#创建项目
npm create vite@latest my-vue-app -- --template vue
#进入项目文件夹
cd my-vue-app
#安装初始化相关依赖
npm install
#测试项目是否能启动
npm run dev

然后把一些不必要的代码和样式删掉

调整 main.js

删除对 style 的引用

import {createApp} from 'vue';
import App from './App.vue';

createApp(App).mount("#app");

调整 App.vue

删除多余的内容得到如下

<script setup>
</script>

<template>
	123
</template>

<style scoped>

</style>

然后再次输入以下命令测试是否正常

npm run dev

继续安装依赖

npm install less
npm install vue-router
npm install element-plus
npm install @element-plus/icons-vue
#下载完了可以去项目根目录下的 package.json 文件中去看是否成功安装

解决别名问题(方便开发引用资源文件)

vite.config.js

通过配置根目录下的 vite.config.js 来解决这个问题,下面是 vite.config.js 中的内容

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  //这个是添加的别名!
  resolve: {
    alias: [
      {
        find: "@",
        replacement: "/src",
      }
    ]
  }
})

配置资源文件

先将课程资料中的文件复制到 src/assets/ 目录下,文件结构如下

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         2025/3/29     15:29                images
#目录下放置了四个图片分别为 
#404.png 
#background.png 
#user.png 
#user-default.png
d-----         2025/3/29     15:29                less
#目录下两个文件分别是 
#index.js 
#reset.js
-a----         2025/3/29     15:21            496 vue.svg
#这个是构建项目的时候就有的

图片见名知意,然后 js 文件我把源码贴出来

index.less

@import './reset.less';

reset.less

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
  box-sizing: border-box;
}

/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: block;
}

body {
  line-height: 1;
}

ol,
ul {
  list-style: none;
}

blockquote,
q {
  quotes: none;
}

blockquote:before,
blockquote:after,
q:before,
q:after {
  content: '';
  content: none;
}

a,
a:hover {
  color: inherit;
  text-decoration: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  background-color: #f5f5f5;
  font-family: 'PingFangSC-Light', 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', 'Arial', 'sans-serif';
}

body {
  overflow: hidden;
}

// 公共样式
.fl {
  float: left;
}

.fr {
  float: right;

  .button-group-item {
    padding-left: 3px;
  }
}

//清除浮动
.clearfix {
  zoom: 1;

  &:after {
    display: block;
    clear: both;
    content: "";
    visibility: hidden;
    height: 0;
  }
}

main.js

然后修改 main.js

// 从 Vue 框架中导入 createApp 函数
// createApp 函数用于创建一个新的 Vue 应用实例
import { createApp } from 'vue';
 
// 导入根组件 App.vue
// App.vue 是 Vue 应用的入口组件,通常包含应用的主要结构和布局
import App from './App.vue';
 
// 导入全局的 Less 样式文件
// 这个文件通常包含应用的全局样式定义
// 使用 @ 符号作为别名,指向 src 目录(需要在 vite.config.js 或 tsconfig.json 中配置)
import "@/assets/less/index.less";
 
// 使用 createApp 函数创建一个新的 Vue 应用实例,并将 App 组件作为根组件传递给它
// 然后将应用实例挂载到 HTML 中 id 为 "app" 的 DOM 元素上
createApp(App).mount("#app");

然后再次输入测试命令

npm run dev

打开浏览器按住F12进入元素查看是否引用成功

引用成功后开始路由的创建

路由创建

在前面已经把路由所需要的依赖下载完毕了,所以直接开始操作

index.js

在 src 文件夹下新建 router 文件夹,在 router 文件下新建 index.js 文件,index.js 文件如下

// 导入Vue Router模块中的createRouter和createWebHashHistory函数
import {createRouter, createWebHashHistory} from "vue-router";

// 定义路由配置数组
const routes= [
    {
        // 根路径
        path: '/',
        // 路由名称
        name: 'main',
        // 异步加载主组件Main.vue
        component:()=>import('@/views/Main.vue')
    }
]

// 创建路由器实例
// 使用哈希历史模式管理会话历史堆栈
// 传入路由配置数组
const router = createRouter({
    history: createWebHashHistory(),
    routes,
})

// 导出路由器实例
export default router;

此时你可能会疑惑@/views/Main.vue是从哪里来的?并且注意这里的写法是 lambda 表达式

所以说接下来在 src 目录下创建,Main.vue 初始化如下

Main.vue

<script setup>
</script>

<template>
  <div>
    我是main组件
  </div>
</template>

<style scoped>

</style>

最终配置 main.ts 文件如下

main.js

// 导入Vue的核心功能模块
import {createApp} from 'vue'
// 导入应用程序的主组件
import App from './App.vue'
// 导入全局样式文件
import "@/assets/less/index.less";
// 导入路由配置模块
import router from "./router/index.js";

// 创建Vue应用实例
const app = createApp(App);
// 使用路由功能并挂载到指定的DOM元素上
app.use(router).mount('#app')

变了两个内容:

  1. 导入路由配置模块
  2. 创建Vue应用实例,使用路由功能并挂载到指定的DOM元素上

[!TIP]

具体可以跟本节上面两个 main.js 文件做对比看看是怎么一步步变成现在的

路径有了之后组件也需要有地方去显示接下来修改 App.vue

App.vue

<script setup>
</script>

<template>
  <router-view></router-view><!-- 路由出口,显示匹配的组件 -->
</template>

<style scoped>

</style>

再运行

npm run dev

看到我是main组件即正常

element-plus

[!TIP]

Element-Plus 是一个基于 Vue 3 的桌面端组件库,它是 Element UI 的升级版本,提供了丰富的组件和强大的功能。

详情请参考官网: 一个 Vue 3 UI 框架 | Element Plus

要使用首先就是要引入

main.js

// 导入Vue的核心功能模块
import {createApp} from 'vue'
// 导入应用程序的主组件
import App from './App.vue'
// 导入全局样式文件
import "@/assets/less/index.less";
// 导入路由配置模块
import router from "./router/index.js";
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

// 创建Vue应用实例
const app = createApp(App);
// 使用ElementPlus组件库
app.use(ElementPlus);
// 使用路由功能并挂载到指定的DOM元素上
app.use(router).mount('#app');

修改 App.vue

App.vue

<script setup>
</script>

<template>
  <router-view></router-view>
  <!-- 使用 el-button 组件显示一个默认样式的按钮 -->
  <el-button>默认</el-button>
  <el-button type="success">默认</el-button>
</template>

<style>
/*
  组件样式定义
  选择器#app匹配具有ID为'app'的元素,此处设置了元素的宽度和高度均为100%,
  并隐藏任何溢出的内容。这是为了确保组件在其父容器内正确显示,且不会出现滚动条。
*/
#app {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

可以看到我们删除了 scoped,解释如下

[!TIP]

在 Vue 单文件组件(SFC)中,<style scoped>scoped 属性用于限制样式的作用范围,使其仅作用于当前组件,而不会影响其他组件

然后就是引入了 UI,具体看上面给的官网链接以后不做赘述。此时运行服务:

npm run dev

看到按钮就一切正常

[!IMPORTANT]

element-plus 有三种引入方式,本项目采取的是全局引入,省事并且对性能影响不大

@element-plus/icons-vue

以下操作看不懂就去看官网:一个 Vue 3 UI 框架 | Element Plus

我相信有的人会有疑问:在上面的继续安装依赖的地方为什么要有两个 element-plus 相关的,难道 icons 不是包含在前面的组件中吗?

[!TIP]

  • element-plus@element-plus/icons-vue 是两个独立的库,分别提供 UI 组件和图标组件。
  • 开发者需要根据项目需求分别安装和使用这两个库。
  • 这种解耦设计有助于保持组件库的灵活性和可维护性,同时提高项目的加载性能。

在 main.js 中加入以下内容

main.js

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

Main.vue 组件的实现

<script setup>
import CommonAside from "@/components/CommonAside.vue";
</script>

<template>
  <div class="common-layout">
    <el-container class="lay-container">
      <common-aside />
      <el-container>
        <el-header class="el-header">
          <common-header />
        </el-header>
        <el-main class="right-main">
          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>

[!NOTE]

在 Vue.js 中,模板中使用的标签名称(即自定义组件的标签)不需要与组件的名字完全相同,但需要遵循一定的规则,以确保组件能够正确匹配和渲染。

便是如此

运行

npm run dev

不懂上面什么意思就去浏览器 F12 看元素

菜单组件的实现

参考之前的官网:一个 Vue 3 UI 框架 | Element Plus

编写 CommonAside.vue

在 components 文件夹中创建一个如上的文件然后调整内容如下

<template>
  <el-aside width="180px">
    <el-menu
     background-color="#545c64"
     text-color="#fff"
    >
      <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: '页面1',
        icon: 'setting',
        url: 'Page1'
      },
      {
        path: '/page2',
        name: 'page2',
        label: '页面2',
        icon: 'setting',
        url: 'Page2'
      }
    ]
  }
])

const noChildren = computed(() => list.value.filter(item => !item.children))
const hasChildren = computed(() => list.value.filter(item => item.children))
</script>
<style lang="less" scoped>
.icons {
  width: 18px;
  height: 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>

编写思路

这里就借用 AI 的分析

1. 需求分析

  • 侧边栏菜单:需要一个侧边栏菜单,用于导航不同的页面或功能模块。
  • 层级结构:菜单项可能包含子菜单,形成层级结构。
  • 图标支持:每个菜单项前需要显示一个图标。
  • 动态数据:菜单项数据应从外部获取或定义,以便于维护和扩展。

2. 技术选型

  • Vue 3:使用 Vue 3 的组合式 API(<script setup>)来编写组件逻辑。
  • Element Plus:使用 Element Plus 的 el-menuel-menu-itemel-sub-menu 等组件来构建菜单。
  • LESS:使用 LESS 预处理器来编写样式,便于管理样式变量和嵌套规则。

3. 组件结构设计

3.1 模板部分 (<template>)

  • el-aside:使用 el-aside 组件作为侧边栏容器,设置宽度为 180px
  • el-menu:使用 el-menu 组件作为菜单容器,设置背景色和文字颜色。
  • 标题:在菜单顶部添加一个标题 "通用后台管理系统"。
  • 无子菜单项:使用 v-for 指令遍历 noChildren 数组,动态生成无子菜单的 el-menu-item
  • 有子菜单项:使用 v-for 指令遍历 hasChildren 数组,动态生成有子菜单的 el-sub-menu。每个子菜单包含一个标题和一组子菜单项。
  • 动态组件:使用 <component :is="..."> 动态渲染图标组件,根据菜单项的数据动态设置图标。

3.2 脚本部分 (<script setup>)

  • refcomputed:使用 Vue 的 refcomputed API 来管理菜单数据。

  • 菜单数据:定义一个 list 数组,包含所有菜单项的数据。每个菜单项包含路径、名称、标签、图标和子菜单(如果有)等信息。

  • 计算属性

    • noChildren:过滤出没有子菜单的菜单项。
  • hasChildren:过滤出有子菜单的菜单项。

3.3 样式部分 (<style lang="less" scoped>)

  • 图标样式:设置图标的大小和右边距,使其与文本对齐。
  • 菜单样式:移除 el-menu 的右边框,设置标题的样式(居中、白色文字)。
  • 侧边栏样式:设置侧边栏的高度和背景色,使其与整体风格一致。

4. 实现细节

4.1 动态菜单生成

  • v-for 指令:使用 v-for 指令遍历菜单数据数组,动态生成菜单项和子菜单项。
  • 动态组件:使用 <component :is="..."> 动态渲染图标组件,根据菜单项的数据动态设置图标名称。

4.2 样式定制

  • 自定义样式:通过覆盖 Element Plus 的默认样式,定制菜单的外观,如背景色、文字颜色、图标大小等。
  • 作用域限制:使用 scoped 属性确保样式只作用于当前组件,避免污染全局样式。

4.3 数据驱动

  • 菜单数据:将菜单数据定义为响应式对象(ref),便于在组件内部或外部进行修改。
  • 计算属性:使用计算属性将菜单数据分为有子菜单和无子菜单两部分,简化模板中的逻辑。

5. 代码优化与扩展

5.1 代码优化

  • 提取公共逻辑:如果多个组件需要类似的菜单逻辑,可以考虑将菜单生成逻辑提取到自定义组件或组合函数中。
  • 样式变量:使用 LESS 变量来定义颜色、尺寸等样式属性,便于统一管理和修改。

5.2 功能扩展

  • 路由集成:可以将菜单项与 Vue Router 集成,实现点击菜单项时自动导航到相应页面。
  • 权限控制:根据用户权限动态显示或隐藏某些菜单项。
  • 多语言支持:使用国际化(i18n)库,根据用户语言动态显示菜单项的标签。

6. 总结

  • 功能实现:这段代码实现了一个动态生成的侧边栏菜单,支持层级结构和图标显示。
  • 技术亮点:使用 Vue 3 的组合式 API、Element Plus 组件库和 LESS 预处理器,使代码简洁、易维护。
  • 扩展性:通过数据驱动和计算属性,代码具有良好的扩展性和可维护性。

需要注意的点

在这里分析下这个组件,首先是重要的 script:

const noChildren = computed(() => list.value.filter(item => !item.children))
const hasChildren = computed(() => list.value.filter(item => item.children))
//filter 方法:
//遍历 list.value 数组中的每个元素。
//对于每个元素,调用回调函数 (item => !item.children) 或 (item => item.children)。
//item 是回调函数的参数,代表当前正在处理的数组元素。

遍历菜单详解:

<el-menu-item
  v-for="item in noChildren"
  :index="item.path"
  :key="item.path"
>
  <!-- 菜单项内容 -->
</el-menu-item>
<!-- 主要就是要懂index和key是用来绑定path属性不用想太多 -->

[!NOTE]

我在这里大大失误了,el-aside 的样式一直不对最后查找问题是父类的影响,最后溯源找到 Main.vue 中的 common layout 出了问题,我把中间的-遗漏了正确的应该是 common-layout 我就说嘛怎么跟见了鬼一样,其实究其根本还是基础不够扎实

header 组件静态搭建

编写 CommonHeader.vue

在 components 文件夹中创建新文件 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>
import {ref, computed} from 'vue'

/**
 * 根据用户信息获取图片URL
 * @param {string} user - 用户标识
 * @returns {string} 图片的URL
 */
const getImageUrl = (user) => {
  return new URL(`../assets/images/${user}.png`, import.meta.url).href
}
</script>

<style lang="less" scoped>
/* 头部样式 */
.header {
  display: flex;//使用 Flexbox 布局,使子元素在主轴上排列。
  justify-content: space-between;//子元素在主轴上两端对齐,中间留空。
  align-items: center;//子元素在交叉轴上居中对齐。
  width: 100%;
  height: 100%;//设置头部宽度和高度为 100%,通常用于占满父容器的空间。
  background-color: #333;
}

/* 图标样式 */
.icons {
  width: 20px;
  height: 20px;
}

/* 左侧内容样式 */
.l-content{
  display: flex;//使用 Flexbox 布局,使子元素在主轴上排列。
  align-items: center;//子元素在交叉轴上居中对齐。
  .el-button{
    margin-right: 20px;
  }//为按钮添加右侧外边距,增加按钮与其他元素之间的间距。
}

/* 右侧内容样式 */
.r-content{
  .user{
    width: 40px;
    height: 40px;
    border-radius: 50%;//将头像的边框半径设置为 50%,使其呈现圆形
  }
}

/* 面包屑导航样式 */
:deep(.bread span){//Vue 提供的深度选择器,用于穿透到子组件的深层 DOM 结构,确保样式能够应用到子组件内部的元素。
  color: #fff !important;
  cursor: pointer !important;
}
</style>

编写思路

首先就是header要分成两个区域左边跟右边,左侧包含一个按钮和面包屑导航,右侧包含一个下拉菜单,用于用户操作。

左侧

[!NOTE]

是 Vue 提供的一个内置组件,用于动态渲染其他组件。

左侧需要一个按钮以及面包屑导航,都是直接调用UI库里的所以直接写就行了

      <el-button size="small">
        <component class="icons" is="menu"></component>
      </el-button>
//经过我的验证这个menu属性似乎是UI库里面自带的
      <el-breadcrumb separator="/" class="bread">
        <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      </el-breadcrumb>
//这个就是直接在官网复制粘贴来删了点东西,然后给了个类名,以便调整样式

右侧

右侧需要做的是用户的头像然后鼠标放到头像上有下拉菜单的效果,这里也要用到官网的下拉菜单

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

span 里面是放置下拉菜单图标的所以这里用 img 标签把头像放上去,然后这里的 getImageUrl 方法要在 script 中去写,则如下

<script setup>
const getImageUrl = (user) => {
  return new URL(`../assets/images/${user}.png`, import.meta.url).href
}
</script>

[!TIP]

  1. import.meta.url 作为基准 URL,使得代码更加模块化,便于在不同环境中部署和使用。
  2. href 是 URL 对象的一个属性,返回 URL 的字符串表示形式。
  3. 不用理解的太深记住有这么回事就行

使用 pinia

引入

菜单组件先引入 Collapse 折叠面板

然后 CommonAside.vue 是需要跟 CommonHeader.vue 发生关联的,我们要实现组件之间数据的共享就需要引入 pinia 来保存数据

安装 pinia

npm install pinia

安装完成后查看 package.json,检查 pinia 是否安装成功

引入 pinia

[!TIP]

因为这个是基于 vue3 的教程所以 pinia 用的也是 Composition API 的写法

main.js

添加三句话就行

import {createPinia} from 'pinia'
const pinia = createPinia()
app.use(pinia);
//标准引入三件套了属于是

然后在 src 目录下创建新的文件夹 stores,之后再在创建的文件夹下创建文件 index.js

index.js

编写以下内容,可以照抄官网的配置,但我这个是博主说的小技巧

import {defineStore} from 'pinia';
import {ref} from "vue";
function initState(){
    return {
        isCollapse: false
    }
}
export const useAllDataStore = defineStore('allData', () => {
    const state = ref(initState());
    return {
        state,
    };
});

修改 CommonAside.vue

就只需要添加一句话

<el-menu
     background-color="#545c64"
     text-color="#fff"
     :collapse="isCollapse"//这一句能够使菜单伸缩变为动态
    >

菜单组件和头部组件联动

编写思路

要想达到折叠面板的效果那么就得让之前的 button 增加一个 @click 事件,让它能够通过点击就能折叠或不折叠。然后就要思考折叠面板后有哪些变化,侧边栏会从宽变窄,字要消失或变短,仅仅展示图标。上面的“通用后台管理”就需要添加判断事件,然后宽度也要变成动态的,添加判断事件,如此一来就能够实现功能。

改写文件

CommonAside.vue

<el-aside :width="width">//改为动态
<h3 v-show="!isCollapse">通用后台管理系统</h3>
<h3 v-show="isCollapse">后台</h3>

import {useAllDataStore} from '@/stores'
const store = useAllDataStore()//引入实例接受函数的值
const isCollapse = computed(() => store.state.isCollapsed)

写完之后可以改变 stores 文件夹下的 index.js 中的

return {isCollapse: false}

来达到测试的效果

CommonHeader.vue

接下来需要对此文件进行编写,使两者联系起来

<el-button size="small" @click="handleCollapse">//添加@click事件来判断
    
const stores = useAllDataStore()//调用方法得值给实例
const handleCollapse = (()=>{
  stores.state.isCollapse = !stores.state.isCollapse
})//简单小逻辑,点击就调用取反以达到更换的效果

首页

编写思路

首先左侧菜单栏点击的时候要跳转,所以就要配好正确的路由,而我们现在的任务是写左侧菜单栏最上面的首页,于是就需要把 router 目录下的 index.js 添加首页路由,并且要实现进项目的时候就是首页所以还需要重定向到此页面,而之前我们并没有写 home 文件所以就需要在 view 文件夹中创建一个 Home.vue,然后之后在 Home.vue 上完成需求即可

配置路由 index.js

import {createRouter, createWebHashHistory} from "vue-router";

const routes= [
    {
        path: '/',
        name: 'main',
        component:()=>import('@/views/Main.vue'),
        redirect: '/home',
        children:[
            {
                path: 'home',
                name: 'home',
                component:()=>import('@/views/Home.vue'),
            },
        ],
    }
]
const router = createRouter({
    history: createWebHashHistory(),
    routes,
})
export default router;

[!NOTE]

注意 redirect 的配置!

并且要清楚 Main.vue 以及 Home.vue 还有 index.js 这三者是如何联系起来的,首先早在上面的 Main.vue 文件的 html 中就已经说明了:

<template>
  <div class="common-layout">
    <el-container class="lay-container">
      <common-aside />
      <el-container>
        <el-header class="el-header">
          <common-header />
        </el-header>
        <el-main class="right-main">
          <router-view />//主要是这句话
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

这个标签的作用就是显示 Main.vue 的子路由⚠️

首页左上方的卡片实现

<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>
        <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>2025年3月30日10:56:03</span></p>
          <p>上次登录地点:<span>🐢⚡️</span></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;

    img {
      width: 150px;
      height: 150px;
      border-radius: 50%;
      margin-right: 40px;
    }
  }

  .login-info {
    p{
      line-height: 30px;
      font-size: 14px;
      color: #999;
      span{
        color: #666;
        margin-left: 60px;
      }
    }
  }
  .user-info{
    p{
      line-height: 40px;
    }
    .user-info-p{
      color: #999;
    }
    .user-info-admin{
      font-size: 30px;
    }
  }
}
</style>

[!TIP]

还是老样子就是注意一下 img 标签后面 src 需要 : 不然就会报错不用 : 就会被当成字符串处理而不是表达式

首页左下方卡片实现

还是上面那个文件,以下代码是增加的部分

<script setup>
import {ref} from "vue"
const tableData = ref([
  {
    name: "Java",
    todayBuy: 100,
    monthBuy: 200,
    totalBuy: 300,
  },
  {
    name: "Python",
    todayBuy: 100,
    monthBuy: 200,
    totalBuy: 300,
  }
])

const tableLabel = ref({
  name: "课程",
  todayBuy: "今日购买",
  monthBuy: "本月购买",
  totalBuy: "总购买",
})
</script>

<template>
  <el-row class="home" :gutter="20">
    <el-col :span="8" style="margin-top: 20px">
      <el-card shadow="hover" class="user-table">
        <el-table :data="tableData">
          <el-table-column
              v-for="(value, key) in tableLabel"
              :key="key"
              :prop="key"
              :label="value"
          >
          </el-table-column>
        </el-table>
      </el-card>
    </el-col>
  </el-row>
</template>

<style scoped lang="less">
  .user-table {
    margin-top: 20px;
  }
}
</style>

[!NOTE]

在这里直接引用ai的来解答表格 v-for,并且一定要结合组件官网的 table 来看

  • tableLabel

    :这是一个对象,定义了表格列的标签和对应的属性名。

    const tableLabel = ref({
      name: "课程",
      todayBuy: "今日购买",
      monthBuy: "本月购买",
      totalBuy: "总购买",
    });
    
  • v-for="(value, key) in tableLabel"

    • value 是当前遍历到的属性值(例如 "课程""今日购买" 等)。
    • key 是当前遍历到的属性键(例如 "name""todayBuy" 等)。
  • 对于 tableLabel 对象中的每个键值对,都会生成一个 <el-table-column> 元素。

  • :prop="key":将 key 绑定到 el-table-columnprop 属性,告诉表格这一列对应的数据字段。

  • :label="value":将 value 绑定到 el-table-columnlabel 属性,设置这一列的标题。

封装 axios

安装 axios

先进入到项目的根目录,然后输入以下命令

npm install axios

待安装完毕了可以到 package.json 文件中看到加入了 axios 的 devDependencies

引入 axios

[!TIP]

看不懂方法的参数就去问 ai,就算不问仔细看我下面的解释还有各个文件新增的内容也能理解

在这里说明 axios 跟 mock 是如何交互的,axios 向发送请求,mock 拦截请求,并且返回模拟的数据,然后 axios 再处理返回数据

这个项目中的 axios 返回的数据就被填充给了 table,table 之前的数据就会被覆盖,呈现 mock 定义的模拟数据

Home.vue

加入以下内容

<script setup>
import axios from 'axios'
axios({
  url: '/api/home/getTableData',
  method: 'get',
}).then(res => {
  console.log(res.data)
  if(res.data.code === 200){
    console.log(res.data.data.tableData)
    tableData.value = res.data.data.tableData
  }
})
</script>

安装 mock

[!NOTE]

mock可以当成是一个伪后端,可以在没有接入后端 api 的时候,让前端开发测试功能正常进行,配合 axios 能够实现伪前后端的功能

进入到根目录,输入安装命令

npm install mockjs

然后还是老样子去看 package.json

配置 mock

  1. 在 src 目录下新建文件夹 api
  2. 在 api 文件夹下新建 mock.js 文件
  3. 在 api 文件夹下新建 mockData 文件夹
  4. 在 mockData 文件夹下新建 home.js 文件

mock.js

import Mock from 'mockjs';
import homeApi from "./mockData/home.js";
Mock.mock("/api/home/getTableData","get",homeApi.getTableData);

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,
                    },
                ],
            },
        }
    }

}

二次封装 axios

创建文件

本节需要在 src/api/ 目录下创建两个文件。一个 request.js,一个 api.js

需要结合官网学习

request.js

import axios from "axios";
import {ElMessage} from "element-plus";

const service = axios.create();
const NETWORK_ERROR = "网络请求异常,请稍后重试"
// 添加请求拦截器
service.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
service.interceptors.response.use(
    (res)=>{
        const {code,data,msg} = res.data
        if(code === 200){
            return data
        }else{
            const NETWORK_ERROR = "网络请求异常,请稍后重试";
            ElMessage.error(msg || NETWORK_ERROR);
            return Promise.reject(msg || NETWORK_ERROR)
        }
    }
);

function request(options){
    options.method = options.method || 'get';
    return service(options);
}

export default request;

需要补充的一点是这里响应拦截器返回的值就是 home.js 里面返回值.data,所以说 Home.vue 那里直接调用就可以了 tableData.value = data.tableData

api.js

//整个api项目统一管理
import request from "./request.js";

//请求首页左侧的表格的数据
export default {
    getTableData: () => {
        return request({
            url: '/api/home/getTableData',
            method: 'get'
        })
    }
}

main.js

添加以下内容

import api from "./api/api.js";
app.config.globalProperties.$api = api;

修改 Home.vue

将之前的 axios 相关的删掉让后加入一以下内容

<script setup>
import {ref, getCurrentInstance, onMounted} from "vue"

const {proxy} = getCurrentInstance()
const getImageUrl = (user) => {
  return new URL(`../assets/images/${user}.png`, import.meta.url).href
}
const getTableData =async () => {
  const data = await proxy.$api.getTableData()
  tableData.value = data.tableData
}
onMounted(() => {
  getTableData()
})
</script>

数据交互升级引入配置文件

这节的内容听了一遍不大懂,于是乎狂问 ai 现在总算有点思路了,首先就先说明各个文件的作用以及相互之间的联系

首先需要先在 src 文件夹下先创建一个 config 文件夹,再在 config 文件夹中创建 index.js 文件如下

index.js

const env = import.meta.env.MODE || "prod";// 导入环境变量,用于确定当前环境模式
const EhvConfig = {// 定义不同环境下的配置对象
    development:{
        baseApi:'/api',
        mockApi:'https://apifoxmock.com/m1/4068509-0-default/api',
    },
    test:{
        baseApi:'//test.future.com/api',
        mockApi:'https://apifoxmock.com/m1/4068509-0-default/api',
    },
    prod:{
        baseApi:'//future.com/api',
        mockApi:'https://apifoxmock.com/m1/4068509-0-default/api'
    }
}

export default {// 导出默认配置对象,根据当前环境模式选择相应的配置
    env,
    ...EhvConfig[env],
    mock:false,
}

request.js

引入了 config 设置,重新声明了service常量,并且给它初始化了一下,重写了 request 方法,这才是配置文件的关键

import config from "@/config";
const service = axios.create({
    baseURL: config.baseApi,
});
function request(options){
    options.method = options.method || 'get';
    if(options.method.toLowerCase() === 'get'){
        options.params = options.data;
    }
    let isMock = config.mock;
    if(typeof options.mock !== 'undefined'){
        isMock = options.mock;
    }
    if(config.env === 'prod'){
        service.defaults.baseURL = config.baseApi;
    }else {
        service.defaults.baseURL = isMock ? config.mockApi : config.baseApi;
    }
    return service(options);
}

这里面很多变量都是 axios 组件自带的,比如 options 所以先不用惊叹这些变量从哪里来的,只需要见名知意即可。需要郑重说明的是 mock 在这里面起的作用就是判断生产环境,从而来决定要调用哪一个 api,不过我这里的代码目前 config.mock 值,对整个程序没什么影响,这其中的 options.mock 更是没有定义所以也没有影响

文件之间的联系

先把关键的 Home 文件中的内容贴出来对比着看

const getTableData =async () => {
  const data = await proxy.$api.getTableData()
  tableData.value = data.tableData
}
onMounted(() => {
  getTableData()
})

首先是 Home 文件一旦挂载就会调用 getTableData 方法,然后在 script 标签中定义了 getTableData 方法,这个方法调用了全局环境变量,也就是 main 中设置的 api ,调用了其中默认导出对象 getTableData 方法,然后这个 api 文件又导入 request 方法,然后这个方法返回的是传入了三个参数 request 方法的结果,然后就调用 request 方法,需要注意的是调用方法的同时请求拦截器就会自动工作,然后 request 方法又调用了 config 文件,但是上面已经传入了三个参数优先级在目前这个 config 文件之上,会覆盖之前 config 中的默认值,最终返回了一个定义的 axios 对象 service,然后响应拦截器开始执行,最终返回 getTableData 中的 data,然后再次到 Home 中使用 await 等待数据加载完成,并把数据传输给对象 data,然后执行下一句(这里的命名都是在有 api 接口文档的情况下是这样命令的不同的情况是不一样的)调用 data 中的 tableData 传参给 tableData.value 从而改变表格中的数据,这样到下面渲染的时候就直接有数据了。

我尽可能的说的清楚,我的逻辑是没有错误的,接下来再把 ai 润色后的版本贴出来,这次应该完全理解了

  1. Home.vue 挂载
    onMounted 调用 getTableData 和 getCountData。
  2. getTableData 方法
    调用 proxy.$api.getTableData()。
  3. api.js 中的 getTableData 方法
    调用 request 方法,传入请求配置。
  4. request.js 中的 request 方法
    设置请求配置。
    请求拦截器处理请求配置。
    通过 service(options) 发起请求。
  5. Mock 数据处理
    如果启用 Mock 模式,返回 Mock 数据。
  6. 响应拦截器
    处理响应数据。
    返回数据或抛出错误。
  7. 数据返回
    getTableData 方法接收到响应拦截器返回的数据。
  8. 数据更新
    tableData.value 更新为返回的 tableData。
  9. 渲染
    Vue 重新渲染 组件,显示最新的表格数据。
    希望这个详细描述能更清晰地展示整个请求流程,包括请求拦截器和响应拦截器的执行时机和作用!如果有其他问题或需要进一步的修改,请随时告知

首页 count 搭建

首页 count 数据获取

首先往 home.js 中添加数据如下

完成以下操作后要是能在控制台上看到数据那便成功

home.js

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",
                },
            ],
        };
    }

mock.js

增加以下

Mock.mock("/api/home/getCountData","get",homeApi.getCountData);

api.js

增加以下内容

export default {
    getCountData: () => {
        return request({
            url: '/home/getCountData',
            method: 'get',
        })
    }
}

Home.vue

发生的变化如下

<script setup>
const tableData = ref([])
const countData = ref([])
const getCountData =async () => {
  const data = await proxy.$api.getCountData()
  console.log(data)
  countData.value = data
}
onMounted(() => {
  getTableData()
  getCountData()
})
</script>

首页 count 部分渲染

Home.vue

加入了以下内容

<template>
  <el-row class="home" :gutter="20">
    <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-col>
  </el-row>
</template>

<style scoped lang="less">
.home {
  .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: 15px;
        text-align: center;
        color: #999;
      }
    }
  }
}
</style>

没什么好说的只要把 v-for 遍历逻辑搞明白就行

首页 chart 搭建

加入以下内容,下面就不说废话了,因为每个步骤是干什么的在上面已经赘述过很多遍了,以此类推即可

本节渲染内容除了最后的表格监听其余的没必要过多解释,因为都是直接用的现成

获取 echart 组件

npm install echarts

首页 chart 数据获取

home.js

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

mock.js

Mock.mock("/api/home/getChartData","get",homeApi.getChartData);

api.js

export default {
    getChartData: () => {
        return request({
            url: '/home/getChartData',
            method: 'get',
        })
    }
}

Home.vue

<script setup>
const chartData = ref([])

const tableLabel = ref({
  name: "课程",
  todayBuy: "今日购买",
  monthBuy: "本月购买",
  totalBuy: "总购买",
})

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 data = await proxy.$api.getChartData()
  console.log(data)
  chartData.value = data
}
onMounted(() => {
  getTableData()
  getCountData()
  getChartData()
})
</script>

折线图的渲染

Home.vue

<script setup>
import * as echarts from 'echarts'

const getChartData = async () => {
  const {orderData} = 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 oneEcharts = echarts.init(proxy.$refs['echart'])
  oneEcharts.setOption(xOptions)
}
</script>

<template>
  <el-row class="home" :gutter="20">
    <el-col :span="16" style="margin-top: 20px">
      <el-card class="top-echart">
        <div ref="echart" style="height: 280px"></div>
      </el-card>
    </el-col>
  </el-row>
</template>

柱状图饼状图以及监听视口变化的实现

Home.vue

<script setup>
const observer = ref(null)
const getChartData = async () => {
  const {orderData, userData, videoData} = await proxy.$api.getChartData()
  //第二个表格渲染
  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 twoEcharts = echarts.init(proxy.$refs['userEchart'])
  twoEcharts.setOption(xOptions)
  //第三个表格渲染
  pieOptions.series = [
    {
      data: videoData,
      type: 'pie',
    }
  ]
  const threeEcharts = echarts.init(proxy.$refs['videoEchart'])
  threeEcharts.setOption(pieOptions)
  //监听页面的变化
  observer.value = new ResizeObserver((en)=>{
    oneEcharts.resize()
    twoEcharts.resize()
    threeEcharts.resize()
  })
  //容器存在
  if(proxy.$refs['echart']){
    observer.value.observe(proxy.$refs['echart'])
  }
}
</script>

<template>
  <el-row class="home" :gutter="20">
    <el-col :span="16" style="margin-top: 20px">
      <el-card class="top-echart" shadow="hover">
        <div ref="echart" style="height: 280px"></div>
      </el-card>
      <div class="graph">
        <el-card shadow="hover">
          <div ref="userEchart" style="height: 240px"></div>
        </el-card>
        <el-card shadow="hover">
          <div ref="videoEchart" style="height: 240px"></div>
        </el-card>
      </div>
    </el-col>
  </el-row>
</template>

<style scoped lang="less">
.home {
  .graph {
    margin-top: 20px;
    display: flex;
    justify-content: space-between;

    .el-card {
      width: 49.3%;
      height: 260px;
    }
  }
}
</style>

监听实现具体方法

根据提供的 Home.vue 文件内容,监听功能主要通过 ResizeObserver 实现

我现在这里说自己的理解,首先在 template 区域的 el-card 中有 echart 响应式数据,所以在组合式 api 中就能调用,在 getChartData 中的 if 判断语句判断此容器是否存在,存在则执行监听

监听实现部分先引入了响应式常量 observer,然后在 getChartData 方法中创建 ResizeObserver 实例,并传递回调函数(三个改变表格展示内容的函数)给 observer,从而实现监听

以下是 ai 的说明:

监听逻辑

  1. 创建 ResizeObserver 实例
    getChartData 方法中,创建了一个 ResizeObserver 实例,并将其赋值给 observer.value

    observer.value = new ResizeObserver((en) => {
      oneEcharts.resize();
      twoEcharts.resize();
      threeEcharts.resize();
    });
    
   
   - 这里的 `ResizeObserver` 用于监听目标元素的尺寸变化。
- 当目标元素的尺寸发生变化时,会触发回调函数,调用 `resize` 方法重新调整 ECharts 图表的大小。
   
2. **绑定监听目标**  
   在确认容器存在后,将 `ResizeObserver` 绑定到 ECharts 容器上:
   
   ```js
   if (proxy.$refs['echart']) {
     observer.value.observe(proxy.$refs['echart']);
   }
  • proxy.$refs['echart'] 是第一个图表的 DOM 容器。
  • 调用 observe 方法将该容器作为监听目标。
  1. 动态调整图表大小
    当监听到容器尺寸变化时,回调函数会依次调用以下方法:

    oneEcharts.resize();
    twoEcharts.resize();
    threeEcharts.resize();
    
    • oneEchartstwoEchartsthreeEcharts 分别是三个 ECharts 实例。
    • 调用 resize 方法确保图表能够根据容器的新尺寸重新渲染。

总结

  • 监听目标:ECharts 容器(<div ref="echart"> 等)。
  • 监听工具ResizeObserver
  • 触发动作:当容器尺寸发生变化时,自动调整 ECharts 图表的大小以适应新尺寸。

用户界面的搭建

用户组件搜索和表格的静态搭建 && 用户表格的数据获取渲染

路由

添加以下内容

index.js

const routes = [
    {
        path: '/',
        name: 'main',
        component: () => import('@/views/Main.vue'),
        redirect: '/home',
        children: [
            {
                path: 'user',
                name: 'user',
                component: () => import('@/views/User.vue'),
            },
        ],
    }
]

api.js

export default {
    getUserData(params) {
        return request({
            url: '/user/getUserData',
            method: 'get',
            data: params
        })
    },
}

mock.js

import userApi from './mockData/user'
Mock.mock(/user\/getUserData/,"get", userApi.getUserList)

数据获取

User.vue

在 src/views 文件夹中创建 User.vue 文件

<script setup>
import {ref, getCurrentInstance, onMounted, reactive} from 'vue'
const handleClick = () => {
  console.log('click')
}

const tableData = ref([])
const {proxy} = getCurrentInstance()
const getUserData = async () => {
  let data = await proxy.$api.getUserData()
  console.log(data)
  tableData.value = data.list.map(item=>({
    ...item,
    sexLabel:item.sex === 1 ? '男' : '女'
  }))
}

const tableLabel = reactive([
  {
    prop:'name',
    label:'姓名',
  },
  {
    prop:'age',
    label:'年龄',
  },
  {
    prop:'sexLabel',
    label:'性别',
  },
  {
    prop:'birth',
    label:'出生日期',
    width:200
  },
  {
    prop:'addr',
    label:'地址',
    width:400
  },
])

onMounted(() => {
  getUserData()
})
</script>

<template>
  <div class="user-header">
    <el-button type="primary">新增</el-button>
    <el-form :inline="true">
      <el-form-item label="请输入">
        <el-input placeholder="请输入用户名"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary">搜索</el-button>
      </el-form-item>
    </el-form>
  </div>
  <div class="table">
    <el-table :data="tableData" style="width: 100%">
      <el-table-column
        v-for="item in tableLabel"
        :key="item.prop"
        :width="item.width ? item.width : 125"
        :prop="item.prop"
        :label="item.label"
      />
      <el-table-column fixed="right" label="Operations" min-width="120">
        <template #default>
          <el-button type="primary" size="small" @click="handleClick">
            编辑
          </el-button>
          <el-button type="danger" size="small">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<style>
.user-header {
  display: flex;
  justify-content: space-between;
}
</style>

user.js

在 src/api/mockData 中创建新文件 user.js 并引入以下内容

import Mock from 'mockjs'

// get请求从config.url获取参数,post从config.body中获取参数
function param2Obj(url) {
    const search = url.split('?')[1]
    if (!search) {
        return {}
    }
    return JSON.parse(
        '{"' +
        decodeURIComponent(search)
            .replace(/"/g, '\\"')
            .replace(/&/g, '","')
            .replace(/=/g, '":"') +
        '"}'
    )
}

let List = []
const count = 200
//模拟200条用户数据
for (let i = 0; i < count; i++) {
    List.push(
        Mock.mock({
            id: Mock.Random.guid(),
            name: Mock.Random.cname(),
            addr: Mock.mock('@county(true)'),
            'age|18-60': 1,
            birth: Mock.Random.date(),
            sex: Mock.Random.integer(0, 1)
        })
    )
}


export default {
    /**
     * 获取列表
     * 要带参数 name, page, limt; name可以不填, page,limit有默认值。
     * @param name, page, limit
     * @return {{code: number, count: number, data: *[]}}
     */
    getUserList: config => {
        //limit默认是10,因为分页器默认也是一页10个
        const { name, page = 1, limit = 10 } = param2Obj(config.url)

        const mockList = List.filter(user => {
            //如果name存在会,根据name筛选数据
            if (name && user.name.indexOf(name) === -1) return false
            return true
        })
        //分页
        const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
        return {
            code: 200,
            data: {
                list: pageList,
                count: mockList.length, //数据总条数需要返回
            }
        }
    },

}

用户搜索和分页的实现

以下是增加的内容

User.vue

<script setup>
const getUserData = async () => {
  let data = await proxy.$api.getUserData(config)
  console.log(data)
  tableData.value = data.list.map(item => ({
    ...item,
    sexLabel: item.sex === 1 ? '男' : '女'
  }));
  config.total = data.count
}

const formInline = reactive({
  keyWord: ''
})
const config = reactive({
  name: '',
  total: 0,
  page: 1,
})
const handleChange = (page) => {
  config.page = page
  getUserData()
}
const handleSearch = () => {
  config.name = formInline.keyWord
  getUserData()
}
</script>

<template>
  <div class="user-header">
    <el-button type="primary">新增</el-button>
    <el-form :inline="true" :model="formInline">
      <el-form-item label="请输入">
        <el-input placeholder="请输入用户名" v-model="formInline.keyWord"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">搜索</el-button>
      </el-form-item>
    </el-form>
  </div>
  <div class="table">
    <el-table :data="tableData" style="width: 100%">
      <el-table-column
          v-for="item in tableLabel"
          :key="item.prop"
          :width="item.width ? item.width : 125"
          :prop="item.prop"
          :label="item.label"
      />
      <el-table-column fixed="right" label="Operations" min-width="120">
        <template #default>
          <el-button type="primary" size="small" @click="handleClick">
            编辑
          </el-button>
          <el-button type="danger" size="small">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
        class="pager"
        background
        layout="prev, pager, next"
        size="small"
        :total="config.total"
        @current-change="handleChange"
    />
  </div>

</template>

<style>
.table {
  position: relative;
  height: 520px;

  .pager {
    position: absolute;
    right: 10px;
    bottom: 30px;
  }

  .el-table {
    height: 500px;
    width: 100%;
  }
}
</style>

突然截止

我宣布此贴终结,没那么多时间来写这个,我昨天+今天一早上就把这东西搞完打包了,可能以后最多再在底下更新一下思路,而且最重要的模拟前后端数据交互的东西写的明明白白,所以就这样了。

0

评论区