自己在B站搞了一个点歌台,用OBS装修了一段时间,感觉效果一般,很多高级的动态效果无法展现。关注到该项目还有websocket服务器与前端交互的功能,可以将该网页输出内嵌到OBS中进行直播。因此想自己定制一个符合自己喜好的界面,下面是本次开发过程的小记。
一、熟悉项目结构
项目地址:https://github.com/AynaLivePlayer/AynaLivePlayerOBSInfo
该项目使用了Vite+Vue3+ts进行项目构建,状态存储使用了pinia,另外还引入了TailWind的样式库,这次编写重点关注的即是pinia中存储的数据格式,以及TailWind样式库的学习。
在作者的项目之中也提供了一些预设的组件,均放置在了项目中的components目录下,如下图:

可以利用这些已有的信息组件对状态存储进行统一的访问。
1.Pinia
插件官网:https://pinia.vuejs.org/zh/
pinia是继vuex之后新一代的状态存储插件,其目的是设计一个拥有组合式 API的 Vue状态管理库。开发人员可以基本无痛地将状态管理从vuex中迁移到pinia中。由于pinia支持热更新,对store数据格式的修改也可以更及时的应用,而且该项目使用了ts进行开发,pinia相对vuex有着更好的ts支持,因此该项目使用了pinia进行状态管理。
打开stores目录,里面存储的即是该项目的基础信息数据格式:

playinfo.ts存储的是一些当前歌曲输出的数据。
// Initialize the current state according to the Media interface
// 当前歌曲基础信息
const current = reactive<Media>({
Info: {
Title: "NoTitle", //歌曲标题,默认为"Notitle"
Artist: "Unknown", //艺人名称,默认为"Unknown"
Cover: {
Url: "", //专辑封面地址
Data: "" //专辑封面图片数据
},
Album: "Unknown", //专辑名称,默认为"Unknown"
Meta: {
Provider: "", //音乐资源来源平台
Identifier: "" //音乐ID
}
},
User: {
Name: "Unknown" //点歌用户用户名
}
});
const paused = ref(true); //播放状态,当前是否暂停,默认为true
const duration = ref(0); //歌曲总时长,单位为秒
const timePos = ref(0); //当前播放时长,单位为秒
const volume = ref(0.0); //当前音量百分比
const currentLyric = reactive({
Lyric: "", //本句歌词
CurrentIndex:-1, //当前歌词索引号,默认为-1
Total: 0 //总歌词数量
})
const lyrics = ref<Lyrics>({
Lang: "", //歌词语言
Content: [] //所有歌词数组
});
const playlist = reactive<Media[]>([]); //播放列表信息(包含哪些歌曲)
根据上述基本状态存储结构,可以在模块中查询所需的信息状态。
2.TailWind
Tailwind官网:https://www.tailwindcss.cn/
TailWind是一个基础样式库,它可以通过加载后不用编写css代码,仅使用TailWind的一些预设类预设样式来快速编写样式效果。
在这里有TailWind的一些常用类名总结:https://blog.csdn.net/weixin_64684095/article/details/143382315
二、部署项目
1.环境安装
首先先安装好Git环境、node环境以及pnpm包管理工具。下面提供一下环境下载地址和部署方法:
Git:https://git-scm.com/downloads
Node.js:https://nodejs.org/zh-cn/download
nvm(可选安装,如果选择用nvm管理nodejs版本的话,上面的nodejs无需下载):
https://github.com/coreybutler/nvm-windows
在Node.js安装完毕后,安装pnpm包管理工具:
Windows下需要先开启可以加载第三方脚本功能:
set-ExecutionPolicy RemoteSigned
然后设置npm镜像仓库,加速国内下载:
npm config set registry https://registry.npmmirror.com
最后安装pnpm:
npm install pnpm -g # -g表示全局安装
2.项目部署启动
选择你想要存储项目的位置,执行将仓库克隆下来:
git clone https://github.com/AynaLivePlayer/AynaLivePlayerOBSInfo.git
推荐使用VSCode作为开发环境,当然也可以使用自己用的顺的IDE,打开刚刚拉取下来的项目文件夹。
在IDE中的终端或者打开文件夹右击使用终端打开,安装项目所需软件包:
pnpm i
安装完毕后可以进行开发,开发测试用热更服务器启动命令是:
pnpm dev
开发完毕后打包上传的命令是:
pnpm build
以上部署准备工作结束后,就正式进入开发相关工作。
三、功能规划
整体划分为几大界面功能模块:标准尺寸精简播放器、小尺寸精简播放器、播放列表、完整界面播放器。
因此在views的user目录中放入了自己的几大基础界面模块:

