前言

前端基础都了解的差不多了,天天看 CSS 啥的也啥都记不住,不如动手做。所以这篇文章记录 Vue 框架的一些实际案例。写基础都太没意思了,跟记流水账一样,所以就不写那些基础操作了。

初始化项目

1
2
3
4
5
6
7
8
# 使用 npm 进行安装
npm create npm create vue@latest
# 使用 pnpm 进行安装
pnpm create npm create vue@latest
# 下载依赖
npm/pnpm install
# 启动项目
pnpm dev

项目结构

  • Vue-app
    • src
      • api <– 网络请求封装(自己加的)
      • assets <– css样式与图片
      • components <– 暂时不知道有啥用
      • router <– 路由
      • views <– 页面
    • App.vue <– 主入口
    • main.ts <– 脚本主入口
  • index.html
  • … <– 一堆默认配置文件
  • vite.config.ts <– vite配置文件

实际场景

将返回信息以列表形式显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<script setup lang="ts">

...

const newSongs = ref<NewSong[]>([]);

const fetchNewSongs = async () => {
try {
const data = await api.get<PersonalizedNewSongRes>('/personalized/newsong');

newSongs.value = (data.result || []).map((item) => ({
id: item.id,
name: item.name,
cover: item.picUrl,
artist: item.song?.artists?.map((a) => a.name).join('/') || '',
}));
// console.log(newSongs.value);
} catch(err) {
console.log("推荐新音乐失败", err);
}
}

...

</script>

<template>
...
<ul class="song-list">
<li
class="song-item"
v-for="song in newSongs"
:key="song.id"
>
<div class="song-cover">
<img :src="song.cover" :alt="song.name">
</div>
<div class="song-info">
<p class="song-name">{{ song.name }}</p>
<p class="song-artist">{{ song.artist }}</p>
</div>
</li>
</ul>
...
</template>

注: 这里已经对网络请求进行了封装,所以直接用 api.get<>('...'); 就能直接从后端获取数据。

简单的翻页效果的实现

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
<script setup lang="ts">

...

interface Artist {
id: number;
name: string;
rank: number;
avatar: string;
}

...

const singerRank = ref<Artist[]>([]);
const currentSingerSlide = ref<number>(0);

const SINGER_PAGE_SIZE = 5;

const singerSlides = computed(() => {
const list = singerRank.value || [];
const sliders: Artist[][] = [];
for (let i = 0; i < list.length; i += SINGER_PAGE_SIZE) {
sliders.push(list.slice(i, i + SINGER_PAGE_SIZE));
}
return sliders;
});

const prevSingerSlide = () => {
if (!singerSlides.value.length) return;
currentSingerSlide.value = (currentSingerSlide.value - 1 + singerSlides.value.length) % singerSlides.value.length;
}

const nextSingerSlide = () => {
if (!singerSlides.value.length) return;
currentSingerSlide.value = (currentSingerSlide.value + 1) % singerSlides.value.length;
}
...

</script>

<template>
...
<div
v-for="(sliders, index) in singerSlides"
:key="index"
class="singer-slide"
v-show="index == currentSingerSlide"
>
<ul class="singer-list">
<li v-for="singer in sliders" :key="singer.id" class="singer-item">
<div class="singer-avatar">
<img :src="singer.avatar" :alt="singer.name">
</div>
<span class="singer-name">{{ singer.rank + 1}} {{ singer.name }}</span>
</li>
</ul>
</div>
...
<div class="singer-carousel-controls" v-if="singerSlides.length > 1">
<button class="singer-arrow" @click="prevSingerSlide"><ChevronLeft :stroke-width="3" /></button>
<button class="singer-arrow" @click="nextSingerSlide"><ChevronRight :stroke-width="3" /></button>
</div>
...
</template>

注: 这里用了 Icons-Lucide 的图标元素<ChevronRight/>

实现页面的跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">

