0%

Vue实践

小敲一下

想写一个与之前微信小程序中相似的实时时间更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//定义一个方法获取时间
updateDateTime: function () {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1; // 月份从0开始,所以需要加1
// 这个月份从0开始,转化到我们这边需要加一
var day = date.getDate();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();

//这里是将时间变为自己想要的样子,如果都行无所谓的话也可以直接
var newdate = new Date().toLocaleString()

// 格式化日期和时间
var formattedDateTime = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
this.setData({
currentDateTime: formattedDateTime
});
},

onLoad(options) {
// 页面加载时开始更新时间
this.updateDateTime();
// 设置定时器,每秒更新一次时间
this.timer = setInterval(this.updateDateTime, 1000);
},
onUnload() {
clearInterval(this.timer);
},

//Vue3中的写法与之类似,但是有个问题,因为Vue3 我使用的是Vue3语法糖就有一些问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts">
//我的想法捏
import { ref,onMounted } from 'vue'

let date = ref(new Date().toLocaleString())

function updateTime() {
date = ref(new Date().toLocaleString())
}
onMounted() {
updateTime()
setInterval(updateTime, 1000);
}
// 但是,不刷新数据是不会自动更新的,也就是说setInterval(updateTime, 1000);并没有起作用
</script>

其实,这里有两个问题,第一个onMounted这个钩子的用法不对,这里写的用法是类似于微信小程序中钩子的写法在Vue3中应该为

1
2
3
4
onMounted(()=>{
updateTime()
setInterval(updateTime, 1000);
})

第二个问题,这里的

image-20240530202913984

需要改为

1
date.value = new Date().toLocaleString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
//我的想法捏
import { ref,onMounted,onUnmounted } from 'vue'

let date = ref(new Date().toLocaleString())
let timer:any

function updateTime() {
date.value = new Date().toLocaleString()
}
onMounted() {
updateTime()
timer = setInterval(updateTime, 1000);
}
onUnmounted(()=>{
clearInterval(timer)
})
// 卸载计时器,因为即使是组件卸载掉了,计时器仍然会继续date依然更新,浪费性能没必要
</script>

但是通过对比发现两者的写法差别是不大的

组件模仿页面切换

但是微信小程序和Vue3 我感觉主要的区别在于,Vue3所有的活动都是在App.vue这个根上的,而微信小程序可以创建很多新的页面,这个时候我准备给Vue3写一个类似于小程序的东西

先搞一个路由

因为只有一个根组件,那么我可以只用路由来达到目的,App.vue中先写一个占位,更换路由占位更换,这就相当于页面更换,这样的话,我感觉就实现了微信小程序页面更换的问题

看一下App.vue

App.vue相当简洁,写了 一个路由占位符,准备渲染组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<router-view></router-view>
</template>

<script setup lang="ts" name="app">

</script>

<style>

</style>

写一下路由组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// index.ts
// 引入组件和路由器,设置一个history传参

import { createRouter, createWebHistory } from "vue-router";
import Index from "./index.vue";
import Demo from "./demo.vue";
import Demolist from "./demolist.vue";
import Get from "./get.vue";
import Text from "./text.vue";
import Mess from "./mess.vue";

const router = createRouter({
history: createWebHistory(),
routes: [{

// 封装路由,将/设置为index组件,默认展示index组件,然后在index路由下封装四个组件准备展示在index组件
path: '/home',
component: Index,
children: [
{
name: 'demo',
path: 'demo',
component: Demo
}, {
name: 'demolist',
path: 'demolist',
component: Demolist
}, {
name: 'get',
path: 'get',
component: Get
}, {
name:'mess',
path: 'mess',
component: Mess
}
]
},{
// 再写一个text组件,脱离index,我打算以点击事件的方式将这个组件渲染到App.vue 来实现类似微信小程序的页面跳转
path: '/text',
component: Text
},{
path:'/',
redirect:'/home'
}]
})


export default router

这样呢是形成了两个页面的效果,一个是/home首页,另一个是/text 因为index.vue内是写了一些样式,是一个路由,所以只能算一个页面,而这个text组件,确实是独立的一个页面效果,他是再index组件卸载后,渲染上去的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// text.vue

<template>
<h2>text</h2>
<button @click="showHome()">点我</button>
</template>

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

let router = useRouter()

function showHome() {
router.push({
path:'/'
})
}
</script>

<style>
</style>

image-20240530222305593

