一、vue3

Vue 3 是 Vue.js 的第三代核心框架,于 2020 年正式发布,是目前 前端开发的主流选择。它以 组合式 API (Composition API)Proxy 响应式系统极致性能TypeScript 原生支持为核心,彻底重构了 Vue 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
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue3 CDN 引入</title>
<!-- 1. 引入 Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<!-- 2. 挂载容器 message -->
<div id="app">{{ message }}</div>

<script>
// 3. 创建 Vue 应用
const { createApp } = Vue;

createApp({
//旧版
/* data(){
return {
message
}
} */

//新版
setup(){
const message = "hello,vue3" //数据

return { //return出使用
message
}
}
}).mount('#app'); // 挂载到 #app
</script>
</body>
</html>

三、插值表达式

1.作用

可以使用表达式进行插值,渲染到页面中, 表达式:是可以被求值的代码,js引擎会将其计算出一个结果

2.语法

1
<div>{{ 字段 }}</div>

3.规则

只解析文本,不能解析 HTML 标签

支持单行表达式,不支持语句、循环、if

数据变更,页面自动刷新

4.代码

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
<!DOCTYPE html>
<html>
<body>
<div id="app">
<!-- 变量插值 -->
<p>姓名:{{ name }}</p>
<!-- 简单运算 -->
<p>年龄+2:{{ age + 2 }}</p>
<!-- 三元表达式 -->
<p>状态:{{ sex ? '男' : '女' }}</p>
<!-- 字符串拼接 -->
<p>拼接:{{ '编号' + id }}</p>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue
createApp({
data(){
return {
name:'张三',
age:18,
sex:true,
id:1001
}
}
}).mount('#app')
</script>
</body>
</html>

四、Vue指令

