Vue3 进阶
1.vue3 基本语法温习
1.1 构建 vite 项目
构建项目:
- 构建 vite 项目:
yarn create vite
----------(如果是 npm 执行:npm init vite@latest
) - 安装依赖:
yarn
----------(如果是 npm 执行:npm install
)
package.json 配置讲解:
"scripts": {
"dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
"build": "vue-tsc --noEmit && vite build", // vite-build 为为生产环境构建产物
"preview": "vite preview" // 本地预览生产构建产物
}
2
3
4
5
1.2 模板语法 & vue 指令
插值运算符
如下,setup 函数不再需要像 vue3.0 那样需要
return { message, message1, message2 }
了
<template>
<div>
{{ message ? '我是真滴' : '我是假滴' }}
{{ message1.split(',').map(v => `¥${v}`) }}
{{ message2 + 1 }}
</div>
</template>
<script setup lang='ts'>
const message: number = 0
const message1: string = '小,朋,友,啦,啦,啦'
let message2: number = 1
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
绑定 style:
这里的 style 动态绑定了一个对象,是以对象形式进行样式编写的
<template>
<div :style="style">
我是 yuanke
</div>
</template>
<script setup lang='ts'>
type Style = {
color: string,
height: string
}
const style: Style = {
color: 'blue',
height: '300px'
}
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
数组格式:
除了可以写三元运算符外,还可以写
['a', 'b']
——表示同时拥有这两种样式
<template>
<div :class="[flag ? cls : 'b']">我是大大</div>
</template>
<script setup lang='ts'>
let flag: boolean = false
type Cls = {
a: boolean,
b: boolean
}
const cls: Cls = {
a: true,
b: true
}
</script>
<style scoped>
.a {
color: red;
}
.b {
border: 1px solid #ccc;
}
</style>
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>
<input v-model="message" type="text">
<div>
{{ message }}
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
const message = ref('test')
</script>
2
3
4
5
6
7
8
9
10
11
1.3 Vue 核心虚拟 Dom 和 diff 算法
(50条消息) 学习Vue3 第五章(Vue核心虚拟Dom和 diff 算法)_小满zs的博客-CSDN博客open in new window
引例 - 为什么不直接操作 dom 而是用虚拟 dom:
let div = document.createElement('div')
let str = ''
for (const key in div) {
str += key + '---'
}
console.log(str)
2
3
4
5
6
上面的操作会打印出 div 的所有属性,所以直接操作 dom 的操作性能太差
具体的 diff 逻辑可以看本节引言部分
1.4 ref 全家桶
ref 类型写法:
- 第一种写法:
let message = ref<string>('yuanke')
- 第二种写法:
let message:Ref<string> = ref('yuanke')
<template>
<div>
<button @click="changeMsg">change</button>
<div>
{{ message }}
</div>
</div>
</template>
<script setup lang='ts'>
import { ref, Ref } from 'vue'
// 第一种写法
// let message = ref<string>('yuake')
// 第二种写法
let message:Ref<string> = ref('yuanke')
const changeMsg = () => {
message.value = 'change msg'
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
isRef:判断是否为响应式数据
import { ref, Ref, isRef } from 'vue'
let message:Ref<string> = ref('yuanke')
let notRef: number = 1
console.log(isRef(message)) // true
console.log(isRef(notRef)) // false
2
3
4
5
shallowRef、triggerRef:在视图层中不是响应式的,只有在模型层是响应式的
ref 属性会导致 shallowRef 属性强制更新
<template>
<div>
<button @click="changeMsg">change</button>
<div>
{{ message }}
</div>
</div>
</template>
<script setup lang='ts'>
import { shallowRefj, triggerRef } from 'vue'
let message = shallowRef({
name: 'yuanke'
})
const changeMsg = () => {
// message.value.name = 'haha' // 视图层不会更新,因为结果为变量
// triggerRef(message) // 配合上面使用,强制更新,使更改的 message 渲染在视图层上 / 如果没有 triggerRef,但 changeMsg 中如果有一个 ref 数据被改变了,则上面也会同时更新
message.value = { name: 'haha' } // 视图层会更新,因为为对象
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
customRef:是个工厂函数要求我们返回一个对象,并且实现 get 和 set
<template>
<div>
<button @click="changeMsg">change</button>
<div>
{{ message }}
</div>
</div>
</template>
<script setup lang='ts'>
import { customRef } from 'vue'
function MyRef<T>(value: T) {
return customRef((trank, trigger) => {
return {
get() {
// 依赖追踪
trank()
return value
},
set(newVal: T) {
console.log('set')
value = newVal
// 强制更新
trigger()
}
}
})
}
let message = MyRef<string>('yuanke')
const changeMsg = () => {
message.value = 'yuanyuan'
}
</script>
<style scoped>
</style>
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
1.5 reactive 全家桶
异步赋值:
<template>
<div>
arr {{ message }}
</div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue';
type O = {
list: number[]
}
let message = reactive<O>({
list: []
})
setTimeout(() => {
message.list = [1, 2, 3, 4]
}, 500);
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
readonly:拷贝一份 proxy 对象将其设置为只读
<template>
<div>
arr {{ message }}
</div>
</template>
<script setup lang='ts'>
import { reactive, readonly } from 'vue';
let person = reactive({
count: 1
})
let copy = readonly(person)
person.count++
// copy.count++ // 报错,只读
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shallowReactive:只能对浅层的数据,如果是深层的数据只会改变值,不会改变视图
<template>
<div>
arr {{ message }}
</div>
<div>
<!-- 点击后视图层会改变 -->
<button @click="change1">1</button>
<!-- 点击后视图层不会改变,但是下面的值会改变 -->
<button @click="change2">2</button>
</div>
</template>
<script setup lang='ts'>
import { shallowReactive } from 'vue';
let message = shallowReactive({
test: '我是 yuanke',
nav: {
bar: {
name: '我是 keyuan'
}
}
})
const change1 = () => {
message.test = '我被改啦'
}
const change2 = () => {
message.nav.bar.name = '俺被改啦'
console.log(message)
}
</script>
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
1.6 toRef、toRefs、toRaw
toRef:
所以如果想让响应式数据和以前的数据关联起来,并且想在更新响应式数据的时候不更新
UI
,那么就使用toRef
<template>
<div>
<div><button @click="change">改变</button></div>
<div>{{ state }}</div>
</div>
</template>
<script setup lang='ts'>
import { reactive, toRef } from 'vue';
// // 如果 obj 是响应式数据,则视图层会更新
// const obj = reactive({
// foo: 1,
// bar: 1
// })
// 如果 obj 不是响应式数据,则 toRef 会把 obj 的 bar 的响应式引用拿来,然后能改变其值,但是视图层不会更新
const obj = {
foo: 1,
bar: 1
}
// 把 obj 里的 bar 拿出来
const state = toRef(obj, 'bar')
const change = () => {
state.value++
console.log('--->原始对象', obj)
console.log('--->引用对象', state)
}
</script>
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
toRefs:
批量转换。将响应式对象转换为普通对象,会将传入对象的每个属性处理为 ref 的值
<template>
<div>
<div>{{ foo }}</div>
<div>{{ bar }}</div>
<div>
<button @click="change">change</button>
</div>
</div>
</template>
<script setup lang='ts'>
import { toRefs, reactive } from 'vue'
let obj = reactive({
foo: 1,
bar: 1
})
// 相当于 let { foo } = toRefs(obj) & let { bar } = toRefs(obj)
let { foo, bar } = toRefs(obj)
console.log(foo, bar)
const change = () => {
foo.value++
bar.value++
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
toRaw:
将响应式数据变回原始数据
import { toRaw, reactive } from 'vue'
let obj = reactive({
foo: 1,
bar: 1
})
const raw = toRaw(obj)
console.log('响应式的', obj)
console.log('raw', raw)
2
3
4
5
6
7
8
1.7 计算属性
基本形式:
<template>
<div>
<input v-model="firstName" type="text">
<input v-model="lastName" type="text">
</div>
<div>
{{ name }}
</div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue';
let firstName = ref('')
let lastName = ref('')
const name = computed(() => {
return firstName.value + '---' + lastName.value
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
get、set:
<template>
<div>
<input v-model="firstName" type="text">
<input v-model="lastName" type="text">
</div>
<div>
{{ name }}
</div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue';
let firstName = ref('')
let lastName = ref('')
const name = computed({
get() {
return firstName.value + lastName.value
},
set() {
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.8 watch 侦听器
input
框里v-model="name"
时监听name
:(监听 ref)
<template>
<div @click="change">change</div>
<input type="text" v-model="name">
</template>
<script setup lang='ts'>
import { ref, watch } from 'vue'
const name = ref('dell')
// 具备一定的惰性 lazy
// 参数可以拿到原始和当前值
watch(name, (currentValue, prevValue) => {
console.log(currentValue, prevValue)
})
const change = () => {
name.value = name.value + '1'
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 监听 reactive 里的值,必须要用 箭头函数:
<template>
<div @click="change">change</div>
<input type="text" v-model="name">
</template>
<script setup lang='ts'>
import { reactive, watch, toRefs } from 'vue'
const nameObj = reactive({ name: 'dell' })
// 如果要监听 name,一定要使用函数形式
watch(() => nameObj.name, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
const { name } = toRefs(nameObj)
const change = () => {
name.value = name.value + '1'
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 监听多个 reactive 里的值:
<template>
<div @click="change">change</div>
{{ name }}
{{ englishName }}
</template>
<script setup lang='ts'>
import { reactive, watch, toRefs } from 'vue'
const nameObj = reactive({
name: 'dell',
englishName: 'lee'
})
// 监听多个 reactive 数据时,注意使用数组;回调函数也使用数组,且一个数组装现值,一个数组装旧值
watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng], [preName, preEng]) => {
console.log(curName, preName, '----', curEng, preEng)
})
const { name, englishName } = toRefs(nameObj)
const change = () => {
name.value = name.value + '1'
englishName.value = englishName.value + '2'
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 定时关闭侦听器:见 1.9 的第 2 点
- 使 watch 由惰性变成非惰性:利用 watch 的 第三个参数:
watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng], [preName, preEng]) => {
console.log(curName, preName, '----', curEng, preEng)
}, {
immediate: true
})
2
3
4
5
1.9 watchEffect 侦听器
- 基本用法:
const nameObj = reactive({
name: 'dell',
englishName: 'lee'
})
// 立即执行,没有惰性 immediate
// 不需要传递要侦听的内容,自动会感知代码依赖。不需要传递很多参数,只要传递一个回调函数
// watchEffect 不能获取之前数据的值
watchEffect(() => {
console.log(nameObj.name)
console.log(nameObj.englishName)
})
2
3
4
5
6
7
8
9
10
11
12
- 定时关闭侦听器:(watch 也支持)
const stop = watchEffect(() => {
console.log(nameObj.name)
console.log(nameObj.englishName)
setTimeout(() => {
stop()
}, 5000);
})
2
3
4
5
6
7
1.10 Provide、Inject
父组件提供数据,子组件不能修改父组件的数据,只能通过父组件来修改。这就是单向数据流。
- 子组件修改父组件的数据(不推荐):
const app = Vue.createApp({
setup() {
const { provide, ref } = Vue
// 左边是名字,右边是值
provide('name', ref('dell'))
return {
}
},
template: `
<div>
<child />
</div>
`
})
app.component('child', {
setup() {
const { inject } = Vue
// 第二个参数是默认值
const name = inject('name', 'hello')
const handleClick = () => {
name.value = 'lee'
}
return {
name,
handleClick
}
},
template: '<div @click="handleClick">{{ name }}</div>'
})
const vm = app.mount('#root')
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
- 子组件通过调用父组件方法修改父组件的值:(推荐)
const app = Vue.createApp({
setup() {
const { provide, ref } = Vue
const name = ref('dell')
provide('changeName', (value) => {
name.value = value
})
return {
}
},
template: `
<div>
<child />
</div>
`
})
app.component('child', {
setup() {
const { inject } = Vue
const changeName = inject('changeName')
const handleClick = () => {
changeName('lee')
}
return {
name,
handleClick
}
},
template: '<div @click="handleClick">{{ name }}</div>'
})
const vm = app.mount('#root')
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
- 点 2 的升级版:
const app = Vue.createApp({
setup() {
const { provide, ref, readonly } = Vue
const name = ref('dell')
provide('name', readonly(name))
provide('changeName', (value) => {
name.value = value
})
return {
}
},
template: `
<div>
<child />
</div>
`
})
app.component('child', {
setup() {
const { inject } = Vue
const name = inject('name')
const changeName = inject('changeName')
const handleClick = () => {
changeName('lee')
}
return {
name,
handleClick
}
},
template: '<div @click="handleClick">{{ name }}</div>'
})
const vm = app.mount('#root')
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
1.11 组件 & 生命周期
composition api 不需要使用
components: xxx
,直接引入就行了
基本形式:
<template>
<div>
<HelloWorldVue></HelloWorldVue>
</div>
</template>
<script setup lang='ts'>
import HelloWorldVue from './components/HelloWorld.vue';
</script>
2
3
4
5
6
7
8
9
生命周期:这个案例可以完美演示生命周期,其中 v-if
可以控制组件的卸载
App.vue:父组件
<template>
<div>
<HelloWorldVue v-if="flag"></HelloWorldVue>
</div>
<div>
<button @click="flag = !flag">改变组件状态</button>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
import HelloWorldVue from './components/HelloWorld.vue';
let flag = ref(true)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
HelloWorld.vue:子组件
<template>
<div id="hello">我是 hello 组件</div>
<div>{{ count }}</div>
<button @click="count++">点击我自加</button>
</template>
<script setup lang='ts'>
import { onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue';
const count = ref<number>(0)
onBeforeMount(() => {
let div = document.querySelector('#hello')
console.log('创建之前 ===> onBeforeMount', div)
})
onMounted(() => {
let div = document.querySelector('#hello')
console.log('创建完成 ===> onBeforeMount', div)
})
onBeforeUpdate(() => {
console.log('更新之前 ===> onBeforeMount')
})
onUpdated(() => {
console.log('更新完毕 ===> onUpdated')
})
onBeforeUnmount(() => {
console.log('卸载之前 ===> onBeforeUnmount')
})
onUnmounted(() => {
console.log('卸载完成 ===> onUnmounted')
})
</script>
<style scoped>
#hello {
border: 1px solid #ccc;
}
</style>
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
1.12 父子组件传参
子组件接收字符串:
<template>
<div class="menu">
菜单区域
{{ title }}
</div>
</template>
<script setup lang='ts'>
type Props = {
title: string
}
defineProps<Props>()
// defineProps<{
// title: string
// }>()
// defineProps({
// title: {
// type: String,
// default: 1,
// validate: (str: string) => {
// return str.length > 2
// }
// }
// })
</script>
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
defineProps 的方法:无 ts 时
//第一种
defineProps(["page"]);
//第二种
defineProps({
page:Number
})
//第三种
defineProps({
page:{
type:Number,
default:2
}
})
2
3
4
5
6
7
8
9
10
11
12
13
defineEmit 的方法:
子组件:
const list = reactive<number[]>([6, 6, 6])
const emit = defineEmits(['on-click'])
const clickTap = () => {
emit('on-click', list, false)
}
2
3
4
5
父组件:
<template>
<Menu @on-click="getList" :data="list" title="我想穿件衣服"></Menu>
</template>
<script setup lang='ts'>
import Menu from './Menu/index.vue'
import { reactive, toRaw } from 'vue';
const list = reactive<number[]>([1, 2, 3])
const getList = (list: number[], flag: boolean) => {
console.log(list, '我是子组件传过来的 list', flag) // Proxy {0: 6, 1: 6, 2: 6} '我是子组件传过来的 list' false
let rawList = toRaw(list)
console.log(rawList) // [6, 6, 6]
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ref 引用子组件属性 / 方法:
在 vue2 中,只要对子组件使用
ref="xxx"
,则可以直接使用this.$refs.xxx
调用子组件的属性和方法。但是在 vue3 中,必须要子组件允许暴露才可以使用
父组件:
其实下面不准确,应该在 onMouted 之后的生命周期中才能获取到完整的组件 ref 信息,而在 setup beforeCreate create 生命周期中是获取不到的
<template>
<Menu ref="menus"></Menu>
</template>
<script>
const menus = ref(null)
const getAttr = () => {
console.log(menus.value)
}
getAttr()
</script>
2
3
4
5
6
7
8
9
10
11
子组件:
const flag = ref(false)
const list = reactive<number[]>([6, 6, 6])
defineExpose({
list,
flag
})
2
3
4
5
6
ref 引用子组件属性 / 方法(ts 版本):
父组件:
// <HomeOrderCard ref="cardRef" :propData="propData" />
const cardRef = ref<InstanceType<typeof HomeOrderCard>>()
onMounted(() => {
cardRef.value?.sayHello()
})
2
3
4
5
6
子组件:
const sayHello = () => {
console.log('hello')
alert('are you ok?')
}
defineExpose({
sayHello
})
2
3
4
5
6
7
defineProps 的默认值:如果父组件没传值,下面为子组件接收值的写法(使用默认值)
type Props = {
title?: string,
data?: number[]
}
withDefaults(defineProps<Props>(), {
title: '我是默认值',
data: () => [1, 2, 3, 4] // 复杂数据类型要用函数
})
2
3
4
5
6
7
8
defineProps 传递对象与数组写法:
父组件:
// <HomeOrderCard :propData="propData" />,html 部分写了左边这坨东西
// 这里的是假数据,需要后期传入后台数据
const propData = reactive({
title: '今日订单数 | 目标订单数(笔)',
sellNum: 2000,
target: 3000,
yesterdaySellNum: 4000,
yesterdayTarget: 5000
})
2
3
4
5
6
7
8
9
10
子组件:
type Props = {
propData?: {
title: string,
sellNum: number,
target: number,
yesterdaySellNum: number,
yesterdayTarget: number
}
}
const prop = withDefaults(defineProps<Props>(), {
propData: () => ({
title: 'noData',
sellNum: -1,
target: -1,
yesterdaySellNum: -1,
yesterdayTarget: -1
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.13 全局组件、局部组件、递归组件
全局组件:
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset.less'
import Card from './components/Card/index.vue'
// 注意:component 一定是放在 createApp 的后面的
createApp(App).component('Card', Card).mount('#app')
2
3
4
5
6
局部组件:
import 一下组件就自动注册了,这就是 setup 语法糖的好处
递归组件:
@/components/Tree/index.vue:
<template>
<div style="margin-left: 10px;">
<!-- 阻止冒泡 -->
<div @click.stop="clickItem(item)" v-for="(item, index) in data" :key="index">
{{ item.name }}
<!-- 引用自己,递归调用自己的 children 属性,判断条件是 children 的长度 -->
<TreeItem @on-click="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
</div>
</div>
</template>
<script setup lang='ts'>
type TreeList = {
name: string,
icon?: string,
children?: TreeList[] | []
}
type Props = {
data?: TreeList[]
}
defineProps<Props>()
const emit = defineEmits(['on-click'])
const clickItem = (item: TreeList) => {
emit('on-click', item)
}
</script>
<!-- 方式二:暴露自己给自己 -->
<!--
<script lang="ts">
export default {
name: 'TreeItem'
}
-->
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
@/components/Menu/index.vue:
<template>
<div class="menu">
菜单区域
<Tree :data="data"></Tree>
</div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue';
import Tree from '../../components/Tree/index.vue'
type TreeList = {
name: string,
icon?: string,
children?: TreeList[] | []
}
const data = reactive<TreeList[]>([
{
name: 'no.1',
children: [
{
name: 'no.1-1',
children: [
{
name: 'no.1-1-1'
}
]
}
]
},
{
name: 'no.2',
children: [
{
name: 'no.2-1'
}
]
},
{
name: 'no.3'
},
{
name: 'no.4',
children: []
}
])
</script>
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
1.14 动态组件
markRaw 可以标记某个属性不被代理。因为 reactive 会进行 proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用 shallowRef 或者 markRaw 跳过 proxy 代理
<template>
<div class="content">
<div class="tab">
<div @click="switchCom(item)" v-for="item in data" :key="item.name">
{{ item.name }}
</div>
<component :is="current.comName"></component>
</div>
</div>
</template>
<script setup lang='ts'>
import { markRaw, reactive } from 'vue';
import Card from '../../components/Card/index.vue';
import A from './A.vue'
import B from './B.vue'
import C from './C.vue'
let obj = { name: 123 }
let o = markRaw(obj)
console.log(o)
type Tabs = {
name: string,
comName: any
}
type Com = Pick<Tabs, 'comName'>
const data = reactive<Tabs[]>([
{
name: '我是 A 组件',
// markRaw 作用:跳过 proxy 代理
comName: markRaw(A)
},
{
name: '我是 B 组件',
comName: markRaw(B)
},
{
name: '我是 C 组件',
comName: markRaw(C)
},
])
let current = reactive<Com>({
comName: data[0].comName
})
const switchCom = (item: Tabs) => {
current.comName = item.comName
}
</script>
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
1.15 slot 插槽 - 父组件向子组件传递标签
slot 与 prop 类似,prop 是传递值,而插槽是在父组件中调用子组件时,在子组件中写入标签,而子组件通过 slot 来接收标签
- 基本形式:如果父组件传递的标签内有动态数据的话,该数据为父组件的数据,是可以渲染到子组件的 slot 里面的
// slot 插槽
const app = Vue.createApp({
template: `
<myform>
<div>提交</div>
</myform>
<myform>
<button>提交</button>
</myform>
`
})
app.component('myform', {
methods: {
handleClick() {
alert(123)
}
},
template: `
<div>
<input />
<!-- slot 上是不能直接绑定事件的,只能外套一层标签以绑定事件 -->
<span @click="handleClick">
<slot></slot>
</span>
</div>
`
})
const vm = app.mount('#root')
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
- 如果父组件没有在子组件内部添加标签,则子组件
<slot>xxx</slot>
中的默认值为xxx
:
// slot 插槽
const app = Vue.createApp({
template: `
<myform>
<div>提交</div>
</myform>
<myform>
<button>提交</button>
</myform>
<myform></myform>
`
})
app.component('myform', {
methods: {
handleClick() {
alert(123)
}
},
template: `
<div>
<input />
<!-- slot 上是不能直接绑定事件的,只能外套一层标签以绑定事件 -->
<span @click="handleClick">
<slot>我是一个默认值,有传值我不显示,没传值我默认显示</slot>
</span>
</div>
`
})
const vm = app.mount('#root')
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
- 具名插槽:
slot
中内容如果如上面这样写,那么这些内容就是一个整体,无法进行拆分,例如父组件中这样写:
<my-componennt>
<header>我是头部</header>
<footer>我是尾部</footer>
</my-component>
2
3
4
这样的话,子组件使用插槽 <slot></slot>
,就不能在 header
与 footer
之间插入其他标签。为此,引入父组件的 v-slot
和子组件的 name
属性:其中,v-slot: 可以简写成 #
const app = Vue.createApp({
data() {
return {
text: '提交'
}
},
template: `
<layout>
<!-- <template v-slot:header> -->
<template #header>
<div>header</div>
</template>
<!-- <template v-slot:footer> -->
<template #footer>
<div>footer</div>
</template>
</layout>
`
})
app.component('layout', {
template: `
<div>
<slot name="header"></slot>
<div>content</div>
<slot name="footer"></slot>S
</div>
`
})
const vm = app.mount('#root')
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
1.16 作用域插槽
父组件调用子组件的数据,通过插槽来调用。标志:
v-slot=""
const app = Vue.createApp({
data() {
return {
text: '提交'
}
},
template: `
<!-- 可以使用解构语法
<list v-slot="slotProps">
<span>{{ slotProps.item }}</span>
</list>
-->
<list v-slot="{item}">
<span>{{ item }}</span>
</list>
`
})
app.component('list', {
data() {
return {
list: [1, 2, 3]
}
},
template: `
<div>
<slot v-for="item of list" :item="item" />
</div>
`
})
const vm = app.mount('#root')
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
1.17 异步组件 & 代码分包 & suspense
效果是:页面显示 loading...,2s 后显示 haha1 haha2 haha3 haha4
父组件:
<template>
<div class="content">
<Suspense>
<!-- #default 是放异步组件的 -->
<template #default>
<A></A>
</template>
<!-- 组件加载时做些什么 -->
<template #fallback>
<div>
loading...
</div>
</template>
</Suspense>
</div>
</template>
<script setup lang='ts'>
import { defineAsyncComponent } from 'vue';
const A = defineAsyncComponent(() => import('../../components/A/index.vue'))
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
子组件:
<template>
<div>
<div v-for="item in list">
{{ item.name }}
</div>
</div>
</template>
<script setup lang='ts'>
import { axios } from './server'
const list = await axios('./data.json')
console.log(list)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
server.ts:
type NameList = {
name: string
}
export const axios = (url: string): Promise<NameList[]> => {
return new Promise((resolve) => {
let xhr: XMLHttpRequest = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status == 200) {
setTimeout(() => {
resolve(JSON.parse(xhr.responseText))
}, 2000);
}
}
xhr.send(null)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data.json:
[
{
"name": "haha1"
},
{
"name": "haha2"
},
{
"name": "haha3"
},
{
"name": "haha4"
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
1.18 Teleport 传送门功能
引入:在一个定位在屏幕正中间的元素内部添加一个蒙层,想达到的效果是:该蒙层通过定位,覆盖整个屏幕
遇到的问题:蒙层只能遮盖住居中的定位元素
const app = Vue.createApp({
data() {
return {
show: false
}
},
methods: {
handleBtnClick() {
this.show = !this.show
}
},
template: `
<div class="area">
<button @click="handleBtnClick">按钮</button>
<div class="mask" v-show="show"></div>
</div>
`
})
// 如果 mounted 和 updated 执行同一个指令,则可以用这个简写
app.directive('pos', (el, binding) => {
console.log(binding) // 可以看到 binding 里有 value(存放传输的值),arg(冒号后面的值)
el.style[binding.arg] = binding.value + 'px'
})
const vm = app.mount('#root')
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
解决办法:为蒙层元素套上一层 <teleport>
标签:
<!-- 可以传送到任意位置,例如 to="#hello" 就是传送到 id="hello" 的元素节点上 -->
<teleport to="body">
<div class="mask" v-show="show"></div>
</teleport>
2
3
4
被
<teleport>
包着的元素仍能使用data
里面的动态值,仍与之前一模一样
1.19 keep-alive 缓存组件
keep-alive 会缓存组件:
下面的 Login 和 Reg 组件里的文本框都在切换后都会保留内容,应为 keep-alive 会缓存组件
<template>
<keep-alive>
<Login v-if="flag"></Login>
<Reg v-else></Reg>
</keep-alive>
</template>
2
3
4
5
6
生命周期变化:
使用 keep-alive 将不会执行 onUnmounted 生命周期且 mounted 声明周期只会走一次。相反地,它会新增 onActivated 和 onDeactivated 两个声明周期
- 当
v-if
切换显示时,会执行onDeactivated
和onActiveated
;当刚进入页面的时候,会执行mounted
和onActivated
:include="['Login']":这样的话,只有 login 组件会被缓存,而另一个组件 Reg 不会被缓存。:exclude
作用相反,:max
可以指定缓存的最大数量
<template>
<div class="content">
<button @click="switchCom">切换</button>
<!-- 指定缓存内容,include 里面是 name,即从 login/index.vue 里面暴露的 name: 'Login' -->
<keep-alive :include="['Login']">
<login></login>
<Reg></Reg>
</keep-alive>
</div>
</template>
2
3
4
5
6
7
8
9
10
2.vue 动画
2.1 transition 组件基础用法
下面写出了 vue 的 6 个基本的动画类
- 六个 vue 自带类:
v-enter-from
:定义进入过渡的开始状态v-enter-active
:定义进入过渡生效时的状态v-enter-to
:定义进入过渡的结束状态v-leave-from
:定义离开过渡的开始状态v-leave-active
:定义离开过渡生效时的状态v-leave-to
:离开过渡的结束状态
transition
标签里面可以定义name="xxx"
,如果定义了,那么原来的例如v-enter-from
类名都得改为xxx-enter-from
<template>
<div class="content">
<button @click="flag = !flag">switch</button>
<transition name="fade">
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const flag = ref<boolean>(true)
</script>
<style scoped lang="less">
.box {
width: 200px;
height: 200px;
background: red;
}
.fade-enter-from {
width: 0;
height: 0;
transform: rotate(360deg);
}
.fade-enter-active {
transition: all 1.5s ease;
}
.fade-enter-to {
width: 200px;
height: 200px;
}
.fade-leave-from {
width: 200px;
height: 200px;
transform: rotate(360deg);
}
.fade-leave-active {
transition: all 5s ease;
}
.fade-leave-to {
width: 0;
height: 0;
}
.content {
flex: 1;
margin: 20px;
border: 1px solid #ccc;
overflow: auto;
&-items {
padding: 20px;
border: 1px solid #ccc;
}
}
</style>
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
2.2 transition 结合 animate.css
6 个动画类设置别名:transition
标签的属性分别有 6 个属性:enter-from-class、enter-active-class、enter-to-class、leave-from-class、leave-active-class、leave-to-class
<transition enter-from-class="e-from" name="fade">
<div v-if="flag" class="box"></div>
</transition>
.e-from {
width: 0;
height: 0;
transform: rotate(360deg);
}
2
3
4
5
6
7
8
9
transition + animate.css:
官网:Animate.css | A cross-browser library of CSS animations.open in new window
- 安装:
yarn add animate.css
- 引入:
import 'animate.css'
- 应用举例(
animate.css
的 4.0 版本规定,要额外添加一个前缀才能生效:animate__animated
)
<transition leave-active-class="animate__animated animate__fadeOut" enter-active-class="animate__animated animate__fadeIn" name="fade">
<div v-if="flag" class="box"></div>
</transition>
2
3
transition 标签的一些属性:
:duration
:动画持续时间::duration="500"
,亦可以单独定义::duration="{enter: 50, leave: 50}"
2.3 transition 生命周期和 GSAP
六个钩子函数:
@before-enter
@enter
@after-enter
@enter-cancelled
@before-leave
@leave
@after-leave
@leave-cancelled
示例代码:
<template>
<div class="content">
<button @click="flag = !flag">switch</button>
<transition
@before-enter="EnterFrom"
@enter="EnterActive"
@after-enter="EnterTo"
@enter-cancelled="EnterCancel"
@before-leave="LeaveFrom"
@leave="LeaveActive"
@after-leave="LeaveTo"
@leave-cancelled="LeaveCancel"
>
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const flag = ref<boolean>(true)
const EnterFrom = (el: Element) => {
console.log('进入之前')
}
const EnterActive = (el: Element, done: Function) => {
console.log('过渡曲线')
// 动画效果 3s 之后完成
setTimeout(() => {
done()
}, 3000);
}
const EnterTo = (el: Element) => {
console.log('过渡完成')
}
const EnterCancel = (el: Element) => {
console.log('过渡效果被打断')
}
const LeaveFrom = (el: Element) => {
console.log('离开之前')
}
const LeaveActive = (el: Element, done: Function) => {
console.log('离开过渡曲线')
setTimeout(() => {
done()
}, 1000);
}
const LeaveTo = (el: Element) => {
console.log('离开完成')
}
const LeaveCancel = (el: Element) => {
console.log('离开过渡曲线')
}
</script>
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
GSAP:
- 安装:
yarn add gsap
(它是一个 js 动画库) - 引入:
import gsap from 'gsap'
- 使用代码:
<template>
<div class="content">
<button @click="flag = !flag">switch</button>
<transition @before-enter="EnterFrom" @enter="EnterActive" @leave="LeaveActive">
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
import gsap from 'gsap'
const flag = ref<boolean>(true)
const EnterFrom = (el: Element) => {
gsap.set(el, {
width: 0,
height: 0
})
}
const EnterActive = (el: Element, done: gsap.Callback) => {
gsap.to(el, {
width: 200,
height: 200,
onComplete: done
})
}
const LeaveActive = (el: Element, done: gsap.Callback) => {
gsap.to(el, {
width: 0,
height: 0,
onComplete: done
})
}
</script>
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
2.4 Appear 属性
通过这三个属性可以设置初始节点过渡,就是页面加载完成就开始动画。对应三个状态
appear-from-class
appear-active-class
appear-to-class
基本使用:
<template>
<div class="content">
<button @click="flag = !flag">switch</button>
<transition appear appear-from-class="from" appear-active-class="active" appear-to-class="to">
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
import gsap from 'gsap'
const flag = ref<boolean>(true)
</script>
<style scoped lang="less">
.box {
width: 200px;
height: 200px;
background: red;
}
.from {
width: 0;
height: 0;
}
.active {
transition: all 2s ease;
}
.to {
width: 200px;
height: 200px;
}
.content {
flex: 1;
margin: 20px;
border: 1px solid #ccc;
overflow: auto;
&-items {
padding: 20px;
border: 1px solid #ccc;
}
}
</style>
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
结合 animate.css:
<template>
<div class="content">
<button @click="flag = !flag">switch</button>
<transition appear appear-active-class="animate__animated animate__bounceOut">
<div v-if="flag" class="box"></div>
</transition>
</div>
</template>
<script setup lang='ts'>
import 'animate.css'
import { ref } from 'vue';
const flag = ref<boolean>(true)
</script>
<style scoped lang="less">
.box {
width: 200px;
height: 200px;
background: red;
}
.content {
flex: 1;
margin: 20px;
border: 1px solid #ccc;
overflow: auto;
&-items {
padding: 20px;
border: 1px solid #ccc;
}
}
</style>
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
2.5 transition-group
同时渲染整个列表,比如使用 v-for,在这种场景下,使用
<transition-group>
组件
tag 属性:
<!-- 写了 tag="section" 之后就会在 v-for 渲染的 div 的父级添加一个 section 标签包着 -->
<transition-group tag="section">
<div class="item" :key="item" v-for="item in list">{{ item }}</div>
</transition-group>
2
3
4
其他属性:<transition-group>
也拥有 enter-to-class、leave-active-class
等动画类,也有 @before-enter、@after-leave
等生命周期函数
示例代码:
<template>
<div class="content">
<button @click="add">Add</button>
<button @click="pop">Pop</button>
<div class="wraps">
<!-- 写了 tag="section" 之后就会在 v-for 渲染的 div 的父级添加一个 section 标签包着 -->
<transition-group
leave-active-class="animate__animated animate__bounceOut"
enter-active-class="animate__animated animate__bounceIn"
>
<div class="item" :key="item" v-for="item in list">{{ item }}</div>
</transition-group>
</div>
</div>
</template>
<script setup lang='ts'>
import 'animate.css'
import { ref, reactive } from 'vue';
const list = reactive<number[]>([1, 2, 3, 4, 5, 6])
const add = () => {
list.push(list.length + 1)
}
const pop = () => {
list.pop()
}
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
word-break: break-all;
border: 1px solid #ccc;
.item {
margin: 10px;
font-size: 30px;
}
}
</style>
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
2.6 列表的移动过渡
transition-group
除了进入和离开,它还可以为定位的改变添加动画,利用v-move
即可实现。它的前缀也是通过name
来定义。
生成一个长度为 81 的数组:
<template>
<div>
<button>random</button>
<transition-group class="wraps" tag="div">
<div class="items" :key="item.id" v-for="item in list">
{{ item.number }}
</div>
</transition-group>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
// 这里会把这个 length 为 81 的对象当做数组遍历,但是每个对应的下标值都是 undefined,然后进行 map 遍历,然后为每一个对象的属性值写上 return 的值
let list = ref(Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1
}
}))
console.log(list.value)
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(25px * 10 + 9px);
.items {
width: 25px;
height: 25px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>
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
利用 lodash 库进行随机打乱数组:
- 安装:
yarn add lodash
- 安装 ts 声明文件库:
yarn add @types/lodash
- 代码如下:
<template>
<div>
<button @click="random">random</button>
<transition-group move-class="mmm" class="wraps" tag="div">
<div class="items" :key="item.id" v-for="item in list">
{{ item.number }}
</div>
</transition-group>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
import _ from 'lodash'
// 这里会把这个 length 为 81 的对象当做数组遍历,但是每个对应的下标值都是 undefined,然后进行 map 遍历,然后为每一个对象的属性值写上 return 的值
let list = ref(Array.apply(null, { length: 81 } as number[]).map((_, index) => {
return {
id: index,
number: (index % 9) + 1
}
}))
console.log(list.value)
const random = () => {
list.value = _.shuffle(list.value)
}
</script>
<style scoped lang="less">
.wraps {
display: flex;
flex-wrap: wrap;
width: calc(25px * 10 + 9px);
.items {
width: 25px;
height: 25px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
}
}
.mmm {
transition: all 1s;
}
</style>
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
2.7 状态过渡
Vue 可以给数字 Svg 背景颜色等添加过渡动画
<template>
<div>
<input type="number" step="20" v-model="num.current">
<div>
{{ num.tweenedNumber.toFixed(0) }}
</div>
</div>
</template>
<script setup lang='ts'>
import { reactive, watch } from 'vue'
import gsap from 'gsap'
const num = reactive({
current: 0,
tweenedNumber: 0
})
watch(() => num.current, (newVal, oldVal) => {
gsap.to(num, {
duration: 1,
tweenedNumber: newVal
})
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3.杂项
3.1 provide、inject 补充
注入响应式数据:
顶层组件:
provide('flag', ref(false))
子组件:
// Ref<boolean> 表示 boolean 的 ref 类型,inject 的第二个参数表示默认值,以防 undefined
let data = inject<Ref<boolean>>('flag', ref(false))
2
3.2 兄弟组件传参 & Bus
方式一:defineEmits('[事件]') 结合 defineProps 通过父组件进行兄弟组件传值:
父组件:
<template>
<div>
<A @on-click="getFlag"></A>
<B :flag="Flag"></B>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
import A from './components/A.vue'
import B from './components/B.vue'
let Flag = ref(false)
const getFlag = (params: boolean) => {
Flag.value = params
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
兄弟组件一:
<template>
<div class="A">
<button @click="emitB">派发一个事件</button>
</div>
</template>
<script setup lang='ts'>
const emit = defineEmits(['on-click'])
let flag:boolean = false
const emitB = () => {
flag = !flag
emit('on-click', flag)
}
</script>
<style scoped>
.A {
width: 200px;
height: 200px;
color: #fff;
background: blue;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
兄弟组件二:
<template>
<div class="B">
{{ flag }}
</div>
</template>
<script setup lang='ts'>
type Props = {
flag: boolean
}
defineProps<Props>()
</script>
<style scoped>
.B {
width: 200px;
height: 200px;
color: #fff;
background: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
方式二:推荐方法
@/Bus.ts:
type BusClass = {
emit: (name: string) => void
on: (name: string, callback: Function) => void
}
type ParamsKey = string | number | symbol
type List = {
[key: ParamsKey]: Array<Function>
}
class Bus implements BusClass {
list: List
constructor() {
this.list = {}
}
emit(name: string, ...args: Array<any>) {
let eventName: Array<Function> = this.list[name]
eventName.forEach(fn => {
fn.apply(this, args)
})
}
// 订阅一个为 name 的 key 值(第一个参数),将一个函数数组作为 value 值,函数为第二个参数
on(name: string, callback: Function) {
let fn: Array<Function> = this.list[name] || []
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus()
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
A.vue:传值的兄弟组件:
<template>
<div class="A">
<button @click="emitB">派发一个事件</button>
</div>
</template>
<script setup lang='ts'>
import Bus from '../Bus'
let flag: boolean = false
const emitB = () => {
flag = !flag
Bus.emit('on-click', flag)
}
</script>
<style scoped>
.A {
width: 200px;
height: 200px;
color: #fff;
background: blue;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
B.vue:监听的兄弟组件
<template>
<div class="B">
{{ Flag }}
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
import Bus from '../Bus'
let Flag = ref(false)
Bus.on('on-click', (flag: boolean) => {
Flag.value = flag
})
</script>
<style scoped>
.B {
width: 200px;
height: 200px;
color: #fff;
background: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
3.3 Mitt
安装:
yarn add mitt
兄弟组件监听
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt'
const Mit = mitt()
const app = createApp(App)
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof Mit
}
}
app.config.globalProperties.$Bus = Mit
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
兄弟组件1:这是传值的组件
<template>
<div>
<h1>我是A</h1>
<button @click="emit">emit</button>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance()
const emit = () => {
instance?.proxy?.$Bus.emit('on-yuanke', 'mittt')
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
兄弟组件2:接收值的组件
<template>
<div>
<h1>我是B</h1>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance()
instance?.proxy?.$Bus.on('on-yuanke', (str) => {
console.log(str, '=====B')
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
监听多条
兄弟组件1:
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance()
const emit = () => {
instance?.proxy?.$Bus.emit('on-yuanke', 'mittt')
instance?.proxy?.$Bus.emit('on-yaunek2', 'mitt2')
}
2
3
4
5
6
7
兄弟组件2:
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance()
instance?.proxy?.$Bus.on('*', (type, str) => {
console.log(type, str, '=====B')
})
2
3
4
5
6
off / all.clear()
instance?.proxy?.$Bus.off('on-yuanke', Bus)
instance?.proxy?.$Bus.all.clear()
2
3.4 TSX
安装
- 安装:
yarn add @vitejs/plugin-vue-jsx
- vite.config.ts 需要设置一下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx()
]
})
2
3
4
5
6
7
8
9
10
- tsconfig.json 配置:
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
2
3
- 创建 App.tsx:
import { ref } from 'vue'
let v = ref<string>('')
const renderDom = () => {
return (
<div>
<input v-model={v.value} type="text" />
<div>{v.value}</div>
</div>
)
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
- 在 App.ts 中引入:
<renderDom></renderDom>
import renderDom from './App';
2
上面第四点中,演示了 v-model 的写法,变量都需要使用大括号包起来
<div v-show={flag}>景天</div>
<div v-show={!flag}>雪见</div>
{/* 下面的不支持 */}
<div v-if={flag}>景天</div>
2
3
4
使用 js 编程思想进行改写
import { ref } from 'vue'
let v = ref<string>('')
let flag = false
const renderDom = () => {
return (
<div>
{ flag ? <div>景天</div> : <div>雪见</div> }
</div>
)
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
v-for
let Arr = [1, 2, 3, 4, 5]
const renderDom = () => {
return (
<div>
{
Arr.map(v => {
return (<div>${v}</div>)
})
}
</div>
)
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
13
14
v-bind
let Arr = [1, 2, 3, 4, 5]
const renderDom = () => {
return (
<div>
{
Arr.map(v => {
return (<div data-index={v}>${v}</div>)
})
}
</div>
)
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
13
14
v-bind 绑定函数时的参数 - bind(this, 参数)
let Arr = [1, 2, 3, 4, 5]
const renderDom = () => {
return (
<div>
{
Arr.map(v => {
return (<div onClick={clickTap.bind(this, v)} data-index={v}>${v}</div>)
})
}
</div>
)
}
const clickTap = (v: number) => {
console.log('我被点了', v);
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
props & emit
父组件:
<template>
<renderDom @on-click="getNum" title="我是标题"></renderDom>
</template>
<script setup lang='ts'>
import renderDom from './App';
const getNum = (num: number) => {
console.log(num, '我接收到了')
}
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
子组件:
let Arr = [1, 2, 3, 4, 5]
type Props = {
title: string
}
const renderDom = (props: Props, ctx: any) => {
return (
<div>
<div>{props.title}</div>
{
Arr.map(v => {
return (<div onClick={clickTap.bind(this, ctx)} data-index={v}>${v}</div>)
})
}
</div>
)
}
const clickTap = (ctx: any) => {
ctx.emit('on-click', 123)
}
export default renderDom
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3.5 深入 v-model & 自动引入
自动引入
- 安装:
yarn add unplugin-auto-import
- 配置 vite.config.ts:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
AutoImport({
imports: ['vue'],
dts: 'src/auto-import.d.ts'
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 演示:
<template>
<button @click="flag = !flag">change flag</button>
<div>{{ flag }}</div>
</template>
<script setup lang='ts'>
let flag = ref<boolean>(false)
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
3.6 v-model
v-model 相当于 prop 和 emit 结合体
子组件接收 v-model 传来的值时,默认传来的值为
modelValue
;也可以通过v-model:xxx="yyy"
向子组件传递 xxx
App.vue:
<template>
<button @click="flag = !flag">change {{ flag }}</button>
<div>标题 {{ title }}</div>
<!-- 如果想传递多个数据给子组件例如 v-model:title="title",则传递的名称就是 title,否则默认为 modelValue -->
<!-- v-model 可以写多个 -->
<Dialog v-model="flag"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from './components/Dialog.vue'
import { ref } from 'vue'
let title = ref<string>('我是个p')
let flag = ref<boolean>(false)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
Dialog.vue:
<template>
<div class="dialog" v-if="modelValue">
<div class="dialog-header">
<div>标题---</div>
<button @click="close">x</button>
</div>
<div class="dialog-content">内容</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
modelValue: boolean
}
defineProps<Props>()
// 这里切记要触发 update 事件,且冒号后为要被修改的 v-model 的绑定值
// 如果想触发多个事件,例如更新传来的 title,则为 ['update:modelValue', 'update:title']
const emit = defineEmits(['update:modelValue'])
const close = () => {
// 接上,触发多个事件,加上 emit('update:title', '我是一只猫')
emit('update:modelValue', false)
}
</script>
<style scoped lang="less">
.dialog {
width: 300px;
height: 300px;
border: 1px solid #ccc;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
&-header {
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
padding: 10px;
}
&-content {
padding: 10px;
}
}
</style>
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
自定义修饰符
父组件:
<Dialog v-model:title.aaa="title" v-model.yuanke="flag"></Dialog>
子组件:
<template>
<div class="dialog" v-if="modelValue">
<div class="dialog-header">
<div>标题---{{ title }}</div>
<button @click="close">x</button>
</div>
<div class="dialog-content">内容</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
modelValue: boolean,
title: string,
// 默认为 modelModifiers,修饰符为布尔值
modelModifiers?: {
yuanke: boolean
},
// 传递名称不同使 xxxModifiers => titleModifiers
titleModifiers?: {
aaa: boolean
}
}
const PropsData = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'update:title'])
const close = () => {
console.log(PropsData.modelModifiers)
if (PropsData.titleModifiers?.aaa) console.log('haha')
if (PropsData.modelModifiers?.yuanke) {
emit('update:title', '我是一条狗')
} else {
emit('update:title', '我是一只猫')
}
emit('update:modelValue', false)
}
</script>
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
v-model 父子组件双向绑定:
父组件:
<CheckBoxCard v-model:checkList="checkList" v-model:supplierList="supplierList" @reset-box="resetCheckBox" />
<script setup lang="ts">
// 供应商筛选条件
const supplierList = renderService.getSupplierList()
const checkList = ref<string[]>([...JSON.parse(JSON.stringify(supplierList))])
const resetCheckBox = () => {
checkList.value = JSON.parse(JSON.stringify(supplierList))
}
</script>
2
3
4
5
6
7
8
9
10
子组件:
<!--
传入数据:
checkList: 与 checkbox 各个选项关联的数组
supplierList: checkbox 的各个选项的 label 值
事件 - @reset-box 在子组件这里就负责 click 后触发父组件的事件,从而执行父组件的对应函数
-->
<template>
<el-popover placement="bottom" title="筛选" :width="200" trigger="hover">
<template #reference>
<div style="cursor: pointer;">
供应商 <SvgIcon xlinkHref="#icon-xiajiantou"></SvgIcon>
</div>
</template>
<el-checkbox-group v-model="checkList">
<el-checkbox v-for="item of supplierList" :key="item" :label="item" />
</el-checkbox-group>
<el-divider></el-divider>
<div style="width: 100%; text-align: right;">
<el-button @click="resetCheckBox" :bg="true" :text="true" type="warning">全选</el-button>
</div>
</el-popover>
</template>
<script setup lang='ts'>
import { ref, watch } from 'vue';
// 接收数据
const prop = defineProps<{
checkList: string[],
supplierList: string[]
}>()
const checkList = ref(prop.checkList)
// 触发父组件事件
const emit = defineEmits(['reset-box', 'update:checkList'])
const resetCheckBox = () => {
emit('reset-box')
}
watch(checkList, () => {
emit('update:checkList', checkList.value)
})
</script>
<style scoped>
</style>
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
3.7 自定义指令
- Vue3 的钩子函数:
created
:元素初始化的时候beforeMount
:指令绑定到元素后调用,只调用一次mounted
:元素插入父级 DOM 调用beforeUpdate
:元素被更新之前调用updated
:之前为update
beforeUnmout
:在元素被移除前调用unmounted
:指令被移除后调用,只调用一次
- Vue2 的钩子函数:
bind、inserted、update、componentUpdated、unbind
- 命名规范:必须以
vNameOfDirective
的形式来命名本地自定义指令,以使得它们可以直接在模板中使用
示例代码
<template>
<div>
<button>切换</button>
<A v-move:aaa.yuanke="{ background: 'green' }"></A>
</div>
</template>
<script setup lang='ts'>
import A from './components/A.vue'
import { ref, Directive, DirectiveBinding } from 'vue'
let flag = ref<boolean>(true)
type Dir = {
background: string
}
const vMove: Directive = {
created() {
console.log('=> created')
},
beforeMount() {
console.log('=> beforeMount')
},
// mounted(...args: Array<any>) {
// console.log(args)
// }
mounted(el: HTMLElement, dir: DirectiveBinding<Dir>) {
console.log('=> mounted')
// 这个应该看得懂
el.style.background = dir.value.background
},
beforeUpdate() {
console.log('=> beforeUpdate')
},
updated() {
console.log('=> updated')
},
beforeUnmount() {
console.log('=> beforeUnmount')
},
unmounted() {
console.log('=> unmounted')
}
}
</script>
<style scoped>
</style>
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
3.8 自定义指令 - 函数简写
若想在 mounted 和 updated 时触发相同行为,而不关心其他的钩子函数,那么可以通过这个函数模式实现
<template>
<div>
<input v-model="value" type="text">
<A v-move="{ background: value }"></A>
</div>
</template>
<script setup lang='ts'>
import A from './components/A.vue'
import { ref, Directive, DirectiveBinding } from 'vue';
type Dir = {
background: string
}
let value = ref<string>('')
const vMove: Directive = (el:HTMLElement, binding: DirectiveBinding<Dir>) => {
el.style.background = binding.value.background
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3.9 自定义指令 - 自定义拖拽
<template>
<div v-move class="box">
<div class="header"></div>
<div>内容</div>
</div>
</template>
<script setup lang='ts'>
import { Directive, DirectiveBinding } from 'vue';
const vMove: Directive<any, void> = (el: HTMLElement, binding: DirectiveBinding) => {
let moveElement: HTMLDivElement = el.firstElementChild as HTMLDivElement
const mouseDown = (e: MouseEvent) => {
let X = e.clientX - el.offsetLeft
let Y = e.clientY - el.offsetTop
const move = (e: MouseEvent) => {
el.style.left = e.clientX - X + 'px'
el.style.top = e.clientY - Y + 'px'
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', move)
})
}
moveElement.addEventListener('mousedown', mouseDown)
}
</script>
<style scoped lang="less">
.box {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 3px solid black;
.header {
height: 20px;
background: black;
}
}
</style>
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
3.10 自定义 Hooks
Vue3 中的 hook 函数相当于 Vue2 中的 mixin,不同在于 hooks 是函数。Vue3 的 hook 函数可以帮助我们提高代码的复用性,让我们能在不同的组件中都利用 hooks 函数
官方提供的 hooks 举例
// 这堆函数是在子组件里面,attr 即为父组件的所有属性
import { useAttrs } from 'vue';
let attr = useAttrs()
console.log(attr)
2
3
4
自定 hooks 将图片转为 base64
index.ts:
import { onMounted } from 'vue'
type Options = {
el: string
}
export default function (options: Options): Promise<{ baseUrl: string }> {
return new Promise((resolve) => {
onMounted(() => {
let img: HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
console.log(img, '=>')
img.onload = () => {
resolve({
baseUrl: base64(img)
})
}
})
const base64 = (el: HTMLImageElement) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = el.width
canvas.height = el.height
ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/png')
}
})
}
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
App.vue:
<template>
<div>
<img src="./assets/logo.png" id="img" width="300" height="300">
</div>
</template>
<script setup lang='ts'>
import useBase64 from './hooks/index'
useBase64({ el: '#img' }).then(res => {
console.log(res.baseUrl)
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
3.11 全局函数和变量
由于 Vue3 中没有 prototype 属性,使用
app.config.globalProperties
代替,然后去定义变量和函数
Vue2
Vue.prototype.$http = () => {}
Vue3
const app = createApp({})
app.config.globalProperties.$http = ()
2
声明全局函数
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
type Filter = {
format: <T>(str: T) => string
}
// 声明要扩充 @vue/runtime-core 包的声明
// 这里扩充 ComponentCustomProperties 接口,因为它是 vue3 中实例的属性的类型
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$filters: Filter
}
}
app.config.globalProperties.$filters = {
format<T>(str: T): string {
return `真·${str}`
}
}
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
App.vue:
<template>
<div>
{{ $filters.format('我是渣渣') }}
</div>
</template>
2
3
4
5
全局变量
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 声明要扩充 @vue/runtime-core 包的声明
// 这里扩充 ComponentCustomProperties 接口,因为它是 vue3 中实例的属性的类型
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$env: string
}
}
app.config.globalProperties.$filters = {
format<T>(str: T): string {
return `真·${str}`
}
}
app.config.globalProperties.$env = 'dev'
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
App.vue:
<template>
<div>
{{ $env }}
</div>
</template>
2
3
4
5
3.12 编写 Vue 插件
插件是自包含的代码,通常向 Vue 添加全局级功能。在使用 createApp() 初始化 Vue 应用后,可以通过 use() 方法将插件添加到应用程序中去
main.ts:声明 $loading
类型
import { createApp } from 'vue'
import App from './App.vue'
import Loading from './components/loading'
const app = createApp(App)
// 声明文件
declare module '@vue/runtime-core' {
// 与之前的 componentCustom
export interface ComponentCustomProperties {
$loading: {
show: () => void,
hide: () => void
}
}
}
app.use(Loading)
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
index.ts:
import { App, createVNode, VNode, render } from 'vue'
import Loading from './index.vue'
export default {
install(app: App) {
// 将 Loading 组件转为虚拟 dom
const vnode: VNode = createVNode(Loading)
// 将虚拟 dom 转成真实 dom 挂载到 body 上
render(vnode, document.body)
// 创建一个全局对象 $loading
app.config.globalProperties.$loading = {
show: vnode.component?.exposed?.show,
hide: vnode.component?.exposed?.hide
}
// // 立即执行一下该全局函数来测试
// app.config.globalProperties.$loading.show()
// 将会打印 index.vue 暴露出来的 show() 和 hide()
console.log(vnode.component?.exposed)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
index.vue:
<template>
<div v-if="isShow" class="loading">
<div class="loading-content">loading...</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
let isShow = ref<boolean>(false)
const show = () => {
isShow.value = true
}
const hide = () => {
isShow.value = false
}
defineExpose({
isShow,
show,
hide
})
</script>
<style scoped lang="less">
.loading {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .8);
display: flex;
justify-content: center;
align-items: center;
&-content {
color: #fff;
font-size: 30px;
}
}
</style>
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
App.vue:使用插件
<template>
<button @click="showLoading">切换</button>
</template>
<script setup lang='ts'>
import { ComponentInternalInstance, getCurrentInstance } from 'vue';
const { appContext } = getCurrentInstance() as ComponentInternalInstance
const showLoading = () => {
// options api 写作 this.$loading.show() 就行
appContext.config.globalProperties.$loading.show()
setTimeout(() => {
appContext.config.globalProperties.$loading.hide()
}, 2000);
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3.13 ElementUI、AntDesign
Element plus
- 安装 Element plus:
yarn add element-plus
- main.ts 引入:(完整导入)
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
2
3
volar
插件支持提示:
tsconfig.json:
{
"compilerOptions": {
"types": ["element-plus/global"]
}
}
2
3
4
5
Ant design
- 安装:
yarn add ant-design-vue@next
- main.ts 引入:
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
app.use(Antd)
2
3
3.14 样式穿透
(51条消息) 学习Vue3 第三十二章(详解Scoped和样式 穿透)_小满zs的博客-CSDN博客open in new window
App.vue:
<template>
<div style="margin: 200px;">
<el-input class="ipt"></el-input>
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang="less">
.ipt {
// input {
// /deep/ input {
:deep(input) {
background: red;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4.pinia
4.1 响应式 proxy 简易举例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
const data = { text: 'yuanke' }
const app = document.querySelector('#app')
const effect = () => {
app.textContent = obj.text
}
let b = new Set()
let obj = new Proxy(data, {
get(target, key) {
b.add(effect)
return target[key]
},
set(target, key, value) {
target[key] = value
b.forEach(fn => fn())
return true
}
})
effect()
setTimeout(() => {
obj.text = 'hahaha'
}, 1000);
</script>
</body>
</html>
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
4.2 pinia 特点与安装
特点
- 完整的 ts 支持
- 轻量,1kb 左右
- 去除
mutation
,只有state
、getters
、actions
actions
支持同步和异步- 代码扁平化没有模块嵌套,只有
store
的概念,store
之间可以自由使用,每一个store
都是独立的 - 无需手动添加
store
,store
一旦创建便会自动添加 - 支持 vue3 和 vue2
安装 & 引入
- 安装:
yarn add pinia
- 引入:
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const store = createPinia()
const app = createApp(App)
app.use(store)
app.mount('#app')
2
3
4
5
6
7
8
9
4.3 初始化仓库 Store
- 第一步,在 main.ts 进行启用插件:
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
const store = createPinia()
const app = createApp(App)
app.use(store)
app.mount('#app')
2
3
4
5
6
7
8
9
- 第二步,在 @/store/index.ts 中编写:
import { defineStore } from 'pinia'
import { Names } from './store-name'
export const useTestStore = defineStore(Names.TEST, {
// () => { return { xxx } } === () => ({ xxx })
state: () => ({
current: 1000,
name: '大傻子'
}),
// 有缓存的,修饰一些值
getters: {
},
// 可以做同步、异步操作,提交 state
actions: {
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
store-name.ts:
export const enum Names {
TEST = 'TEST'
}
2
3
- 在 App.vue 中使用 pinia 里的数据:
<template>
<div>
pinia: {{ Test.current }} --- {{ Test.name }}
</div>
</template>
<script setup lang='ts'>
import { useTestStore } from './store'
const Test = useTestStore()
</script>
2
3
4
5
6
7
8
9
10
11
4.4 state
共有 5 种方式修改 state 里面的值
App.vue:
<template>
<div>
pinia: {{ Test.current }} --- {{ Test.name }}
<button @click="change">change</button>
</div>
</template>
<script setup lang='ts'>
import { useTestStore } from './store'
const Test = useTestStore()
// 方式一:直接修改
// const change = () => {
// Test.current++
// }
// 方式二:通过 $patch 批量修改
// const change = () => {
// Test.$patch({
// current: 888,
// name: '娃娃'
// })
// }
// 方式三:函数的写法
// const change = () => {
// Test.$patch((state) => {
// state.current = 999
// state.name = 'yuankeke'
// })
// }
// 方式四:全部都要覆盖修改的写法
// const change = () => {
// Test.$state = {
// current: 2000,
// name: 'xiaoxiao'
// }
// }
// 方式五:通过 actions 修改 state 里的值
const change = () => {
Test.setCurrent(567)
}
</script>
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
index.ts:
import { defineStore } from 'pinia'
import { Names } from './store-name'
export const useTestStore = defineStore(Names.TEST, {
// () => { return { xxx } } === () => ({ xxx })
state: () => ({
current: 1000,
name: '大傻子'
}),
// 有缓存的,修饰一些值
getters: {
},
// 可以做同步、异步操作,提交 state
actions: {
setCurrent(num: number) {
this.current = num
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4.5 解构 pinia
利用 storeToRefs 就可以解构了
<template>
<div>origin value {{ Test.current }}</div>
<div>
<!-- 没有加 storeToRefs 前,这里不是响应式的 -->
pinia: {{ current }} --- {{ name }}
change:
<button @click="change">change</button>
</div>
</template>
<script setup lang='ts'>
import { storeToRefs } from 'pinia';
import { useTestStore } from './store'
const Test = useTestStore()
// 不具有响应式,和 reactive 一样
const { current, name } = storeToRefs(Test)
console.log(current, name)
const change = () => {
Test.current += 1
}
</script>
<style scoped>
</style>
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
4.6 actions - getters
actions 里使用异步:
import { defineStore } from 'pinia'
import { Names } from './store-name'
type User = {
name: string,
age: number
}
const Login = (): Promise<User> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
name: '飞机',
age: 999
})
}, 2000)
})
}
export const useTestStore = defineStore(Names.TEST, {
state: () => ({
user: <User>{},
// 默认值是小飞机,2s 后变成大飞机
name: '小飞机'
}),
getters: {
},
actions: {
async setUser() {
const result = await Login()
this.user = result
// 可以相互调用下面的 setName
this.setName('大飞机')
},
setName (name: string) {
this.name = name
}
}
})
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
getters 里的函数相互调用:
import { defineStore } from 'pinia'
import { Names } from './store-name'
type User = {
name: string,
age: number
}
const Login = (): Promise<User> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
name: '飞机',
age: 999
})
}, 2000)
})
}
export const useTestStore = defineStore(Names.TEST, {
state: () => ({
user: <User>{},
// 默认值是小飞机,2s 后变成大飞机
name: '小飞机'
}),
getters: {
newName(): string {
return `$-${this.name} - ${this.getUserAge}`
},
getUserAge(): number {
return this.user.age
}
},
actions: {
async setUser() {
const result = await Login()
this.user = result
// 可以相互调用下面的 setName
this.setName('大飞机')
},
setName (name: string) {
this.name = name
}
}
})
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
4.7 例 API
$reset:
作用:将 state 恢复至初始值
import { useTestStore } from './store'
const Test = useTestStore()
const reset = () => {
Test.$reset()
}
2
3
4
5
$subscribe((args, state) => {}):
作用:state 发生改变时就会调用。得到一些关于 state 的详细信息,如 storeId、state 中的值等
import { useTestStore } from './store'
const Test = useTestStore()
Test.$subscribe((args, state) => {
console.log('->', args)
console.log('->', state)
})
// ->
// {storeId: 'TEST', type: 'direct', events: {…}}
// events: {effect: ReactiveEffect, target: {…}, type: 'set', key: 'name', newValue: 'asd', …}
// storeId: "TEST"
// type: "direct"
// [[Prototype]]: Object
// ->
// Proxy {user: {…}, name: 'asd'}
// [[Handler]]: Object
// [[Target]]: Object
// name: "大飞机"
// user: {name: '飞机', age: 999}
// [[Prototype]]: Object
// [[IsRevoked]]: false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$onAction:
作用:调用 action 就会触发
// 第二个参数是为了销毁后仍存在监听
Test.$onAction((args) => {
args.after(() => {
console.log('after')
})
console.log(args) // 先打印这个,再打印上面
}, true)
2
3
4
5
6
7
4.8 pinia 插件
pinia 和 vuex 有一个通病,就是页面刷新状态会刷新。可以写一个 pinia 插件缓存它的值
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia, PiniaPluginContext } from 'pinia'
type Options = {
key?: string
}
const __piniaKey__: string = 'yuanke'
const setStorage = (key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value))
}
const getStorage = (key: string) => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {}
}
const piniaPlugin = (options: Options) => {
// 函数柯里化,因为如果直接 const piniaPlugin = (context: PiniaPluginContext) => {} 的话
// 接上,会导致 getStorage(key, value) 时的 key 不好定义
return (context: PiniaPluginContext) => {
const { store } = context
// 这里是为了防止 store.use(piniaPlugin()) 的情况(即不传参数的情况)
const data = getStorage(`${options?.key ?? __piniaKey__}-${store.$id}`)
store.$subscribe(() => {
console.log('change')
setStorage(`${options?.key ?? __piniaKey__}-${store.$id}`, toRaw(store.$state))
})
console.log(store, 'store')
// 这样的话,只要 state 改变,就能持久化 state 的值
return {
...data
}
}
}
const store = createPinia()
store.use(piniaPlugin({
key: 'pinia'
}))
const app = createApp(App)
app.use(store)
app.mount('#app')
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
5.可视化实战
5.1 前置准备
npm install ts-node -g
npm init -y
npm install @types/node -D
npm install express -S
npm install @types/express -D
npm install axios -S
index.ts:
import express, { Express, Router, Request, Response } from 'express'
import axios from 'axios'
const app: Express = express()
const router: Router = express.Router()
app.use('/api', router)
router.get('/list', async (req: Request, res: Response) => {
const result = await axios.post('https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=localCityNCOVDataList,diseaseh5Shelf')
res.json({
data: result.data
})
})
app.listen(3333, () => {
console.log('success server http://localhost:3333')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package.json:
{
"name": "nodedemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^18.0.0"
},
"dependencies": {
"axios": "^0.27.2",
"express": "^4.18.1"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
5.2 接口与初始化
localhost:3333:
import express, { Express, Router, Request, Response, NextFunction } from 'express'
import axios from 'axios'
const app: Express = express()
// 由于这里是 http://localhost:3333,而 getList 是在 http://localhost:3000 来拿的,所以需要设置跨域
app.use('*', (req: Request, res: Response, next: NextFunction) => {
res.header('Access-Control-Allow-Origin', '*')
next()
})
const router: Router = express.Router()
app.use('/api', router)
router.get('/list', async (req: Request, res: Response) => {
const result = await axios.post('https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=localCityNCOVDataList,diseaseh5Shelf')
res.json({
data: result.data
})
})
app.listen(3333, () => {
console.log('success server http://localhost:3333')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
src/server/index.ts:
import axios from 'axios'
const server = axios.create({
baseURL: 'http://localhost:3333'
})
export const getApiList = () => server.get('/api/list').then(res => res.data)
2
3
4
5
6
7
src/store/index.ts:
import { defineStore } from 'pinia'
import { getApiList } from '../server'
export const useStore = defineStore({
id: 'counter',
state: () => ({
list: {}
}),
actions: {
async getList() {
const result = await getApiList()
console.log(result)
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src/app.vue:
<template>
<div :style="{background: `url(${bg})`}" class="box">
<div class="box-left"></div>
<div class="box-center"></div>
<div class="box-right"></div>
</div>
</template>
<script setup lang='ts'>
import bg from './assets/1.jpg'
import { useStore } from './stores'
const store = useStore()
console.log(store.getList())
</script>
<style lang="less">
* {
padding: 0;
margin: 0;
}
html,
body,
#app {
height: 100%;
overflow: hidden;
}
.box {
height: 100%;
display: flex;
overflow: hidden;
&-left {
width: 400px;
}
&-center {
flex: 1;
}
&-right {
width: 400px;
}
}
</style>
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
5.3 Map
- 安装:
npm install echarts -S
- 代码:
App.vue:
<template>
<div :style="{ background: `url(${bg})` }" class="box">
<div class="box-left"></div>
<div id="china" class="box-center"></div>
<div class="box-right"></div>
</div>
</template>
<script setup lang='ts'>
import bg from './assets/1.jpg'
import { useStore } from './stores'
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import './assets/china.js'
import { geoCoordMap } from './assets/geoMap'
const store = useStore()
onMounted(async () => {
await store.getList()
const city = store.list.diseaseh5Shelf.areaTree[0].children
console.log(city)
// 在原来的 data 数组中覆盖新的 data 数组
const data = city.map(v => {
// console.log(v.name, geoCoordMap[v.name].concat(v.total.nowConfirm))
return {
name: v.name,
value: geoCoordMap[v.name].concat(v.total.nowConfirm)
}
})
const charts = echarts.init(document.querySelector('#china') as HTMLElement)
// 旧的 data 数组
// var data = [
// {
// name: "内蒙古",
// itemStyle: {
// areaColor: "#56b1da",
// },
// value: [110.3467, 41.4899]
// },
// ]
charts.setOption({
geo: {
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"],
layoutSize: "100%",
itemStyle: {
// normal: {
areaColor: {
type: "linear-gradient",
x: 0,
y: 1200,
x2: 1000,
y2: 0,
colorStops: [
{
offset: 0,
color: "#152E6E", // 0% 处的颜色
},
{
offset: 1,
color: "#0673AD", // 50% 处的颜色
},
],
global: true, // 缺省为 false
},
shadowColor: "#0f5d9d",
shadowOffsetX: 0,
shadowOffsetY: 15,
opacity: 0.5,
// },
},
emphasis: {
areaColor: "#0f5d9d",
},
regions: [
{
name: "南海诸岛",
itemStyle: {
areaColor: "rgba(0, 10, 52, 1)",
borderColor: "rgba(0, 10, 52, 1)",
// normal: {
opacity: 0,
label: {
show: false,
color: "#009cc9",
},
// },
},
label: {
show: false,
color: "#FFFFFF",
fontSize: 12,
},
},
],
},
series: [
{
type: "map",
selectedMode: "multiple",
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"], //地图位置
layoutSize: "100%",
label: {
show: true,
color: "#FFFFFF",
fontSize: 12,
},
itemStyle: {
//normal: {
areaColor: "#0c3653",
borderColor: "#1cccff",
borderWidth: 1.8,
//},
},
emphasis: {
areaColor: "#56b1da",
label: {
show: true,
color: "#fff"
},
},
data: data,
},
{
type: 'scatter',
coordinateSystem: 'geo',
// symbol: 'image://http://ssq168.shupf.cn/data/biaoji.png',
// symbolSize: [30,120],
// symbolOffset:[0, '-40%'] ,
symbol: 'pin',
symbolSize: [45, 45],
label: {
show: true,
color: '#fff',
formatter(value: any) {
// console.log('====>', value)
return value.data.value[2]
}
},
itemStyle: {
// normal: {
color: '#D8BC37', //标志颜色
// }
},
data: data
},
]
})
})
</script>
<style lang="less">
* {
padding: 0;
margin: 0;
}
html,
body,
#app {
height: 100%;
overflow: hidden;
}
.box {
height: 100%;
display: flex;
overflow: hidden;
&-left {
width: 400px;
}
&-center {
flex: 1;
}
&-right {
width: 400px;
}
}
</style>
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
src/store/index.ts:
import { defineStore } from 'pinia'
import { getApiList } from '../server'
// 下面是利用一个插件转 JSON 为 ts 类型,JSON 可以在浏览器发送请求的 network 处查看响应
import type { RootObject } from './type'
export const useStore = defineStore({
id: 'counter',
state: () => ({
list: <RootObject>{}
}),
actions: {
async getList() {
const result = await getApiList()
this.list = result
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/assets/geoMap.ts:
export const geoCoordMap: Record<string, Array<number>> = {
'台湾': [121, 23],
'黑龙江': [127, 48],
'内蒙古': [110.3467, 41.4899],
"吉林": [125.8154, 44.2584],
'北京': [116.4551, 40.2539],
"辽宁": [123.1238, 42.1216],
"河北": [114.4995, 38.1006],
"天津": [117.4219, 39.4189],
"山西": [112.3352, 37.9413],
"陕西": [109.1162, 34.2004],
"甘肃": [103.5901, 36.3043],
"宁夏": [106.3586, 38.1775],
"青海": [99.4038, 36.8207],
"新疆": [87.9236, 43.5883],
"西藏": [88.388277, 31.56375],
"四川": [103.9526, 30.7617],
"重庆": [108.384366, 30.439702],
"山东": [117.1582, 36.8701],
"河南": [113.4668, 34.6234],
"江苏": [118.8062, 31.9208],
"安徽": [117.29, 32.0581],
"湖北": [114.3896, 30.6628],
"浙江": [119.5313, 29.8773],
"福建": [119.4543, 25.9222],
"江西": [116.0046, 28.6633],
"湖南": [113.0823, 28.2568],
"贵州": [106.6992, 26.7682],
"云南": [102.9199, 25.4663],
"广东": [113.12244, 23.009505],
"广西": [108.479, 23.1152],
"海南": [110.3893, 19.8516],
'上海': [121.4648, 31.2891],
'香港': [114.30, 22.9],
'澳门': [113.5, 22.2]
};
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
5.4 地图点击省份显示具体地方疫情情况
app.vue:
<template>
<div :style="{ background: `url(${bg})` }" class="box">
<div class="box-left"></div>
<div id="china" class="box-center"></div>
<div style="color: white;" class="box-right">
<table class="table" cellspacing="0" border="1">
<thead>
<tr>
<th>地区</th>
<th>新增确诊</th>
<th>累计确诊</th>
<th>治愈</th>
<th>死亡</th>
</tr>
</thead>
<transition-group enter-active-class="animate__animated animate__flipInY" tag="tbody">
<tr :key="item.name" v-for="item in store.item">
<td align="center">{{ item.name }}</td>
<td align="center">{{ item.today.confirm }}</td>
<td align="center">{{ item.total.confirm }}</td>
<td align="center">{{ item.total.heal }}</td>
<td align="center">{{ item.total.dead }}</td>
</tr>
</transition-group>
</table>
</div>
</div>
</template>
<script setup lang='ts'>
import bg from './assets/1.jpg'
import { useStore } from './stores'
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import './assets/china.js'
import { geoCoordMap } from './assets/geoMap'
import 'animate.css'
const store = useStore()
onMounted(async () => {
await store.getList()
initCharts()
})
const initCharts = () => {
const city = store.list.diseaseh5Shelf.areaTree[0].children
console.log(city)
// 在原来的 data 数组中覆盖新的 data 数组
const data = city.map(v => {
// console.log(v.name, geoCoordMap[v.name].concat(v.total.nowConfirm))
return {
name: v.name,
value: geoCoordMap[v.name].concat(v.total.nowConfirm),
children: v.children
}
})
const charts = echarts.init(document.querySelector('#china') as HTMLElement)
charts.setOption({
geo: {
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"],
layoutSize: "100%",
itemStyle: {
// normal: {
areaColor: {
type: "linear-gradient",
x: 0,
y: 1200,
x2: 1000,
y2: 0,
colorStops: [
{
offset: 0,
color: "#152E6E", // 0% 处的颜色
},
{
offset: 1,
color: "#0673AD", // 50% 处的颜色
},
],
global: true, // 缺省为 false
},
shadowColor: "#0f5d9d",
shadowOffsetX: 0,
shadowOffsetY: 15,
opacity: 0.5,
// },
},
emphasis: {
areaColor: "#0f5d9d",
},
regions: [
{
name: "南海诸岛",
itemStyle: {
areaColor: "rgba(0, 10, 52, 1)",
borderColor: "rgba(0, 10, 52, 1)",
// normal: {
opacity: 0,
label: {
show: false,
color: "#009cc9",
},
// },
},
label: {
show: false,
color: "#FFFFFF",
fontSize: 12,
},
},
],
},
series: [
{
type: "map",
// selectedMode: "multiple",
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"], //地图位置
layoutSize: "100%",
label: {
show: true,
color: "#FFFFFF",
fontSize: 12,
},
itemStyle: {
//normal: {
areaColor: "#0c3653",
borderColor: "#1cccff",
borderWidth: 1.8,
//},
},
emphasis: {
areaColor: "#56b1da",
label: {
show: true,
color: "#fff"
},
},
data: data,
},
{
type: 'scatter',
coordinateSystem: 'geo',
// symbol: 'image://http://ssq168.shupf.cn/data/biaoji.png',
// symbolSize: [30,120],
// symbolOffset:[0, '-40%'] ,
symbol: 'pin',
symbolSize: [45, 45],
label: {
show: true,
color: '#fff',
formatter(value: any) {
// console.log('====>', value)
return value.data.value[2]
}
},
itemStyle: {
// normal: {
color: '#D8BC37', //标志颜色
// }
},
data: data
},
]
})
charts.on('click', (e: any) => {
console.log(e)
store.item = e.data.children
})
}
</script>
<style lang="less">
* {
padding: 0;
margin: 0;
}
html,
body,
#app {
height: 100%;
overflow: hidden;
}
.box {
height: 100%;
display: flex;
overflow: hidden;
&-left {
width: 400px;
}
&-center {
flex: 1;
}
&-right {
width: 400px;
}
}
.table {
width: 100%;
background: #212028;
tr {
th {
padding: 5px;
white-space: nowrap;
}
td {
padding: 5px 10px;
width: 100px;
white-space: nowrap;
}
}
}
</style>
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
5.5 左上角的图
app.vue:
<template>
<div :style="{ background: `url(${bg})` }" class="box">
<div style="color: white;" class="box-left">
<div class="box-left-card">
<section>
<div>较上日+{{ store.chinaAdd.localConfirmH5 }}</div>
<div>{{ store.chinaTotal.localConfirm }}</div>
<div>本土现有确诊</div>
</section>
<section>
<div>较上日+{{ store.chinaAdd.nowConfirm }}</div>
<div>{{ store.chinaTotal.nowConfirm }}</div>
<div>现有确诊</div>
</section>
<section>
<div>较上日+{{ store.chinaAdd.confirm }}</div>
<div>{{ store.chinaTotal.confirm }}</div>
<div>累计确诊</div>
</section>
<section>
<div>较上日+{{ store.chinaAdd.noInfect }}</div>
<div>{{ store.chinaTotal.noInfect }}</div>
<div>无症状感染者</div>
</section>
<section>
<div>较上日+{{ store.chinaAdd.importedCase }}</div>
<div>{{ store.chinaTotal.importedCase }}</div>
<div>境外输入</div>
</section>
<section>
<div>较上日+{{ store.chinaAdd.dead }}</div>
<div>{{ store.chinaTotal.dead }}</div>
<div>累计死亡</div>
</section>
</div>
</div>
<div id="china" class="box-center"></div>
<div style="color: white;" class="box-right">
<table class="table" cellspacing="0" border="1">
<thead>
<tr>
<th>地区</th>
<th>新增确诊</th>
<th>累计确诊</th>
<th>治愈</th>
<th>死亡</th>
</tr>
</thead>
<transition-group enter-active-class="animate__animated animate__flipInY" tag="tbody">
<tr :key="item.name" v-for="item in store.item">
<td align="center">{{ item.name }}</td>
<td align="center">{{ item.today.confirm }}</td>
<td align="center">{{ item.total.confirm }}</td>
<td align="center">{{ item.total.heal }}</td>
<td align="center">{{ item.total.dead }}</td>
</tr>
</transition-group>
</table>
</div>
</div>
</template>
<script setup lang='ts'>
import bg from './assets/1.jpg'
import { useStore } from './stores'
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import './assets/china.js'
import { geoCoordMap } from './assets/geoMap'
import 'animate.css'
const store = useStore()
onMounted(async () => {
await store.getList()
initCharts()
})
const initCharts = () => {
const city = store.list.diseaseh5Shelf.areaTree[0].children
store.item = city[6].children
console.log('city', city)
// 在原来的 data 数组中覆盖新的 data 数组
const data = city.map(v => {
// console.log(v.name, geoCoordMap[v.name].concat(v.total.nowConfirm))
return {
name: v.name,
value: geoCoordMap[v.name].concat(v.total.nowConfirm),
children: v.children
}
})
const charts = echarts.init(document.querySelector('#china') as HTMLElement)
charts.setOption({
geo: {
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"],
layoutSize: "100%",
itemStyle: {
// normal: {
areaColor: {
type: "linear-gradient",
x: 0,
y: 1200,
x2: 1000,
y2: 0,
colorStops: [
{
offset: 0,
color: "#152E6E", // 0% 处的颜色
},
{
offset: 1,
color: "#0673AD", // 50% 处的颜色
},
],
global: true, // 缺省为 false
},
shadowColor: "#0f5d9d",
shadowOffsetX: 0,
shadowOffsetY: 15,
opacity: 0.5,
// },
},
emphasis: {
areaColor: "#0f5d9d",
},
regions: [
{
name: "南海诸岛",
itemStyle: {
areaColor: "rgba(0, 10, 52, 1)",
borderColor: "rgba(0, 10, 52, 1)",
// normal: {
opacity: 0,
label: {
show: false,
color: "#009cc9",
},
// },
},
label: {
show: false,
color: "#FFFFFF",
fontSize: 12,
},
},
],
},
series: [
{
type: "map",
// selectedMode: "multiple",
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"], //地图位置
layoutSize: "100%",
label: {
show: true,
color: "#FFFFFF",
fontSize: 12,
},
itemStyle: {
//normal: {
areaColor: "#0c3653",
borderColor: "#1cccff",
borderWidth: 1.8,
//},
},
emphasis: {
areaColor: "#56b1da",
label: {
show: true,
color: "#fff"
},
},
data: data,
},
{
type: 'scatter',
coordinateSystem: 'geo',
// symbol: 'image://http://ssq168.shupf.cn/data/biaoji.png',
// symbolSize: [30,120],
// symbolOffset:[0, '-40%'] ,
symbol: 'pin',
symbolSize: [45, 45],
label: {
show: true,
color: '#fff',
formatter(value: any) {
// console.log('====>', value)
return value.data.value[2]
}
},
itemStyle: {
// normal: {
color: '#D8BC37', //标志颜色
// }
},
data: data
},
]
})
charts.on('click', (e: any) => {
console.log(e)
store.item = e.data.children
})
}
</script>
<style lang="less">
* {
padding: 0;
margin: 0;
}
@itemColor: #41b0db;
@itemBg: #223651;
@itemBorder: #212028;
html,
body,
#app {
height: 100%;
overflow: hidden;
}
.box {
height: 100%;
display: flex;
overflow: hidden;
&-left {
width: 400px;
&-card {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
section {
background: @itemBg;
border: 1px solid @itemBorder;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
div:nth-child(2) {
color: @itemColor;
padding: 10px 0;
font-size: 20px;
font-weight: bold;
}
}
}
}
&-center {
flex: 1;
}
&-right {
width: 400px;
}
}
.table {
width: 100%;
background: #212028;
tr {
th {
padding: 5px;
white-space: nowrap;
}
td {
padding: 5px 10px;
width: 100px;
white-space: nowrap;
}
}
}
</style>
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
5.6 折线图
index.ts:
import { defineStore } from 'pinia'
import { getApiList } from '../server'
// 下面是利用一个插件转 JSON 为 ts 类型,JSON 可以在浏览器发送请求的 network 处查看响应
import type { Children, RootObject, ChinaAdd, ChinaTotal } from './type'
export const useStore = defineStore({
id: 'counter',
state: () => ({
list: <RootObject>{},
item: <Children[]>[],
chinaAdd: <ChinaAdd>{},
chinaTotal: <ChinaTotal>{},
cityDetail: <Children[]>[]
}),
actions: {
async getList() {
const result = await getApiList()
this.list = result
this.chinaAdd = this.list.diseaseh5Shelf.chinaAdd
this.chinaTotal = this.list.diseaseh5Shelf.chinaTotal
this.cityDetail = this.list.diseaseh5Shelf.areaTree[0].children
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
App.vue:
<script setup lang='ts'>
import bg from './assets/1.jpg'
import { useStore } from './stores'
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import './assets/china.js'
import { geoCoordMap } from './assets/geoMap'
import 'animate.css'
const store = useStore()
onMounted(async () => {
await store.getList()
initCharts()
initLine()
})
const initCharts = () => {
const city = store.list.diseaseh5Shelf.areaTree[0].children
store.item = city[6].children
console.log('city', city)
// 在原来的 data 数组中覆盖新的 data 数组
const data = city.map(v => {
// console.log(v.name, geoCoordMap[v.name].concat(v.total.nowConfirm))
return {
name: v.name,
value: geoCoordMap[v.name].concat(v.total.nowConfirm),
children: v.children
}
})
const charts = echarts.init(document.querySelector('#china') as HTMLElement)
charts.setOption({
geo: {
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"],
layoutSize: "100%",
itemStyle: {
// normal: {
areaColor: {
type: "linear-gradient",
x: 0,
y: 1200,
x2: 1000,
y2: 0,
colorStops: [
{
offset: 0,
color: "#152E6E", // 0% 处的颜色
},
{
offset: 1,
color: "#0673AD", // 50% 处的颜色
},
],
global: true, // 缺省为 false
},
shadowColor: "#0f5d9d",
shadowOffsetX: 0,
shadowOffsetY: 15,
opacity: 0.5,
// },
},
emphasis: {
areaColor: "#0f5d9d",
},
regions: [
{
name: "南海诸岛",
itemStyle: {
areaColor: "rgba(0, 10, 52, 1)",
borderColor: "rgba(0, 10, 52, 1)",
// normal: {
opacity: 0,
label: {
show: false,
color: "#009cc9",
},
// },
},
label: {
show: false,
color: "#FFFFFF",
fontSize: 12,
},
},
],
},
series: [
{
type: "map",
// selectedMode: "multiple",
map: "china",
aspectScale: 0.8,
layoutCenter: ["50%", "50%"], //地图位置
layoutSize: "100%",
label: {
show: true,
color: "#FFFFFF",
fontSize: 12,
},
itemStyle: {
//normal: {
areaColor: "#0c3653",
borderColor: "#1cccff",
borderWidth: 1.8,
//},
},
emphasis: {
areaColor: "#56b1da",
label: {
show: true,
color: "#fff"
},
},
data: data,
},
{
type: 'scatter',
coordinateSystem: 'geo',
// symbol: 'image://http://ssq168.shupf.cn/data/biaoji.png',
// symbolSize: [30,120],
// symbolOffset:[0, '-40%'] ,
symbol: 'pin',
symbolSize: [45, 45],
label: {
show: true,
color: '#fff',
formatter(value: any) {
// console.log('====>', value)
return value.data.value[2]
}
},
itemStyle: {
// normal: {
color: '#D8BC37', //标志颜色
// }
},
data: data
},
]
})
charts.on('click', (e: any) => {
console.log(e)
store.item = e.data.children
})
}
const initLine = () => {
const charts = echarts.init(document.querySelector('.box-left-line') as HTMLElement)
charts.setOption({
backgroundColor: '#223651',
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: store.cityDetail.map(v => v.name).slice(0, 10),
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
margin: 1
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
label: {
show: true
},
series: [
{
data: store.cityDetail.map(v => v.today.confirm).slice(0, 10),
type: 'line',
smooth: true
}
]
})
}
</script>
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
6.vue router
6.1 前置准备
- 安装:
npm init vite@latest
- 代码:
src/app.vue:
<template>
<div>
<h1>yuanke好帅</h1>
<div>
<!-- 相当于 a 标签 -->
<router-link to="/">Login</router-link>
<router-link style="margin-left: 10px;" to="/reg">Reg</router-link>
</div>
<br />
<!-- 用来展示路由内容,由于 routes 中规定了 / 的内容,故在 / 上会自动展示该内容 -->
<router-view></router-view>
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
src/router/index.ts:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('../components/login.vue')
},
{
path: '/reg',
component: () => import('../components/reg.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
6.2 路由模式
模式一 - hash 实现:
改变 URL 的 hash 部分不会引起页面刷新
通过 hashchange
事件监听 URL 的变化,改变 URL 的方式只有几种:
- 通过浏览器前进后退改变 URL
- 通过
<a>
标签改变 URL - 通过
window.location
改变 URL
控制台:
- window.addEventListener('hashchange', e => {
console.log(e)
})
undefined
- location.hash
'#/reg'
- location.hash = '/'
'/'
HashChangeEvent {isTrusted: true, oldURL: 'http://localhost:3000/#/reg', newURL: 'http://localhost:3000/#/', type: 'hashchange', target: Window, …}
- location.hash = '/reg'
'/reg'
HashChangeEvent {isTrusted: true, oldURL: 'http://localhost:3000/#/', newURL: 'http://localhost:3000/#/reg', type: 'hashchange', target: Window, …}
2
3
4
5
6
7
8
9
10
11
12
src/router/index.ts:
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/', // http://localhost:3000/#/
component: () => import('../components/login.vue')
},
{
path: '/reg', // http://localhost:3000/#/reg
component: () => import('../components/reg.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
模式二 - history 实现:
history 提供了
pushState
和replaceState
两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
history 提供类似 hashchange
事件的 popstate
事件,但是 popstate
事件有些不同:
- 通过浏览器前进后退改变 URL 时会触发
popstate
事件 - 通过
pushState
/replaceState
或<a>
标签改变 URL 不会触发popstate
事件- 控制台输入
history.pushState({state: 1}, '', '/ccc')
,回车后会发现 URL 变成localhost:3000/ccc
,但是并没有触发popstate
事件
- 控制台输入
- 好在我们可以拦截
pushState
/replaceState
的调用和<a>
标签的点击事件来检测 URL 变化 - 通过 js 调用 history 的
back
、go
、forword
方法触发该事件 - 总之,监听 URL 变化可以实现,只是没有
hashchange
那么方便
控制台:
- window.addEventListener('popstate', e => {
console.log(e)
})
undefined
PopStateEvent {isTrusted: true, state: {…}, type: 'popstate', target: Window, currentTarget: Window, …} // 这里是按了后退按钮,但是如果直接点击 <router-link> 标签内容并没有反应
2
3
4
5
6.3 编程式导航
router-link 的另一种跳转方式:
src/router/index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Login', // 为路由命名好名称
component: () => import('../components/login.vue')
},
{
path: '/reg',
name: 'Reg',
component: () => import('../components/reg.vue')
}
]
2
3
4
5
6
7
8
9
10
11
12
src/App.vue:
<!-- <router-link to="/">Login</router-link> -->
<router-link :to="{name: 'Login'}"></router-link>
<!-- <router-link style="margin-left: 10px;" to="/reg">Reg</router-link> -->
<router-link :to="{name: 'Reg'}" style="margin-left: 10px;"></router-link>
<!-- 下面这种方式会刷新页面 -->
<!-- <a href="/reg" style="margin-left: 10px;">Reg</a> -->
2
3
4
5
6
7
编程式导航 - 三种方式:
src/App.vue:
<template>
<div>
<h1>yuanke好帅</h1>
<div>
<button @click="toPage('/')">login</button>
<button @click="toPage('/reg')">Reg</button>
</div>
<br />
<!-- 用来展示路由内容,由于 routes 中规定了 / 的内容,故在 / 上会自动展示该内容 -->
<router-view></router-view>
</div>
</template>
<script setup lang='ts'>
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = (url: string) => {
// 方式一:字符串
// router.push(url)
// 方式二:对象,方便设置
// router.push({
// path: url
// })
// 方式三:命名式,其中上面应该改为 @click="toPage('Login')",Login 为 name 的值
router.push({
name: 'Login'
})
}
</script>
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
6.4 历史记录
普通的隐藏历史记录:
<router-link replace to="/">Login</router-link>
<router-link replace to="/reg" style="margin-left: 10px;">Reg</router-link>
2
编程式路由隐藏历史记录:
<script setup lang='ts'>
import { useRouter } from 'vue-router'
const router = useRouter()
const toPage = (url: string) => {
router.replace(url)
}
</script>
2
3
4
5
6
7
8
9
前进后退 - 编程式路由:
<button @click="next()" style="margin-left: 10px;">next</button>
<button @click="prev()" style="margin-left: 10px">prev</button>
<script setup lang='ts'>
import { useRouter } from 'vue-router'
const router = useRouter()
const next = () => {
router.go(1)
}
const prev = () => {
// router.go(-1)
router.back()
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
6.5 路由传参 - 方式一
安装插件 json to ts,快捷键:ctrl + shift + alt + s
src/components/list.json:
{
"data": [
{
"name": "哈哈哈我是啥比",
"price": 500,
"id": 1
},
{
"name": "无情者伤人",
"price": 800,
"id": 2
},
{
"name": "有情者自伤",
"price": 800,
"id": 3
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/components/login.vue:
这是传参的组件
<template>
<div>嘿嘿嘿!我是列表页面</div>
<table cellspacing="0" class="table" border="1">
<thead>
<tr>
<th>品牌</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr :key="item.id" v-for="item in data">
<th>{{ item.name }}</th>
<th>{{ item.price }}</th>
<th>
<button @click="toDetail(item)">详情</button>
</th>
</tr>
</tbody>
</table>
</template>
<script setup lang='ts'>
import { data } from './list.json'
import { useRouter } from 'vue-router'
const router = useRouter()
type Item = {
name: string;
price: number;
id: number;
}
const toDetail = (item: Item) => {
router.push({
path: '/reg',
// 传参,必须为对象
query: item
})
}
</script>
<style scoped>
.table {
width: 400px;
}
</style>
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
src/components/reg.vue:
这是接收参数的组件,要使用 route 来接收
<template>
<div class="reg">
<button @click="router.back()">返回</button>
<h3>嘿嘿嘿!我是详情页面</h3>
</div>
<div>品牌:{{ route.query.name }}</div>
<div>价格:{{ route.query.price }}</div>
<div>ID:{{ route.query.id }}</div>
</template>
<script setup lang='ts'>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
</script>
<style scoped>
.reg {
background: red;
height: 400px;
width: 400px;
font-size: 20px;
color: white;
}
</style>
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
6.6 路由传参 - 方式二(推荐)
router.push({ name: 'xxx', params }) 这种形式是第二种路由传参的方法,其中 path 改成 name,query 改成 params。好处:不会显示在 url 上,而是储存在内存当中
方式一:储存在内存之中的参数(刷新会丢失):
login.vue:
const toDetail = (item: Item) => {
router.push({
name: 'Reg',
params: item
})
}
2
3
4
5
6
reg.vue:
<div>品牌:{{ route.params.name }}</div>
<div>价格:{{ route.params.price }}</div>
<div>ID:{{ route.params.id }}</div>
2
3
方式二:动态路由参数:
login.vue:
const toDetail = (item: Item) => {
router.push({
name: 'Reg',
params: {
id: item.id
}
})
}
2
3
4
5
6
7
8
reg.vue:
<template>
<div>
<button @click="router.back()">返回</button>
<h3>嘿嘿嘿!我是详情页面</h3>
</div>
<div>品牌:{{ item?.name }}</div>
<div>价格:{{ item?.price }}</div>
<div>ID:{{ item?.id }}</div>
</template>
<script setup lang='ts'>
import { data } from './list.json'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 找到匹配的对象并返回
const item = data.find(v => v.id === Number(route.params.id))
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
src/router/index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Login', // 为路由命名好名称
component: () => import('../components/login.vue')
},
{
path: '/reg/:id',
name: 'Reg',
component: () => import('../components/reg.vue')
}
]
2
3
4
5
6
7
8
9
10
11
12
6.7 嵌套路由
src/router/index.ts:
const routes: Array<RouteRecordRaw> = [
{
// 这里的 / 是相对路径,如果改成 /user,则 router-link 需要写 to="/user/reg" 才行
path: '/',
component: () => import('../components/footer.vue'),
children: [
{
path: '',
name: 'Login', // 为路由命名好名称
component: () => import('../components/login.vue')
},
{
path: '/reg',
name: 'Reg',
component: () => import('../components/reg.vue')
}
]
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.vue:
<!-- 展示 / 的内容,即 footer 组件的内容 -->
<router-view></router-view>
2
footer.vue:
<template>
<div>
<!-- 这里展示子路由的内容 -->
<router-view></router-view>
<hr>
<h1>我是父路由</h1>
<div>
<!-- 点击标签,上面 router-view 展示子路由的内容 -->
<router-link to="/">Login</router-link>
<router-link to="/reg" style="margin-left: 10px;">Reg</router-link>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
6.8 命名视图
命名视图可以在同一级(同一个组件)中展示更多的路由视图,而不是嵌套显示。命名视图可以让一个组件中具有多个路由渲染出口,这对于一些特定的布局组件非常有用。命名视图的概念类似于“具名卡槽”,并且视图的默认名称也为 default。一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件,确保正确使用 components。
index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('../components/root.vue'),
children: [
{
path: '/user1',
components: {
default: () => import('../components/A.vue')
}
},
{
path: '/user2',
components: {
bbb: () => import('../components/B.vue'),
ccc: () => import('../components/C.vue')
}
}
]
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root.vue:
<template>
<div>
<router-link to="/user1">/user1</router-link>
<router-link to="/user2" style="margin-left: 30px;">/user2</router-link>
<!-- 点击 user1,显示默认的;点击 user2,显示两个组件 -->
<router-view></router-view>
<router-view name="bbb"></router-view>
<router-view name="ccc"></router-view>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
6.9 重定向 - 别名
redirect - 重定向 - 多种写法:
index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('../components/root.vue'),
// 当访问 / 时,默认变成访问 /user1(由于为父子路由,所以两者内容都显示了)
// 第二种方法:将 children 的 path: 'user1' 改成 path: '' 这种默认路由写法,这样就默认 / 显示二者内容
// redirect: '/user1',
// redirect: {
// path: '/user1'
// },
// redirect(to) {
// console.log(to, '===>')
// return '/user1'
// },
redirect(to) {
console.log(to, '<====')
return {
path: '/user1',
query: {
name: 'hahaha' // http://localhost:3000/#/user1?name=hahaha
}
}
},
children: [
{
path: '/user1',
components: {
default: () => import('../components/A.vue')
}
},
{
path: '/user2',
components: {
bbb: () => import('../components/B.vue'),
ccc: () => import('../components/C.vue')
}
}
]
}
]
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
alias - 别名:
index.ts:
path: '/',
component: () => import('../components/root.vue'),
alias: ['/root', '/root1', '/root2'], // 访问 localhost:3000/#/root1 也 ok
2
3
6.10 导航守卫 - 前置守卫
login.vue:
<template>
<div class="login">
<el-card class="box-card">
<el-form ref="form" :rules="rules" :model="formInline" class="demo-form-inline">
<el-form-item prop="user" label="账号:">
<el-input v-model="formInline.user" placeholder="请输入账号" />
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input v-model="formInline.password" placeholder="请输入密码" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang='ts'>
import type { FormItemRule, FormInstance } from 'element-plus';
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
type Form = {
user: string,
password: string
}
type Rules = {
[K in keyof Form]?: Array<FormItemRule>
}
const formInline = reactive<Form>({
user: '',
password: '',
})
const form = ref<FormInstance>()
const onSubmit = () => {
form.value?.validate((validate) => {
console.log(validate)
if (validate) {
router.push('/index')
localStorage.setItem('token', '1')
} else {
ElMessage.error('请输入完整')
}
})
}
const rules = reactive<Rules>({
user: [
{
required: true,
message: '请输入账号',
type: 'string'
}
],
password: [
{
required: true,
message: '请输入密码',
type: 'string'
}
]
})
</script>
<style scoped lang="less">
.login {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
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
main.ts:
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
const whiteList = ['/']
// 当进入每个路由时都会被拦截
router.beforeEach((to, from, next) => {
// 当在首页时(白名单)或者已经登录过的情况下,直接进入
if (whiteList.includes(to.path) || localStorage.getItem('token')) {
next()
} else {
next('/')
}
})
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
6.11 导航首位 - 后置守卫
定时器一直是 JavaScript 的核心技术,而编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够端,这样才能让不同的动画效果显得平滑流畅;另一方面,循环间隔还要足够场,这样才能保证浏览器有能力渲染产生的变化。
浏览器重绘操作会加以限制,一般每秒重绘 60 次。最平滑的循环间隔是 1000ms / 60,约等于 16.6ms。
但是 setTimeout 和 setInterval 都不准确,它们内在运行机制决定了时间间隔实际上知识指定了把动画代码添加到浏览器 UI 现成队列中等待执行的事件。如果队列前面已经加入了其他任务,那动画代码要等到前面任务完成后执行。
requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,不会因为间隔事件过短,造成过渡绘制,增加开销;也不会因为间隔事件太短,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
loadingBar.vue:
<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>
<script setup lang='ts'>
import { ref, onMounted } from 'vue'
let speed = ref<number>(1)
let bar = ref<HTMLElement>()
let timer = ref<number>(0)
const startLoading = () => {
let dom = bar.value as HTMLElement
speed.value = 1
timer.value = window.requestAnimationFrame(function fn() {
if (speed.value < 90) {
speed.value += 1
dom.style.width = speed.value + '%'
timer.value = window.requestAnimationFrame(fn)
} else {
speed.value = 1
window.cancelAnimationFrame(timer.value)
}
})
}
const endLoading = () => {
let dom = bar.value as HTMLElement
setTimeout(() => {
window.requestAnimationFrame(() => {
speed.value = 100
dom.style.width = speed.value + '%'
})
}, 500)
}
defineExpose({
startLoading,
endLoading
})
</script>
<style scoped lang="less">
.wraps {
position: fixed;
top: 0;
width: 100%;
height: 2px;
.bar {
height: inherit;
width: 0;
background: blue;
}
}
</style>
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
main.ts:
import { createApp, createVNode, render } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import loadingBar from './components/loadingBar.vue'
// 将 dom 转为虚拟 dom
const Vnode = createVNode(loadingBar)
// 将虚拟 dom 挂载到 body 上面去
render(Vnode, document.body)
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
const whiteList = ['/']
router.beforeEach((to, from, next) => {
Vnode.component?.exposed?.startLoading()
if (whiteList.includes(to.path) || localStorage.getItem('token')) {
next()
} else {
next('/')
}
})
router.afterEach((to, from) => {
Vnode.component?.exposed?.endLoading()
})
app.mount('#app')
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
6.12 路由元信息
通过路由记录的 meta 属性可以定义路由的元信息,使用路由元信息可以在路由中附加自定义的数据,例如:
- 权限校验标识
- 路由组件的过渡名称
- 路由组件持久化缓存(keep-alive)的相关配置
- 标题名称
实现功能:每进到一个路由,都将元信息记录的 title 赋值到
document.title
当中
- 新增 meta 属性:
src/router/index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta: {
title: '登录页面'
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta: {
title: '首页'
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 由于
document.title
是字符串类型,而 meta 是 unknown 类型,所以需要拓展RouteMeta
接口来输入 meta 字段(可以将鼠标移至 meta 上方查看):
src/router/index.ts:
declare module 'vue-router' {
interface RouteMeta {
title: string
}
}
2
3
4
5
main.ts:
router.beforeEach((to, from, next) => {
console.log(to)
document.title = to.meta.title
Vnode.component?.exposed?.startLoading()
next()
})
2
3
4
5
6
6.13 路由过渡动效
利用 animate.css 实现的
src/router/index.ts:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta: {
title: '登录页面',
transition: 'animate__fadeIn'
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta: {
title: '首页',
transition: 'animate__bounceIn'
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src/app.vue:
<template>
<router-view #default="{route, Component}">
<transition :enter-active-class="`animate__animated ${route.meta.transition}`">
<!-- 这里是典型的动态组件 -->
<component :is="Component"></component>
</transition>
</router-view>
</template>
<script setup lang='ts'>
import 'animate.css'
</script>
2
3
4
5
6
7
8
9
10
11
12
6.14 滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面一样,vue-router 可以自定义路由切换时页面如何滚动
index.ts:
const router = createRouter({
history: createWebHashHistory(),
scrollBehavior(to, from, savePosition) {
// 1.如果返回,则返回该页面之前位置,否则返回到顶部
// console.log(savePosition)
// if (savePosition) {
// return savePosition
// } else {
// return {
// top: 0
// }
// }
// 2.无论路由怎么跳转,默认跳转到距离顶部 50px
// return {
// top: 50
// }
// 3.2s 后,自动跳转到底部
return new Promise((r) => {
setTimeout(() => {
r({
top: 999999
})
}, 2000)
})
},
routes
})
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
6.15 动态路由
一般使用动态路由都是后台会返回一个路由表前端通过调接口拿到后处理(后端处理路由)。主要使用的方法就是
route.addRoute
src/views/index.vue:
<template>
<h1>
进球了!!!!哈哈哈
</h1>
<router-link to="/demo1">demo1</router-link>
<router-link to="/demo2">demo2</router-link>
<router-link to="/demo3">demo3</router-link>
</template>
2
3
4
5
6
7
8
src/views/Login.vue:
type RouteType = {
path: string,
name: string,
component: RawRouteComponent
}
const initRouter = async () => {
const result = await axios.get('http://localhost:9999/login', {
params: formInline
})
// console.log(result)
result.data.route.forEach((v: RouteType) => {
router.addRoute({
path: v.path,
name: v.name,
component: () => import(`../views/${v.component}`)
})
})
router.push('/index')
console.log(router.getRoutes())
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
后端:
import express, { Express, Request, Response } from 'express'
const app: Express = express()
app.get('/login', (req: Request, res: Response) => {
res.header('Access-Control-Allow-Origin', '*')
if (req.query.user == 'admin' && req.query.password == '123456') {
res.json({
route: [
{
path: '/demo1',
name: 'Demo1',
component: 'demo1.vue'
},
{
path: '/demo2',
name: 'Demo2',
component: 'demo2.vue'
},
{
path: '/demo3',
name: 'Demo3',
component: 'demo3.vue'
}
]
})
} else if (req.query.user == 'admin2' && req.query.password == '123456') {
res.json({
route: [
{
path: '/demo1',
name: 'Demo1',
component: 'demo1.vue'
},
{
path: '/demo2',
name: 'Demo2',
component: 'demo2.vue'
}
]
})
} else {
res.json({
code: 400,
message: '账号密码错误'
})
}
})
app.listen(9999, () => {
console.log('http://localhost:9999')
})
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