这样这个效果基本实现,但是是这个跳转方法是不太好复现的,因为无论是history还是hash跳转都是卸载掉当前组件,然后再挂载新的组件的,毕竟一直在那里的只有app.vue,但是还是有点相像的毕竟history可以有浏览器提供的返回键来返回,就相当于wx.navigateTo这种跳转,hash不能由浏览器提供的返回键返回,是属于覆盖性的,就相当于wx.redirectTo跳转了

引入Vant组件库

之前在微信小程序中使用的 vant4 组件库也支持Vue版本,这边搞一下

先在Vue3项目中下载Vant

1
npm i vant

官方文档:https://vant-ui.github.io/vant/#/zh-CN/quickstart

全局引用

因为 我的Vue3是使用vite 来构建的,因此,会有一个vite.comfig.js文件

更改这个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import vue from '@vitejs/plugin-vue'

// 看变量名也可以猜出来,自动引入组件API
import AutoImport from 'unplugin-auto-import/vite'

// 这个会在使用组件后,自动解析并注册组件
import Components from 'unplugin-vue-components/vite'

// 这个从vant中拿到相应的样式
import { VantResolver } from '@vant/auto-import-resolver'

export default {
plugins: [
vue(),
AutoImport({
resolvers: [VantResolver()],
}),
Components({
resolvers: [VantResolver()],
}),
],
};

如果报错的话,依次下载三个引入的地址

1
2
3
npm i unplugin-auto-import -D
npm i unplugin-vue-components -D
npm i @vant/auto-import-resolver -D

这个时候就可以直接在页面中使用vant组件了,不再需要任何的引用

image-20240531115246422

按需引用

按需引用比较麻烦,需要再 main.js 的入口文件中引入

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

// 这个代码的意思是将Button这个组件中拿出来,如果我还需要 cell,calendae 等等,这里就会写很长
import { Button } from 'vant'
// 引入组件样式
import 'vant/lib/index.css';

const app = createApp(App)

// 这里要使用app.use使用组件,感觉这种方式有点麻烦,不如全局引入,一劳永逸
app.use(Button)
app.mount('#app')

剩下的就用就完了,没啥好说的。

引入three.js

官方文档:https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene

1
npm i three

安装版本号

image-20240531135049372

这个时候新建一个组件开始敲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div ref="threeContainer" class="three-container"></div>
</template>

<script setup lang="ts" name="ThreeComponent">
import * as THREE from 'three'
import { ref,onMounted } from 'vue';

const threeContainer = ref(null)

onMounted (()=>{
// 初始化Three,获取容器信息
const container = threeContainer.value


// 创建场景,创建一个新的 Three.js 场景
const scene = new THREE.Scene();


// 创建相机,创建一个透明相机,并设置其位置
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1 ,1000 );
// 这个postion.z 是设置相机高度
camera.position.z = 5


// 创建渲染器,将其大小设置为容器的宽度和高度,然后将渲染器的 DOM 元素添加到容器中
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild( renderer.domElement )


// 创建一个立方体,并将其组合成一个网格对象,然后将该网络对象添加到场景中
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color:0x00ff00 })
// 设置立方体颜色,color
const cube = new THREE.Mesh(geometry,material)
scene.add(cube)
// 将这个cude添加到 Three.js 中


// 渲染循环,创建一个动画循环,在每一帧中,旋转立方体,渲染场景
const animate = () => {
requestAnimationFrame(animate);

cube.rotation.x += 0.01
cube.rotation.y += 0.01
// x,y 分别为x,y方向旋转的速度
renderer.render(scene,camera)
}
animate()
})

</script>

<style>
.three-container {
width: 100%;
height: 100%;
overflow: hidden;
}
// 设置div样式,使其覆盖整个视口并隐藏溢出内容
</style>

image-20240531143824887

1
npm install --save-dev @types/three

这样就写出来了一个自动旋转的绿色正方体

image-20240531150142785

注意这里是有循环的,要使用卸载钩子将组件不引用时卸载掉three场景,释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 引入监听页面卸载的钩子,小改一下代码,懒得写注释了

<script setup lang="ts" name="ThreeComponent">
import * as THREE from 'three'
import { ref,onMounted,onUnmounted } from 'vue';

const threeContainer = ref<HTMLDivElement | null>(null)
const cube = new THREE.Mesh
const renderer = new THREE.WebGLRenderer();
let animationId:number