指令 功能 简写 用法示例
v-text 渲染纯文本 <span v-text="msg"></span>
v-html 解析渲染 HTML <div v-html="htmlStr"></div>
v-bind 绑定元素属性 : <div :class="cls"></div>
v-on 绑定事件监听 @ <button @click="func"></button>
v-model 表单双向绑定 <input v-model="value">
v-if 条件销毁渲染 <div v-if="bool"></div>
v-else 条件互斥分支 <div v-else></div>
v-else-if 多条件判断 <div v-else-if="num>5"></div>
v-show 样式切换显隐 <div v-show="bool"></div>
v-for 列表循环遍历 <li v-for="item in arr"></li>
v-once 元素仅渲染一次 <div v-once>{{txt}}</div>
v-pre 跳过编译解析 <span v-pre>{{原样显示}}</span>
v-cloak 解决插值闪烁 <div v-cloak>{{msg}}</div>
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue3 全部指令合集案例</title>
<style>
[v-cloak] { display: none; }
.red { color: red; }
.box { padding: 8px; margin: 5px 0; border: 1px #ccc solid; }
.bind1{
color: green;
}
.bind2{
font-size: 30px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- 1. v-text 文本渲染 -->
<div class="box">v-text:<span v-text="txt"></span></div>

<!-- 2. v-html 解析HTML -->
<div class="box">v-html:<span v-html="htmlTxt"></span></div>

<!-- 3. v-bind 属性绑定 -->
<div class="box" :class="isRed ? 'red' : ''">v-bind 动态样式</div>

<!-- v-bind 类名绑定 true为执行,false为不执行 -->
<div class="box" :class="{bind1: true,bind2: false}">v-bind 动态类名样式</div>

<!-- v-bind 样式绑定 -->
<div class="box" :style="{color: 'blue', backgroundColor: 'yellow'}">v-bind 动态样式</div>

<!-- 4. v-on 事件绑定 -->
<div class="box">
v-on:<button @click="num++">点击次数{{num}}</button>
</div>

<!-- 5. v-model 双向绑定 -->
<div class="box">
v-model:<input v-model="inputVal"> 显示: {{inputVal}}
</div>

<!-- 6. v-if / v-else-if / v-else 条件渲染 -->
<div class="box">
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
</div>

<!-- 7. v-show 显隐切换 -->
<div class="box">
<div v-show="isShow">v-show 显示内容</div>
<button @click="isShow=!isShow">切换显隐</button>
</div>

<!-- 8. v-for 列表循环 (元素,下标) in 数组名 :key表示绑定下标,以防出错 -->
<div class="box">
v-for:
<p v-for="(item,idx) in list" :key="idx">
{{idx+1}}.{{item}}
</p>
</div>

<!-- 9. v-once 单次渲染 -->
<div class="box">v-once:<span v-once>{{onceTxt}}</span></div>

<!-- 10. v-pre 原样输出 -->
<div class="box">v-pre:<span v-pre>{{原样不会解析}}</span></div>

<!-- 11. v-cloak 防插值闪烁 -->
<div class="box" v-cloak>{{cloakTxt}}</div>
</div>

<script>
const { createApp } = Vue
createApp({
data(){
return {
txt: '普通文本内容',
htmlTxt: '<b style="color:blue;">加粗蓝色文字</b>',
isRed: true,
num: 0,
inputVal: '',
score: 78,
isShow: true,
list: ['Vue','JS','HTML','CSS'],
onceTxt: '只会渲染一次',
cloakTxt: '解决页面插值闪烁',
}
}
}).mount('#app')
</script>
</body>
</html>

2.指令修饰符

分类 修饰符 作用
v-model 修饰符 .trim 去除输入首尾空格
.number 自动转换为数字类型
.lazy 失焦后才同步数据
事件修饰符 .stop 阻止事件冒泡
.prevent 阻止标签默认行为
.self 仅自身元素触发事件
.once 事件只执行一次
按键修饰符 .enter 按下回车键触发
.space 按下空格键触发
.esc 按下退出键触发
.delete 按下删除键触发
鼠标修饰符 .left 鼠标左键触发
.right 鼠标右键触发
.middle 鼠标滚轮键触发
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
77
78
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>指令修饰符</title>
<style>
.box{margin:8px 0;padding:8px;border:1px #ccc solid;}
[v-cloak]{display:none;}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- v-model 修饰符 -->
<div class="box">
.trim去空格:<input v-model.trim="val1"><br>
.number转数字:<input v-model.number="val2"><br>
.lazy失焦更新:<input v-model.lazy="val3">
<p>实时值:{{val1}} | {{val2}} | {{val3}}</p>
</div>

<!-- 事件修饰符 -->
<div class="box" @click="parentClick">
父容器
<button @click.stop="childClick">.stop阻止冒泡</button>
<a href="https://www.baidu.com" @click.prevent>
.prevent阻止跳转
</a>
</div>

<!-- 按键修饰符 -->
<div class="box">
回车提交:<input @keyup.enter="enterFn"><br>
空格触发:<input @keyup.space="spaceFn">
</div>

<!-- 鼠标修饰符 -->
<div class="box">
<button @click.left="leftFn">左键点击</button>
<button @click.right="rightFn">右键点击</button>
</div>
</div>

<script>
const {createApp} = Vue
createApp({
data(){
return{
val1:'',
val2:'',
val3:''
}
},
methods:{
parentClick(){
alert('父级点击')
},
childClick(){
alert('子级点击,不会触发父级')
},
enterFn(){
alert('按下回车')
},
spaceFn(){
alert('按下空格')
},
leftFn(){
alert('左键单击')
},
rightFn(){
alert('右键单击')
}
}
}).mount('#app')
</script>
</body>
</html>

五、项目构建安装

一般情况使用Vue3做项目都是使用框架!! 需要下载node.js

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
#vue3官方安装
npm create vue@latest

配置项目名称

√ Project name: vue3_test

是否添加TypeScript支持

√ Add TypeScript? Yes

是否添加JSX支持

√ Add JSX Support? No

是否添加路由环境

√ Add Vue Router for Single Page Application development? No

是否添加pinia环境

√ Add Pinia for state management? No

是否添加单元测试

√ Add Vitest for Unit Testing? No

是否添加端到端测试方案

√ Add an End-to-End Testing Solution? » No

是否添加ESLint语法检查

√ Add ESLint for code quality? Yes

是否添加Prettiert代码格式化
√ Add Prettier for code formatting? No




#vite安装 这里直接选择这个即可
npm install vite@latest
pnpm install vite #推荐使用 pnpm 安装pnpm npm i pnpm -g

六、文件详解

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
E:\frontend\vue3-ts\
├── node_modules/ # 依赖包(自动生成,勿修改)
├── public/ # 静态资源(不打包)
│ └── favicon.ico
├── src/ # 核心开发目录
│ ├── assets/ # 图片、样式、字体
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置(TS)
│ │ └── index.ts
│ ├── stores/ # Pinia 状态管理(TS)
│ │ └── counter.ts
│ ├── views/ # 页面组件
│ │ ├── Home.vue
│ │ └── About.vue
│ ├── App.vue # 根组件
│ ├── main.ts # 项目入口(TS 版本)
│ ├── env.d.ts # TS 类型声明文件
│ └── vite-env.d.ts # Vite 类型声明
├── .gitignore # Git 忽略文件
├── index.html # 入口 HTML
├── package.json # 项目配置
├── tsconfig.json # TypeScript 配置
├── tsconfig.node.json # TS Node 配置
├── vite.config.ts # Vite 配置(TS 版本)
└── README.md # 项目说明

src/main.ts文件详解:

1
2
3
4
5
import {createVue} from 'vue'		//导入vue实例
import 'style.css' //全局通用样式
import App from './App.vue' //导入根组件 (所有组件的壳子)

createVue(App).mount('#app') //创建vue实例并挂载到 index.html 的 <div id="app"> 里面

src/app.vue文件详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 1. 模板:页面结构(HTML) -->
<template>

</template>


<!-- 2. 脚本:业务逻辑(TS + <script setup> 语法糖) -->
<script setup lang="ts">
// 这里写 TS 逻辑、引入组件、定义变量等
// 项目启动后,最先执行这里的代码

</script>


<!-- 3. 样式:全局/组件样式(CSS/SCSS) -->
<style scoped>
/* scoped 表示样式只作用于当前组件 */
</style>

七、基本语法

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
<script lang="ts">
export default {
setup() {
let name = "小陈"
let age = 18
let count = 0

function countAdd() {
count++; <!-- 点击按钮后,页面的count不会变,因为vue3默认不是响应式,但数据会修改!!!! -->
}
return {
name,
age,
count,
countAdd
}
},
//嵌套vue2写法
data() {
return {
msg: "你好",
count2: 0 <!-- 当点击时,页面的count2会变,vue2是响应式 -->
}
},
methods: {
sayHello() {
return "你好呀"
}
}
}

</script>

<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<div>{{ msg }}</div>
<div>{{ sayHello() }}</div>
<button @click="countAdd()">{{ count }}</button>
<button @click="count2++">{{ count2 }}</button>
</template>


<!-- 注:vue3语法也可以和vue2的data、methods同时存在,旧语法可以用this读新语法的数据,但新的不可以读旧的!!!!!-->

setup语法糖

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>

</template>

<!-- setup语法糖 省去: export default return{}-->
<script setup lang="ts">
let name = "小陈"
let age = 19
</script>

八、基本数据类型和对象类型响应式数据 ref、reactive

1. ref基本/对象引入类型 let 变量名 = ref(值)

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
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="ageAdd()">点击</button>
<div>{{ getObject() }}</div>

</template>

<!-- setup语法糖-->
<script setup lang="ts">
import {ref} from 'vue' //导入 基本/对象数据类型 ref

let name = ref("小陈") //使用括号包裹 注:在这里面的数据使用ref会自动加value 如:name.value,age.value
let age = ref(19)
let games = ref([ //ref也支持对象类型
{id:"game1",name:"王者荣耀"},
{id:"game2",name:"和平精英"}
])
let games.value[0] = {id:"game",name:"部落冲突"} //ref可以直接修改整个对象

function ageAdd(){
age.value++; //修改值需要加上 .value!!!!
}

function getObject(){
return games.value[0].name //获取数组对象类型在 .value[下标].名称
}
</script>




<!-- 记得导入到app.vue根组件里面!!! -->
<script setup lang="ts">
// import HelloWorld from './components/HelloWorld.vue'
import SetupTang from './components/SetUpTang.vue'
</script>

<template>
<!-- <HelloWorld /> -->
<SetupTang />
</template>

2. recative对象数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>recative: {{ hobby[0].name }}</div>
<button @click="setHobby()">点击修改</button>

</template>

<!-- setup语法糖-->
<script setup lang="ts">
import {reactive} from 'vue' //导入 基本/对象数据类型 ref

let hobby = reactive([
{id:"hobby",name:"唱"},
{id:"hobby2",name:"跳"},
])
//let sex = reactive("男") 无法赋值 除了object类型的值
//let hobby[0] = {id:"11",name:"篮球"} reactive修改整个对象会丢失响应式!!!!

function setHobby(){
hobby[0].name = "篮球" //recative可以不使用 .value获取值
}
</script>

3.区别

1.ref可以定义基本类型和对象类型,而reactive不可以定义基本数据类型;

2.使用官方自带插件可以自动提示.value!!!!!

3.ref可以修改整个对象,而reactive不可以!!!!!

如:

function changCar(){
cat.value = {brand:”宝马”,price:2000000} //reactive不可以这样写!!!
}

4.如果使用reactive又想要修改整个对象,使用object.assign(对象名,{brand:”宝马”,price:2000000})

5.如果对象层次更深,推荐使用reactive

九、对象结构响应式 toRefs、toRef

上面ref和reactive如果他们的对象进行解构,此时解构的对象不会是响应式,那该怎么办???? 使用toRefs或toRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>{{ nameOne }}</div>
<div>{{ name }}</div>
<div>{{ id }}</div>
</template>



<script setup lang="ts">
import {ref,toRefs,toRef} from 'vue'

let hobby = ref({id:"one",name:"java"})

//进行解构
let nameOne = toRef(hobby.value,"name") //单值对象解构 ToRef(对象名,"值")
let {name,id} = toRefs(hobby.value) //对象解构 ToRefs(对象名)
</script>

十、computed计算属性(缓存优化)

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
<template>
<div>{{ NewNum }}</div>
<div>{{ num }}</div>
<div>{{ plusNum }}</div>
<button @click="setNum">修改Num</button>

</template>


<script setup lang="ts">
import {ref,computed} from 'vue'

let num = ref(1)

//只读无法修改
let NewNum = computed(() => {
// return num.value = num.value * 2 这样会报错!!!!
return num.value * 2 //只能获取
})

//可读可写
let plusNum = computed({
get(){
return num.value + 2
},
set(val){
num.value = val - 1
}
})

function setNum(){
plusNum.value = 4 //当 plusNum修改时触发set
}
</script>

十一、watch监听器

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
<script setup lang="ts">
import {ref,reactive,watch} from 'vue' //引入

let num = ref(1)
let my = reactive({name: "小陈","age": 19,hobby: {one: "唱",two: "跳"}}) //使用reactive

//监听变量 watch(变量名,(新值,旧值) => {})
const stopWatchB = watch(num,(newValue,oldValue) => {
console.log("原值:" + oldValue,"新值:" + newValue);
if(num.value > 10){
console.log("监听结束");
stopWatchB()
}
})

//监听单个对象值 watch(() => 对象值名,(新值) => {})
watch(
() => my.name, //必须写成函数返回值
(newName) => {
console.log("修改值:" + newName);
}
)

//监听多个对象值 watch([() => 对象值名,() => 对象值名2],() => {})
watch(
[() => my.name,() => my.age],
(newVlaue) => {
console.log("修改值:",newVlaue);
},
)

//监听对象 watch(对象名,(新值) => {})
watch(
my,
(newMy) => {
console.log("修改值:",newMy);
},
{immediate: true,deep: true} // immediate 页面刷新就监听, deep 开启深度监听
)

function setNum(){
num.value += 1
}
function setObjectOne(){
my.name = "小丽"
my.age = 113
}
function setMy(){
my.name = "小美"
my.age = 29
my.hobby.one = "篮球"
}
</script>


<!-- 注意事项:
immediate: true 开启页面刷新即监听
deep: true 开启深度监听 reactive自动开启 ref手动开启
当有两个以上的嵌套时,如果ref不开启监听不到!!!
-->

watchEffect自动监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref, watchEffect } from 'vue'

const num = ref(1)

watchEffect(() => {
// 里面用到了 num → 自动监听 num
console.log('num 变了:', num.value)
})
</script>

<!--
一开始立即执行一次
num.value 一变 → 重新执行
无法获取旧值
不用写 immediate: true,默认就执行 -->

十二、标签ref属性和scoped样式

1
<style scoped> </style>     scoped表示style写的所有样式仅在当前页面生效!!!

ref(可用于dom和组件上):

dom:

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
<template>
<div>
<p ref="father">ref获取当前标签</p>
<button @click="changeColor">点击变红</button>
</div>

</template>


<script setup lang="ts">
import {ref,onMounted} from 'vue'
let father = ref()

//为什么不直接使用document?? Vue 是异步更新的,你拿不到 DOM
// const p = document.querySelector("p")


//挂载之后可拿到
onMounted(() => {
console.log('获取DOM:', father.value)
})

function changeColor() {
father.value.style.color = 'red' //修改元素信息
}

</script>

父组件调用子组件方法和数据:

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
<!-- 子组件 -->
<template>

</template>

<script setup lang="ts">
import {ref} from 'vue'

let a = ref(0)
let b = ref(1)
let c = ref(2)

function child(){
return "我是子组件"
}

defineExpose({a,b,c,child}) //1.必须导入出去 defineExpose
</script>



<!-- 父组件 -->
<template>
<div>
<button @click="show">点我显示</button>
<ToRef_s ref="childs" /> <!-- 2.使用子组件-->
</div>

</template>


<script setup lang="ts">
import ToRef_s from './ToRef_s.vue';
import {ref} from 'vue'


let childs = ref()

function show(){
if(childs.value){
console.log(childs.value.a); //获取子组件数据
console.log(childs.value.b);
console.log(childs.value.c);
console.log(childs.value.child());
}
}
</script>

十三、vue3生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
创建阶段

setup() // 最早执行,没有 this

挂载阶段

onBeforeMount // 挂载前

onMounted ✅ 最常用:DOM 渲染完,发请求、操作DOM

更新阶段

onBeforeUpdate // 数据更新,DOM 还没更

onUpdated // DOM 更新完成

卸载阶段

onBeforeUnmount // 销毁前

onUnmounted ✅ 常用:清除定时器、取消监听

演示:

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
<template>
<div>
<h2>生命周期演示</h2>
<p>计数:{{ count }}</p>
<button @click="count++">点我更新</button>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

const count = ref(0)
let timer: number | null = null

console.log('========== setup 最先执行 ==========')

// 1. 挂载完成:DOM 已渲染
onMounted(() => {
console.log('onMounted:组件挂到页面了')
console.log('可以操作 DOM、发请求、开定时器')

// 开定时器
timer = setInterval(() => {
console.log('定时器运行中...')
}, 1000)
})

// 2. 更新完成:数据变 → DOM 变完
onUpdated(() => {
console.log('onUpdated:页面更新完成')
})

// 3. 卸载之前:清理工作
onUnmounted(() => {
console.log('onUnmounted:组件要销毁了')

// 必须清理定时器!
if (timer) {
clearInterval(timer)
console.log('定时器已清理')
}
})
</script>

<!-- 注:在父子组件中,子组件会被先挂载,父后挂载 -->

十四、自定义Hook

把组件里重复的逻辑抽出来,单独放一个 js/ts 文件,这就叫自定义 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/hook/..
//ts文件
import {ref} from 'vue'

export function useNum(){ //一般已 use开头!!!!
let num = ref(1)

const addNum = () => num.value++;

return {
num,
addNum
}
}
1
2
3
4
5
6
7
8
9
10
11
<template>
<div>{{ num }}</div>
<button @click="addNum">点击增加</button>
</template>


<script setup lang="ts">
import { useNum } from '../hook/useNum' //导入

const {num,addNum} = useNum() //解构即可使用
</script>

案例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//te文件    封装鼠标位置
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
const x = ref(0)
const y = ref(0)

const update = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}

onMounted(() => {
window.addEventListener('mousemove', update)
})

onUnmounted(() => {
window.removeEventListener('mousemove', update)
})

return { x, y }
}
1
2
3
4
5
6
7
8
9
10
<template>
<p>鼠标位置: {{ x }},{{ y }}</p>
</template>