并且在一个界面编写完毕后,在路由配置,router的index.ts中加入自己写好界面的路由:
引入界面:

编写路由信息:

四、标准尺寸精简播放器
该播放器是包含相对完整功能的基础播放器界面,其中包含专辑封面、歌曲和艺术家信息、点歌用户信息、歌词和进度条。
界面分区设计:

左侧1/3区域设计为专辑封面显示区域,右侧2/3区域设计为信息显示区域,包括歌曲和用户基础信息以及播放进度。
为了一定的美观性,将整体设计成胶囊形。
基于以上分区和整体形状,结合TailWind编写出该界面HTML层次:
<div class="aii-player-normal grid grid-cols-3 w-4 h-4 rounded-full border-2 border-indigo-300 shadow-lg border-dashed">
<div class="player-album col-span-1 p-4 justify-center content-center">
<!-- 专辑封面 -->
</div>
<div class="music-info col-span-2 flex flex-col items-start justify-between">
<div class="title-info">
<!-- 歌曲标题 -->
</div>
<div class="artist-info">
<!-- 艺人 -->
</div>
<div class="user-info">
<!-- 点歌用户 -->
</div>
<div class="lyric">
<!-- 歌词 -->
</div>
<div class="progress-bar">
<!-- 进度条 -->
</div>
</div>
</div>
另外还有一些自定义样式:
.aii-player-normal {
width: 600px;
height: 200px;
font-family: '江城圆体 600W', sans-serif;
text-shadow: 1px 1px 2px rgb(118, 118, 118);
font-size: 20px;
background-image: linear-gradient(#e0fdff, #f9faff);
}
.player-album {
width: 200px;
height: 200px;
padding: 0;
position: relative;
overflow: hidden;
}
.music-info {
width: 400px;
height: 200px;
padding: 15px 20px 15px 0;
}
.info-type {
width: 145px;
}
.media-title-info {
width: 200px;
}
.title-info {
width: 340px;
height: 30px;
}
.artist-info {
width: 340px;
height: 30px;
}
.user-info {
width: 340px;
height: 30px;
}
.progress-bar {
width: 340px;
height: 30px;
font-size: 14px;
}
.lyric {
width: 340px;
height: 30px;
}
这里为了做出旋转唱片的效果,首先先去获取一个空心透明的唱片素材,中间空白部分可以透出底下唱片封面,再将该元素与封面绝对定位在左侧区域中心,并设置旋转和暂停的动画。旋转和暂停控制是将两个效果分到不同的两个类中,通过计算方法获取store中的paused属性,从而跟随歌曲的播放暂停实时控制动画是否播放。这个旋转动画的本质即是使用10秒时间匀速旋转图像一周。因此结合以上描述,将该效果主意进行编写:
HTML部分:
<img class="cd-img" src="@/assets/imgs/Aiikisaraki/cd.png" alt=""> <!-- 该图片即是一个黑胶唱片外圈 -->
<div class="cover-ctl" :class="{ img_al_active: playStatus, img_al_pauesd: !playStatus }"> <!-- 通过playStatus这一计算方法控制是否旋转 -->
<MediaCover/> <!-- 作者封装的获取封面组件 -->
</div>
JS部分:
const playInfoStore = usePlayInfoStore();
const playStatus = computed(() => {
if (playInfoStore.paused === null) return false;
return !playInfoStore.paused;
});
CSS部分:
.player-album .cd-img {
width: 180px;
height: 180px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.player-album .cover-ctl {
width: 120px;
height: 120px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 0;
animation: rotate_ar 10s linear infinite;
}
.player-album .cover-ctl .media-cover {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
object-position: center;
}
.player-album .img_al_active {
animation-play-state: running;
}
.player-album .img_al_pauesd {
animation-play-state: paused;
}
/* 动画中两个变换的原因是固定到中心的方式是通过变换实现的,因此每一帧都需要重新固定到中心再旋转 */
@keyframes rotate_ar {
0% {
transform: translate(-50%, -50%) rotateZ(0deg);
}
100% {
transform: translate(-50%, -50%) rotateZ(360deg);
}
}
这样左侧旋转小唱片的效果就完成了,接下来便是完成右侧的信息展示部分,右侧信息展示前三栏都是以XXX:XXX的形式展现,因此将前面提示部分固定不动,将后半信息部分使用滚动文字进行展示,防止超出边框。这里使用flex布局以及靠头部对齐justify-content: start,这一个效果在TailWind中对应的是justify-content-start类。另外还需要将每个部分的宽高做好限定,这样弹性布局才不会出现过分的变形。
在进度条部分参考的是作者在player1中的进度条,其核心还是flex配合绝对定位来实现,flex控制进度条背景会随着窗口比例变化而变化,靠头部对齐,绝对定位是定位进度条相对于进度条底色的位置不会发生偏移。而内部进度条长度跟随歌曲进度实时计算。
以下则是整个信息部分实现:
HTML部分:
<div class="title-info flex justify-content-start">
<div class="info-type text-indigo-900">标题:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="media-title-info">
<MediaTitle class="text-indigo-700"/>
</ScrollLeftRight>
</div>
<div class="artist-info flex justify-content-start">
<div class="info-type text-indigo-900">艺人:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="media-artist-info">
<MediaArtist class="text-indigo-700"/>
</ScrollLeftRight>
</div>
<div class="user-info flex justify-content-start">
<div class="info-type text-indigo-900">点歌用户:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="username-info">
<MediaUsername class="text-indigo-700" />
</ScrollLeftRight>
</div>
<div class="lyric text-pink-600">
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="lyric">
<CurrentLyricCN class="text-nowrap"/>
</ScrollLeftRight>
</div>
<!-- 参考内置Player1的进度条实现 -->
<div class="progress-bar flex flex-row items-center space-x-2 pr-2">
<current-time format="m:s"></current-time>
<div class="flex-grow progress-bar-bg rounded-full h-2 relative">
<div
class="progress-bar-body absolute h-2 rounded-full"
:style="{ width: progressPercentage + '%' }"
></div>
</div>
<total-time format="m:s"></total-time>
</div>
JS部分:
const progressPercentage = computed(() => {
if (playInfoStore.duration > 0) {
return (playInfoStore.timePos / playInfoStore.duration) * 100;
}
return 0;
});
CSS部分:
.music-info {
width: 400px;
height: 200px;
padding: 15px 20px 15px 0;
}
.info-type {
width: 145px;
}
.media-title-info {
width: 200px;
}
.title-info {
width: 340px;
height: 30px;
}
.artist-info {
width: 340px;
height: 30px;
}
.user-info {
width: 340px;
height: 30px;
}
.progress-bar {
width: 340px;
height: 30px;
font-size: 14px;
}
.lyric {
width: 340px;
height: 30px;
}
.progress-bar-bg{
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity))
}
.progress-bar-body {
--tw-bg-opacity: 1;
background-color: rgb(6, 28, 102)
}
结合以上全部设计,将代码结构整理后,该组件的文件最后为这样:
<script lang="ts" setup>
import MediaCover from "@/components/current/MediaCover.vue";
import MediaTitle from "@/components/current/MediaTitle.vue";
import MediaArtist from "@/components/current/MediaArtist.vue";
import MediaUsername from "@/components/current/MediaUsername.vue";
import { computed, onMounted, ref, watch } from "vue";
import CurrentTime from "@/components/current/CurrentTime.vue";
import TotalTime from "@/components/current/TotalTime.vue";
import { usePlayInfoStore } from "@/stores/playinfo";
import CurrentLyricCN from "@/components/current/CurrentLyricCN.vue";
import ScrollLeftRight from "@/components/common/ScrollLeftRight.vue";
const playInfoStore = usePlayInfoStore();
const playStatus = computed(() => {
if (playInfoStore.paused === null) return false;
return !playInfoStore.paused;
});
const progressPercentage = computed(() => {
if (playInfoStore.duration > 0) {
return (playInfoStore.timePos / playInfoStore.duration) * 100;
}
return 0;
});
</script>
<template>
<div class="aii-player-normal grid grid-cols-3 w-4 h-4 rounded-full border-2 border-indigo-300 shadow-lg border-dashed">
<div class="player-album col-span-1 p-4 justify-center content-center">
<img class="cd-img" src="@/assets/imgs/Aiikisaraki/cd.png" alt="">
<div class="cover-ctl" :class="{ img_al_active: playStatus, img_al_pauesd: !playStatus }">
<MediaCover/>
</div>
</div>
<div class="music-info col-span-2 flex flex-col items-start justify-between">
<div class="title-info flex justify-content-start">
<div class="info-type text-indigo-900">标题:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="media-title-info">
<MediaTitle class="text-indigo-700"/>
</ScrollLeftRight>
</div>
<div class="artist-info flex justify-content-start">
<div class="info-type text-indigo-900">艺人:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="media-artist-info">
<MediaArtist class="text-indigo-700"/>
</ScrollLeftRight>
</div>
<div class="user-info flex justify-content-start">
<div class="info-type text-indigo-900">点歌用户:</div>
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="username-info">
<MediaUsername class="text-indigo-700" />
</ScrollLeftRight>
</div>
<div class="lyric text-pink-600">
<ScrollLeftRight :stay_ms="1000" :px_per_ms="50" class="lyric">
<CurrentLyricCN class="text-nowrap"/>
</ScrollLeftRight>
</div>
<!-- 参考内置Player1的进度条实现 -->
<div class="progress-bar flex flex-row items-center space-x-2 pr-2">
<current-time format="m:s"></current-time>
<div class="flex-grow progress-bar-bg rounded-full h-2 relative">
<div
class="progress-bar-body absolute h-2 rounded-full"
:style="{ width: progressPercentage + '%' }"
></div>
</div>
<total-time format="m:s"></total-time>
</div>
</div>
</div>
</template>
<style scoped>
.aii-player-normal {
width: 600px;
height: 200px;
font-family: '江城圆体 600W', sans-serif;
text-shadow: 1px 1px 2px rgb(118, 118, 118);
font-size: 20px;
background-image: linear-gradient(#e0fdff, #f9faff);
}
.player-album {
width: 200px;
height: 200px;
padding: 0;
position: relative;
overflow: hidden;
}
.player-album .cd-img {
width: 180px;
height: 180px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.player-album .cover-ctl {
width: 120px;
height: 120px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 0;
animation: rotate_ar 10s linear infinite;
}
.player-album .cover-ctl .media-cover {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
object-position: center;
}
.player-album .img_al_active {
animation-play-state: running;
}
.player-album .img_al_pauesd {
animation-play-state: paused;
}
@keyframes rotate_ar {
0% {
transform: translate(-50%, -50%) rotateZ(0deg);
}
100% {
transform: translate(-50%, -50%) rotateZ(360deg);
}
}
.music-info {
width: 400px;
height: 200px;
padding: 15px 20px 15px 0;
}
.info-type {
width: 145px;
}
.media-title-info {
width: 200px;
}
.title-info {
width: 340px;
height: 30px;
}
.artist-info {
width: 340px;
height: 30px;
}
.user-info {
width: 340px;
height: 30px;
}
.progress-bar {
width: 340px;
height: 30px;
font-size: 14px;
}
.lyric {
width: 340px;
height: 30px;
}
.progress-bar-bg{
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity))
}
.progress-bar-body {
--tw-bg-opacity: 1;
background-color: rgb(6, 28, 102)
}
</style>
视觉效果如下图:

到此第一个模块开发完毕,下一篇开始完整界面的开发。
Comments NOTHING