onMounted (()=>{
if (!threeContainer.value) return;

// 初始化Three
const container = threeContainer.value

// 创建场景
const scene = new THREE.Scene();

// 创建相机
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1 ,1000 );
camera.position.z = 3

// 创建渲染器
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild( renderer.domElement )

// 创建一个立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color:0x00ff00 })
const cube = new THREE.Mesh(geometry,material)
scene.add(cube)

// 渲染循环
const animate = () => {
animationId = requestAnimationFrame(animate);

cube.rotation.x += 0.01
cube.rotation.y += 0.01
console.log('1')
renderer.render(scene,camera)
}
animate()
}),
onUnmounted (()=>{
// 停止动画循环
cancelAnimationFrame(animationId)

// 清除几何体材质
cube.geometry.dispose();
(cube.material as THREE.Material).dispose()

// 移除渲染器的 DOM 元素
if (renderer && renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}

// 清理渲染器
renderer.dispose();
})

</script>

这里写了一个打印1的地方,通过编程式导航来进行跳转发现,返回后,1停止打印,说明已经释放资源

防止父容器的点击态

想写一个俄罗斯方块,写到暂停键的时候,遇到了问题,先看一下能不能看出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<div class="box-right">
<div style="margin-top: 80px;">{{forTime}}</div>
<div class="box-right-icon" @click="stop">
<div @click="stop" :class="status?'icon-content':'icon-stop'" ></div>
</div>
</div>
</template>

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

let num = 200
let hour = ref(0)
let minute = ref(0)
let second = ref(0)
let timer:any
let forTime = ref('')
let status = ref(true)


function updatatime() {
if (status.value=true) {
if (second.value == 59) {
second.value = 0
minute.value += 1
} else {
second.value += 1
}
} else {
return
}
}
function stop() {
console.log('stop triggered')
status.value = !status.value
if (status.value) {
timer = setInterval(updatatime, 1000); // 继续计时
} else {
clearInterval(timer)
}
}
onMounted(()=>{
updatatime()
timer = setInterval(updatatime,1000)
})
onUnmounted(()=>{
clearInterval(timer)
})

const time = watch(second,(value)=>{
if (minute.value<10 && second.value<10) {
forTime.value = hour.value+':0'+minute.value+':0'+second.value
} else if(second.value<10) {
forTime.value = hour.value+':'+minute.value+':0'+second.value
} else if(minute.value<10) {
forTime.value = hour.value+':0'+minute.value+':'+second.value
} else {
forTime.value = hour.value+':'+minute.value+':'+second.value
}
})

</script>

我是通过打印事件,排查出来的,点击样式内的div后,会触发两次点击事件,true变为false又变为false,以此,导致了视觉上的子容器的点击事件不好使的情况

解决方法

1
2
3
4
5
6
7
8
9
10
11
	function stop(event: Event) {
event.stopPropagation()
status.value = !status.value
if (status.value) {
timer = setInterval(updatatime, 1000); // 继续计时
} else {
clearInterval(timer)
}
}

// 给他来一个event.stopPropagation()阻止冒泡即可,之前从未遇到过这样需求,长知识了

获取非普通标签的DOM元素

写俄罗斯方块的时候,我将方块封装为不同的组件

封装组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="lang" ref="rootElement">
<div v-for="x in 4" class="lang-bor"></div>
</div>
</template>

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

const rootElement =ref<HTMLElement | null>(null)
</script>

想要获取组件的DOM元素,只需要给最外层一个ref即可

然后再调用组件的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<long-vue ref="movableDiv"></long-vue>
</template>

<script setup lang="ts">
import { ref,watch,onMounted,onUnmounted,nextTick } from 'vue';
import longVue from '../components/long.vue';

onMounted(async ()=>{
await nextTick()
if (movableDiv.value) {
const movawidth = movableDiv.value.$el.offsetWidth
const movaheight = movableDiv.value.$el.offsetHeight
console.log(movawidth,movaheight)
}
})
</script>

比直接获取HTML标签的DOM元素中多了一个 $el 加上就可以直接拿到组件的DOM 元素

导出微信语音

首先将微信呢语音收藏,然后呢在手机版中,将语音转化为笔记

再在..\FileStorage\Fav\Temp\xxxxx\res中 找到一个拓展名为silk的文件

利用一个小工具silk2mp3,我是不敢在真机中使用,所以扔虚拟机了

直接解码为mp3即可导出微信语音

属性 描述
autoplay autoplay 如果出现该属性,则音频在就绪后马上播放。
controls controls 如果出现该属性,则向用户显示音频控件(比如播放/暂停按钮)。
loop loop 如果出现该属性,则每当音频结束时重新开始播放。
muted muted 如果出现该属性,则音频输出为静音。
preload auto
metadata
none
规定当网页加载时,音频是否默认被加载以及如何被加载。
src URL 规定音频文件的 URL。

audio属性