<script setup lang="ts">
import { useMouse } from '../hook/useMouse'

const {x,y} = useMouse()
</script>

十五、路由

快速入门

下载路由:

1
npm install vue-router		#如果使用了pnpm的项目, 则改成  pnpm install vue-router

创建页面文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- src/views/..   或者   src/pages/..   -->
<!-- Home.vue文件 -->
<template>
<div>这是首页</div>
</template>

<script setup lang="ts">

</script>


<!-- My.vue文件 -->
<template>
<div>这是我的</div>
</template>

<script setup lang="ts">

</script>

创建路由文件:

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
// src/route/index.ts
import {createRouter,createWebHistory} from 'route'
import My from '../views/My.vue'
import Home from '../views/Home.vue'

const route = createRouter({
history:createWebHistory(), //路由工作模式 createWebHashHistory() 为老式,http://localhost:5173/#/home 会带 # 号!!!
routes:[
{
path: "/"
//redirect: '/home' //重定向到home页面
redirect: { //带参数重定向
path: '/father/son',
query: { id: 666 }
}
},
{
path: "/home", //每一个对象都是一个页面路由
component: Home
},
{
path: "/my",
component: My
}
]
})

export default route

mian.ts文件引入路由:

1
2
3
4
5
6
7
import {createApp} from 'vue'
import route from './route/index'
import App from './app.vue'

const app = createApp(App)
app.use(route) //使用路由 use
app.mount("#app")

app.vue测试路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import { RouterLink,RouterView } from 'vue-router'; //引入 RouterLink 跳转链接 RouterView 路由视图,当路由跳转显示位置
</script>

<template>
<div>
<p>路由测试</p>
<RouterLink to="/home">首页</RouterLink> <!-- to和它搭配!!!! -->

<router-link to="/my">我的</router-link> <!-- vue传统写法 -->

<div>
<p>路由页面</p>
<RouterView></RouterView> <!-- 此时你的路由页面在这里显示 <router-view></router-view> 传统写法-->
</div>
</div>
</template>

注意事项:

1.路由组件通常放到pages或view文件夹里面,一般组件放到component文件夹

2.路由组件切换另一个路由组件时,前面的会被卸载掉,需要使用的时候又会重新挂载!!!!

3.路由工作模式:

history模式
格式: history:createWebHistory(); 优点:url不带井号,更接近传统网站url,但需要配合服务端处理路径问题,否则有404
hash模式:
格式: history:createWebHashHistory(); 优点:兼容性更好,不需要服务端配合,且对搜索引擎优化差

4.to的两种写法

写法1:<RouterLink to="/news" active-class="active">新闻</RouterLink> to=”/路由路径”
写法2:<RouterLink :to={path:”/news”} active-class=”active”>新闻</RouterLink> :to={path:”/路由路径”}

