# TadmPlayer
**Repository Path**: tadm/TadmPlayer
## Basic Information
- **Project Name**: TadmPlayer
- **Description**: A simple player by vue family~
- **Primary Language**: HTML
- **License**: GPL-3.0
- **Default Branch**: master
- **Homepage**: http://tadm.gitee.io/tadmplayer
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-04-04
- **Last Updated**: 2021-03-21
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Tadm-Player-Vue
> 利用 Vue 全家桶实现多源音乐平台,妈妈再也不用担心我听不到喜欢的歌曲啦!
- 前台 --- vue 全家桶
- 后端
- 网易云 - [NetEaseApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- 酷狗 - 暂只支持开发环境
- 其他数据源敬请期待~
## Preview
- Github: https://github.com/Liuhongwei3/TadmPlayerVue
- Online Demo: https://tadm.gitee.io/tadmplayer (建议 PC 端体验)
## Usage
```
git clone https://github.com/Liuhongwei3/TadmPlayerVue.git
npm install
npm run serve
npm run build
```
## Announcement
本项目仅供学习使用,不提供任何资源存储,歌曲等数据来自互联网,其版权归资源所属版权方~
## 项目技能树
`Vue` + `Vue-Router` + `Vuex` +`Vue-lazyload` + `ES6` + `Axios` + `Webpack` + `Fontawesome` + `Better-scroll` + `Element-UI` + `async-await` + `NProgress` + `v-viewer` + `...`
## Features
- Play music and remember songs to history
- Visit comments & Enjoy The CloudHotComments
- Enjoy Top Song list & playlists
- Visit your NetEaseCloudMusic Song list & Followers
- Search more songs & Users & details & albums & singers & ...
- Visit Top Singers & enjoy your favorite player's songs & listen them
- watch mvs (all/top/...)
- watch shared/searched videos
- Maybe more ~
## Shortcut
> 部分截图或许过时,以线上站点为主。
- 首页

- 榜单

- 搜索

- 用户

- 评论

- More
> preview online web~
## 项目文件目录树
点击查看
tadm-player-vue
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── README.md
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── 404.jpg
│ │ └── play_icon.png
│ ├── components
│ │ ├── common
│ │ │ ├── backTop
│ │ │ │ └── BackTop.vue
│ │ │ ├── items
│ │ │ │ └── Items.vue
│ │ │ ├── Nav-bar
│ │ │ │ └── NavBar.vue
│ │ │ ├── noResult
│ │ │ │ └── NoResult.vue
│ │ │ └── scroll
│ │ │ ├── HorizontalScroll.vue
│ │ │ └── VerticalScroll.vue
│ │ └── content
│ │ ├── CommContent.vue
│ │ ├── DetailContent.vue
│ │ ├── Drawer.vue
│ │ ├── Event.vue
│ │ ├── EventContent.vue
│ │ ├── LoadMoreFoot.vue
│ │ ├── LogContent.vue
│ │ ├── MvContent.vue
│ │ ├── RLyric.vue
│ │ ├── ShowMv.vue
│ │ ├── ShowVideo.vue
│ │ ├── UserContent.vue
│ │ └── VideoContent.vue
│ ├── css
│ │ ├── App.css
│ │ └── element.css
│ ├── element.js
│ ├── features.js
│ ├── filters.js
│ ├── main.js
│ ├── network
│ │ ├── netease
│ │ │ ├── doReq.js
│ │ │ ├── index.js
│ │ │ └── requests.js
│ │ ├── qq
│ │ │ ├── doReq.js
│ │ │ ├── index.js
│ │ │ └── requests.js
│ │ └── req.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── mutations.js
│ │ └── state.js
│ ├── utils.js
│ └── views
│ ├── About.vue
│ ├── Album.vue
│ ├── Comment.vue
│ ├── Detail.vue
│ ├── History.vue
│ ├── Home.vue
│ ├── HotDetail.vue
│ ├── Mv.vue
│ ├── MyVideo.vue
│ ├── Play.vue
│ ├── Search.vue
│ ├── Singer.vue
│ ├── Top.vue
│ └── User.vue
└── vue.config.js
## 细节处理
### Axios 封装
- index.js
```js
import axios from "axios";
import NProgress from "nprogress";
export function request(config) {
const baseURL =
process.env.NODE_ENV === "development"
? "http://localhost:3000/"
: "https://api.mtnhao.com/";
const instance = axios.create({
baseURL,
timeout: 10000,
});
instance.interceptors.request.use(
(config) => {
NProgress.start();
return config;
},
(error) => {
const { response } = error;
if (!response) {
error = { response: { statusText: "网络错误,请检查您的网络连接!" } };
}
return Promise.reject(error);
}
);
instance.interceptors.response.use(
(response) => {
NProgress.done();
return response;
},
(error) => {
const { response } = error;
if (!response) {
error = { response: { statusText: "网络错误,请检查您的网络连接!" } };
}
return Promise.reject(error);
}
);
return instance(config); // Promise
}
```
- doReq.js
```js
import { request } from "./index";
import { to, notify } from "@/utils";
const doReq = async (url) => {
// notify("info", "信息提示", "加载数据中!");
let [err, data] = await to(request({ url }));
if (err) {
notify("error", "加载错误", err.response.statusText);
return false;
} else {
return data;
}
};
export default doReq;
```
- request.js
```js
import { request } from "./index";
import doReq from "./doReq";
// 0: pc
// 1: android
// 2: iphone
// 3: ipad
const getBanner = async (type = 0) => {
let flag = await doReq(`/banner?type=${type}`);
if (!flag || !flag.data || !flag.data.banners) {
return [];
}
return flag.data.banners;
};
more...
```
### 歌词部分
- 歌词解析
```js
export function parseLyric(data) {
let lrc = data.lrc.lyric;
let tlrc = data.tlyric.lyric;
let lyrics = lrc.split("\n");
let tlyrics = tlrc ? tlrc.split("\n") : [];
let lrcObj = [];
let lrcMap = new Map();
let tlrcMap = new Map();
let lrcTime = new Map();
let j = 0;
for (let i = 0; i < lyrics.length; i++) {
let lyric = decodeURIComponent(lyrics[i]);
let timeReg = /\[\d*:\d*((\.|\:)\d*)*\]/g;
let timeRegExpArr = lyric.match(timeReg);
if (!timeRegExpArr) continue;
let clause = lyric.replace(timeReg, "");
for (let k = 0, h = timeRegExpArr.length; k < h; k++) {
let t = timeRegExpArr[k];
let min = Number(String(t.match(/\[\d*/i)).slice(1)),
sec = Number(String(t.match(/\:\d*/i)).slice(1));
let time = min * 60 + sec;
if (clause) {
lrcTime.set(time, j++);
lrcMap.set(time, clause);
lrcObj.push({ time: time, text: clause });
}
}
}
for (let i = 0; i < tlyrics.length; i++) {
let tlyric = decodeURIComponent(tlyrics[i]);
let timeReg = /\[\d*:\d*((\.|\:)\d*)*\]/g;
let timeRegExpArr1 = tlyric.match(timeReg);
if (!timeRegExpArr1) continue;
let clause1 = tlyric.replace(timeReg, "");
for (let k = 0, h = timeRegExpArr1.length; k < h; k++) {
let t = timeRegExpArr1[k];
let min = Number(String(t.match(/\[\d*/i)).slice(1)),
sec = Number(String(t.match(/\:\d*/i)).slice(1));
let time = min * 60 + sec;
if (clause1) {
tlrcMap.set(time, clause1);
}
}
}
return [lrcObj, lrcMap, tlrcMap, lrcTime];
}
```
- 歌词滚动
```html
{{ item.text }}
{{ item.ttext }}
```
```js
computed: {
lyricTop() {
return `transform :translate3d(0, ${
-lineH * (this.lyricIndex - this.top)
}px, 0)`;
},
},
watch: {
lyricIndex(newValue) {
this.lineH = document
.getElementsByClassName("lyrics")
[newValue].getBoundingClientRect().height;
this.calcTop();
if (newValue > this.top && newValue < this.lyrics.length - this.top) {
let y = (newValue - this.top) * this.lineH;
this.$refs.musicLyric.scrollTo(0, -y, 1000);
}
},
},
methods:{
calcTop() {
let style = document.documentElement.getBoundingClientRect();
let height = style.width >= 768 ? style.height * 0.5 : style.height * 0.4;
this.top = Math.floor(height / this.lineH / 2);
},
}
```
- 当前歌词高亮
```js
audio.addEventListener("timeupdate", () => {
let curTime = Math.floor(audio.currentTime);
// 使用 map 代替之前的 for 循环比较,避免不必要的性能消耗
if (this.lyricsMap.has(curTime)) {
this.activeLyric = this.lyricsMap.get(curTime);
}
if (this.lyricsTime.has(curTime)) {
this.lyricIndex = this.lyricsTime.get(curTime);
}
});
```
### 音乐下载
```js
// PC
export function createDownload(name, player, data) {
let blob = new Blob([data], { type: "audio/mpeg;charset=utf-8" });
let downloadElement = document.createElement("a");
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = name + "-" + player + ".mp3";
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
}
```
### Better-scroll
- 公共组件(Vertical)
```html
```
```js
// https://better-scroll.github.io/docs/zh-CN/
import BScroll from "better-scroll";
import { debounce, throttle } from "@/utils";
export default {
name: "VerticalScroll",
data() {
return {
scroll: null,
};
},
props: {
probeType: {
type: Number,
default: 3,
},
data: {
type: Array,
default: () => {
return [];
},
},
listenScroll: {
type: Boolean,
default: false,
},
pullUpLoad: {
type: Boolean,
default: false,
},
},
mounted() {
setTimeout(this.__initScroll, 500);
window.addEventListener("resize", this.refresh);
},
methods: {
__initScroll() {
// 1.初始化BScroll对象
if (!this.$refs.wrapper) return;
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
click: true,
dblclick: true,
stopPropagation: true,
mouseWheel: true,
pullUpLoad: this.pullUpLoad,
});
// 2.将监听事件回调
this.listenScroll &&
this.scroll.on(
"scroll",
throttle((position) => {
this.$emit("scroll", position);
})
);
// 3.监听上拉到底部
this.pullUpLoad &&
this.scroll.on(
"pullingUp",
this.pullUpLoad &&
debounce(() => {
this.$emit("pullingUp");
})
);
},
refresh() {
// 重新计算 BetterScroll,当 DOM 结构发生变化的时候务必要调用确保滚动的效果正常
this.scroll && this.scroll.refresh && this.scroll.refresh();
},
finishPullUp() {
// finishPullUp: 这个类似控制一个开关,比如在触发 pullingUp 事件的时候,插件肯定会把一个开关给关掉,防止用户重复上拉
// 在数据加载完成以后,需要执行 finishPullUp() 把开关打开,以便下次可以继续执行上拉刷新
this.scroll && this.scroll.finishPullUp && this.scroll.finishPullUp();
},
scrollTo(x, y, time) {
this.scroll && this.scroll.scrollTo && this.scroll.scrollTo(x, y, time);
},
},
beforeDestroy() {
this.scroll.destroy();
},
};
```
- Usage
```html
```
### async-await 统一错误处理
```js
export function to(promise) {
return promise
.then((data) => {
return [null, data];
})
.catch((err) => {
return [err];
});
}
```
### 懒加载
> 由于本站图片较多,因此使用了**懒加载以及分页**的策略来避免性能消耗过大
### 防抖 & 节流
```js
export function debounce(func, delay = 500, immediate = false) {
let timer;
return function(...args) {
let context = this;
timer && clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => (timer = null), delay);
if (callNow) {
func.apply(context, args);
}
} else {
timer = setTimeout(() => func.apply(context, ...args), delay);
}
};
}
export function throttle(func, delay = 500, immediate = false) {
let prev = Date.now();
return function(...args) {
let now = Date.now(),
context = this;
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
};
}
```
### 其他封装
> 详见 `components` 文件夹
## About
> If you like it,Thanks to star and talk more~
## Thanks
> NetEaseCloudMusic & NetEaseMusic & All tools producer ~
Github Repo: https://github.com/Liuhongwei3/TadmPlayerVue
Gitee Repo: https://gitee.com/tadm/TadmPlayer