前言
前端基础都了解的差不多了,天天看 CSS 啥的也啥都记不住,不如动手做。所以这篇文章记录 Vue 框架的一些实际案例。写基础都太没意思了,跟记流水账一样,所以就不写那些基础操作了。
初始化项目
1 2 3 4 5 6 7 8 9 10 11 12
| npm create npm create vue@latest
pnpm create npm create vue@latest
npm/pnpm install
pnpm dev
npm install @lucide/vue pnpm add @lucide/vue
|
项目结构
- Vue-app
- src
- stores <– 状态管理说是…
- 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('/') || '', })); } 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
| <script setup lang="ts">
...
const singerRankLoading.value = false; const singerRank = ref<Artist[]>([]);
const singerRankLoading = ref<boolean>(false);
const fetchSingerRank = async () => { singerRankLoading.value = true; try { const data = await api.get<TopArtistsRes>('/top/artists', { limit: 20 });
singerRank.value = (data.artists || []).map((item, index) => ({ id: item.id, name: item.name, rank: index, avatar: item.picUrl, })); singerRankLoading.value =false; } catch (err) { console.log("获取歌手榜单失败", err); singerRankLoading.value = false; } }
...
</script>
<template> ... <h2 class="section-title section-title--sub section-title--singer">歌手榜单</h2> <div class="tip" v-if="singerRankLoading">数据加载中...</div> <div v-else-if="!singerRank.length">数据获取失败</div> <template v-else> <div class="singer-carousel" v-if="singerSlides.length"> ... </div> </template> ... </template>
|
<template>标签可以在不加块元素的情况下实现对内部元素的控制,通常配合 v-if 和 v-else 来使用
简单的翻页效果的实现
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
| npm install @lucide/vue pnpm add @lucide/vue
|
实现页面的跳转
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 实现进度条效果
二维码登录逻辑的实现
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 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
| <script setup lang="ts">
...
const loginKey = ref<string>('');
const qrImg = ref<string>('');
const qrCheckTimer = ref<number | null>(null);
const userStore = useUserStore();
const fetchloginKey = async () => { try { const data = await api.get<LoginQrKeyRes>("/login/qr/key"); loginKey.value = data.data?.unikey || ''; } catch(err) { console.log("获取二维码 Key 失败", err); loginKey.value = ''; } }
const fetchQrImg = async (key: string) => { if (!key) return; try { const data = await api.get<LoginQrCreateRes>("/login/qr/create",{ key, timeStamp: Date.now(), ua: "pc", qrimg: true, }); qrImg.value = data.data?.qrimg || ''; } catch(err) { console.log("获取二维码图片失败", err); qrImg.value = ''; } }
watch(() => loginKey.value, (val: string) => { if (val) { fetchQrImg(val); } })
const startQrCheak = (key: string) => { if (!key) return; if (qrCheckTimer.value) { clearInterval(qrCheckTimer.value); } qrCheckTimer.value = setInterval(async () => { try { const data = await api.get<LoginQrCheckRes>("/login/qr/check", { key, timeStamp: Date.now(), ua: "pc", })
if (data.code === 803) { if (qrCheckTimer.value) clearInterval(qrCheckTimer.value); qrCheckTimer.value = null;
userStore.setCookie(data.cookie);
try { const statusRes = await api.get<LoginStatusRes>("/login/status", { timeStamp: Date.now(), ua: "pc", cookie: userStore.cookie, })
const profile = statusRes.data?.profile;
if (profile) { userStore.setUser({ id: profile.userId, avatar: profile.avatarUrl, nickname: profile.nickname, }) } router.push('/'); } catch (err) { console.log("获取登录状态失败", err); }
} else if(data.code === 800) {
if (qrCheckTimer.value) clearInterval(qrCheckTimer.value); qrCheckTimer.value = null; fetchloginKey(); } } catch (err) { console.log("检查二维码登录状态失败", err); } }, 3000) }
watch(() => qrImg.value, (val: string) => { if (val && loginKey.value) { startQrCheak(loginKey.value); } })
onMounted(() => { fetchloginKey(); })
onBeforeUnmount(() => { if (qrCheckTimer.value) { clearInterval(qrCheckTimer.value); qrCheckTimer.value = null; } })
...
</script>
<template> ... <div class="qrcode-placeholder"> <template v-if="qrImg"> <img :src="qrImg" alt="登录二维码"> </template> <template v-else> <span>二维码加载中...</span> </template> </div> ... </template>
|
这部分还有用户状态管理相关的函数 (暂存用户数据),但是没有在这写出来
结语
依旧为了方便翻阅复习,于是写在了博客上。还有一大堆 interface 就不在文章里列举了…