嵌套路由

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
import {createRouter,createWebHistory} from 'vue-router'
import My from '../pages/My.vue'
import Home from '../pages/Home.vue'
import Name from '../pages/Name.vue'



const route = createRouter({
history: createWebHistory(),
routes: [
{
path: "/home",
component: Home,
children:[ //使用children标签
{path: "name",component: Name}
]
},
{
path: "/my",
component: My
}
]
})


export default route
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<p class="one">这是首页</p>
<p class="two">访问次数: {{ count }}</p>
<button @click="addCount">点击</button>
<!-- 子路由出口(必须写) 子组件在这里显示 -->
<router-view />

<router-link to="/home/name">去 name 子页面</router-link>
</div>

</template>

<script setup lang="ts">
import {ref} from 'vue'

let count = ref(1)

function addCount() {
count.value++;
}
</script>

路由传参

如果我要获取父组件发来的消息,怎么办?? 使用 useRoute; useRoute里面含有query方法,里面携带参数值!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<ul>
<li>子组件</li>
<li>{{ routeData.query.id }}</li> <!--自带 qurery属性 -->
<li>{{ routeData.query.data }}</li>
</ul>
</template>


<script setup lang="ts">
import {useRoute} from 'vue-router'
let routeData = useRoute()
</script>

父组件方式1(普通路径传参):

1
2
3
4
5
6
7
8
9
10
<template>
<div>
父组件
<router-link to="/father/son?id=1&data=今天美">跳转</router-link> <!--标准地址-->
<router-view></router-view>
</div>

</template>
<script setup lang="ts">
</script>

父组件方式2(模板字符串传参):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
父组件
<router-link :to="`/father/son?id=${id}&data=${data}`">跳转</router-link> <!-- 使用 :to 解析js语句-->
<router-view></router-view>
</div>

</template>


<script setup lang="ts">
import {ref} from 'vue'

let id = ref(1)
let data = ref("天气良好")
</script>

父组件传参方式3(对象方式传递):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
父组件
<router-link :to="{ <!-- path 路径, query 参数 -->
path:'/father/son',
query:{
id,
data
}
}"
>跳转</router-link>
<router-view></router-view>
</div>

</template>


<script setup lang="ts">
import {ref} from 'vue'

let id = ref(1)
let data = ref("天气良好")
</script>

父组件传参方式4(params占位符传递):

路由:

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
import {createRouter,createWebHistory} from 'vue-router'
import My from '../pages/My.vue'
import Home from '../pages/Home.vue'
import Name from '../pages/Name.vue'
import father from '../pages/father.vue'
import son from '../pages/son.vue'


const route = createRouter({
history: createWebHistory(),
routes: [
{
path:"/father",
component: father,
children:[
{
path: "son/:id/:data?", //1.使用占位符 :/ ?表示可不传参
component: son,
name: "son" //name必须写
}
]
}
]
})


export default route

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
父组件
<router-link :to="{
name:'son', <!-- 这个name 和路由的一一对应 -->
params: {id,data} <!-- 使用params传递 子组件使用 params.字段访问-->
}">跳转</router-link>

<router-view></router-view>
</div>

</template>


<script setup lang="ts">
import {ref} from 'vue'

let id = ref(1)
let data = ref("天气良好")
</script>

props配置传参:

路由:

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
import {createRouter,createWebHistory} from 'vue-router'
import My from '../pages/My.vue'
import Home from '../pages/Home.vue'
import Name from '../pages/Name.vue'
import father from '../pages/father.vue'
import son from '../pages/son.vue'


const route = createRouter({
history: createWebHistory(),
routes: [
{
path:"/father",
component: father,
children:[
// {path: "son/:id/:data",component: son,name: "son"}
{
path: "son",
component: son,
props: route => //1.props作为函数使用,此时有一个参数,表示useRoute!!!! 里面有query!!
route.query //返回所有父组件传递的参数!!!!
}
]
}
]
})


export default route

父组件(path + query):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
父组件
<router-link :to="{
path:'/father/son',
query:{
id,
data
}
}"
>跳转</router-link>

<router-view></router-view>
</div>

</template>


<script setup lang="ts">
import {ref} from 'vue'

let id = ref(1)
let data = ref("天气良好")
</script>

子组件(defineProps):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<ul>
<li>子组件</li>
<!-- <li>{{ routeData.params.id }}</li>
<li>{{ routeData.params.data }}</li> -->
<li>{{ id }}</li>
<li>{{ data }}</li>
</ul>
</template>


<script setup lang="ts">
defineProps(["id","data"]) //获取参数
</script>

编程式导航(useRouter)

在开发中,不可以只用<RouterLink>标签进行路由跳转,需要使用函数式导航!!

如果我想要点击按钮也可以查看新闻,该怎么办??? 引入 useRouter

1. useRouter().push跳转方式:

跳转方式1(无参数跳转):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import { RouterLink,RouterView,useRouter } from 'vue-router';

const route = useRouter()

function pushOne(){
route.push("/home") //无参数跳转 route.push("路径名")
}
</script>

<template>
<div>
<p>路由测试</p>
<button @click="pushOne">无参数跳转</button> <br>


<div>
<p>路由页面</p>
<!-- <RouterView></RouterView> -->
<router-view></router-view>
</div>
</div>
</template>

跳转方式2(带query参数跳转):

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
<script setup lang="ts">
import { RouterLink,RouterView,useRouter } from 'vue-router';

const route = useRouter()

function pushOne(){
route.push({
path: "/my",
query: {
name: "小陈"
}
})
}
</script>

<template>
<div>
<p>路由测试</p>
<button @click="pushOne">带query参数跳转</button> <br>


<div>
<p>路由页面</p>
<!-- <RouterView></RouterView> -->
<router-view></router-view>
</div>
</div>
</template>


<!-- 接收的页面使用 useRoute 接取即可!! -->

跳转方式3(带params参数跳转):

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
<script setup lang="ts">
import { RouterLink,RouterView,useRouter } from 'vue-router';

const route = useRouter()

function pushOne(){
route.push({
path: "/my",
params: {name: "小陈"}
}).catch(err => {console.log("跳转出错",err)}) //设定异常处理
}
})
</script>

<template>
<div>
<p>路由测试</p>
<button @click="pushOne">带params参数跳转</button> <br>


<div>
<p>路由页面</p>
<!-- <RouterView></RouterView> -->
<router-view></router-view>
</div>
</div>
</template>


<!-- 动态路径需要在route路由文件修改 -->

2. useRoute()r.back()返回 / forword()前进:

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
<script setup lang="ts">
import { RouterLink,RouterView,useRouter } from 'vue-router';

const route = useRouter()

function pushOne(){
route.push({
path: "/my",
query: {
name: "小陈"
}
})
}

function backOne(){
route.back() //返回上一页
}
</script>

<template>
<div>
<p>路由测试</p>

<button @click="pushOne">带query参数跳转</button> <br>
<button @click="backOne">返回上一页</button> <br>



<div>
<p>路由页面</p>
<!-- <RouterView></RouterView> -->
<router-view></router-view>
</div>
</div>
</template>

3. useRouter().replace()跳转并不留下历史记录

用法和 push一模一样这里不重复演示!!!!
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
<script setup lang="ts">
import { RouterLink,RouterView,useRouter } from 'vue-router';

const route = useRouter()

function backOne(){
route.back()
}

function replaceOne(){
// route.push("/home") //无参数跳转
route.push({
path: "/my",
query: {
name: "小陈"
}
})
}

</script>

<template>
<div>
<p>路由测试</p>
<button @click="replaceOne">跳转并删除历史记录</button> <br>


<div>
<p>路由页面</p>
<!-- <RouterView></RouterView> -->
<router-view></router-view>
</div>
</div>
</template>