...
const router = useRouter();
const pageChange = (id:number) => {
router.push({
name: "pagename",
id: {id},
});
}
...

</script>

<template>
...
<ul>
<li @click = "pageChange(something.id)">
...
<li>
<ul>
...
</template>

点击元素就能跳转页面

从页面 url 中获取信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">

...
const route = useRoute();
const somethingId = computed(() => Number(route.query.id) || 0);


onMounted(() => {
console.log(songthingId);
});
...

</script>

<template>
...
<p>{{ somethingId }}<p>
...
</template>

这样就能把信息在控制台和页面中显示出来了,onMounted()是生命周期函数…目前只知道把函数丢进去它就能跑

实现音乐的播放

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
<script setup lang="ts">

...

const audioRef = ref<HTMLAudioElement | null>(null);

const audioUrl = ref<string>('');
const currentTime = ref<number>(0);
const duration = ref<number>(0);
const isPlay = ref<boolean>(false);

const fetchSongUrl = async () => {
const id: number = songId.value;
if (!id) return;
try {
const data = await api.get<SongUrlRes>("/song/url", { id: id });
audioUrl.value = data.data[0]?.url || '';
console.log(audioUrl.value);
duration.value = 0;
currentTime.value = 0;
isPlay.value = false;

} catch(err) {
console.log("获取音乐播放地址失败", err);
audioUrl.value = '';
isPlay.value = false;
}
}

const handleLoadedMetadata = () => {
const audio = audioRef.value;
if (!audio) return;
duration.value = audio.duration || 0;
currentTime.value = audio.currentTime || 0;
}

const handleTogglePlay = () => {
const audio = audioRef.value;
if (!audio) return;
if (audio.paused) {
audio.play().then(() => {
isPlay.value = true;
}).catch(() => {});
} else {
audio.pause();
isPlay.value = false;
}
}

const handleAudioEnded = () => {
isPlay.value = false;
}

const handleTimeUpdate = () => {
const audio = audioRef.value;
if(!audio) return;
currentTime.value = audio.currentTime || 0;
}

...

</script>

<template>
...
<div class="controls-main">
<button class="btn-circle btn-large" @click="handleTogglePlay">
<Pause v-if="isPlay"/>
<Play v-else/>
</button>
</div>
...
<audio
v-if="audioUrl"
class="audio-hidden"
ref="audioRef"
:src="audioUrl"
@loadedmetadata="handleLoadedMetadata"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
></audio>
...
</template>

还要使用 audio 元素,这个还是第一次接触并使用

实现进度条控制歌曲

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
<script setup lang="ts">

...

const currentTime = ref<number>(0);
const duration = ref<number>(0);

const formateTime = (sec: number) => {
if (!sec || !Number.isFinite(sec)) return '00:00';
const s = Math.floor(sec);
const m = Math.floor(s / 60);
const rs = s % 60;
const mm = m.toString().padStart(2, '0');
const ss = rs.toString().padStart(2, '0');
return `${mm}:${ss}`;
}

const handleProgressClick = (event: MouseEvent) => {
const bar = event.currentTarget as HTMLElement;
const audio = audioRef.value;
if(!audio || !bar) return;

const rect = bar.getBoundingClientRect();
if (rect.width === 0) return;
const ratio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));

const newTime = duration.value * ratio;
audio.currentTime = newTime;
currentTime.value = newTime;
}

...

</script>

<template>
...
<div class="progress-wrap">
<span class="time-label">{{ formateTime(currentTime) }}</span>
<div class="progress-bar" @click="handleProgressClick">
<div class="progress-inner" :style="{ width: duration ? `${(currentTime / duration) * 100}%`: '0%' }"></div>
</div>
<span class="time-label">{{ formateTime(duration) }}</span>
</div>
...
</template>

通过 progress-bar 和 progress-inner 实现进度条效果

结语

依旧为了方便翻阅复习,于是写在了博客上。还有一大堆 interface 就不在文章里列举了…