01-全栈项目搭建

参考资料:

技术栈:

前端:vue3 + vite4 + ElementPlus2

后端:jdk17 + Maven3.6.3 + SpringBoot2.7 + MyBatis + 蚂蚁地理空间数据可视化2.x

1. 前端架构搭建

1.1 创建项目

vite 创建项目:npm create vite@4.4.0

npm 安装依赖:npm i

npm 启动项目:npm run dev

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<!-- 路由容器:路径匹配则自动加载 -->
<router-view></router-view>
</div>
</template>

<script setup>
</script>

<style>
*{
margin: 0;
padding: 0;
}
</style>

main.js

1.2 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public/
src/
assets/
components/
moduleA/
Xxx.vue
router/
index.js
store/
xxStrore.js
util/
xxx.js
views/
moduleA/
Xxx.vue
App.vue
main.js
style.css
.gitignore
index.html
package-lock.json
package.json
README.md
vite.config.js

1.3 创建路由

安装路由:npm i vue-router

router/index.js

  • 创建路由并导出、通过 ConfigRouter 动态添加路由
  • 重定向、404等配置
  • 路由拦截校验登陆鉴权的配置,重定向到登陆页
  • 判断用户路由权限菜单

项目使用路由 main.js:

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
// import './style.css'
import App from './App.vue'
import router from './router' //导入路由

createApp(App)
.use(router) // 使用路由
.mount('#app')

App.vue

1
2
3
4
5
6
7
<template>
<div>
<!-- 路由容器:路径匹配则自动加载 -->
<router-view></router-view>
</div>
</template>
...

1.3.1 配置动态路由

router/config.js

1.4 引入状态管理 pinia

安装:npm i pinia

使用:

store/useRouterStore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import {defineStore} from 'pinia'
import { ref } from 'vue'

export const useRouterStore = defineStore("router", () => {
const isGetterRouter = ref(false) //全局可用
const changeRouter = (value) => {
isGetterRouter.value = value
}
return {
isGetterRouter,
changeRouter
}
})

1.5 引入 pinia 持久化插件

安装:npm i pinia-plugin-persistedstate

引入:main.js

1
2
3
4
5
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

开启持久化:useUserStore.js - { persist: true }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {defineStore} from 'pinia'
import { ref } from 'vue'

// defineStore 第三个参与用于 pinia 持久化插件配置
export const useUserStore = defineStore("user", () => {
const user = ref({}) //全局可用
const changeUser = (value) => {
user.value = value
}
return {
user,
changeUser
}
}, {
persist: true //持久化设置
})

触发持久化 store 时,会将公共状态数据 user 存储在 localStorage 中。

1.6 登陆&退出登陆

Login.vue - 登陆与退登

MainBox.vue - 核心容器

1.7 登录页 - 引入element-plus

组件库:element-Plus

安装:npm i element-plus

  • 写样式记得安装 sass: npm i sass

引入:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

Login.vue 完善。

1.8 引入粒子库 particles

particles 粒子库官网:https://particles.js.org/

image-20260107183325381

点击跳转 github 对应 vue3 文档:https://github.com/tsparticles/vue3

安装:npm i @tsparticles/vue3npm i tsparticles

1.8.1 使用默认粒子效果

main.js - 导入和使用

1
2
3
4
5
6
7
8
9
10
11
import Particles from "@tsparticles/vue3";
import { loadFull } from "tsparticles";

createApp(App)
.use(Particles, {
init: async engine => {
await loadFull(engine);
// await loadSlim(engine);
},
})
.mount('#app')

Login.vue - 复制 github 说明文档中的 <vue-particles ...> 标签内容,放在 form 表单外即可。将 options 单独维护为 xxxConfig.js 文件进行导入。

1
2
3
4
5
6
7
8
9
10
11
<template>
<vue-particles id="tsparticles" @particles-loaded="particlesLoaded" :options="tsparticlesConfig" />
</template>

