├── src
├── store
│ ├── modules
│ │ ├── global
│ │ │ ├── state.js
│ │ │ ├── mutations.js
│ │ │ └── index.js
│ │ ├── user
│ │ │ ├── state.js
│ │ │ ├── mutations.js
│ │ │ ├── index.js
│ │ │ ├── actions.js
│ │ │ └── getters.js
│ │ └── music
│ │ │ ├── index.js
│ │ │ ├── state.js
│ │ │ ├── mutations.js
│ │ │ ├── actions.js
│ │ │ └── getters.js
│ ├── helper
│ │ ├── global.js
│ │ ├── music.js
│ │ └── user.js
│ └── index.js
├── style
│ ├── index.scss
│ ├── themes
│ │ ├── variables-red.js
│ │ ├── variables.js
│ │ └── variables-white.js
│ ├── variables.scss
│ ├── app.scss
│ ├── mixin.scss
│ ├── reset.scss
│ └── element-overwrite.scss
├── api
│ ├── album.js
│ ├── artist.js
│ ├── search.js
│ ├── index.js
│ ├── user.js
│ ├── mv.js
│ ├── discovery.js
│ ├── playlist.js
│ ├── comment.js
│ └── song.js
├── assets
│ └── image
│ │ ├── play-bar.png
│ │ └── play-bar-support.png
├── utils
│ ├── index.js
│ ├── mixin.js
│ ├── config.js
│ ├── rem.js
│ ├── dom.js
│ ├── global.js
│ ├── business.js
│ ├── lrcparse.js
│ ├── axios.js
│ └── common.js
├── base
│ ├── empty.vue
│ ├── title.vue
│ ├── loading.vue
│ ├── pagination.vue
│ ├── nbutton.vue
│ ├── play-icon.vue
│ ├── highlight-text.vue
│ ├── scroller.vue
│ ├── video-player.vue
│ ├── toggle.vue
│ ├── card.vue
│ ├── volume.vue
│ ├── confirm.vue
│ ├── icon.vue
│ ├── progress-bar.vue
│ └── tabs.vue
├── main.js
├── page
│ ├── discovery
│ │ ├── index.vue
│ │ ├── banner.vue
│ │ ├── new-playlists.vue
│ │ ├── new-mvs.vue
│ │ └── new-songs.vue
│ ├── songs
│ │ └── index.vue
│ ├── search
│ │ ├── mvs.vue
│ │ ├── playlists.vue
│ │ ├── index.vue
│ │ └── songs.vue
│ ├── mvs
│ │ └── index.vue
│ ├── playlists
│ │ └── index.vue
│ ├── playlist-detail
│ │ ├── detail-header.vue
│ │ └── index.vue
│ └── mv
│ │ └── index.vue
├── App.vue
├── components
│ ├── share.vue
│ ├── routes-history.vue
│ ├── song-card.vue
│ ├── with-pagination.vue
│ ├── playlist-card.vue
│ ├── top-playlist-card.vue
│ ├── comment.vue
│ ├── mv-card.vue
│ ├── theme.vue
│ ├── playlist.vue
│ ├── user.vue
│ ├── comments.vue
│ ├── search.vue
│ ├── song-table.vue
│ ├── mini-player.vue
│ └── player.vue
├── layout
│ ├── index.vue
│ ├── menu.vue
│ └── header.vue
└── router.js
├── public
├── favicon.ico
└── index.html
├── babel.config.js
├── .gitignore
├── release.sh
├── vue.config.js
├── LICENSE
├── README.md
├── CHANGELOG.md
├── package.json
└── STUDY.md
/src/store/modules/global/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | axiosLoading: false,
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JYbmarawcp/vue-netease-music/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | @import "./element-overwrite.scss";
2 | @import "./reset.scss";
3 | @import "./app.scss";
4 |
--------------------------------------------------------------------------------
/src/api/album.js:
--------------------------------------------------------------------------------
1 | import { request } from "@/utils"
2 |
3 | export const getAlbum = id => request.get(`/album?id=${id}`)
--------------------------------------------------------------------------------
/src/assets/image/play-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JYbmarawcp/vue-netease-music/HEAD/src/assets/image/play-bar.png
--------------------------------------------------------------------------------
/src/store/modules/user/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // 登录用户
3 | user: {},
4 | // 登录用户歌单
5 | userPlaylist: []
6 | }
--------------------------------------------------------------------------------
/src/assets/image/play-bar-support.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JYbmarawcp/vue-netease-music/HEAD/src/assets/image/play-bar-support.png
--------------------------------------------------------------------------------
/src/api/artist.js:
--------------------------------------------------------------------------------
1 | import { request } from "@/utils"
2 |
3 | // 获取歌手单曲
4 | export const getArtists = id => request.get(`/artists?id=${id}`)
5 |
--------------------------------------------------------------------------------
/src/store/modules/global/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setAxiosLoading(state, loading) {
3 | state.axiosLoading = loading
4 | }
5 | }
--------------------------------------------------------------------------------
/src/store/helper/global.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations } = createNamespacedHelpers('global')
--------------------------------------------------------------------------------
/src/store/helper/music.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('music')
--------------------------------------------------------------------------------
/src/store/helper/user.js:
--------------------------------------------------------------------------------
1 | import { createNamespacedHelpers } from 'vuex'
2 |
3 | export const { mapState, mapMutations, mapGetters, mapActions } = createNamespacedHelpers('user')
4 |
--------------------------------------------------------------------------------
/src/store/modules/global/index.js:
--------------------------------------------------------------------------------
1 | import state from './state'
2 | import mutations from './mutations'
3 |
4 | export default {
5 | namespaced: true,
6 | state,
7 | mutations
8 | }
--------------------------------------------------------------------------------
/src/store/modules/user/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setUser(state, user) {
3 | state.user = user
4 | },
5 | setUserPlaylist(state, playlist) {
6 | state.userPlaylist = playlist
7 | }
8 | }
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './axios'
2 |
3 | export * from './common'
4 |
5 | export * from './business'
6 |
7 | export * from './rem'
8 |
9 | export * from './dom'
10 |
11 | export * from './mixin'
12 |
13 | export * from './config'
--------------------------------------------------------------------------------
/src/utils/mixin.js:
--------------------------------------------------------------------------------
1 | import store from "@/store"
2 |
3 | export const hideMenuMixin = {
4 | created () {
5 | store.commit('music/setMenuShow', false)
6 | },
7 | beforeDestroy () {
8 | store.commit('music/setMenuShow', true)
9 | }
10 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ],
5 | plugins: [
6 | [
7 | 'component',
8 | {
9 | libraryName: 'element-ui',
10 | styleLibraryName: 'theme-chalk'
11 | }
12 | ]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/store/modules/user/index.js:
--------------------------------------------------------------------------------
1 | import state from "./state"
2 | import * as getters from "./getters"
3 | import mutations from "./mutations"
4 | import actions from "./actions"
5 | export default {
6 | namespaced: true,
7 | state,
8 | getters,
9 | mutations,
10 | actions,
11 | }
--------------------------------------------------------------------------------
/src/store/modules/music/index.js:
--------------------------------------------------------------------------------
1 | import state from './state'
2 | import * as getters from './getters'
3 | import mutations from './mutations'
4 | import actions from './actions'
5 |
6 | export default {
7 | namespaced: true,
8 | state,
9 | getters,
10 | mutations,
11 | actions,
12 | }
--------------------------------------------------------------------------------
/src/api/search.js:
--------------------------------------------------------------------------------
1 | import { request } from "@/utils"
2 |
3 | export const getSearchHot = () => request.get('/search/hot')
4 |
5 | export const getSearchSuggest = (keywords) => request.get('/search/suggest', {params: {keywords}})
6 |
7 | export const getSearch = (params) => request.get('/search', { params })
8 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | export * from './discovery'
2 |
3 | export * from './mv'
4 |
5 | export * from './song'
6 |
7 | export * from './playlist'
8 |
9 | export * from './artist'
10 |
11 | export * from './comment'
12 |
13 | export * from './search'
14 |
15 | export * from './user'
16 |
17 | export * from './album'
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import { requestWithoutLoading } from "@/utils"
2 |
3 | export const getUserDetail = (uid) => requestWithoutLoading.get("/user/detail", { params: {uid}})
4 |
5 | const PLAYLIST_LIMIT = 1000
6 | export const getUserPlaylist = (uid) => requestWithoutLoading.get("/user/playlist", {params: {uid, limit: PLAYLIST_LIMIT}})
--------------------------------------------------------------------------------
/src/api/mv.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getMvDetail = id => request.get(`/mv/detail?mvid=${id}`)
4 |
5 | export const getMvUrl = id => request.get(`/mv/url?id=${id}`)
6 |
7 | export const getSimiMv = id => request.get(`/simi/mv?mvid=${id}`)
8 |
9 | export const getAllMvs = (params) => request.get(`mv/all`, { params })
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
24 | # custom
25 | music
--------------------------------------------------------------------------------
/src/base/empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 暂无内容
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/api/discovery.js:
--------------------------------------------------------------------------------
1 | import { request } from '@/utils'
2 |
3 | export const getBanner = () => request.get('/banner?type=0')
4 |
5 | export const getNewSongs = () => request.get('/personalized/newsong')
6 |
7 | export const getPersonalized = params => request.get('/personalized', { params })
8 |
9 | export const getPersonalizedMv = () => request.get('/personalized/mv')
10 |
--------------------------------------------------------------------------------
/src/style/themes/variables-red.js:
--------------------------------------------------------------------------------
1 | import variablesWhite from './variables-white'
2 |
3 | export default {
4 | ...variablesWhite,
5 | ['--header-bgcolor']: '#D74D45',
6 | ['--header-font-color']: '#EFB6B2',
7 | ['--header-input-color']: '#EFB6B2',
8 | ['--header-input-bgcolor']: '#DD6861',
9 | ['--header-input-placeholder-color']: '#EFB6B2',
10 | ['--round-hover-bgcolor']: '#CA4841',
11 | }
--------------------------------------------------------------------------------
/src/api/playlist.js:
--------------------------------------------------------------------------------
1 | import { request, requestWithoutLoading } from "@/utils"
2 |
3 | // 获取歌单
4 | export const getPlaylists = (params) => request.get('/top/playlist', { params })
5 |
6 | // 精品歌单
7 | export const getTopPlaylists = (params) => request.get('/top/playlist/highquality', { params })
8 |
9 | // 获取相似歌单
10 | export const getSimiPlaylists = (id, option) =>
11 | requestWithoutLoading.get(`/simi/playlist?id=${id}`, option)
12 |
--------------------------------------------------------------------------------
/src/base/title.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/base/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import App from './App.vue'
4 | import router from './router'
5 |
6 | import '@/style/index.scss'
7 | import '@/utils/rem'
8 | import '@/utils/axios'
9 | import store from './store/index'
10 | import global from './utils/global'
11 |
12 | Vue.config.productionTip = false
13 | Vue.config.performance = process.env.NODE_ENV !== "production"
14 |
15 | Vue.use(global)
16 |
17 | new Vue({
18 | router,
19 | store,
20 | render: h => h(App),
21 | }).$mount('#app')
22 |
--------------------------------------------------------------------------------
/src/base/pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import musicModule from './modules/music'
4 | import userModule from './modules/user'
5 | import globalModule from './modules/global'
6 | import createLogger from 'vuex/dist/logger'
7 |
8 | Vue.use(Vuex)
9 | const debug = process.env.NODE_ENV !== 'production'
10 |
11 | export default new Vuex.Store({
12 | modules: {
13 | music: musicModule,
14 | user: userModule,
15 | global: globalModule,
16 | },
17 | plugins: debug ? [createLogger()] : []
18 | })
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | export const playModeMap = {
2 | sequence: {
3 | code: 'sequence',
4 | icon: 'sequence',
5 | name: '顺序播放'
6 | },
7 | loop: {
8 | code: 'loop',
9 | icon: 'loop',
10 | name: '单曲循环'
11 | },
12 | random: {
13 | code: 'random',
14 | icon: 'random',
15 | name: '随机播放'
16 | },
17 | }
18 |
19 | // 存储播放记录
20 | export const PLAY_HISTORY_KEY = '__play_history__'
21 |
22 | // 用户id
23 | export const UID_KEY = '__uid__'
24 |
25 | // 音量
26 | export const VOLUME_KEY = "__volume__"
--------------------------------------------------------------------------------
/src/api/comment.js:
--------------------------------------------------------------------------------
1 | import { requestWithoutLoading } from "@/utils"
2 |
3 | // 热门评论
4 | export const getHotComment = params =>
5 | requestWithoutLoading.get(`/comment/hot`, { params })
6 | // 歌曲评论
7 | export const getSongComment = params =>
8 | requestWithoutLoading.get(`/comment/music`, { params })
9 |
10 | // 歌单评论
11 | export const getPlaylistComment = params =>
12 | requestWithoutLoading.get('/comment/playlist', { params })
13 |
14 | // Mv评论
15 | export const getMvComment = params =>
16 | requestWithoutLoading.get('/comment/mv', { params })
17 |
--------------------------------------------------------------------------------
/src/api/song.js:
--------------------------------------------------------------------------------
1 | import { request, requestWithoutLoading } from "@/utils"
2 |
3 |
4 | // 获取音乐详情(支持多个id,用 , 隔开)
5 | export const getSongDetail = ids => request.get(`/song/detail?ids=${ids}`)
6 |
7 | // 新歌速递
8 | export const getTopSongs = type => request.get(`/top/song?type=${type}`)
9 |
10 | // 歌单详情
11 | export const getListDetail = params => request.get('/playlist/detail', { params })
12 |
13 | // 相似音乐
14 | export const getSimiSongs = (id, option) =>
15 | requestWithoutLoading.get(`/simi/song?id=${id}`, option)
16 |
17 | // 歌词
18 | export const getLyric = id => request.get(`/lyric?id=${id}`)
--------------------------------------------------------------------------------
/src/store/modules/music/state.js:
--------------------------------------------------------------------------------
1 | import storage from "good-storage";
2 | import { PLAY_HISTORY_KEY } from "@/utils"
3 |
4 | export default {
5 | // 当前播放歌曲
6 | currentSong: {},
7 | // 当前播放时长
8 | currentTime: 0,
9 | // 播放状态
10 | playing: false,
11 | // 播放模式
12 | playMode: "sequence",
13 | // 播放列表显示
14 | isPlaylistShow: false,
15 | // 歌曲详情页显示
16 | isPlayerShow: false,
17 | // 播放提示显示
18 | isPlaylistPromptShow: false,
19 | // 播放列表数据
20 | playlist: [],
21 | // 播放历史数据
22 | playHistory: storage.get(PLAY_HISTORY_KEY, []),
23 | // 菜单显示
24 | isMenuShow: true,
25 | }
--------------------------------------------------------------------------------
/src/page/discovery/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
31 |
32 |
39 |
--------------------------------------------------------------------------------
/src/components/share.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
32 |
33 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echoCommon()
4 | {
5 | echo -e "\033[33m $1 \033[0m"
6 | }
7 |
8 | echoFail()
9 | {
10 | echo -e "\033[31m $1 \033[0m"
11 | }
12 |
13 | echoSuccess()
14 | {
15 | echo -e "\033[32m $1 \033[0m"
16 | }
17 |
18 | git pull
19 | echoSuccess 'git pull success'
20 | git add .
21 | echoSuccess 'git add success'
22 | echo
23 | git cz
24 | echoSuccess 'commit success'
25 | echo
26 | echoCommon "选择要发布的方式?"
27 | select var in release-major release-minor release-patch;
28 | do
29 | break
30 | done
31 |
32 | npm run $var
33 |
34 | if [ $? -eq 0 ]; then
35 | echoSuccess "release success"
36 | echoCommon "start building"
37 | git push --follow-tags origin master
38 | npm run build
39 | else
40 | echoFail "release failed"
41 | fi
42 |
--------------------------------------------------------------------------------
/src/base/nbutton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/routes-history.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 |
17 |
18 |
30 |
31 |
--------------------------------------------------------------------------------
/src/utils/rem.js:
--------------------------------------------------------------------------------
1 | import { throttle } from './common'
2 |
3 | export const remBase = 14
4 |
5 | let htmlFontSize
6 | !(function() {
7 | const calc = function() {
8 | const maxFontSize = 18
9 | const minFontSize = 14
10 | const html = document.getElementsByTagName('html')[0]
11 | const width = html.clientWidth
12 | let size = remBase * (width / 1440)
13 | size = Math.min(maxFontSize, size)
14 | size = Math.max(minFontSize, size)
15 | html.style.fontSize = size + 'px'
16 | htmlFontSize = size
17 | }
18 | calc()
19 | window.addEventListener('resize', throttle(calc, 500))
20 | })()
21 |
22 | // 根据基准字号计算
23 | // 用于静态样式
24 | export function toRem(px) {
25 | return `${px / remBase}rem`
26 | }
27 |
28 | // 根据当前的html根字体大小计算
29 | // 用于某些js的动态计算
30 | export function toCurrentRem(px) {
31 | return `${px / htmlFontSize}rem`
32 | }
--------------------------------------------------------------------------------
/src/base/play-icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
13 |
14 |
15 |
35 |
36 |
--------------------------------------------------------------------------------
/src/page/discovery/banner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
26 |
27 |
40 |
--------------------------------------------------------------------------------
/src/style/variables.scss:
--------------------------------------------------------------------------------
1 | $theme-color: #d33a31;
2 | $black: #000;
3 | $white: #fff;
4 | $gold: #e7aa5a;
5 | $blue: #517eaf;
6 |
7 | $font-size: 14px;
8 | $font-size-medium-sm: 13px;
9 | $font-size-lg: 16px;
10 | $font-size-sm: 12px;
11 | $font-size-xs: 10px;
12 | $font-size-medium: 15px;
13 | $font-size-title: 18px;
14 | $font-size-title-lg: 24px;
15 |
16 | $font-weight-bold: 700;
17 |
18 | $font-color-transparent: rgba(255, 255, 255, 0.5);
19 |
20 | $border: 1px solid #3f3f3f;
21 |
22 | $page-padding: 16px 32px;
23 | // layout
24 | $layout-content-min-width: 700px;
25 | // content
26 | $center-content-max-width: 1000px;
27 | // header
28 | $header-height: 50px;
29 |
30 | // mini-player
31 | $mini-player-height: 60px;
32 | $mini-player-z-index: 1002;
33 |
34 | //playlist
35 | $playlist-z-index: 1001;
36 |
37 | //search
38 | $search-panel-z-index: 1001;
39 |
40 | //song-detail
41 | $song-detail-z-index: 1000;
42 |
--------------------------------------------------------------------------------
/src/store/modules/user/actions.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 | import { getUserDetail, getUserPlaylist } from "@/api"
3 | import { UID_KEY } from "@/utils"
4 | import {notify, isDef } from "@/utils"
5 |
6 | export default {
7 | async login ({ commit }, uid) {
8 | const error = () => {
9 | notify.error('登陆失败,请输入正确的uid。')
10 | return false
11 | }
12 | if (!isDef(uid)) {
13 | return error()
14 | }
15 |
16 | try {
17 | const user = await getUserDetail(uid)
18 | const { profile } = user
19 | commit('setUser', profile)
20 | storage.set(UID_KEY, profile.userId)
21 | } catch (e) {
22 | return error()
23 | }
24 |
25 | const { playlist } = await getUserPlaylist(uid)
26 | commit('setUserPlaylist', playlist)
27 | return true
28 | },
29 | logout ({ commit }) {
30 | commit('setUser', {})
31 | commit('setUserPlaylist', [])
32 | storage.set(UID_KEY, null)
33 | }
34 | }
--------------------------------------------------------------------------------
/src/page/discovery/new-playlists.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
36 |
37 |
--------------------------------------------------------------------------------
/src/page/discovery/new-mvs.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
43 |
44 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const WorkboxPlugin = require("workbox-webpack-plugin")
2 |
3 | const isProd = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = {
6 | outputDir: 'music',
7 | configureWebpack: {
8 | devServer: {
9 | open: true,
10 | port: 8998,
11 | },
12 | externals: isProd ? {
13 | vue: 'Vue',
14 | 'vue-router': 'VueRouter',
15 | vuex: 'Vuex',
16 | axios: 'axios',
17 | } : {},
18 | plugins: [
19 | new WorkboxPlugin.GenerateSW()
20 | ]
21 | },
22 | css: {
23 | loaderOptions: {
24 | postcss: {
25 | plugins: [
26 | require('postcss-pxtorem')({ // 把px单位换算成rem单位
27 | rootValue: 14, // 换算的基数(设计图750的根字体为75)
28 | // selectorBlackList: ['weui', 'mu'], // 忽略转换正则匹配项
29 | propList: ['*']
30 | })
31 | ]
32 | },
33 | sass: {
34 | prependData: `
35 | @import "@/style/variables.scss";
36 | @import "@/style/mixin.scss";
37 | `
38 | },
39 | },
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/src/style/app.scss:
--------------------------------------------------------------------------------
1 | /**
2 | ** 应用级别的一些样式
3 | **/
4 | body {
5 | color: var(--font-color);
6 | }
7 |
8 | ::selection {
9 | background-color: $theme-color;
10 | color: white;
11 | }
12 |
13 | // 滚动条
14 | ::-webkit-scrollbar-track {
15 | background-color: var(--menu-bgcolor);
16 | }
17 |
18 | ::-webkit-scrollbar {
19 | width: 5px;
20 | height: 5px;
21 | background-color: #f5f5f5;
22 | }
23 |
24 | ::-webkit-scrollbar-thumb {
25 | background-color: var(--scrollbar-color);
26 | }
27 |
28 | .slide-enter-active,
29 | .slide-leave-active {
30 | transition: all 0.5s;
31 | transform: none;
32 | }
33 | .slide-enter,
34 | .slide-leave-to {
35 | transform: translateY(100%);
36 | }
37 |
38 | .fade-enter-active,
39 | .fade-leave-active {
40 | transition: all 0.5s;
41 | opacity: 1;
42 | }
43 | .fade-enter,
44 | .fade-leave-to {
45 | opacity: 0;
46 | }
47 |
48 | // 转化为rem
49 | .iconfont {
50 | font-size: 16px;
51 | }
52 |
53 | a {
54 | color: $theme-color;
55 |
56 | &:hover,
57 | &:focus,
58 | &:visited {
59 | color: $theme-color;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 杭少爱吸猫
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/base/highlight-text.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
--------------------------------------------------------------------------------
/src/store/modules/music/mutations.js:
--------------------------------------------------------------------------------
1 | import { shallowEqual } from "@/utils"
2 |
3 | export default {
4 | setCurrentSong(state, song) {
5 | state.currentSong = song
6 | },
7 | setCurrentTime(state, time) {
8 | state.currentTime = time
9 | },
10 | setPlayingState(state, playing) {
11 | state.playing = playing
12 | },
13 | setPlayMode(state, mode) {
14 | state.playMode = mode
15 | },
16 | setPlaylistShow(state, show) {
17 | state.isPlaylistShow = show
18 | },
19 | setPlayerShow(state, show) {
20 | state.isPlayerShow = show
21 | },
22 | setMenuShow(state, show) {
23 | state.isMenuShow = show
24 | },
25 | setPlaylistPromptShow(state, show) {
26 | state.isPlaylistPromptShow = show
27 | },
28 | setPlaylist(state, playlist) {
29 | const { isPlaylistShow, playlist: oldPlaylist } = state
30 | state.playlist = playlist
31 | // 播放列表未显示 并且两次播放列表的不一样 则弹出提示
32 | if (!isPlaylistShow && !shallowEqual(oldPlaylist, playlist, 'id')) {
33 | state.isPlaylistPromptShow = true
34 | setTimeout(() => {
35 | state.isPlaylistPromptShow = false
36 | }, 2000)
37 | }
38 | },
39 | setPlayHistory(state, history) {
40 | state.playHistory = history
41 | }
42 | }
--------------------------------------------------------------------------------
/src/store/modules/user/getters.js:
--------------------------------------------------------------------------------
1 | import { isDef } from "@/utils"
2 |
3 | export const isLogin = (state) => isDef(state.user.userId)
4 |
5 | // 根据用户请求的数据整合出菜单
6 | export const userMenus = (state) => {
7 | const { user, userPlaylist} = state
8 | const retMenus = []
9 | const userCreatelist = []
10 | const userCollectList = []
11 |
12 | userPlaylist.forEach(playlist => {
13 | const { userId } = playlist
14 | if (user.userId === userId) {
15 | userCreatelist.push(playlist)
16 | } else {
17 | userCollectList.push(playlist)
18 | }
19 | })
20 |
21 | const genPlaylistChildren = playlist =>
22 | playlist.map(({ id, name }) => ({
23 | path: `/playlist/${id}`,
24 | meta: {
25 | title: name,
26 | icon: "playlist-menu"
27 | },
28 | }))
29 |
30 | if (userCreatelist.length) {
31 | retMenus.push({
32 | type: "playlist",
33 | title: "创建的歌单",
34 | children: genPlaylistChildren(userCreatelist)
35 | })
36 | }
37 |
38 | if (userCollectList.length) {
39 | retMenus.push({
40 | type: "playlist",
41 | title: "收藏的歌单",
42 | children: genPlaylistChildren(userCollectList)
43 | })
44 | }
45 |
46 | return retMenus
47 | }
--------------------------------------------------------------------------------
/src/utils/dom.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | let elementStyle = document.createElement('div').style
4 |
5 | let vendor = ( () => {
6 | let transformNames = {
7 | // safari浏览器
8 | webkit: 'webkitTransform',
9 | // Firefox浏览器
10 | Moz: 'MozTransform',
11 | // Opera浏览器
12 | O: 'OTransform',
13 | // IE
14 | ms: 'msTransform',
15 | standard: 'transform'
16 | }
17 | for (let key in transformNames) {
18 | if (elementStyle[transformNames[key]] !== undefined) {
19 | return key
20 | }
21 | }
22 | return false
23 | })()
24 |
25 | export function prefixStyle(style) {
26 | if (vendor === false) {
27 | return false
28 | }
29 |
30 | if (vendor === 'standard') {
31 | return style
32 | }
33 |
34 | return vendor + style.charAt(0).toUpperCase() + style.substr(1)
35 | }
36 |
37 | export function hasParent(dom, parentDom) {
38 | parentDom = Array.isArray(parentDom) ? parentDom : [parentDom]
39 | while(dom) {
40 | if (parentDom.find(p => p === dom)) {
41 | return true
42 | } else {
43 | dom = dom.parentNode
44 | }
45 | }
46 | }
47 |
48 | export function scrollInto(dom) {
49 | dom.scrollIntoView({ behavior: "smooth" })
50 | }
51 |
52 | export const EMPTY_IMG = "data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
--------------------------------------------------------------------------------
/src/base/scroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
55 |
56 |
63 |
--------------------------------------------------------------------------------
/src/utils/global.js:
--------------------------------------------------------------------------------
1 | import {
2 | Input,
3 | Dialog,
4 | Button,
5 | Loading,
6 | Carousel,
7 | CarouselItem,
8 | Table,
9 | TableColumn,
10 | Popover,
11 | Pagination
12 | } from "element-ui"
13 | import Meta from "vue-meta"
14 | import VueLazyload from 'vue-lazyload'
15 | import * as utils from './index'
16 | import { EMPTY_IMG } from './dom'
17 |
18 | export default {
19 | install(Vue) {
20 | const requireComponent = require.context(
21 | "@/base",
22 | true,
23 | /[a-z0-9]+\.(jsx?|vue)$/i,
24 | )
25 | //批量注册base组件
26 | requireComponent.keys().forEach(fileName => {
27 | const componentConfig = requireComponent(fileName)
28 | const componentName = componentConfig.default.name
29 | if(componentName) {
30 | Vue.component(componentName, componentConfig.default || componentConfig)
31 | }
32 | })
33 | // 修改默认尺寸大小
34 | Vue.prototype.$ELEMENT = { size: 'small' }
35 | Vue.prototype.$utils = utils
36 |
37 | Vue.use(Input)
38 | Vue.use(Dialog)
39 | Vue.use(Button)
40 | Vue.use(Loading)
41 | Vue.use(Carousel)
42 | Vue.use(CarouselItem)
43 | Vue.use(Table)
44 | Vue.use(TableColumn)
45 | Vue.use(Popover)
46 | Vue.use(Pagination)
47 |
48 | Vue.use(Meta)
49 |
50 | Vue.use(VueLazyload, {
51 | loading: EMPTY_IMG,
52 | error: EMPTY_IMG,
53 | })
54 | }
55 | }
--------------------------------------------------------------------------------
/src/base/video-player.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
54 |
55 |
--------------------------------------------------------------------------------
/src/base/toggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
58 |
59 |
--------------------------------------------------------------------------------
/src/style/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin text-ellipsis() {
2 | overflow: hidden;
3 | text-overflow: ellipsis;
4 | white-space: nowrap;
5 | }
6 |
7 | @mixin text-ellipsis-multi($line) {
8 | display: -webkit-box;
9 | overflow: hidden;
10 | text-overflow: ellipsis;
11 | -webkit-line-clamp: $line;
12 | -webkit-box-orient: vertical;
13 | }
14 |
15 | @mixin flex-center($direction: row) {
16 | display: flex;
17 | flex-direction: $direction;
18 | align-items: center;
19 | justify-content: center;
20 | }
21 |
22 | @mixin box-shadow {
23 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
24 | }
25 |
26 | @mixin img-wrap($width, $height: $width) {
27 | width: $width;
28 | height: $height;
29 | flex-shrink: 0;
30 |
31 | img {
32 | width: 100%;
33 | height: 100%;
34 | }
35 | }
36 |
37 | @mixin abs-stretch {
38 | position: absolute;
39 | left: 0;
40 | bottom: 0;
41 | top: 0;
42 | right: 0;
43 | }
44 |
45 | @mixin abs-center {
46 | position: absolute;
47 | left: 50%;
48 | top: 50%;
49 | transform: translate(-50%, -50%);
50 | }
51 |
52 | @mixin round($d) {
53 | width: $d;
54 | height: $d;
55 | border-radius: 50%;
56 | }
57 |
58 | @mixin list($item-width) {
59 | .list-wrap {
60 | display: flex;
61 | flex-wrap: wrap;
62 | margin: 0 -12px;
63 |
64 | .list-item {
65 | width: $item-width;
66 | margin-bottom: 36px;
67 | padding: 0 12px;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/store/modules/music/actions.js:
--------------------------------------------------------------------------------
1 | import storage from 'good-storage'
2 | import { PLAY_HISTORY_KEY, getSongImg } from "@/utils"
3 |
4 | export default {
5 | // 整合歌曲信息 并且开始播放
6 | async startSong({ commit, state }, rawSong) {
7 | // 浅拷贝一份 改变引用
8 | // 1.不污染元数据
9 | // 2.单曲循环为了触发watch
10 | const song = Object.assign({}, rawSong)
11 | if (!song.img) {
12 | if (song.albumId) {
13 | song.img = await getSongImg(song.id, song.albumId)
14 | }
15 | }
16 | commit('setCurrentSong', song)
17 | commit('setPlayingState', true)
18 | // 历史记录
19 | const { playHistory } = state
20 | const playHistoryCopy = playHistory.slice()
21 | const findedIndex = playHistoryCopy.findIndex(({id}) => song.id === id)
22 | // 删除旧那一项 插入到最前面
23 | if (findedIndex !== -1) {
24 | playHistoryCopy.splice(findedIndex, 1)
25 | }
26 | playHistoryCopy.unshift(song)
27 | commit('setPlayHistory', playHistoryCopy)
28 | storage.set(PLAY_HISTORY_KEY, playHistoryCopy)
29 | },
30 | clearPlaylist({ commit }) {
31 | commit('setPlaylist', [])
32 | },
33 | clearHistory({ commit }) {
34 | const history = []
35 | commit('setPlayHistory', history)
36 | storage.set(PLAY_HISTORY_KEY, history)
37 | },
38 | addToPlaylist({ commit, state }, song) {
39 | const { playlist } = state
40 | const copy = playlist.slice()
41 | if (!copy.find(({id}) => id === song.id)) {
42 | copy.unshift(song)
43 | commit('setPlaylist', copy)
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/utils/business.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 业务工具方法
3 | */
4 | import { getAlbum, getMvDetail } from "@/api"
5 | import router from '../router'
6 | import { isDef, notify } from "./common"
7 |
8 | export function createSong (song) {
9 | const { id, name, img, artists, duration, albumId, albumName, mvId, ...rest } = song
10 |
11 | return {
12 | id,
13 | name,
14 | img,
15 | artists,
16 | duration,
17 | albumName,
18 | url: genSongPlayUrl(song.id),
19 | artistsText: getArtistsText(artists),
20 | durationSecond: duration / 1000,
21 | // 专辑 如果需要额外请求封面的话必须加上
22 | albumId,
23 | // mv的id 如果有的话会在songTable组件中加上mv链接
24 | mvId,
25 | ...rest
26 | }
27 | }
28 |
29 | export async function getSongImg (id, albumId) {
30 | if (!isDef(albumId)) {
31 | throw new Error('need albumId')
32 | }
33 | const { songs } = await getAlbum(albumId)
34 | const {
35 | al: { picUrl }
36 | } = songs.find(({ id: songId }) => songId === id) || {}
37 | return picUrl
38 | }
39 |
40 | export function getArtistsText (artists) {
41 | return (artists || []).map(({ name }) => name).join('/')
42 | }
43 |
44 | // 有时候虽然有mvId 但是请求却404,所以跳转前先请求一把
45 | export async function goMvWithCheck (id) {
46 | try {
47 | await getMvDetail(id)
48 | goMv(id)
49 | } catch (error) {
50 | notify("mv获取失败")
51 | }
52 | }
53 |
54 | export function goMv (id) {
55 | router.push(`/mv/${id}`)
56 | }
57 |
58 | function genSongPlayUrl(id) {
59 | return `https://music.163.com/song/media/outer/url?id=${id}.mp3`
60 | }
--------------------------------------------------------------------------------
/src/base/card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
8 |
9 |
10 |
{{ name }}
11 |
12 | {{ desc }}
13 |
14 |
15 |
16 |
17 |
18 |
32 |
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎵基于Vue2、Vue-CLI3的高仿网易云mac客户端播放器(PC) Online Music Player
2 |
3 | 音乐播放器虽然烂大街了,但是作为前端没自己撸一个一直是个遗憾,而且偶然间发现 pc 端 web 版的网易云音乐做的实在是太简陋了,社区仿 pc 客户端的网易云也不多见,为了弥补这个遗憾,就用 vue 全家桶模仿 mac 客户端的 ui 实现了一个,欢迎提出意见和 star~
4 |
5 | 💐[预览地址](https://www.musics.net.cn)
6 |
7 | 💐[源码地址](https://github.com/JYbmarawcp/vue-netease-music)
8 |
9 | ### 进度
10 |
11 | - [x] mv 页(3.0 新增)
12 | - [x] cd 页 (2.0 新增)
13 | - [x] 搜索建议
14 | - [x] 搜索详情
15 | - [x] 播放(版权歌曲无法播放)
16 | - [x] 发现页
17 | - [x] 播放列表
18 | - [x] 播放记录
19 | - [x] 全部歌单
20 | - [x] 歌单详情
21 | - [x] 最新音乐
22 | - [x] 主题换肤功能
23 | - [x] 登录(网易云 uid 登录)
24 |
25 | ### 后端接口
26 |
27 | https://binaryify.github.io/NeteaseCloudMusicApi
28 |
29 | ### 技术栈
30 |
31 | - ***Vue*** 全家桶 Vue-CLI3 create 出来的。
32 | - ***Element-Ui*** 魔改样式。
33 | - ***better-scroll*** 歌词滚动部分用了黄轶老大的 (贼爽)
34 | - ***CSS Variables*** 主题换肤。
35 | - ***ES 6 / 7*** (JavaScript 语言的下一代标准)
36 | - ***Sass***(CSS 预处理器)
37 | - ***postcss-pxtorem***(自动处理 rem,妈妈再也不用担心屏幕太大太小了)
38 | - ***workbox-webpack-plugin*** 谷歌开发的利用 Service Work 预缓存 chunks 的 webpack 插件。
39 |
40 | ### Screenshots
41 |
42 | 
43 |
44 | 
45 |
46 | 
47 |
48 | 
49 |
50 | ### 安装与使用
51 |
52 | ```
53 | npm i
54 | npm run dev
55 | ```
56 |
57 | ### 友情链接
58 | [mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
59 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/song-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ order }}
5 |
6 |
7 |
![]()
8 |
9 |
10 |
11 |
{{ name }}
12 |
{{ artistsText }}
13 |
14 |
15 |
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/src/utils/lrcparse.js:
--------------------------------------------------------------------------------
1 | export default function lyricParser (lrc) {
2 | return {
3 | 'lyric': parseLyric(lrc.lrc.lyric || ''),
4 | 'tlyric': parseLyric(lrc.tlyric.lyric || ''),
5 | 'lyricuser': lrc.lyricUser,
6 | 'transUser': lrc.transUser,
7 | }
8 | }
9 |
10 | export function parseLyric (lrc) {
11 | const lyrics = lrc.split('\n')
12 | const lrcObj = []
13 | for (let i = 0; i < lyrics.length; i++) {
14 | const lyric = decodeURIComponent(lyrics[i])
15 | const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g
16 | const timeRegExpArr = lyric.match(timeReg)
17 | if (!timeRegExpArr) continue
18 | const content = lyric.replace(timeReg, '')
19 | for (let k = 0, h = timeRegExpArr.length; k < h; k++) {
20 | const t = timeRegExpArr[k]
21 | const min = Number(String(t.match(/\[\d*/i)).slice(1))
22 | const sec = Number(String(t.match(/:\d*/i)).slice(1))
23 | const time = min * 60 + sec
24 | if (content !== '') {
25 | lrcObj.push({ time: time, content })
26 | }
27 | }
28 | }
29 | return lrcObj
30 | }
31 |
32 | // lyric: "[00:00.000] 作曲 : 爱星人↵[00:00.826] 作词 : 爱星人↵[00:02.478]作曲:爱星人↵[00:03.559]作词:爱星人↵[00:04.533]编曲:爱星人↵[00:05.642]吉他:爱星人,Jeff(菲律宾),庄高你很爱我↵[02:59.643]只是那爱情出错↵[03:04.540]爱如天气如火花的你↵[03:09.193]在心中与我对望OH↵[03:21.274]在心中与我对望 Woo ~↵[03:26.559]爱与你对望 Woo ~↵[03:31.411]爱与你对望↵[03:33.455]Shorty↵[03:34.939]you gotta get that↵[03:36.483]Sometimes Men will go little whack↵[03:40.124]they just wanna find a Hera(赫拉女神,宙斯妻子)↵[03:42.265]but can‘t take a moment to get to know her↵[03:45.689]Don't worry↵[03:47.238]I'm not one of their kind.Everything will be fine.↵"
33 | // tlyric: "[59]↵[03:31.411]↵[03:33.455]Shtory↵[03:34.939]你要明白↵[03:36.483]有时男人会变得失常↵[03:40.124]他们只中意追寻赫拉↵[03:42.265]但却不能费时体会她↵[03:45.689]不要担心↵[03:47.238]我不是他们中的一员 一切都会好的↵"
34 |
35 |
--------------------------------------------------------------------------------
/src/style/themes/variables.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ['--body-bgcolor']: '#252525',
3 | ['--light-bgcolor']: '#2e2e2e',
4 |
5 | ['--font-color']: '#b1b1b1',
6 | ['--font-color-shallow']: '#6f6f6f',
7 | ['--font-color-white']: '#dcdde4',
8 | ['--font-color-grey']: '#5C5C5C',
9 | ['--font-color-grey2']: '#808080',
10 | ['--font-color-grey-shallow']: '#727272',
11 | ['--border']: '#3F3F3F',
12 | ['--scrollbar-color']: '#3a3a3a',
13 | ['--round-hover-bgcolor']: '#373737',
14 | ['--stripe-bg']: '#323232',
15 | ['--shallow-theme-bgcolor']: '#2D2625',
16 | ['--shallow-theme-bgcolor-hover']: '#352726',
17 |
18 | //header
19 | ['--header-bgcolor']: '#252525',
20 | ['--header-font-color']: '#b1b1b1',
21 | ['--header-input-color']: '#b1b1b1',
22 | ['--header-input-bgcolor']: '#4B4B4B',
23 | ['--header-input-placeholder-color']: '#727272',
24 |
25 | //menu
26 | ['--menu-bgcolor']: '#202020',
27 | ['--menu-item-hover-bg']: '#1d1d1d',
28 | ['--menu-item-active-bg']: '#1b1b1b',
29 |
30 | //player
31 | ['--player-bgcolor']: '#252525',
32 |
33 | //playlist
34 | ['--playlist-bgcolor']: '#363636',
35 | ['--playlist-hover-bgcolor']: '#2e2e2e',
36 |
37 | //search
38 | ['--search-bgcolor']: '#363636',
39 |
40 | //progress
41 | ['--progress-bgcolor']: '#232323',
42 |
43 | //input
44 | ['--input-color']: '#b1b1b1',
45 | ['--input-bgcolor']: '#4B4B4B',
46 |
47 | //button
48 | ['--button-border-color']: '#454545',
49 | ['--button-hover-bgcolor']: '#3E3E3E',
50 | //tab
51 | ['--tab-item-color']: '#797979',
52 | ['--tab-item-hover-color']: '#B4B4B4',
53 | ['--tab-item-active-color']: '#fff',
54 | //modal
55 | ['--modal-bg-color']: '#202020',
56 | // prompt
57 | ['--prompt-bg-color']: '#363636',
58 | //song-detail
59 | ['--song-shallow-grey-bg']: '#2A2A2A',
60 | }
--------------------------------------------------------------------------------
/src/style/themes/variables-white.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ['--body-bgcolor']: '#fff',
3 | ['--light-bgcolor']: '#f5f5f5',
4 |
5 | ['--font-color']: '#4a4a4a',
6 | ['--font-color-shallow']: '#404040',
7 | ['--font-color-white']: '#333333',
8 | ['--font-color-grey']: '#5c5c5c',
9 | ['--font-color-grey2']: '#909090',
10 | ['--font-color-grey-shallow']: '#BEBEBE',
11 | ['--border']: '#f2f2f1',
12 | ['--scrollbar-color']: '#D0D0D0',
13 | ['--round-hover-bgcolor']: '#EBEBEB',
14 | ['--stripe-bg']: '#FAFAFA',
15 | ['--shallow-theme-bgcolor']: '#fdf6f5',
16 | ['--shallow-theme-bgcolor-hover']: '#FBEDEC',
17 |
18 | //header
19 | ['--header-bgcolor']: '#F9F9F9',
20 | ['--header-font-color']: '#4a4a4a',
21 | ['--header-input-color']: '#4a4a4a',
22 | ['--header-input-bgcolor']: '#EDEDED',
23 | ['--header-input-placeholder-color']: '#BEBEBE',
24 |
25 | // menu
26 | ['--menu-bgcolor']: '#ededed',
27 | ['--menu-item-hover-bg']: '#e7e7e7',
28 | ['--menu-item-active-bg']: '#e2e2e2',
29 |
30 | //player
31 | ['--player-bgcolor']: '#F9F9F9',
32 |
33 | //playlist
34 | ['--playlist-bgcolor']: '#fff',
35 | ['--playlist-hover-bgcolor']: '#EFEFEF',
36 |
37 | //search
38 | ['--search-bgcolor']: '#fff',
39 | //progress
40 | ['--progress-bgcolor']: '#F5F5F5',
41 |
42 | //input
43 | ['--input-color']: '#4a4a4a',
44 | ['--input-bgcolor']: '#EDEDED',
45 |
46 | //button
47 | ['--button-border-color']: '#D9D9D9',
48 | ['--button-hover-bgcolor']: '#F5F5F5',
49 |
50 | //tab
51 | ['--tab-item-color']: '#7F7F81',
52 | ['--tab-item-hover-color']: '#343434',
53 | ['--tab-item-active-color']: '#000',
54 |
55 | //modal
56 | ['--modal-bg-color']: '#F9F9F9',
57 | // prompt
58 | ['--prompt-bg-color']: '#fff',
59 | //song-detail
60 | ['--song-shallow-grey-bg']: '#E6E5E6',
61 | }
62 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * 新增百度统计 ([30a017a](https://github.com/JYbmarawcp/vue-netease-music/commit/30a017acd28d19fd4d8fc2efebe1ca21533839c8))
4 | * 获取歌手单曲 ([ccfcd6a](https://github.com/JYbmarawcp/vue-netease-music/commit/ccfcd6a23f142a0a93e4718900192f94bc0d40d9))
5 |
6 | ## [1.0.0](https://github.com/JYbmarawcp/vue-netease-music/compare/v0.1.1...v1.0.0) (2020-08-13)
7 |
8 |
9 | ### Features
10 |
11 | * 添加了预缓存和vuex打印日志 ([ea841ef](https://github.com/JYbmarawcp/vue-netease-music/commit/ea841ef493400ce8960cb3d7f370d0a30aef8d61))
12 | * 预览地址 ([a3c48e9](https://github.com/JYbmarawcp/vue-netease-music/commit/a3c48e9d9dac9e6d0ccad25bd90e7b5aa8278b6c))
13 | * 预览地址 ([896e214](https://github.com/JYbmarawcp/vue-netease-music/commit/896e21463337aeb0ffdd4a6a2df715dfc4dbe296))
14 |
15 | ## [0.2.0](https://github.com/JYbmarawcp/vue-netease-music/compare/v0.1.1...v0.2.0) (2020-08-13)
16 |
17 |
18 | ### Features
19 |
20 | * 添加了预缓存和vuex打印日志 ([ea841ef](https://github.com/JYbmarawcp/vue-netease-music/commit/ea841ef493400ce8960cb3d7f370d0a30aef8d61))
21 | * 预览地址 ([a3c48e9](https://github.com/JYbmarawcp/vue-netease-music/commit/a3c48e9d9dac9e6d0ccad25bd90e7b5aa8278b6c))
22 | * 预览地址 ([896e214](https://github.com/JYbmarawcp/vue-netease-music/commit/896e21463337aeb0ffdd4a6a2df715dfc4dbe296))
23 |
24 | ### [0.1.1](https://github.com/JYbmarawcp/vue-netease-music/compare/v0.2.0...v0.1.1) (2020-08-11)
25 |
26 |
27 | ### Features
28 |
29 | * workboxPlugin ([8b21134](https://github.com/JYbmarawcp/vue-netease-music/commit/8b2113480c14851e42ee535fc02febfb37dd2af3))
30 |
31 | ## 0.2.0 (2020-08-11)
32 |
33 |
34 | ### Features
35 |
36 | * 优化自动commitChangeLog ([a5f855f](https://github.com/JYbmarawcp/vue-netease-music/commit/a5f855f1932b2833a27c015043db59b671fe10a4))
37 | * 合并 ([d4ac624](https://github.com/JYbmarawcp/vue-netease-music/commit/d4ac624e0de902326bbfabec3365d14bc1133f73))
38 |
39 | # 0.1.0 (2020-08-11)
40 |
--------------------------------------------------------------------------------
/src/page/songs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
72 |
73 |
--------------------------------------------------------------------------------
/src/base/volume.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
65 |
66 |
82 |
--------------------------------------------------------------------------------
/src/page/search/mvs.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
66 |
67 |
--------------------------------------------------------------------------------
/src/page/search/playlists.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
60 |
61 |
--------------------------------------------------------------------------------
/src/components/with-pagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
78 |
79 |
--------------------------------------------------------------------------------
/src/base/confirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | {{ title || '提示' }}
9 | {{ text }}
10 |
18 |
19 |
20 |
21 |
74 |
75 |
89 |
--------------------------------------------------------------------------------
/src/components/playlist-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 | {{ desc }}
7 |
8 |
9 |
10 |
{{ name }}
11 |
12 |
13 |
14 |
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-netease-music",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "commit": "git-cz",
10 | "release-f": "standard-version -f",
11 | "release-major": "standard-version -r major",
12 | "release-minor": "standard-version -r minor",
13 | "release-patch": "standard-version -r patch"
14 | },
15 | "dependencies": {
16 | "@better-scroll/core": "^2.0.0-beta.10",
17 | "@better-scroll/mouse-wheel": "^2.0.0-beta.10",
18 | "@better-scroll/scroll-bar": "^2.0.0-beta.10",
19 | "axios": "^0.19.2",
20 | "clipboard": "^2.0.6",
21 | "core-js": "^3.6.5",
22 | "element-ui": "^2.13.2",
23 | "good-storage": "^1.1.1",
24 | "lodash-es": "^4.17.15",
25 | "node-sass": "^4.14.1",
26 | "vue": "^2.6.11",
27 | "vue-lazyload": "^1.3.3",
28 | "vue-meta": "^2.4.0",
29 | "vue-router": "^3.3.4",
30 | "vuex": "^3.5.1",
31 | "xgplayer": "^2.9.8"
32 | },
33 | "devDependencies": {
34 | "@vue/cli-plugin-babel": "~4.4.0",
35 | "@vue/cli-plugin-eslint": "~4.4.0",
36 | "@vue/cli-service": "~4.4.0",
37 | "babel-eslint": "^10.1.0",
38 | "babel-plugin-component": "^1.1.1",
39 | "commitizen": "^4.1.2",
40 | "cz-conventional-changelog": "^3.2.0",
41 | "eslint": "^6.7.2",
42 | "eslint-plugin-vue": "^6.2.2",
43 | "postcss-pxtorem": "^5.1.1",
44 | "sass-loader": "^8.0.0",
45 | "standard-version": "^8.0.2",
46 | "vue-cli-plugin-element": "~1.0.1",
47 | "vue-template-compiler": "^2.6.11",
48 | "workbox-webpack-plugin": "^5.1.3"
49 | },
50 | "eslintConfig": {
51 | "root": true,
52 | "env": {
53 | "node": true
54 | },
55 | "extends": [
56 | "plugin:vue/essential",
57 | "eslint:recommended"
58 | ],
59 | "parserOptions": {
60 | "parser": "babel-eslint"
61 | },
62 | "rules": {}
63 | },
64 | "browserslist": [
65 | "> 1%",
66 | "last 2 versions",
67 | "not dead"
68 | ],
69 | "config": {
70 | "commitizen": {
71 | "path": "./node_modules/cz-conventional-changelog"
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/base/icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
69 |
70 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 | <% if ( NODE_ENV === 'production' ) { %>
33 |
34 |
35 |
36 |
37 | <%} %>
38 |
39 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { Loading } from "element-ui";
3 | import { confirm } from "@/base/confirm";
4 | import store from "@/store";
5 |
6 | const BASE_URL = "https://api.mtnhao.com/";
7 | //不带全局loading的请求实例
8 | export const requestWithoutLoading = createBaseInstance();
9 | //带全局loading的请求实例
10 | //传入函数是因为需要在处理请求结果handleResponse之前处理loading
11 | //所以要在内部插入loading拦截器的处理逻辑
12 | export const request = createBaseInstance();
13 | mixinLoading(request.interceptors)
14 | //通用的axios的实例
15 | function createBaseInstance() {
16 | const instance = axios.create({
17 | baseURL: BASE_URL,
18 | });
19 |
20 | instance.interceptors.response.use(handleResponse, handleError);
21 | return instance;
22 | }
23 |
24 | function handleError(e) {
25 | confirm(e.message, "出错啦!");
26 | throw e;
27 | }
28 | //返回响应的结果data
29 | function handleResponse(response) {
30 | return response.data;
31 | }
32 |
33 | let loading;
34 | let loadingCount = 0
35 | const SET_AXIOS_LOADING = 'global/setAxiosLoading'
36 | function mixinLoading(interceptors) {
37 | interceptors.request.use(loadingRequestInterceptor)
38 | interceptors.response.use(
39 | loadingResponseInterceptor,
40 | loadingResponseErrorInterceptor
41 | )
42 |
43 | function loadingRequestInterceptor(config) {
44 | if (!loading) {
45 | loading = Loading.service({
46 | target: 'body',
47 | background: 'transparent',
48 | text: '载入中'
49 | })
50 | store.commit(SET_AXIOS_LOADING, true)
51 | }
52 | loadingCount++
53 |
54 | return config
55 | }
56 |
57 | function loadingResponseInterceptor(response) {
58 | handleResponseLoading()
59 | return response
60 | }
61 |
62 | function loadingResponseErrorInterceptor(e) {
63 | handleResponseLoading()
64 | throw e
65 | }
66 |
67 | function handleResponseLoading() {
68 | loadingCount--
69 | if (loadingCount === 0) {
70 | loading.close()
71 | loading = null
72 | store.commit(SET_AXIOS_LOADING, false)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/top-playlist-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
8 |
9 | 精品歌单
10 |
11 |
{{ name }}
12 |
{{ desc }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
--------------------------------------------------------------------------------
/src/page/search/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
17 |
18 |
19 |
20 | 暂无结果
21 |
22 |
23 |
24 |
25 |
26 |
73 |
74 |
--------------------------------------------------------------------------------
/src/components/comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/mv-card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 | {{ $utils.formatNumber(playCount) }}
7 |
8 |
11 |
12 | {{ $utils.formatTime(duration / 1000 )}}
13 |
14 |
15 |
{{ name }}
16 |
{{ author }}
17 |
18 |
19 |
20 |
34 |
35 |
--------------------------------------------------------------------------------
/src/store/modules/music/getters.js:
--------------------------------------------------------------------------------
1 | import { isDef, playModeMap } from "@/utils"
2 |
3 | export const currentIndex = (state) => {
4 | const { currentSong, playlist } = state
5 | return playlist.findIndex(({ id }) => id === currentSong.id)
6 | }
7 |
8 | export const nextSong = (state, getters) => {
9 | const { playlist, playMode } = state
10 | const nextStartMap = {
11 | [playModeMap.sequence.code]: getSequenceNextIndex,
12 | [playModeMap.loop.code]: getLoopNextIndex,
13 | [playModeMap.random.code]: getRandomNextIndex
14 | }
15 | const getNextStart = nextStartMap[playMode]
16 | const index = getNextStart()
17 |
18 | return playlist[index]
19 |
20 | // 顺序
21 | function getSequenceNextIndex () {
22 | let nextIndex = getters.currentIndex + 1
23 | if (nextIndex > playlist.length - 1) {
24 | nextIndex = 0
25 | }
26 | return nextIndex
27 | }
28 |
29 | // 随机
30 | function getRandomNextIndex () {
31 | return getRandomIndex(playlist, getters.currentIndex)
32 | }
33 |
34 | // 单曲
35 | function getLoopNextIndex () {
36 | return getters.currentIndex
37 | }
38 | }
39 |
40 |
41 | // 上一首歌
42 | export const prevSong = (state, getters) => {
43 | const { playlist, playMode } = state
44 | const prevStratMap = {
45 | [playModeMap.sequence.code]: genSequencePrevIndex,
46 | [playModeMap.loop.code]: getLoopPrevIndex,
47 | [playModeMap.random.code]: getRandomPrevIndex
48 | }
49 | const getPrevStrat = prevStratMap[playMode]
50 | const index = getPrevStrat()
51 |
52 | return playlist[index]
53 |
54 | function genSequencePrevIndex () {
55 | let prevIndex = getters.currentIndex - 1
56 | if (prevIndex < 0) {
57 | prevIndex = playlist.length - 1
58 | }
59 | return prevIndex
60 | }
61 |
62 | function getLoopPrevIndex () {
63 | return getters.currentIndex
64 | }
65 |
66 | function getRandomPrevIndex () {
67 | return getRandomIndex(playlist, getters.currentIndex)
68 | }
69 | }
70 |
71 | // 当前是否有歌曲在播放
72 | export const hasCurrentSong = (state) => {
73 | return isDef(state.currentSong.id)
74 | }
75 |
76 | function getRandomIndex (playlist, currentIndex) {
77 | // 防止无限循环
78 | if (playlist.length === 1) {
79 | return currentIndex
80 | }
81 | let index = Math.round(Math.random() * (playlist.length - 1))
82 | if (index === currentIndex) {
83 | index = getRandomIndex(playlist, currentIndex)
84 | }
85 | return index
86 | }
--------------------------------------------------------------------------------
/src/page/search/songs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
15 |
19 |
{{themeValue.title}}
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
90 |
91 |
--------------------------------------------------------------------------------
/src/page/discovery/new-songs.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
95 |
96 |
--------------------------------------------------------------------------------
/src/style/reset.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | overflow: hidden;
5 | font-weight: 400;
6 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
7 | sans-serif;
8 | }
9 |
10 | html,
11 | body,
12 | div,
13 | span,
14 | object,
15 | iframe,
16 | h1,
17 | h2,
18 | h3,
19 | h4,
20 | h5,
21 | h6,
22 | p,
23 | blockquote,
24 | pre,
25 | abbr,
26 | address,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | samp,
37 | small,
38 | strong,
39 | sub,
40 | sup,
41 | var,
42 | b,
43 | i,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | figcaption,
67 | figure,
68 | footer,
69 | header,
70 | hgroup,
71 | menu,
72 | nav,
73 | section,
74 | summary,
75 | time,
76 | mark,
77 | audio,
78 | video {
79 | margin: 0;
80 | padding: 0;
81 | border: 0;
82 | outline: 0;
83 | font-size: 100%;
84 | vertical-align: baseline;
85 | background: transparent;
86 | list-style: none;
87 | box-sizing: border-box;
88 | }
89 |
90 | body {
91 | line-height: 1.2;
92 | }
93 |
94 | :focus {
95 | outline: 1;
96 | }
97 |
98 | article,
99 | aside,
100 | canvas,
101 | details,
102 | figcaption,
103 | figure,
104 | footer,
105 | header,
106 | hgroup,
107 | menu,
108 | nav,
109 | section,
110 | summary {
111 | display: block;
112 | }
113 |
114 | nav ul {
115 | list-style: none;
116 | }
117 |
118 | blockquote,
119 | q {
120 | quotes: none;
121 | }
122 |
123 | blockquote:before,
124 | blockquote:after,
125 | q:before,
126 | q:after {
127 | content: "";
128 | content: none;
129 | }
130 |
131 | a {
132 | margin: 0;
133 | padding: 0;
134 | border: 0;
135 | font-size: 100%;
136 | vertical-align: baseline;
137 | background: transparent;
138 | }
139 |
140 | ins {
141 | background-color: #ff9;
142 | color: #000;
143 | text-decoration: none;
144 | }
145 |
146 | mark {
147 | background-color: #ff9;
148 | color: #000;
149 | font-style: italic;
150 | font-weight: bold;
151 | }
152 |
153 | del {
154 | text-decoration: line-through;
155 | }
156 |
157 | abbr[title],
158 | dfn[title] {
159 | border-bottom: 1px dotted #000;
160 | cursor: help;
161 | }
162 |
163 | table {
164 | border-collapse: collapse;
165 | border-spacing: 0;
166 | }
167 |
168 | hr {
169 | display: block;
170 | height: 1px;
171 | border: 0;
172 | border-top: 1px solid #cccccc;
173 | margin: 1em 0;
174 | padding: 0;
175 | }
176 |
177 | input,
178 | select {
179 | vertical-align: middle;
180 | }
181 |
182 | img[src=""],
183 | img:not([src]) {
184 | opacity: 0;
185 | }
186 |
--------------------------------------------------------------------------------
/src/layout/menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
62 |
63 |
--------------------------------------------------------------------------------
/src/page/mvs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 地区:
5 |
11 |
12 |
13 | 类型:
14 |
20 |
21 |
22 | 排序:
23 |
29 |
30 |
38 |
54 |
55 |
56 |
57 |
58 |
105 |
106 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Router from "vue-router";
3 |
4 | const Discovery = () => import(/* webpackChunkName: "Discovery" */ '@/page/discovery')
5 | const Songs = () => import(/* webpackChunkName: "Songs" */ '@/page/songs')
6 | const Playlists = () => import(/* webpackChunkName: "Playlists" */ '@/page/playlists')
7 | const PlaylistDetail = () => import(/* webpackChunkName: "PlaylistDetail" */ '@/page/playlist-detail')
8 | const Mvs = () => import(/* webpackChunkName: "Mvs" */ '@/page/mvs')
9 | const Mv = () => import(/* webpackChunkName: "Mv" */ '@/page/mv')
10 | const Search = () => import(/* webpackChunkName: "Search" */ '@/page/search')
11 | const SearchSongs = () => import(/* webpackChunkName: "SearchSongs" */ '@/page/search/songs')
12 | const SearchPlaylists = () => import(/* webpackChunkName: "SearchPlaylists" */ '@/page/search/playlists')
13 | const SearchMvs = () => import(/* webpackChunkName: "SearchMvs" */ '@/page/search/mvs')
14 |
15 | //内容需要居中的页面
16 | export const LayoutCenterNames = ['discovery', 'songs', 'mvs']
17 |
18 | //需要显示在侧边栏菜单的页面
19 | export const menuRoutes = [
20 | {
21 | path: '/discovery',
22 | name: 'discovery',
23 | component: Discovery,
24 | meta: {
25 | title: '发现音乐',
26 | icon: 'music',
27 | }
28 | },
29 | {
30 | path: '/playlists',
31 | name: 'playlists',
32 | component: Playlists,
33 | meta: {
34 | title: '推荐歌单',
35 | icon: 'playlist-menu'
36 | }
37 | },
38 | {
39 | path: '/songs',
40 | name: 'songs',
41 | component: Songs,
42 | meta: {
43 | title: '最新音乐',
44 | icon: 'yinyue'
45 | }
46 | },
47 | {
48 | path: '/mvs',
49 | name: 'mvs',
50 | component: Mvs,
51 | meta: {
52 | title: '最新MV',
53 | icon: 'mv'
54 | }
55 | }
56 | ]
57 | // 解决ElementUI导航栏中的vue-router在3.0版本以上重复点菜单报错问题
58 | const originalPush = Router.prototype.push
59 | Router.prototype.push = function push(location) {
60 | return originalPush.call(this, location).catch(err => err)
61 | }
62 | Vue.use(Router);
63 |
64 | export default new Router({
65 | mode: "hash",
66 | routes: [
67 | {
68 | path: "/",
69 | redirect: '/discovery',
70 | },
71 | {
72 | path: "/playlist/:id",
73 | name: 'playlist',
74 | component: PlaylistDetail
75 | },
76 | {
77 | path: '/mv/:id',
78 | name: 'mv',
79 | component: Mv,
80 | props: (route) => ({id: +route.params.id}),
81 | },
82 | {
83 | path: '/search/:keywords',
84 | name: 'search',
85 | component: Search,
86 | props: true,
87 | children: [
88 | {
89 | path: '/',
90 | redirect: 'songs',
91 | },
92 | {
93 | path: 'songs',
94 | name: 'searchSongs',
95 | component: SearchSongs,
96 | },
97 | {
98 | path: 'playlists',
99 | name: 'searchPlaylists',
100 | component: SearchPlaylists,
101 | },
102 | {
103 | path: 'mvs',
104 | name: 'searchMvs',
105 | component: SearchMvs,
106 | }
107 | ]
108 | },
109 | ...menuRoutes,
110 | ],
111 | });
112 |
--------------------------------------------------------------------------------
/src/page/playlists/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
15 |
16 |
17 |
24 |
25 |
35 |
43 |
44 |
45 |
46 |
121 |
122 |
--------------------------------------------------------------------------------
/src/base/progress-bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
86 |
87 |
--------------------------------------------------------------------------------
/src/components/playlist.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
28 |
29 |
33 |
37 |
38 |
39 | 你还没有添加任何歌曲
40 |
41 |
42 |
43 |
44 |
45 |
46 |
94 |
95 |
--------------------------------------------------------------------------------
/src/page/playlist-detail/detail-header.vue:
--------------------------------------------------------------------------------
1 |
2 |
37 |
38 |
39 |
68 |
69 |
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | import { Notification } from 'element-ui'
2 |
3 | export { debounce, throttle } from 'lodash-es'
4 |
5 | export function pad (num, n = 2) {
6 | let len = num.toString().length
7 | while (len < n) {
8 | num = '0' + num
9 | len++
10 | }
11 | return num
12 | }
13 |
14 | export function isDef (v) {
15 | return v !== undefined && v !== null
16 | }
17 |
18 | export function formatDate (date, fmt = 'yyyy-MM-dd hh:mm:ss') {
19 | date = date instanceof Date ? date : new Date(date)
20 | if (/(y+)/.test(fmt)) {
21 | fmt = fmt.replace(
22 | RegExp.$1,
23 | (date.getFullYear() + '').substr(4 - RegExp.$1.length)
24 | )
25 | }
26 | let o = {
27 | 'M+': date.getMonth() + 1,
28 | 'd+': date.getDate(),
29 | 'h+': date.getHours(),
30 | 'm+': date.getMinutes(),
31 | 's+': date.getSeconds(),
32 | }
33 | for (let k in o) {
34 | if (new RegExp(`(${k})`).test(fmt)) {
35 | let str = o[k] + ''
36 | fmt = fmt.replace(
37 | RegExp.$1,
38 | RegExp.$1.length === 1 ? str : padLeftZero(str)
39 | )
40 | }
41 | }
42 | return fmt
43 | }
44 | function padLeftZero (str) {
45 | return ('00' + str).substr(str.length)
46 | }
47 |
48 | export function formatTime (interval) {
49 | interval = interval | 0
50 | const minute = pad((interval / 60) | 0)
51 | const second = pad(interval % 60)
52 | return `${minute}:${second}`
53 | }
54 |
55 | export function formatNumber (number) {
56 | number = Number(number) || 0
57 | return number > 100000 ? `${Math.round(number / 10000)}万` : number
58 | }
59 |
60 | export function genImgUrl (url, w, h) {
61 | if (!h) {
62 | h = w
63 | }
64 | url += `?param=${w}y${h}`
65 | return url
66 | }
67 |
68 | export function isLast(index, arr) {
69 | return index === arr.length - 1
70 | }
71 |
72 | export function shallowEqual(a, b, compareKey) {
73 | if (a.length !== b.length) {
74 | return false
75 | }
76 | for (let i = 0; i < a.length; i++) {
77 | let compareA = a[i]
78 | let compareB = b[i]
79 | if (compareKey) {
80 | compareA = compareA[compareKey]
81 | compareB = compareB[compareKey]
82 | }
83 | if (!Object.is(compareA, compareB)) {
84 | return false
85 | }
86 | }
87 | return true
88 | }
89 |
90 | export function notify (message, type) {
91 | const params = {
92 | message,
93 | duration: 1500,
94 | }
95 | const fn = type ? Notification[type] : Notification
96 | return fn(params)
97 | }
98 |
99 | ['success', 'warning', 'info', 'error'].forEach((key) => {
100 | notify[key] = (message) => {
101 | return notify(message, key)
102 | }
103 | })
104 |
105 | export function requestFullScreen(element) {
106 | const docElm = element;
107 | if (docElm.requestFullScreen) {
108 | docElm.requestFullScreen()
109 | } else if (docElm.msRequestFullscreen) {
110 | docElm.msRequestFullscreen()
111 | } else if (docElm.mozRequestFullScreen) {
112 | docElm.mozRequestFullScreen()
113 | } else if (docElm.webkitRequestFullScreen) {
114 | docElm.webkitRequestFullScreen()
115 | }
116 | }
117 |
118 | export function exitFullscreen() {
119 | const de = window.parent.document
120 | if (de.exitFullscreen) {
121 | de.exitFullscreen()
122 | } else if (de.mozCancelFullScreen) {
123 | de.mozCancelFullScreen()
124 | } else if (de.webkitCancelFullScreen) {
125 | de.webkitCancelFullScreen()
126 | } else if (de.msExitFullscreen) {
127 | de.msExitFullscreen()
128 | }
129 | }
130 |
131 | export function isFullscreen() {
132 | return document.fullScreen ||
133 | document.mozFullScreen ||
134 | document.webkitIsFullScreen
135 | }
136 |
137 | export function getPageOffset (page, limit) {
138 | return (page - 1) * limit
139 | }
140 |
--------------------------------------------------------------------------------
/src/components/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
![]()
12 |
{{ user.nickname }}
13 |
14 |
15 |
16 |
21 | 登录
22 |
23 |
28 |
29 |
注:默认登陆的是我的id,想登陆你的就按下面的步骤来吧⬇
30 |
31 | 1、请
32 | 点我(http://music.163.com)
35 | 打开网易云音乐官网
36 |
37 |
2、点击页面右上角的“登录”
38 |
3、点击您的头像,进入我的主页
39 |
40 | 4、复制浏览器地址栏 /user/home?id= 后面的数字(网易云 UID)
41 |
42 |
43 |
44 |
54 |
55 |
56 |
57 |
58 |
112 |
113 |
161 |
--------------------------------------------------------------------------------
/src/layout/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
50 |
51 |
52 |
87 |
88 |
--------------------------------------------------------------------------------
/src/style/element-overwrite.scss:
--------------------------------------------------------------------------------
1 | // table
2 | .el-table th,
3 | .el-table td {
4 | padding: 4px !important;
5 | font-size: $font-size-sm !important;
6 | }
7 | .el-table::before {
8 | height: 0 !important;
9 | }
10 | .el-table--enable-row-hover .el-table__body tr:hover > td {
11 | background-color: var(--playlist-hover-bgcolor) !important;
12 | }
13 | // 空数据
14 | .el-table__empty-block {
15 | background: var(--body-bgcolor);
16 | color: var(--font-color);
17 | }
18 | .el-table__header-wrapper th {
19 | color: var(--font-color);
20 | }
21 | .el-table {
22 | background-color: var(--body-bgcolor) !important;
23 | }
24 |
25 | // 表格单元格的通用样式
26 | @mixin el-td-style($color) {
27 | td,
28 | th,
29 | tr {
30 | background-color: $color !important;
31 | transition: background-color 0s !important;
32 | border-bottom: none !important;
33 |
34 | .cell {
35 | white-space: nowrap !important;
36 | }
37 | }
38 | }
39 | .el-table,
40 | .el-table {
41 | @include el-td-style(var(--body-bgcolor));
42 |
43 | tr.el-table__row--striped {
44 | @include el-td-style(var(--stripe-bg));
45 | }
46 | }
47 | // 允许外部在某个类下面覆写table样式
48 | @mixin el-table-theme($color, $stripe-color: var(--stripe-bg)) {
49 | /deep/.el-table {
50 | @include el-td-style($color);
51 |
52 | tr.el-table__row--striped {
53 | @include el-td-style(#{$stripe-color});
54 | }
55 | }
56 | }
57 | // carosel
58 | .el-carousel--horizontal {
59 | overflow: hidden;
60 | }
61 |
62 | // popover
63 | @each $direction in 'bottom' 'top' 'left' 'right' {
64 | .el-popper[x-placement^='#{$direction}'] .popper__arrow,
65 | .el-popper[x-placement^='#{$direction}'] .popper__arrow::after {
66 | border-#{$direction}-color: var(--prompt-bg-color) !important;
67 | }
68 | }
69 | .el-popover {
70 | background: var(--prompt-bg-color) !important;
71 | border: none !important;
72 | text-align: left;
73 | @include box-shadow;
74 | }
75 |
76 | // input
77 | $input-height: 24px;
78 | @mixin el-input-style($color, $bg-color, $placeholder-color) {
79 | .el-input__inner {
80 | height: $input-height !important;
81 | line-height: $input-height !important;
82 | background: #{$bg-color} !important;
83 | border: none !important;
84 | color: #{$color} !important;
85 |
86 | &:hover {
87 | border: none !important;
88 | }
89 | }
90 | .el-input__prefix {
91 | i {
92 | line-height: $input-height + 1px !important;
93 | color: #{$color} !important;
94 | transition: none !important;
95 | }
96 | }
97 |
98 | input::-webkit-input-placeholder {
99 | color: #{$placeholder-color} !important;
100 | }
101 | }
102 |
103 | // 外部覆写input-theme样式
104 | @mixin el-input-theme($color, $bg-color, $placeholder-color: $color) {
105 | /deep/.el-input {
106 | @include el-input-style($color, $bg-color, $placeholder-color);
107 | }
108 | }
109 |
110 | .el-input {
111 | @include el-input-style(
112 | var(--input-color),
113 | var(--input-bgcolor),
114 | var(--font-color-grey-shallow)
115 | );
116 | }
117 |
118 | // pagination
119 | .el-pagination,
120 | .el-pagination button,
121 | .el-pager li {
122 | background: inherit !important;
123 | color: var(--font-color) !important;
124 |
125 | .active {
126 | color: $theme-color !important;
127 | }
128 | }
129 |
130 | // dialog
131 | .el-dialog {
132 | background: var(--modal-bg-color) !important;
133 | @include box-shadow;
134 |
135 | .el-dialog__body {
136 | color: var(--font-color) !important;
137 | }
138 |
139 | // 右上角图标
140 | .el-dialog__headerbtn:focus .el-dialog__close,
141 | .el-dialog__headerbtn:hover .el-dialog__close {
142 | color: $theme-color;
143 | }
144 | }
145 |
146 | // button
147 | .el-button--primary {
148 | background: $theme-color !important;
149 | border-color: $theme-color !important;
150 | }
151 |
152 | // loading
153 | .el-loading-spinner {
154 | circle {
155 | stroke: $theme-color !important;
156 | }
157 | .el-loading-text {
158 | color: $theme-color !important;
159 | }
160 | .el-icon-loading {
161 | color: $theme-color !important;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/comments.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
143 |
144 |
159 |
--------------------------------------------------------------------------------
/src/page/playlist-detail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
24 |
25 |
26 | 未能找到和
27 | “{{ searchValue }}”
28 | 相关的任何音乐
29 |
30 |
33 |
34 |
35 |
36 |
137 |
138 |
--------------------------------------------------------------------------------
/src/page/mv/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
MV详情
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
![]()
14 |
15 |
{{ artist.name }}
16 |
17 |
18 |
{{ mvDetail.name }}
19 |
20 | 发布:{{
22 | $utils.formatDate(mvDetail.publishTime, 'yyyy-MM-dd')
23 | }}
25 | 播放: {{ mvDetail.playCount }}次
26 |
27 |
28 |
31 |
32 |
33 |
34 |
相关推荐
35 |
36 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
131 |
132 |
211 |
--------------------------------------------------------------------------------
/src/base/tabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 | {{tab.title}}
18 |
19 |
20 |
21 | -
29 | {{ tab.title }}
30 |
31 |
32 |
33 |
34 |
35 |
138 |
139 |
--------------------------------------------------------------------------------
/src/components/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
17 |
21 |
25 |
30 |
31 |
35 | {{normalizedSuggest.title}}
36 |
37 |
50 |
51 |
52 |
55 |
56 |
热门搜索
57 |
58 |
64 | {{hot.first}}
65 |
66 |
67 |
68 |
69 |
70 |
搜索历史
71 |
75 |
81 | {{history.first}}
82 |
83 |
84 |
暂无搜索历史
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
219 |
220 |
--------------------------------------------------------------------------------
/src/components/song-table.vue:
--------------------------------------------------------------------------------
1 |
236 |
237 |
--------------------------------------------------------------------------------
/STUDY.md:
--------------------------------------------------------------------------------
1 | ### Bug Fixes
2 |
3 | - 完美解决 Cannot download "https://github.com/sass/node-sass/releases/download/binding.nod的问题
4 | 【新版解决方案】:一句命令解决 npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
5 |
6 | - vue 为编写插件提供了一个 install(Vue, Option) 方法,该方法为 vue 添加全局功能;
7 | 该方法有两个参数,第一个是 Vue 构造器,第二个是可选的参数
8 |
9 | - require.context 是 Webpack 中用来管理依赖的一个函数,此方法会生成一个上下文模块,包含目录下所有模块的引用,通过正则表达式匹配,然后 require 进来
10 | require.context('.', false, /\.vue\$/)
11 | 参数一:要查询的目录,上述代码指的是当前目录
12 | 参数二:是否要查询子孙目录,方法默认的值为 false
13 | 参数三:要匹配的文件的后缀,是一个正则表达式,上述我要查询的是.vue 文件)
14 | 正则表达匹配中是有区分大小写的,i 就是说明不区分大小写
15 |
16 | - 在 flex 容器中,当空间不够的时候,flex-shrink 不为 0 的元素会被压缩,所以解决的方法就是给图片设置:flex-shrink:0。
17 |
18 | - 为了防止组件多个实例之间共享属性,所以需要通过工厂函数来获取值
19 | 数组
20 | data: {
21 | default: () => []
22 | }
23 |
24 | 对象
25 | data: {
26 | default: () => ({})
27 | }
28 |
29 | - vm.$slots
30 | 用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。
31 |
32 | - substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符。
33 |
34 | - number | 0 可以向下取整(先将数值转换成 32 位二进制整数值(如果有小数则忽略)
35 |
36 | - vue-lazyLoad
37 | preLoad (预加载高度比例)
38 | error (图片路径错误时加载图片)
39 | loading (预加载图片)
40 | attempt (尝试加载图片数量)
41 |
42 | - watch
43 | watch: {
44 | a: function (val, oldVal) {
45 | console.log('new: %s, old: %s', val, oldVal)
46 | },
47 | // 方法名
48 | b: 'someMethod',
49 | // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
50 | c: {
51 | handler: function (val, oldVal) { /_ ... _/ },
52 | deep: true
53 | },
54 | // 该回调将会在侦听开始之后被立即调用
55 | d: {
56 | handler: 'someMethod',
57 | immediate: true
58 | },
59 | // 你可以传入回调数组,它们会被逐一调用
60 | e: [
61 | 'handle1',
62 | function handle2 (val, oldVal) { /* ... */ },
63 | {
64 | handler: function handle3 (val, oldVal) { /* ... */ },
65 | /* ... */
66 | }
67 | ],
68 | // watch vm.e.f's value: {g: 5}
69 | 'e.f': function (val, oldVal) { /_ ... _/ }
70 | }
71 |
72 | - 让当前的元素滚动到浏览器窗口的可视区域内。定义动画过渡效果, 定义垂直方向,定义水平方向
73 | element.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
74 |
75 | - model
76 | 类型:{ prop?: string, event?: string }
77 | 允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。
78 | // 定义 model 属性
79 | model: {
80 | // prop 即 父组件使用 v-model 绑定的属性,这个名称是自定义的
81 | prop: "value",
82 | // event 即 子组件会向父组件触发的事件,父组件的 v-model 可以监听到这个事件,并将 vlaue 赋值给 v-model 绑定的属性
83 | event: "input"
84 | },
85 | props: {
86 | // 这里的 prop 定义必须与 model 中定义的 prop 同名
87 | value: String
88 | },
89 | - align-self 属性定义 flex 子项单独在侧轴(纵轴)方向上的对齐方式。
90 | - :not(selector) 选择器匹配非指定元素/选择器的每个元素。
91 | - filter(滤镜) 属性
92 | blur(px)给图像设置高斯模糊。"radius"一值设定高斯函数的标准差,或者是屏幕上以多少像素融在一起, 所以值越大越模糊
93 | - v-slot:header 可以被重写为 #header
94 |
95 | * v-loading 优雅的使用 loading
96 | element-loading-text="拼命加载中" 设置 loading 文字
97 |
98 | element-loading-background="rgba(0, 0, 0, 0.8)" 设置 loading 背景
99 |
100 | element-loading-spinner="el-icon-loading" 设置 loading 图标
101 |
102 | - inject 和 provide
103 | provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
104 |
105 | - 换肤
106 | document.documentElement.style.setProperty(key, value) 来设置变量
107 |
108 | - :show.sync
109 | props 的绑定默认是单向的,我们要在组件内部更新 show 值,需要在父组件上添加 .sync 修饰符,以创建『双向绑定』:
110 |
111 | 上面的代码会被 Vue 扩展为:
112 | msgShow = val" />
113 | 因为父组件有 update:show 事件监听,所以我们能在组件内部使用 $emit 来关闭消息提示: this.$emit('update:show', false)
114 |
115 | - undefined 若函数没有返回值则默认返回一个 undefined,布尔值为 fales
116 |
117 | - Element.requestFullscreen() 方法用于发出异步请求使元素进入全屏模式。
118 | toFullScreen:全屏
119 | function toFullScreen(){
120 | let elem = document.body;
121 | elem.webkitRequestFullScreen
122 | ? elem.webkitRequestFullScreen()
123 | : elem.mozRequestFullScreen
124 | ? elem.mozRequestFullScreen()
125 | : elem.msRequestFullscreen
126 | ? elem.msRequestFullscreen()
127 | : elem.requestFullScreen
128 | ? elem.requestFullScreen()
129 | : alert("浏览器不支持全屏");
130 | }
131 |
132 | exitFullscreen:退出全屏
133 | function exitFullscreen(){
134 | let elem = parent.document;
135 | elem.webkitCancelFullScreen
136 | ? elem.webkitCancelFullScreen()
137 | : elem.mozCancelFullScreen
138 | ? elem.mozCancelFullScreen()
139 | : elem.cancelFullScreen
140 | ? elem.cancelFullScreen()
141 | : elem.msExitFullscreen
142 | ? elem.msExitFullscreen()
143 | : elem.exitFullscreen
144 | ? elem.exitFullscreen()
145 | : alert("切换失败,可尝试 Esc 退出");
146 | }
147 | 是否全屏
148 | export function isFullscreen() {
149 | return document.fullScreen ||
150 | document.mozFullScreen ||
151 | document.webkitIsFullScreen
152 | }
153 |
154 | - 1turn 一圈
155 | - animation-play-state: paused; 暂停动画
156 | - border-color 这个属性的初始值(Initial value)就是 currentcolor
157 |
158 | - decodeURIComponent() 函数可对 encodeURIComponent() 函数编码的 URI 进行解码。
159 | http%3A%2F%2Fwww.w3school.com.cn%2FMy%20first%2F
160 | http://www.w3school.com.cn/My first/
161 |
162 | * require("workbox-webpack-plugin")
163 | module.exports = {
164 | // Other webpack config...
165 | plugins: [
166 | // Other plugins...
167 | new GenerateSW()
168 | ]
169 | };
170 | 这将为您的所有Webpack资产生成具有预缓存设置的服务工作者。
171 |
172 | * 浏览器兼容方面
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | * 自动将http的不安全请求升级为https
181 |
182 |
183 | *
184 | width:控制 viewport 的大小,可以指定的一个值,如果 600,或者特殊的值,如 device-width 为设备的宽度(单位为缩放为 100% 时的 CSS 的像素)。
185 | height:和 width 相对应,指定高度。
186 | initial-scale:初始缩放比例,也即是当页面第一次 load 的时候缩放比例。
187 | maximum-scale:允许用户缩放到的最大比例。
188 | minimum-scale:允许用户缩放到的最小比例。
189 | user-scalable:用户是否可以手动缩放
190 |
191 | * JPG 有损压缩、体积小、加载快、不支持透明
192 | 用于轮播图
193 |
194 | * PNG-8 与 PNG-24 无损压缩、质量高、体积大、支持透明
195 | 用于 Logo
196 |
197 | * SVG 文本文件、体积小、不失真、兼容性好(渲染成本高)
198 | 阿里矢量图标库
199 |
200 | * Base64 文本文件、依赖编码、小图标解决方案
201 | 图片的实际尺寸很小(大家可以观察一下掘金页面的 Base64 图,几乎没有超过 2kb 的)
202 | 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)
203 | 图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)
204 |
205 | * 内核分为渲染引擎和js引擎,但随着js引擎越来越独立,内核成了渲染引擎的代称
206 |
207 | 渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。
208 | Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)
209 | HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。
210 |
211 | CSS 解释器:解析 CSS 文档, 生成样式规则。
212 |
213 | 图层布局计算模块:布局计算每个对象的精确位置和大小。
214 |
215 | 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。
216 |
217 | JavaScript 引擎:编译执行 Javascript 代码。
218 |
219 | CSS 选择符是从右到左进行匹配的
220 | 避免使用通配符,只对需要用到的元素进行选择。
221 | 关注可以通过继承实现的属性,避免重复匹配重复定义。
222 | 少用标签选择器。如果可以,用类选择器替代
223 | 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。
224 |
225 | JS的三种加载方式
226 | 正常模式:
227 |
228 | 这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。
229 |
230 | async 模式:
231 |
232 | async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
233 |
234 | defer 模式:
235 |
236 | defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
237 |
238 |
239 | * 如何规避回流与重绘
240 | 将“导火索”缓存起来,避免频繁改动
241 | 避免逐条改变样式,使用类名去合并样式
242 | 将 DOM “离线” display:none => *操作* => display: block
243 |
244 | 当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!
245 | “像这样”的属性,到底是像什么样?——这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。
246 |
247 | * 动态添加多个样式
248 | :class="[songCls(song.id), { border: index === playlist.length-1}]"
249 |
250 | * window.open有什么弊端
251 | window.open打开的网页可以通过window.opener属性获取到来源网站的window对象,或者通过document.referrer获取到来源网站的地址。所以在使用window.open时,可以把第三个参数设置为noopener=yes,noreferrer=yes。
252 |
253 | * vm.$props
254 | v-bind="$props"
255 | 通过 $props 将父组件的 props 一起传给子组件
256 | * vm.$attrs
257 | v-bind="$attrs"
258 | 实现父级传来的属性透传给内部组件(不包括prop,class和style)
259 | * vm.$listeners
260 | v-on="$listeners"
261 | 实现父级传来的方法透传给内部组件
--------------------------------------------------------------------------------
/src/components/mini-player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
![]()
9 |
10 |
11 |
12 |
13 |
14 |
15 |
{{ currentSong.name }}
16 |
-
17 |
{{ currentSong.artistsText }}
18 |
19 |
20 |
21 | {{ $utils.formatTime(currentTime) }}
22 |
23 | /
24 |
25 | {{ $utils.formatTime(currentSong.duration / 1000) }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
40 | 请点击开始播放
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {{ playModeText }}
54 |
61 |
62 |
63 |
69 | 已更新歌单
70 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
93 |
101 |
102 |
103 |
104 |
284 |
285 |
429 |
--------------------------------------------------------------------------------
/src/components/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |

11 |

16 |
17 |
22 |
23 |
![]()
24 |
25 |
26 |
27 |
28 |
29 |
30 |
{{ currentSong.name }}
31 |
MV
34 |
35 |
36 |
37 |
歌手:
38 |
{{ currentSong.artistsText }}
39 |
40 |
41 |
42 |
还没有歌词哦~
43 |
50 |
51 |
58 |
63 | {{ content }}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
包含这首歌的歌单
83 |
88 |
93 |
94 |
95 |
96 |
97 | {{ $utils.formatNumber(simiPlaylist.playCount) }}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
相似歌曲
107 |
112 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
363 |
364 |
600 |
--------------------------------------------------------------------------------
精彩评论
9 |18 | 最新评论 19 | ({{ total }}) 20 |
21 |