4. useRouter().go() 前进和后退

1
2
3
router.go(-1)   // 后退一页(同 router.back())
router.go(-2) // 后退两页
router.go(1) // 前进一页

5.useRouter().addRoute() 动态添加路由

1
2
3
4
5
6
function addRoutes(){
route.addRoute({
path: "/my",
component: Son2
})
}

6.useRoute().hasRoute() 判断路由存在

1
2
3
4
5
6
7
if (!router.hasRoute('Son')) {		//一般作用于动态路由
router.addRoute({
name: 'Son',
path: '/father/son',
component: Son
})
}

7.useRouter.currentRoute()

1
2
3
4
5
6
7
8
9
10
11
12
import { useRouter } from 'vue-router'
const router = useRouter()

// 获取当前路由对象
const route = router.currentRoute.value

console.log(route.path)
console.log(route.query)
console.log(route.params)
console.log(route.fullPath)

//可以在任何 ts js文件获取路由参数, useRoute只能在setup函数获取!!!!!

路由守卫

全局路由守卫:

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
import {createRouter,createWebHistory} from 'vue-router'
import My from '../pages/My.vue'
import Home from '../pages/Home.vue'
import Name from '../pages/Name.vue'
import father from '../pages/father.vue'
import son from '../pages/son.vue'

const route = createRouter({
history: createWebHistory(),
routes: [
{
path: "/home",
component: Home,
children:[
{path: "name",component: Name}
]
},
{
path: "/my",
component: My
}
]
})

route.beforeEach((to,from,next) => {
// 1. 获取 token(判断是否登录)
const token = localStorage.getItem('token')

if(to.path != "/my"){ //只要路径不是 /my 直接放行
return next() //直接放行
}

if(token){
next()
}else{
alert("没有登录!!")
next("/home") //返回首页
}
})


// 每次跳转完 → 自动改页面标题 一般用来改标题、埋点
router.afterEach((to) => {
document.title = to.meta.title || '后台管理系统'
})

export default route

独享路由守卫:

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
import {createRouter,createWebHistory} from 'vue-router'
import My from '../pages/My.vue'
import Home from '../pages/Home.vue'
import Name from '../pages/Name.vue'
import father from '../pages/father.vue'
import son from '../pages/son.vue'