<script setup>
import { tsparticlesConfig } from '../util/tsparticlesConfig'
const particlesLoaded = async container => {
console.log("粒子库 Particles container loaded", container);
};
...
</script>

src/util/tsparticlesConfig.js

效果

image-20260108120837033

1.8.2 使用其他粒子效果

particles 粒子库官网:https://particles.js.org/

跳转到

image-20260107195418831

image-20260107185216663

拷贝其配置内容,替换掉 src/util/tsparticlesConfig.js 即可。

  • 其中修改背景图,在属性 background - image 中修改,图片文件放在 public/bg.jpg 目录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const tsparticlesConfig = {
"autoPlay": true,
"background": {
"color": {
"value": "#043564"
},
"image": "url('./bg.jpg')", //指定图片,直接引用就是放在 public 下
"position": "center", //定位到中心
"repeat": "no-repeat",
"size": "cover", //覆盖铺满
"opacity": 1,
"attachment": "fixed" //不随窗口滚动
},
...

1.9 布局

目录

1
2
3
4
5
6
7
src/
components/
MainBox/
SideMenu.vue
TopHeader.vue
views/
MainBox.vue

MainBox.vue - 涉及布局组件 Container

SideMenu.vue

TopHeader.vue - 主要支持标题、欢迎语、用户名显示,以及退出 或 跳转个人中心等

1.10 SideMenu 侧边栏

SideMenu.vue

  • height: 100vh 可以解决侧边栏滚动条没有占满上下屏幕
  • :router=true 在 el-menu 中开启后可以基于 index 配置的 path 路径跳转
  • icon图标:使用动态组件 <component> 的 :is 属性动态绑定图标组件实例对象,不是字符串,因此需要 map 映射取值
  • route.fullPath 当前相对路径,如 /user-manage/list 用于 :default-active 动态绑定实现当前菜单高亮

依赖权限菜单 json 数据(临时准备)。

权限菜单 json 数据结构 -(从后端接口获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
[
{
"title": "首页",
"path": "/index",
"icon": "HomeFilled",
"children": []
},
{
"title": "用户管理",
"path": "/user-manage",
"icon": "User",
"children": [
{
"title": "用户列表",
"path": "/user-manage/list",
"icon": "List"
}
]
},
{
"title": "权限管理",
"path": "/right-manage",
"icon": "Key",
"children": [
{
"title": "角色列表",
"path": "/right-manage/rolelist",
"icon": "List"
},
{
"title": "权限列表",
"path": "/right-manage/rightlist",
"icon": "List"
}
]
},
{
"title": "实验室管理",
"path": "/lab-manage",
"icon": "OfficeBuilding",
"children": [
{
"title": "实验室列表",
"path": "/lab-manage/lablist",
"icon": "List"
},
{
"title": "添加实验室",
"path": "/lab-manage/addlab",
"icon": "List"
}
]
},
{
"title": "预约管理",
"path": "/book-manage",
"icon": "UploadFilled",
"children": [
{
"title": "审核列表",
"path": "/book-manage/auditlist",
"icon": "List"
},
{
"title": "预约列表",
"path": "/book-manage/booklist",
"icon": "List"
},
{
"title": "预约实验室",
"path": "/book-manage/addbook",
"icon": "List"
}
]
}
]
效果

image-20260108120946052

1.11 TopHeader 顶部栏

TopHeader.vue

  • 默认头像的放置位置不同,则取值方式就会不同。

assets/avatar.png - 头像

效果

image-20260108124401264

1.12 RightList 权限列表

RightList.vue

  • Table 表格组件: 要支持树形展开子级只需要设置 row-key="唯一属性"
  • scope.row 在 el-table 组件中默认取 tableData 的一行数据,直接用 . 取值
效果

更新 - 弹窗和Message提示;删除 - 气泡确认框和Message提示

image-20260108195741083

1.13 RoleList 角色列表

RoleList.vue

  • Popover 弹出框组件,鼠标移入悬浮树形显示权限列表
  • 更新角色权限的树形可选显示
  • Tree 树形控件:show-checkbox 变成可选节点,做默认选中必须设置 node-key 值唯一; :check-strictly="true" 父子不关联
  • Tree 树形控件:自定义节点 的渲染方法 renderContent
效果

image-20260109115937617

image-20260109115919677

1.14 UserList 用户列表

UserList.vue - 增删改查完整逻辑。

效果

image-20260109164432301

image-20260109164443631

image-20260109164507593

1.15 AddLab 添加实验室-蚂蚁地理空间

AddLab.vue

  • 蚂蚁地理空间 平面区域标注 图表的基本使用
    • 修改背景图、点击弹出 Popup 信息窗、弹出 Form 表单提交窗、遍历添加文本标注、**坐标位置定位(鼠标点击位置 x,y 倍数计算)**等
  • 数据流的处理逻辑
效果

点击地图任意位置

image-20260110185004510

【选择位置】

image-20260110185112776

image-20260110185135686

1.16 LabList 实验室列表

LabList.vue

  • 增删改查的基本显示逻辑、数据流
  • el-tag 组件,动态控制不同的颜色显示
  • dialog 对话框组件中封装自定义组件全屏显示v-if完全创建和销毁(防止数据残留)
效果

image-20260110191547388

1.17 LabMap 自定义组件

LabMap.vue

  • 复用 添加实验室 AddLab 组件的蚂蚁地理空间代码,注意:在 dialog 对话框中弹窗出来的,所以 onMounted 时还不能获取到宽度,会导致变形,需要 nextTick() 即 dom 渲染完就可以解决问题
  • 父传子:进行全屏回显、显示具体实验室标注文本

父(LabList.vue):

1
2
3
4
5
<!-- dialog 对话框组件- v-if 对数据进行完全创建和删除,防止数据残留 -->
<el-dialog v-model="dialogVisible" title="实验室位置" :fullscreen="true" v-if="dialogVisible">
<!-- 封装自定义组件 -->
<LabMap :x="currentItem.x" :y="currentItem.y" :t="currentItem.title"></LabMap>
</el-dialog>
效果

image-20260110191836696

1.18 AddBook 预约实验室

AddBook.vue

  • 获取已创建在数据库中的数据遍历标注在图中
  • 绑定点击标注文字的事件,弹出 dialog 对话框(对话框中处理对 DatePicker 日期组件的国际化、可选日期范围、select选项的关联禁用)
  • 添加预约的数据组装
效果

image-20260112135432847

1.19 BookList 预约列表

BookList.vue

  • 使用自定义指令 v-admin 和 v-teacher 控制展示不同的列在不同的用户预约列表
  • el-table 中自定义表头,实现在表头输入后直接过滤列表数据
  • moment 库做时间格式化
效果

image-20260112135941673

1.20 AuditList 审核列表

AuditList.vue

  • 主要涉及预约状态的业务逻辑,只展示审核中的数据,对其进行批准和驳回操作
效果

image-20260112140022578

1.21 Home 首页大数据预览

Home.vue

  • 对文本标注绑定点击事件,点击弹出 popup 弹窗,聚合请求并组装预约列表数据 html 渲染显示

聚合请求:

1
2
3
4
5
6
7
8
9
10
11
textlayer.on("click", async (e) => {
console.log(e); //拿到文本标注对象信息
//0-预约中、1-已批准 聚合查询:Promise.all([a1, a2])
let url = `/admin/books/list?labId=${e.feature.id}`
const res = await Promise.all([
axios.get(url + `&bookState=0`),
axios.get(url + `&bookState=1`)
])
//console.log(...res[0].data, ...res[1].data) // res结果是一个数组,对应了 axios 请求的顺序
let list = [...res[0].data, ...res[1].data]
...
效果

image-20260112140629261

1.22 Upload 组件

TopHeader.vue

  • 使用 el-dialog 包裹 el-upload 上传器组件
  • drag 支持拖拽;:data 支持携带参数(如token);:on-success 支持上传成功回调事件(如重新载入页面)
1
2
//让页面 reload 一遍
window.location.reload()
效果

image-20260112203245383

image-20260112203257298

1.99 通用封装

util/type.js

  • 作为写死的常量值(或维护在后端数据库通过接口交互)

2. 后端架构搭建

1
2
> java --version
java 17.0.7

jdk17 兼容 springboot2 和 springboot3,且确保 maven 的 settings.xml 中使用阿里云镜像仓库,以及对 profile 已切换至 jdk17。

1.1 创建项目

IDEA - 新建项目

image-20260108131822624

image-20260108132101356

注意 maven 仓库路径的设置:

image-20260112200958095

配置文件 application.yml (将 .properties 修改为 .yml 配置文件) - 配置服务启动端口和本地数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 5000

spring:
# mysql 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/lab_system
username: root
password: 123456

# mybatis 配置映射 xml 文件,并配置打印sql语句
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

1.2 数据库 sql

基于本地数据库: mysql57 (localhost:3306, root, 123456)

mysql57 服务器的启动和停止:

net start mysql57 - 启动服务

net stop mysql57 - 停止服务

tasklist | findstr /i "mysql" - 查询 mysqld 进程是否存在(启动才会存在)

添加初始化数据 sql:mysql.sql

1.3 通用 Result 封装示例

pom.xml - 手动添加 lombok

1
2
3
4
5
<!--添加lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

utils/SysConstant.java

1
2
3
4
5
6
7
8
9
10
11
12
public interface SysConstant {
Integer CODE_SUCCESS = 0;
Integer CODE_ERROR = -1;
String ADD_SUCCESS = "添加成功";
String ADD_ERROR = "添加失败";
String UPDATE_SUCCESS = "更新成功";
String UPDATE_ERROR = "更新失败";
String DELETE_SUCCESS = "删除成功";
String DELETE_ERROR = "删除失败";
String LOGIN_SUCCESS = "登陆成功";
String LOGIN_ERROR = "登录失败";
}

utils/ResultObj.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import lombok.Data;

@Data
public class ResultObj {
private Integer code;
private String msg;
private Object data;

public ResultObj(Integer code) {
this.code = code;
}

public ResultObj(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

public ResultObj(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public static final ResultObj ADD_SUCCESS = new ResultObj(SysConstant.CODE_SUCCESS, SysConstant.ADD_SUCCESS);
public static final ResultObj ADD_ERROR = new ResultObj(SysConstant.CODE_ERROR, SysConstant.ADD_ERROR);

public static final ResultObj UPDATE_SUCCESS = new ResultObj(SysConstant.CODE_SUCCESS, SysConstant.UPDATE_SUCCESS);
public static final ResultObj UPDATE_ERROR = new ResultObj(SysConstant.CODE_ERROR, SysConstant.UPDATE_ERROR);

public static final ResultObj DELETE_SUCCESS = new ResultObj(SysConstant.CODE_SUCCESS, SysConstant.DELETE_SUCCESS);
public static final ResultObj DELETE_ERROR = new ResultObj(SysConstant.CODE_ERROR, SysConstant.DELETE_ERROR);
}

1.4 接口代码

接口规范化:Apifox接口文档一体化协作平台

已推github项目代码:https://github.com/janycode/lab-system-admin

1.5 上传接口&拦截器

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/admin/upload")
public class UploadController {
@Autowired
private UploadService uploadService;

@PostMapping
public ResultObj upload(@RequestParam("file") MultipartFile file) {
try {
uploadService.upload(file);
return new ResultObj(SysConstant.CODE_SUCCESS, "上传成功");
} catch (IOException e) {
return new ResultObj(SysConstant.CODE_ERROR, "上传失败");
}
}
}

service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class UploadServiceImpl implements UploadService {
/**
* 上传
*
* @param file 要上传到的文件对象
*/
@Override
public void upload(MultipartFile file) throws IOException {
// 用同文件进行存储和覆盖,因为就一张背景图
String fileName = "bg.jpg";
String filePath = ResourceUtils.getURL("classpath:").getPath() + "static/upload/";
// fullFilePath: /E:/....../target/classes/static/upload/bg.jpg
// 访问: http://localhost:5000/static/upload/bg.jpg
String fullFilePath = filePath + fileName;
System.out.println("fullFilePath = " + fullFilePath);
File dist = new File(fullFilePath);
if (!dist.getParentFile().exists()) {
dist.getParentFile().mkdirs();
}
//上传
file.transferTo(dist);
}
}

拦截器

WebConfig.java - 校验 token 拦截器放行静态资源路径 /static/**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;

/**
* 路径拦截
*
* @param registry 注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,拦截指定的路径 或 排除路径
String[] staticPath = {"/static/**"};
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(staticPath);
}

/**
* CORS 跨域拦截
*
* @param registry 注册器
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
//WebMvcConfigurer.super.addCorsMappings(registry);
registry.addMapping("/static/**")
.allowedOrigins("*")
.allowedMethods("GET");
}
}

application.yml - 暴露 static 静态资源可访问

1
2
3
4
5
spring:
...
# 暴露可访问的路径(用于直接访问静态资源,如图片)
mvc:
static-path-pattern: /static/**

3. 核心涉及

3.1 反向代理 vite.config.js

vite.config.js - 前后端均可解决跨域,在前端可以配置如下反向代理,让请求模拟从本域请求本域,就不会有跨域问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],
// 解决跨域问题
server: {
proxy: {
"/admin": {
target: "http://localhost:5000",
changeOrigin: true
}
}
}
})

3.2 蚂蚁地理空间数据可视化

官网:https://l7.antv.antgroup.com/

官网:https://antv-2018.alipay.com/zh-cn/l7/1.x/index.html

官方教程文档:https://www.yuque.com/antv/l7/yzkq8e

安装L7依赖:npm install –save @antv/l7

安装第三方底图依赖npm install –save @antv/l7-maps

示例图表:

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div>
<div style="min-height: 500px; justify-content: center; position: relative" id="map" />
</div>
</template>
<script setup>
import { onMounted } from "vue"
import { ImageLayer, PointLayer, Scene } from "@antv/l7"
import { Map } from "@antv/l7-maps"

onMounted(() => {
const scene = new Scene({
id: "map",
map: new Map({
center: [500, 500],
zoom: 3,
version: "SIMPLE",
mapSize: 1000,
maxZoom: 5,
minZoom: 2,
pitchEnabled: false,
rotateEnabled: false,
}),
})
scene.setBgColor("rgb(94, 182, 140)")

const imagelayer = new ImageLayer({}).source(
"https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*I0X5R4jAUQ4AAAAAAAAAAAAAARQnAQ",
{
parser: {
type: "image",
extent: [360, 400, 640, 600],
},
}
)

scene.on("loaded", () => {
fetch("https://gw.alipayobjects.com/os/bmw-prod/7dc0d454-fabc-4461-a5d5-d404dadb49a9.json")
.then(res => res.json())
.then(data => {
const textlayer = new PointLayer({ zIndex: 2 })
.source(data, {
parser: {
type: "json",
x: "x",
y: "y",
},
})
.shape("t", "text")
.size(12)
.active({
color: "#00f",
mix: 0.9,
})
.color("rgb(86, 156, 214)")
.style({
textAnchor: "center", // 文本相对锚点的位置 center|left|right|top|bottom|top-left
spacing: 2, // 字符间距
fontWeight: "800",
padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
stroke: "#ffffff", // 描边颜色
strokeWidth: 2, // 描边宽度
textAllowOverlap: true,
})
scene.addLayer(textlayer)
})

scene.addLayer(imagelayer)
})
})
</script>

3.3 i18n 国际化

参考官网:https://element-plus.org/zh-CN/guide/i18n#%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE

element-Plus 只需要 main.js 引入 i18n 中文支持即可,比如用于 日历组件中周几的 中文 显示(而非英文)。

main.js

1
2
3
4
5
6
7
8
9
10
11
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus' //导入 element-plus 样式
import 'element-plus/dist/index.css' //导入 element-plus 样式
import zhCn from 'element-plus/es/locale/lang/zh-cn' //全局引入国际化

createApp(App)
.use(ElementPlus, {
locale: zhCn,
}) //使用 element-plus,并添加国际化
.mount('#app')

3.4 JWT - 登陆鉴权 token

JWT,Json Web Token. 官网:https://www.jwt.io/

后端 token 拦截

pom.xml 引入依赖

  • 如果 jdk版本>1.8 时需要引入 jaxb-api 依赖(≤1.8时不需要)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--引入JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<!-- jdk版本>1.8 时需要引入 jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>

application.yml 添加加盐配置

1
2
3
4
5
6
7
8
9
# JWT 配置
config:
jwt:
# 自定义的加盐字符串
secret: jerry_define_code
# 过期时间(秒):1小时(60s用于测试)
expire: 60
# header中的字段
header: token

添加拦截器配置:

config/ - 核心拦截器配置

JwtConfig.java

TokenInterceptor.java

WebConfig.java

controller/UserController.java/login 接口逻辑中调用 jwtConfig 创建 token 并返回

pojo/User.java 中新增 token 字段用于登陆时返回

前端 axios 拦截

axios 源码文档拦截器:https://github.com/axios/axios#interceptors

src/util/axios.config.js - axios请求拦截器

main.js 引入

1
2
3
4
5
6
...
import './util/axios.config' //导入 axios 配置(拦截器)

createApp(App)
...
.mount('#app')

3.5 loading 与 nprogress

两者选其一,nprogress 进度条 用户体验更优秀。

loading 加载中

elementPlus 中 Loading 加载框,以服务的方式来调用

src/util/axios.config.js - axios请求拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import axios from "axios"
import { ElLoading } from 'element-plus'

let loadingInstance = null
// 请求之前
axios.interceptors.request.use(function (config) {
loadingInstance = ElLoading.service({ fullscreen: true }) //创建 loading 框
...//其他代码
return config;
}, function (error) {
return Promise.reject(error);
});

// 响应之前
axios.interceptors.response.use(function (response) {
...//其他代码
loadingInstance?.close() // 关闭 loading 框
return response;
}, function (error) {
...//其他代码
return Promise.reject(error);
});

效果:触发接口请求时,全屏的屏幕中间转圈的 loading 加载效果。

nprogress 进度条(★)

文档:https://github.com/rstacruz/nprogress

安装:npm i nprogress

引入:(在路由 router/index.js 中使用和引入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createRouter, createWebHashHistory } from 'vue-router'
import NProgress from 'nprogress' //[模块化]引入进度条
import 'nprogress/nprogress.css' //[模块化]引入进度条样式

const routes = [...]
const router = createRouter({
history: createWebHashHistory(), // 带 # 的路径
routes
})
// 路由拦截
router.beforeEach((to, from, next) => {
NProgress.start() // 进入路由前 加载进度条
...
})
router.afterEach(() => {
NProgress.done() // 进入路由后 关闭进度条
})

效果:每个页面浸入时,顶部从左向右一条很细的加载进度条。

3.6 前后端分页逻辑融合


01-全栈项目搭建
https://janycode.github.io/2023/09/28/17_项目设计/04_全栈项目设计/01-全栈项目搭建/
作者
Jerry(姜源)
发布于
2023年9月28日
许可协议