属性 描述
audioTracks 返回表示可用音频轨道的 AudioTrackList 对象
autoplay 设置或返回是否在加载完成后随即播放音频
buffered 返回表示音频已缓冲部分的 TimeRanges 对象
controller 返回表示音频当前媒体控制器的 MediaController 对象
controls 设置或返回音频是否显示控件(比如播放/暂停等)
crossOrigin 设置或返回音频的 CORS 设置
currentSrc 回当前音频的 URL
currentTime 设置或返回音频中的当前播放位置(以秒计)
defaultMuted 设置或返回音频默认是否静音
defaultPlaybackRate 设置或返回音频的默认播放速度
duration 返回当前音频的长度(以秒计)
ended 返回音频的播放是否已结束
error 返回表示音频错误状态的 MediaError 对象
loop 设置或返回音频是否应在结束时重新播放
mediaGroup 设置或返回音频所属的组合(用于连接多个音频元素)
muted 设置或返回音频是否静音
networkState 返回音频的当前网络状态
paused 设置或返回音频是否暂停
playbackRate 设置或返回音频播放的速度
played 返回表示音频已播放部分的 TimeRanges 对象
preload 设置或返回音频是否应该在页面加载后进行加载
readyState 返回音频当前的就绪状态
seekable 返回表示音频可寻址部分的 TimeRanges 对象
seeking 返回用户是否正在音频中进行查找
src 设置或返回音频元素的当前来源
textTracks 返回表示可用文本轨道的 TextTrackList 对象
volume 设置或返回音频的音量

audio方法

方法 描述
addTextTrack() 在音频中添加一个新的文本轨道
canPlayType() 检查浏览器是否可以播放指定的音频类型
fastSeek() 在音频播放器中指定播放时间。
getStartDate() 返回一个新的Date对象,表示当前时间轴偏移量
load() 重新加载音频元素
play() 开始播放音频
pause() 暂停当前播放的音频

封装图片验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<template>
<div class="img-verify">
<canvas ref="verify" :width="state.width" :height="state.height" @click="handleDraw"></canvas>
</div>
</template>

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

const verify = ref(null)
const emit = defineEmits(["get-code"])
const state = reactive({
pool:"qwertyuiopasdfghjklzxcvbnm1234567890",
width:120,
height:40,
imgCode:'',
})

// 随机数字
const randomNum = (min,max) => {
return parseInt(Math.random() * (max - min) + min)
}

// 随机颜色
const randomColor = (min,max) => {
const r = randomNum(min,max)
const g = randomNum(min,max)
const b = randomNum(min,max)
return `rgb(${r},${g},${b})`
}

// 绘制图片
const draw = () => {
// 填充背景颜色
const ctx = verify.value.getContext("2d")
// 填充颜色
ctx.fillStyle = randomColor(180,230)
// 填充位置
ctx.fillRect(0,0,state.width,state.width)

// 定义paramText
let imgCode = '';

// 产生随机数字,并且随即旋转
for (let i = 0; i < 4; i++) {
// 产生随机数字
const text = state.pool[randomNum(0,state.pool.length)]
imgCode += text

// 随机大小字体
const fontSize = randomNum(18,40);

// 随机字体角度
const deg = randomNum(-30,30)

ctx.font = fontSize + 'px Simhei';
ctx.textBaseline = "top";
ctx.fillStyle = randomColor(80,150)


ctx.save()
ctx.translate(30 * i + 15,15);
ctx.rotate((deg * Math.PI) / 180)


ctx.fillText(text, -15 + 5, -15)
ctx.restore()
}

// 随机5条干扰线
for (var i = 0; i < 5; i++) {
ctx.beginPath();
ctx.moveTo(randomNum(0,state.width),randomNum(0,state.height))
ctx.lineTo(randomNum(0,state.width),randomNum(0,state.height))
ctx.strokeStyle = randomColor(180,230);
ctx.closePath()
ctx.stroke()
}

// 产生40个干扰点
for (var i = 0; i < 40; i++) {
ctx.beginPath();
ctx.acr(
randomNum(0,state.width),
randomNum(0,state.height),
1,
0,
2 * Math.PI
);
ctx.closePath();
ctx.fillStyle = randomColor(150,200);
ctx.fill();
}
return imgCode
}

// 点击切换图片
const handleDraw = () => {
state.imgCode = draw();
emit("get-code",state.imgCode)
}

// 初始化
onMounted(()=>{
state.imgCode = draw();
emit("get-code",state.imgCode)
})

</script>

<style>
.img-verify {
height: 40px;
margin: 0 0.1rem;
}
.img-verify canvas {
cursor: pointer;
}
</style>