const route = createRouter({
history: createWebHistory(),
routes: [
{
path: "/home",
component: Home,
meta: {
title: "首页"
},
children:[
{path: "name",component: Name}
],
beforeEnter: (to,from,next) => { //独享路由守卫
//逻辑...
next()
}
}
})
export default route

组件内守卫(onBeforeRouteLeave):

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
<template>
<div>
<h1>表单页面</h1>
<input
type="text"
v-model="formData"
placeholder="请输入内容"
@input="handleInput"
/>
<button @click="save">保存</button>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'

const formData = ref('')
const isChanged = ref(false)

// 输入后标记为已修改
const handleInput = () => {
isChanged.value = true
}

// 保存后重置状态
const save = () => {
// 模拟保存
isChanged.value = false
alert('保存成功')
}

// 离开页面时判断是否未保存
onBeforeRouteLeave((to, from) => {
if (isChanged.value) {
return '您有未保存的内容,确定要离开吗?'
}
})
</script>

十六、pinia

pinia是什么?

它也充当的是一个存储数据的作用,存储在pinia的数据允许我们在各个组件中使用。

简单来说就是一个存储数据的地方,存放在Vuex中的数据在各个组件中都能访问到,它是Vue生态中重要的组成部分。

1.安装与配置:

1
2
npm i pinia     #如果是pnpm 使用  pnpm i pinia 
npm i pinia-plugin-persistedstate #数据持久化,刷新页面不丢失 #如果是pnpm 使用pnpm

main.ts文件:

1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue'
import {createPinia} from 'pinia' //1.引入创建 细节: 这里引入必须在 import App 之前!!!!
import persist from 'pinia-plugin-persistedstate' //开启持久化如果不使用可以不写!!!!!
import './style.css'
import App from './App.vue'


const app = createApp(App)
const pinia = createPinia() //2.创建pinia

app.use(pinia.use(persist)) //3.使用pinia + 持久化 如果不使用: app.use(pinia) 即可
app.mount("#app")

创建文件(src/stores/..)

选项式(类似vue2):

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
import { defineStore } from "pinia";
import "pinia-plugin-persistedstate"; //开启持久化,必须引入 ts报错

const useStore = defineStore('user',{ // user 相当于 key
//数据存放
state: () => ({
name: "小陈",
age: 20,
token: ''
}),
persist : true, //开启持久化
getters: {
NewAge: (state) => state.age * 2 //getters可以对数据二次加工
},
actions: { //actions 相当于方法
updateName(name:string) {
this.name = name
},
login(token:string){
this.token = token
}
}
})


export default useStore

组合式(类型vue3 推荐):

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
import { defineStore } from "pinia";
import "pinia-plugin-persistedstate";
import {ref,computed} from 'vue'

const useStore = defineStore('user',() => { //这里使用函数,不是对象!!!!
let name = ref("小陈")
let age = ref(20)
let token = ref('')

let NewAge = computed(() => age.value * 2)

function updateName(Newname:string) {
name.value = Newname
}

function login(NewToken:string){
token.value = NewToken
}

function persists(){
token.value = "10001"
}


return{ //这里必须return出去
name,
age,
token,
NewAge,
updateName,
login,
persists
}
},{
persist: true //开启持久化 defineStore("id",() => {...},{persist: true})
})


export default useStore

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
25
26
27
28
29
30
31
32
33
34
<template>
<button @click="per">修改token</button>
</template>


<script setup lang="ts">
import useStore from '../stores' //1.引入

const store = useStore()

//获取数据
console.log(store.age);
console.log(store.$state.name); //也可以使用 $state 获取
console.log(store.NewAge);

//修改数据 3种方式
store.name = "小王" //直接修改
store.updateName("小丽") //使用函数,推荐!!
store.$patch({ //使用 $patch函数批量修改
name: "校长",
age: 56
})
console.log("修改后的名称:" + store.name);

store.login("10000")
console.log(store.token);

//演示开启持久化区别 不开启: 点击按钮修改,此时刷新页面修改失效 开启没问题
function per(){
store.persists()
console.log(store.token);
}

</script>

3.解构pinia数据(storeToRefs)

如果我想将他们进行解构,但数据不会是响应式的,怎么办?? 使用storeToRefs

1
2
3
4
5
6
7
8
9
10
11
<script>
import { useCountStore } from '@/store/counts';
import {storeToRefs} from 'pinia';

let countStore = useCountStore()

let {sum,school,address} = storeToRefs(countStore); //此时数据为响应式

//提问:可以使用toRefs吗?? 可以,但是他会输出所有数据,使性能变差,所以一般不使用!!
</script>

4.pinia监听器($subscribe)

1
2
3
4
5
6
store.$subscribe((mutation, state) => {
// state 就是最新的整个仓库数据
console.log('最新 token:', state.token)
})

//可以在ts文件使用vue3的 watch

十七、自定义事件 defineEmits()

@click@input 是浏览器自带事件

你自己起个名字,比如 @getMsg@update → 这就叫自定义事件

代码流程:

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
<!-- 创建一个组件 -->
<template>
</template>


<script setup lang="ts">
const emit = defineEmits(["data"]) //使用defineEmits(['data']) 导出自定义事件
emit("data",123) //存入数据 emit("自定义事件名",数据)
</script>




<!-- 使用 -->
<template>
<div>
<Son @data="SonMsg"></Son> <!-- 这里使用自定义事件名 -->
<p>
{{childMsg}}
</p>
</div>

</template>


<script setup lang="ts">
import { ref } from 'vue';
import Son from './son.vue'

let childMag = ref()


let SonMsg = (val:any) => { //val就是数据
console.log(val);
childMag.value = val
}

</script>

表单案例:

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
<template>
<div>
<form action="">
姓名:<input type="text" v-model="form.username">
密码: <input type="password" v-model="form.password">
<button type="button" @click="send">提交</button>
</form>
</div>
</template>


<script setup lang="ts">
import {ref} from 'vue'

let form = ref({
username: '',
password: ''
})

const emit = defineEmits(["data"])


function send(){
emit("data",form.value)
}
</script>


<!-- 使用 -->
<template>
<div>
<p>父盒子</p>
<Son @data="SonMsg"></Son>
<div v-if="childMag">
<p>姓名: {{ childMag.username }}</p>
<p>密码: {{ childMag.password }}</p>

</div>

</div>

</template>


<script setup lang="ts">
import { ref } from 'vue';
import Son from './son.vue'

let childMag = ref()


let SonMsg = (val:any) => {
console.log(val);
childMag.value = val
}

</script>

十八、组件通讯

1.父子通讯

父传子(defineProps):

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
<!-- 父组件  -->
<template>
<ToRef_s :msg="message" :age :count="999"/> <!-- 细节: 必须是 :变量名 格式, 变量名一致可省略,也可直接赋值!!!!-->
</template>


<script setup lang="ts">
import ToRef_s from './ToRef_s.vue';
import {ref} from 'vue'

let message = ref("父亲的语句")
let age = ref(50)

</script>


<!-- 子组件 -->
<template>
<div>{{ msg }}</div>
<div>{{ age }}</div>
<div>{{ count }}</div>
</template>


<script setup lang="ts">
import {ref} from 'vue'

// const data = defineProps({ //普通语法
// msg:String,
// age: Number,
// count: Number
// })

const props = defineProps<{ //ts语法
msg?: String
age?: Number
count?: Number
}>()
</script>

子传父(自定义事件名 ):

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
<!-- 子组件 -->
<template>
<div>
<button @click="send">发送给父</button> <!-- 1.设置一个点击事件 -->
</div>
</template>


<script setup lang="ts">
const emit = defineEmits(["childMsg"]) //3.导出自定义数据名 defineEmits()


function send(){
emit('childMsg',"我是子组件") // 2.使用 emit("自定义事件名",数据)
}
</script>



<!-- 父组件 -->
<template>
<div>
<Son @childMsg="SonMsg"></Son> <!-- 5.在子组件使用自定义事件名,获取数据 -->
<p>{{ childMag }}</p>
</div>

</template>


<script setup lang="ts">
import { ref } from 'vue';
import Son from './son.vue' // 4.获取子组件

let childMag = ref()


let SonMsg = (val:string) => { // 6.使用函数,获取数据 val就是数据!!!
console.log(val);
childMag.value = val
}

</script>

2.祖孙通讯(了解)

方式1($attr):

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
<!-- 爷爷组件 -->
<template>
<div>
<h2>爷爷</h2>
<Father name="张三" age="18" msg="来自爷爷" /> <!-- 数据传递给父亲 -->
</div>
</template>

<script setup>
import Father from './Father.vue'
</script>


<!-- 父亲组件 -->
<template>
<div>
<h3>爸爸(中间人)</h3>
<Son v-bind="$attrs" /> <!-- 继续传递,如果父亲在此使用 defineProps() 读取部分数据, 剩下的会继续传递,使用的不会!!! -->
</div>
</template>

<script setup>
import Son from './Son.vue'
</script>


<!-- 孙子组件 -->
<template>
<div>
<h4>孙子</h4>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>消息:{{ msg }}</p>
</div>
</template>

<script setup>
defineProps(['name', 'age', 'msg']) //直接使用 defineProps() 读取即可
</script>

方式2(provide /inject):

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
<!-- 爷爷组件  -->
<template>
<div class="grandpa">
<h3>我是爷爷</h3>
<Son/>
</div>
</template>

<script setup lang="ts">
import { provide } from 'vue'
import Son from './son.vue'

// 给后代提供数据 provide("名称",数据)
provide('gift', '爷爷给的红包')
</script>


<!-- 父亲组件 什么操作都不要-->
<template>
<div class="father">
<h3>我是爸爸(中间人)</h3>
<Child/>
</div>
</template>

<script setup lang="ts">
import Child from './grandChild.vue'
</script>


<!-- 孙子组件 -->
<template>
<div class="son">
<h3>我是孙子</h3>
<p>拿到:{{ gift }}</p>
</div>
</template>

<script setup lang="ts">
import { inject } from 'vue'

// 直接拿爷爷 provide 的东西
const gift = inject('gift')
</script>

3.父组件修改子组件数据 ref和defineExpose

在第九,这里不演示

4.获取 子、父实例(了解)

父获得子($ref):

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
<!-- 子组件 -->
<script setup>
const sayHi = () => {
alert('你好,我是子组件!')
}

// 必须暴露,外面才能访问
defineExpose({ sayHi })
</script>


<!-- 父组件 -->
<template>
<div>
<Son ref="mySon" />
<button @click="get($refs)"></button> <!-- 使用 $refs -->
</div>
</template>

<script setup>
import Son from './Son.vue'
import { ref } from 'vue'

// 对应模板里的 ref="mySon"
const mySon = ref(null)

function get(x){
x.value.sayHi()
}
</script>

子获得父($parent)

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
<!--  子组件 -->
<template>
<div class="child2">
<h3>子组件2</h3>
<button @click="minusHouse($parent)">干掉父亲房产</button> //1.创建函数,参数是: $parent
</div>
</template>

<script setup lang="ts" name="Child2">
function minusHouse(value:any){
value.house -= 1 //2.进行操作属性
}
</script>



<!-- 父组件 -->
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import { ref,reactive } from "vue";

let house = ref(4)
let c1 = ref()
let c2 = ref()

defineExpose({house}) //3.将父属性进行公开!!!!!
</script>


5. 全局组件通信 mitt(了解)

Mitt 就是 Vue3 里最简洁的「全局事件总线」,用来解决 任意组件间通信(父子、祖孙、兄弟、跨页面),比 $parent$attrs 干净、通用得多。

安装与配置:

1
npm i mitt    #如果是 pnpm   使用 pnpm i mitt

写一个util文件:

1
2
3
4
5
6
7
//src/util/..

import mitt from 'mitt'

const emitter = mitt()

export default emitter

常用API:

方法 作用 用法示例 说明
on(事件名, 回调) 监听事件 bus.on('msg', (data) => {}) 等待别人发送,一触发就执行
emit(事件名, 数据) 触发事件 bus.emit('msg', 'hello') 发送消息,可带参数
off(事件名, 回调) 取消监听 bus.off('msg', fn) 组件销毁时必须用,防内存泄漏
on('*', 回调) 监听所有事件 bus.on('*', (type, data) => {}) 监听任意事件触发
all 所有事件集合 bus.all 很少用,清空事件用

代码演示:

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
<!-- 发送者 -->
<template>
<button @click="datas">点我测试</button>
</template>

<script setup lang="ts">
import mitter from '../util/mitter'

function datas() {
mitter.emit('grand', '我是传递的数据')
}
</script>



<!-- 接收者 -->
<template>
<div>接收组件</div>
</template>

<script setup lang="ts">
import mitter from '../util/mitter'
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
mitter.on('grand', (msg) => { //获取
alert("收到数据:" + msg)
})
})

onUnmounted(() => {
mitter.off('grand') //销毁
})
</script>


<!-- 注: app.vue要同时使用这两个组件 -->

十九、插槽

在组件中,如果我想在里面写入标签,如: <Gategory> <span>111<span> </Gateory>,但span不会生效,怎么办??

使用 <slot></slot> 默认插槽

让父组件可以往子组件里塞 HTML 内容

默认插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 子组件 -->
<template>
<button>子按钮</button>
<!-- 插入的标签放到这里面 只能有一个默认插槽!!-->
<slot></slot>
</template>

<script setup lang="ts">

</script>



<!-- 父组件 -->
<template>
<son>
<h1>插入内容</h1> <!-- 直接在子组件插入 -->
</son>
</template>

<script setup lang="ts">
import son from './son.vue'

</script>

具名插槽

给插槽写入name属性就叫具名插槽

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
<!-- 子组件 -->
<template>
<button>子按钮</button>
<!-- 插入的标签放到这里面-->
<slot></slot>

<footer>
<slot name="floor"></slot> <!-- 具名插槽 可以有多个-->
<slot name="two">hello</slot> <!-- 如果父组件没有使用这个插槽,默认内容显示 hello
</footer>
</template>

<script setup lang="ts">

</script>


<!-- 父组件 -->
<template>
<son>
<h1>插入内容</h1> <!-- 直接在子组件插入 -->
<template #floor> <!-- 使用 template 标签 # + 具名名称 <template #floor />-->
<p>结语:好好敲!!!</p>
</template>
</son>

</template>

<script setup lang="ts">
import son from './son.vue'

</script>

作用域插槽

子组件把数据传给父组件插槽用。

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
<!-- 子组件 -->
<template>
<button>子按钮</button>
<!-- 插入的标签放到这里面-->
<slot></slot>

<main>
<slot name="main" :data="{name: '小陈',age : 20}"></slot> <!-- 具名 + 作用域(data) -->
</main>

<footer>
<slot name="floor"></slot> <!-- 具名插槽 可以有多个-->
<slot name="two">hello</slot> <!-- 如果父组件没有使用这个插槽,默认内容显示 hello
</footer>
</template>

<script setup lang="ts">

</script>


<!-- 父组件 -->
<template>
<son>
<h1>插入内容</h1> <!-- 直接在子组件插入 -->

<template #main="{ data }"> <!-- {} 表示解构的数据 -->
传递数据:{{ data.name }},{{ data.age }}

</template>
<template #floor> <!-- 使用 template 标签 # + 具名名称 <template #floor />-->
<p>结语:好好敲!!!</p>
</template>
</son>

</template>

<script setup lang="ts">
import son from './son.vue'

</script>

二十、全局API

1. app.createApp()

作用:创建 Vue 实例(入口)

1
2
3
4
5
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

2. app.component()

作用:注册全局组件(任何页面都能用)

1
2
3
// 全局注册组件
import Card from './components/Card.vue'
app.component('Card', Card)

3. app.use()

作用:安装插件(最常用)

1
2
3
4
import Pinia from 'pinia'
import router from './router'
app.use(Pinia)
app.use(router)

4. app.provide()

作用:全局注入值(所有组件都能 inject 拿到)

1
app.provide('globalMsg', '我是全局数据')

二十一、其他常用API

1.shallowRef和shallowReactove

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
shallowRef:
只监听 .value 的变化,内部属性不监听
<script setup>
import { shallowRef } from 'vue'

const obj = shallowRef({
name: '小明',
info: { age: 18 }
})

// 不会更新视图 ❌(内部属性不监听)
const change1 = () => {
obj.value.name = '小红'
}

// 会更新视图 ✅(因为改了 .value)
const change2 = () => {
obj.value = { name: '小红' }
}
</script>



shallowReactove:
只监听对象第一层,深层不监听
<script setup>
import { shallowReactive } from 'vue'

const obj = shallowReactive({
name: '小明',
info: { age: 18 }
})

// 会更新 ✅(第一层)
const change1 = () => {
obj.name = '小红'
}

// 不会更新 ❌(深层)
const change2 = () => {
obj.info.age = 20
}
</script>

2.readonly和shallReadonly

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
readonly:
深度只读,所有层级都不能改
<script setup>
import { reactive, readonly } from 'vue'

const obj = reactive({
name: 'aaa',
info: { age: 18 }
})

const readOnlyObj = readonly(obj)

// 全部报错 ❌
readOnlyObj.name = 'bbb'
readOnlyObj.info.age = 20
</script>


shallReadonly:
浅只读,只有第一层不能改,深层可以改
<script setup>
import { reactive, shallowReadonly } from 'vue'

const obj = reactive({
name: 'aaa',
info: { age: 18 }
})

const sReadonly = shallowReadonly(obj)

// 第一层:报错 ❌
sReadonly.name = 'bbb'

// 深层:可以改 ✅
sReadonly.info.age = 20
</script>

3.toRow和markRaw

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
toRow:
从 reactive/readonly 代理中,取出原本的普通对象(脱离响应式)。
修改原始对象 不触发视图更新
只对 reactive/readonly 有效,对 ref 对象要 .value
<script setup>
import { reactive, toRaw } from 'vue'

const state = reactive({ name: '小明', age: 18 })

// 获取原始对象
const rawState = toRaw(state)

// 修改响应式 → 视图更新 ✅
const changeReactive = () => {
state.age++
}

// 修改原始 → 不更新 ❌
const changeRaw = () => {
rawState.age++
}

console.log(state === rawState) // false(代理 vs 原始)
console.log(toRaw(state) === rawState) // true
</script>



markRaw:
给对象打标记 → 永远不会被 reactive 变成响应式。
<script setup>
import { reactive, markRaw } from 'vue'

// 标记为“永远原始”
const config = markRaw({
theme: 'dark',
api: 'https://xxx'
})

// 放进 reactive 也不会变响应式
const state = reactive({
user: { name: '小红' }, // 响应式 ✅
config // 永远非响应式 ❌
})

// 修改 config → 不触发更新
const changeConfig = () => {
state.config.theme = 'light'
}

// 修改 user → 正常更新 ✅
const changeUser = () => {
state.user.name = '小兰'
}
</script>

4.customRef

自己封装一个自定义 ref,可以手动控制依赖收集触发更新,常用于做防抖、节流、懒加载等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script
import { customRef } from 'vue'

function myRef(value) {
return customRef((track, trigger) => {
return {
get() {
track() // 收集依赖
return value
},
set(newVal) {
value = newVal
trigger() // 触发更新
}
}
})
}
</script>

防抖案例:

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
<script setup>
import { customRef } from 'vue'

// 防抖 ref
function useDebounceRef(value, delay = 300) {
let timer = null

return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newVal) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newVal
trigger()
}, delay)
}
}
})
}

// 使用
const keyword = useDebounceRef('', 500)
</script>

<template>
<input v-model="keyword" />
<p>{{ keyword }}</p>
</template>

5.Teleport传送标签

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
<template>
<div>
<button @click="show = true">打开</button>

<Teleport to="body">
<div class="modal">
弹窗内容
<button @click="show = false">关闭</button>
</div>
</Teleport>
</div>
</template>

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

const show = ref(false)
const isMounted = ref(false)

// 等组件挂载完成再渲染 Teleport
onMounted(() => {
isMounted.value = true
})
</script>

<style>
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,.5);
z-index: 9999;
color: white;
}
</style>

6.Suspense加载标签

等待异步组件加载,期间显示 loading

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
<!-- AsyncPage.vue -->
<script setup>
// 里面有 await 就会变成异步组件
await new Promise(resolve => setTimeout(resolve, 1500))
</script>

<template>
<div>我是异步加载的页面</div>
</template>



<script setup>
// 直接引入
import AsyncPage from './AsyncPage.vue'
</script>

<template>
<Suspense>
<!-- 真正要显示的内容 -->
<template #default>
<AsyncPage /> <!-- 异步内容 -->
</template>

<!-- 加载中显示的内容 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>

7.defineAsyncComponent 异步组件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { defineAsyncComponent } from 'vue'

// 定义异步组件
const Dialog = defineAsyncComponent(() =>
import('./components/Dialog.vue')
)

// 也可以加加载中、错误处理
const Dialog2 = defineAsyncComponent({
loader: () => import('./components/Dialog.vue'),
loadingComponent: () => <div>加载中...</div>,
errorComponent: () => <div>加载失败</div>,
delay: 200,
timeout: 5000
})
</script>

<template>
<!-- 用到时才会加载 -->
<Dialog v-if="show" />
</template>

二十二、ElementPlus组件库

ElementPlus组件库,里面有非常多的组件可以供我们使用!!!!!!!

https://element-plus.org/zh-CN/

安装与配置

1
npm i element-plus		#如果是 pnpm  pnpm i element-plus

mian.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue'
import App from './App.vue'

// 引入 ElementPlus
import ElementPlus from 'element-plus'
// 引入样式
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus) // 安装
app.mount('#app')

简单使用规则

组件名全部以 el- 开头

事件和原生一样 @click

绑定数据用 v-model

样式自动带,不用自己写

代码演示

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
<template>
<div>
<h2>Element Plus 测试</h2>

<!-- 按钮 -->
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="danger">危险按钮</el-button>

<!-- 输入框 -->
<el-input v-model="inputValue" placeholder="请输入" />

<!-- 弹窗 -->
<el-dialog v-model="dialogVisible" title="标题">
<span>这是一段信息</span>
</el-dialog>
</div>
</template>

<script setup>
import { ref } from 'vue'

const inputValue = ref('')
const dialogVisible = ref(false)
</script>

常用组件:

1.基础组件

组件名 标签 常用属性
Button 按钮 <el-button> type、size、plain、round、circle、disabled、loading
Link 链接 <el-link> type、disabled、underline、href
Container 布局 <el-container> direction
Header 头部 <el-header> height
Aside 侧边栏 <el-aside> width
Main 内容区 <el-main>
Footer 底部 <el-footer> height
Row 行 <el-row> gutter、justify、align
Col 列 <el-col> span、offset、pull、push、xs/sm/md/lg/xl
Space 间距 <el-space> size、direction、wrap、alignment

2.表单组件

组件名 标签 常用属性
Form 表单 <el-form> model、rules、inline、label-width、status-icon
FormItem 表单项 <el-form-item> prop、label、required、rules
Input 输入框 <el-input> v-model、type、placeholder、disabled、maxlength、clearable
InputNumber 数字框 <el-input-number> v-model、min、max、step、disabled、controls
Select 选择器 <el-select> v-model、placeholder、disabled、clearable、multiple
Option 选项 <el-option> label、value、disabled
Checkbox 多选框 <el-checkbox> v-model、label、disabled、border
Radio 单选框 <el-radio> v-model、label、disabled、border
Switch 开关 <el-switch> v-model、disabled、active-text、inactive-text
DatePicker 日期 <el-date-picker> v-model、type、placeholder、disabled、format
Upload 上传 <el-upload> action、multiple、limit、accept、file-list
Rate 评分 <el-rate> v-model、max、disabled、allow-half、show-score

3.数据展示

组件名 标签 常用属性
Table 表格 <el-table> data、border、stripe、size、height
TableColumn 列 <el-table-column> prop、label、width、fixed、sortable
Pagination 分页 <el-pagination> total、page-size、current-page、layout
Tag 标签 <el-tag> type、size、closable、disable-transitions
Card 卡片 <el-card> header、shadow
Collapse 折叠面板 <el-collapse> v-model、accordion
Tree 树形控件 <el-tree> data、props、node-key、default-expand-all
Image 图片 <el-image> src、alt、fit、lazy、preview-src-list
Empty 空状态 <el-empty> description、image-size

4.导航

组件名 标签 常用属性
Menu 菜单 <el-menu> mode、default-active、background-text-color
SubMenu 子菜单 <el-sub-menu> index、disabled
MenuItem 菜单项 <el-menu-item> index、disabled
Tabs 标签页 <el-tabs> v-model、type、closable、tab-position
Breadcrumb 面包屑 <el-breadcrumb> separator
Dropdown 下拉 <el-dropdown> trigger
Steps 步骤条 <el-steps> active、space、direction

5.反馈

组件名 标签 常用属性
Dialog 弹窗 <el-dialog> v-model、title、width、close-on-click-modal
Drawer 抽屉 <el-drawer> v-model、title、direction、size
Tooltip 提示 <el-tooltip> content、placement、effect
Popover 弹出框 <el-popover> placement、trigger
Alert 警告 <el-alert> title、type、description、closable
Loading 加载 v-loading lock、background、spinner

6.常用 JS 调用(非标签)

方法 调用方式 常用配置
Message 消息提示 ElMessage message、type、duration、showClose
MessageBox 确认框 ElMessageBox.confirm title、message、type
Notification 通知 ElNotification title、message、type、duration

二十三、axios(扩展)

安装

1
npm install axios    #pnpm install axios

使用:

跨域问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//前端解决  vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})


//后端解决
@Controller
@CrossOrigin //声明即可
public class UserController {}

//任选其一

get用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function getList() {		// axios.get("路径",{参数配置})
const res = await axios.get('/api/list', {
// 1. 查询参数 ?id=1&name=xxx
params: { id: 1, name: '小明' },

// 2. 请求头(放token、身份信息最常用)
headers: {
token: 'xxxxxxxxxxxx',
'Content-Type': 'application/json'
},

// 3. 超时时间
timeout: 5000,

// 4. 响应类型(blob下载、arraybuffer、text、stream)
responseType: 'json', // 可选:blob / text / arraybuffer

// 5. 跨域带cookie
withCredentials: true,
})

console.log(res.data)
}

post用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function addData() {
const postData = {
name: "张三",
age: 18
}


// 第三个参数是配置 axios.post("路径",{数据},{配置参数})
const res = await axios.post("/api/add", postData, {
// 请求头 token
headers: {
token: "xxxxxxxxxx",
"Content-Type": "application/json"
},
// 超时
timeout: 10000,
// 跨域带 cookie
withCredentials: true
})

console.log(res.data)
}

delete:

1
2
3
4
5
6
7
8
async function deleteData() {		//和get一样
const res = await axios.delete('/api/delete', {
params: { id: 123 },
headers: { token: 'xxx' },
timeout: 10000,
withCredentials: true
})
}

put:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function addData() {
const postData = {
name: "张三",
age: 18
}


// 第三个参数是配置 axios.put("路径",{数据},{配置参数})
const res = await axios.post("/api/add", postData, {
// 请求头 token
headers: {
token: "xxxxxxxxxx",
"Content-Type": "application/json"
},
// 超时
timeout: 10000,
// 跨域带 cookie
withCredentials: true
})

console.log(res.data)
}

工具封装:

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
import axios from 'axios'

// 1. 创建 axios 实例
const service = axios.create({
baseURL: 'http://localhost:8080', // 后端地址
timeout: 10000 // 超时时间
})

// 2. 请求拦截器(自动带 token)
service.interceptors.request.use(
(config) => {
// 自动从本地拿 token
const token = localStorage.getItem('token')
if (token) {
config.headers.token = token
}
return config
},
(error) => {
return Promise.reject(error)
}
)

// 3. 响应拦截器(统一处理返回值)
service.interceptors.response.use(
(response) => {
// 直接返回 data,不用每次写 res.data
return response.data
},
(error) => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)

// 4. 统一封装 GET POST PUT DELETE
const request = {
// GET 查询
get(url, params = {}) {
return service.get(url, { params })
},

// POST 新增
post(url, data = {}) {
return service.post(url, data)
},

// PUT 修改
put(url, data = {}) {
return service.put(url, data)
},

// DELETE 删除
delete(url, params = {}) {
return service.delete(url, { params })
}
}

export default request