├── README.md
└── vue3NeteaseCloud
├── .babelrc
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── api
│ ├── album.js
│ ├── home.js
│ ├── http.js
│ ├── mv.js
│ ├── playList.js
│ ├── search.js
│ ├── singer.js
│ └── sort.js
├── assets
│ ├── common.scss
│ ├── cover.png
│ ├── head.jpg
│ ├── liverpool.jpg
│ ├── logo.png
│ ├── mui
│ │ ├── css
│ │ │ ├── mui.css
│ │ │ └── mui.min.css
│ │ ├── fonts
│ │ │ └── mui.ttf
│ │ └── js
│ │ │ ├── mui.js
│ │ │ └── mui.min.js
│ └── style.scss
├── components
│ ├── banners
│ │ ├── banners.vue
│ │ └── getImgs.js
│ ├── common.vue
│ ├── footer
│ │ ├── footer.scss
│ │ └── footer.vue
│ ├── header
│ │ ├── headNav.vue
│ │ ├── header.scss
│ │ └── header.vue
│ ├── home
│ │ ├── modMv.vue
│ │ ├── modSection.vue
│ │ └── modTitle.vue
│ ├── mv
│ │ ├── MvBlock.vue
│ │ ├── MvTitle.vue
│ │ └── OneMv.vue
│ ├── playBar
│ │ ├── playBar.scss
│ │ └── playBar.vue
│ ├── searchBar
│ │ └── searchBar.vue
│ ├── songItem.vue
│ ├── sort
│ │ ├── sortPannel.vue
│ │ └── trackPannel.vue
│ ├── switchPannel
│ │ └── switchPannel.vue
│ └── video
│ │ └── VideoComponent.vue
├── main.js
├── router
│ └── index.js
├── store
│ └── index.js
├── tools
│ ├── common.js
│ ├── rem.js
│ └── sessionStore.js
└── views
│ ├── Home.vue
│ ├── MvIndex.vue
│ ├── album.vue
│ ├── highqualitySquare.vue
│ ├── playList.vue
│ ├── playSquare.vue
│ ├── search.vue
│ ├── singer.vue
│ ├── singerDetails.vue
│ ├── sort.vue
│ └── sortDetails.vue
├── vue.config.js
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # 网易云移动端vue3.0
2 |
3 | 预览地址:[仿网易移动App](http://zhoup.top:7002/#/)
4 |
5 | ## 前言
6 |
7 | 2020年9月18日,是一个特殊的日子,这一天是*九一八事变*,对于前端界来讲,也有一条震惊全球的消息,我们的尤大佬宣布,vue3.0正式发布!
8 |
9 | 对于我们做前端的人来讲,面临的又是一个困难的抉择,是继续玩着2.x?还是立马进入3.0的实验?我个人选择了后者,因为我是一个喜欢新鲜事物的人😂😁😀,所以决定采用vue3.0去重构之前写了无数次的网易移动端app。
10 |
11 | ## 正文
12 |
13 | 之所以选择重构网易,因为我们有着现成的后台系统,且功能强大到几乎与官方相等,前端方面,vant几乎同时发布了3.0的版本,这也是重要的一套UI框架,省去了再手写UI轮子的步骤,vue router4.0以及vuex4.0也相继发布,所以我可以放心的使用这些生态。
14 |
15 | 先看下预览效果,本人是学友哥十年粉丝,所以示例图用大都为学友的作品😂🤣😅
16 | 
17 | 
18 | 
19 | 
20 | 
21 | 
22 |
23 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "import",
5 | {
6 | "libraryName": "vant",
7 | "libraryDirectory": "es",
8 | "style": true
9 | }
10 | ]
11 | ]
12 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/README.md:
--------------------------------------------------------------------------------
1 | # app3.0beta
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | yarn lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | ['import', {
4 | libraryName: 'vant',
5 | libraryDirectory: 'es',
6 | style: true
7 | }, 'vant']
8 | ]
9 | };
10 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app3.0beta",
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 | },
10 | "dependencies": {
11 | "axios": "^0.20.0",
12 | "better-scroll": "^2.0.6",
13 | "core-js": "^3.6.5",
14 | "fastclick": "^1.0.6",
15 | "vant": "^3.0.0-beta.0",
16 | "vconsole": "^3.3.4",
17 | "vue": "3.0.0-rc.1",
18 | "vue-router": "^4.0.0-alpha.6",
19 | "vue3-lazy": "^1.0.0-alpha.1",
20 | "vuex": "^4.0.0-alpha.1"
21 | },
22 | "devDependencies": {
23 | "@vue/cli-plugin-babel": "~4.5.0",
24 | "@vue/cli-plugin-eslint": "~4.5.0",
25 | "@vue/cli-plugin-router": "~4.5.0",
26 | "@vue/cli-plugin-vuex": "~4.5.0",
27 | "@vue/cli-service": "~4.5.0",
28 | "@vue/compiler-sfc": "^3.0.0-beta.1",
29 | "babel-eslint": "^10.1.0",
30 | "babel-plugin-import": "^1.13.0",
31 | "eslint": "^6.7.2",
32 | "eslint-plugin-vue": "^7.0.0-alpha.0",
33 | "node-sass": "^4.12.0",
34 | "postcss-pxtorem": "^5.1.1",
35 | "sass-loader": "^8.0.2",
36 | "vue-cli-plugin-vue-next": "~0.1.4"
37 | },
38 | "eslintConfig": {
39 | "root": true,
40 | "env": {
41 | "node": true
42 | },
43 | "extends": [
44 | "plugin:vue/vue3-essential",
45 | "eslint:recommended"
46 | ],
47 | "parserOptions": {
48 | "parser": "babel-eslint"
49 | },
50 | "rules": {}
51 | },
52 | "browserslist": [
53 | "> 1%",
54 | "last 2 versions",
55 | "not ie <= 8",
56 | "ios >= 8",
57 | "android >= 4.0"
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'autoprefixer': {
4 | browsers: [
5 | "> 1%",
6 | "last 5 versions",
7 | "not ie <= 8",
8 | "ios >= 7",
9 | "android >= 4.0"
10 | ]
11 | },
12 | 'postcss-pxtorem': {
13 | rootValue: 20,//结果为:设计稿元素尺寸/16,比如元素宽320px,最终页面会换算成 20rem
14 | propList: ['*']
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/public/favicon.ico
--------------------------------------------------------------------------------
/vue3NeteaseCloud/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
45 |
46 |
49 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/album.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const getAlbum = async (id) => { //专辑
4 | let res = await httpGet(`/album?id=${id}`).catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/home.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const getRecommend = async (num=6) => { //获取推荐歌单
4 | let res = await httpGet(`/personalized?limit=${num}`).catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data.result;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
13 |
14 | export const getTopPlay= async (num=6) => { //获取精选歌单
15 | let res = await httpGet(`/top/playlist?limit=${num}`).catch(err => {
16 | return Promise.reject(err);
17 | })
18 | if(res.status === 200){
19 | return res.data.playlists;
20 | }else{
21 | return Promise.reject(res.status);
22 | }
23 | }
24 |
25 | export const getNewMv = async (num=14) => { //获取最新MV
26 | let res = await httpGet(`/mv/first?limit=${num}`).catch(err => {
27 | return Promise.reject(err);
28 | })
29 | if(res.status === 200){
30 | return res.data.data;
31 | }else{
32 | return Promise.reject(res.status);
33 | }
34 | }
35 |
36 | export const login = async (phone='', password='') => { //登录
37 | let res = await httpGet(`/login/cellphone?phone=${phone}&password=${password}`);
38 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/http.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | const timeout = 15000;
3 |
4 | export const httpGet = (url) => {
5 | return axios({
6 | method: 'get',
7 | timeout: timeout,
8 | url: url
9 | }).then(res => {
10 | return Promise.resolve(res);
11 | }).catch(err => {
12 | return Promise.reject(err);
13 | })
14 | }
15 | export const httpPost = (options) => {
16 | return axios({
17 | method: 'post',
18 | timeout: timeout,
19 | url: options.url,
20 | data: options.data,
21 | headers: { 'content-type': 'application/json' }
22 | }).then(res => {
23 | return Promise.resolve(res);
24 | }).catch(err => {
25 | return Promise.reject(err);
26 | })
27 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/mv.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 |
4 | export default {
5 | async getAllMv (area='全部', order='上升最快', limit=4, offset=0) { //获取全部MV
6 | let res = await httpGet(`/mv/all?area=${area}&order=${order}&limit=${limit}&offsset=${offset}}`).catch(err => {
7 | return Promise.reject(err);
8 | })
9 | if(res.status === 200){
10 | return res.data.data;
11 | }else{
12 | return Promise.reject(res.status);
13 | }
14 | },
15 | async getRecommendMv() { //推荐mv
16 | let res = await httpGet(`/personalized/mv`).catch(err => {
17 | return Promise.reject(err);
18 | })
19 | if(res.status === 200){
20 | return res.data.result;
21 | }else{
22 | return Promise.reject(res.status);
23 | }
24 | },
25 | async getNewMv(area="", limit=30) { //最新mv
26 | let res = await httpGet(`/mv/first?area=${area}&limit=${limit}`).catch(err => {
27 | return Promise.reject(err);
28 | })
29 | if(res.status === 200){
30 | return res.data.data;
31 | }else{
32 | return Promise.reject(res.status);
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/playList.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const playListRequest = {
4 | async getTags() { //获取标签
5 | let res = await httpGet(`/playlist/highquality/tags`).catch(err => {
6 | return Promise.reject(err);
7 | })
8 | if(res.status === 200 && res.data.code === 200) {
9 | return Promise.resolve(res.data.tags);
10 | }else {
11 | return Promise.reject(res.data);
12 | }
13 | },
14 | async getListByTag(tag, limit = 20, before = '') { //根据标签获取歌单列表
15 | let res = await httpGet(`/top/playlist/highquality?tag=${tag}&limit=${limit}&before=${before}`).catch(err => {
16 | return Promise.reject(err);
17 | })
18 | if(res.status === 200 && res.data.code === 200) {
19 | return Promise.resolve(res.data);
20 | }else {
21 | return Promise.reject(res.data);
22 | }
23 | },
24 | async getCatlist() { //所有精选标签列表
25 | let res = await httpGet(`/playlist/catlist`).catch(err => {
26 | return Promise.reject(err);
27 | })
28 | if(res.status === 200 && res.data.code === 200) {
29 | return Promise.resolve(res.data);
30 | }else {
31 | return Promise.reject(res.data);
32 | }
33 | },
34 | async getQualityListByCat(cat = '', limit = 20, offset = 0) { //通过分类获取歌单
35 | let res = await httpGet(`/top/playlist?limit=${limit}&offset=${offset}&cat=${cat}`).catch(err => {
36 | return Promise.reject(err);
37 | })
38 | if(res.status === 200 && res.data.code === 200) {
39 | return Promise.resolve(res.data.playlists);
40 | }else {
41 | return Promise.reject(res.data);
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/search.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const getHotSearch = async () => { //搜索热榜
4 | let res = await httpGet(`/search/hot/detail`).catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data.data;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
13 |
14 | export const getSuggestSearch = async (kw) => { //建议搜索
15 | let res = await httpGet(`/search/suggest?keywords= ${kw}&type=mobile`).catch(err => {
16 | return Promise.reject(err);
17 | })
18 | if(res.status === 200){
19 | return res.data.result;
20 | }else{
21 | return Promise.reject(res.status);
22 | }
23 | }
24 |
25 | export const getSearchByKw = async (kw, limit=20, offset) => { //通过关键字搜索
26 | let res = await httpGet(`/cloudsearch?keywords= ${kw}&limit=${limit}&offset=${offset}`).catch(err => {
27 | return Promise.reject(err);
28 | })
29 | if(res.status === 200){
30 | return res.data.result;
31 | }else{
32 | return Promise.reject(res.status);
33 | }
34 | }
35 |
36 | export const getSongUrl = async (id) => { //获取music url
37 | let res = await httpGet(`/song/url?id=${id}`).catch(err => {
38 | return Promise.reject(err);
39 | })
40 | if(res.status === 200){
41 | return res.data.data[0];
42 | }else{
43 | return Promise.reject(res.status);
44 | }
45 | }
46 |
47 | export const getLrc = async (id) => { //获取music lrc
48 | let res = await httpGet(`/lyric?id=${id}`).catch(err => {
49 | return Promise.reject(err);
50 | })
51 | if(res.status === 200){
52 | return res.data.lrc;
53 | }else{
54 | return Promise.reject(res.status);
55 | }
56 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/singer.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const getSingerList = async (type=-1, area=-1, limit=20, offset) => { //歌手榜
4 | let res = await httpGet(`/artist/list?type=${type}&area=${area}&limit=${limit}&offset=${offset}`).catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data.artists;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
13 |
14 | export const getSingerAlbum = async (id, limit=20, offset=0) => { //歌手专辑
15 | let res = await httpGet(`/artist/album?id=${id}&limit=${limit}&offset=${offset}`).catch(err => {
16 | return Promise.reject(err);
17 | })
18 | if(res.status === 200){
19 | return res.data;
20 | }else{
21 | return Promise.reject(res.status);
22 | }
23 | }
24 |
25 |
26 | export const getSingerSong = async (id, limit=20, offset=0) => { //歌手全部歌曲
27 | let res = await httpGet(`/artist/songs?id=${id}&limit=${limit}&offset=${offset}`).catch(err => {
28 | return Promise.reject(err);
29 | })
30 | if(res.status === 200){
31 | return res.data.songs;
32 | }else{
33 | return Promise.reject(res.status);
34 | }
35 | }
36 |
37 |
38 | export const getSingerMV = async (id, limit=20, offset=0) => { //歌手全部歌曲
39 | let res = await httpGet(`/artist/mv?id=${id}&limit=${limit}&offset=${offset}`).catch(err => {
40 | return Promise.reject(err);
41 | })
42 | if(res.status === 200){
43 | return res.data.mvs;
44 | }else{
45 | return Promise.reject(res.status);
46 | }
47 | }
48 |
49 | export const getMVUrl = async (id, r=1080) => { //mv地址
50 | let res = await httpGet(`/mv/url?id=${id}&r=${r}`).catch(err => {
51 | return Promise.reject(err);
52 | })
53 | if(res.status === 200){
54 | return res.data.data.url;
55 | }else{
56 | return Promise.reject(res.status);
57 | }
58 | }
59 |
60 | export const getMVDetails = async (id) => { //mv详情
61 | let res = await httpGet(`/mv/detail?mvid=${id}`).catch(err => {
62 | return Promise.reject(err);
63 | })
64 | if(res.status === 200){
65 | return res.data.data;
66 | }else{
67 | return Promise.reject(res.status);
68 | }
69 | }
70 |
71 | export const getSingerDesc = async (id) => { //歌手简介
72 | let res = await httpGet(`/artist/desc?id=${id}`).catch(err => {
73 | return Promise.reject(err);
74 | })
75 | if(res.status === 200){
76 | return res.data;
77 | }else{
78 | return Promise.reject(res.status);
79 | }
80 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/api/sort.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from './http';
2 |
3 | export const getTopList = async () => { //排行榜
4 | let res = await httpGet(`/toplist/detail`).catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data.list;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
13 |
14 | export const getTopDetails = async (id) => { //排行榜详情
15 | let res = await httpGet(`/playlist/detail?id=${id}`).catch(err => {
16 | return Promise.reject(err);
17 | })
18 | if(res.status === 200){
19 | return res.data;
20 | }else{
21 | return Promise.reject(res.status);
22 | }
23 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/common.scss:
--------------------------------------------------------------------------------
1 | .play-square{
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100vw;
6 | height: 100vh;
7 | overflow: auto;
8 | background: #fff;
9 | padding-bottom: 50px;
10 | }
11 | .item-wrap{
12 | display: flex;
13 | justify-content: flex-start;
14 | flex-wrap: wrap;
15 | padding: 12px;
16 | margin: 0 -6px;
17 | .items{
18 | width: 33.333%;
19 | position: relative;
20 | padding: 6px;
21 | .items-box{
22 | width: 100%;
23 | height: 0;
24 | padding-bottom: 100%;
25 | position: relative;
26 | >img{
27 | width: 100%;
28 | height: 100%;
29 | display: block;
30 | position: absolute;
31 | left: 9;
32 | top: 0;
33 | border-radius: 2px;
34 | }
35 | }
36 |
37 | .intr{
38 | display: -webkit-box;
39 | -webkit-box-orient: vertical;
40 | -webkit-line-clamp: 2;
41 | overflow: hidden;
42 | font-size: 12px;
43 | color: #333;
44 | margin-top: 5px;
45 | }
46 | }
47 | .play-num{
48 | position: absolute;
49 | top: 5px;
50 | right: 5px;
51 | color: #fff;
52 | font-size: 12px;
53 | }
54 | }
55 | .no-data{
56 | padding: 20%;
57 | text-align: center;
58 | }
59 | .text-center{
60 | text-align: center;
61 | font-size: 13px;
62 | color: #969799;
63 | }
64 | .load-bottom{
65 | width: 100%;
66 | padding: 10px;
67 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/src/assets/cover.png
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/head.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/src/assets/head.jpg
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/liverpool.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/src/assets/liverpool.jpg
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/src/assets/logo.png
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/mui/fonts/mui.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/vue3-project/37d2ba95dd211d08ce0ef4ab2d309160cc0d88c7/vue3NeteaseCloud/src/assets/mui/fonts/mui.ttf
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/assets/style.scss:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | list-style-type: none;
6 | }
7 | #app{
8 | background-color: #fafafa;
9 | min-height: 100vh;
10 | }
11 | .box-wrap{
12 | position: absolute;
13 | top: 4.7rem;
14 | bottom: 0px;
15 | padding: {
16 | bottom: 50px;
17 | };
18 | width: 100vw;
19 | left: 0;
20 | overflow: auto;
21 | }
22 | @mixin flex($x: center, $y: space-between) {
23 | display: flex;
24 | align-items: $x;
25 | justify-content: $y;
26 | }
27 | @mixin elp{
28 | overflow: hidden;
29 | text-overflow: ellipsis;
30 | white-space: nowrap;
31 | }
32 | @mixin elpnum($num:1){
33 | display: -webkit-box;
34 | -webkit-box-orient: vertical;
35 | -webkit-line-clamp: $num;
36 | white-space: normal;
37 | word-wrap: break-word;
38 | word-break: normal;
39 | overflow: hidden;
40 | text-overflow: ellipsis;
41 | }
42 |
43 | .slide-enter-active,.slide-leave-active{
44 | transition: all 0.3s;
45 | }
46 | .slide-enter-from, .slide-leave-to{
47 | transform: translateX(100%);
48 | }
49 |
50 |
51 | .sort-details{
52 | position: fixed;
53 | z-index: 10;
54 | top: 0;
55 | left: 0;
56 | width: 100%;
57 | height: 100%;
58 | background-color: #fff;
59 | overflow: auto;
60 | }
61 | .album-cover-box{
62 | overflow: hidden;
63 | position: relative;
64 | z-index: 1;
65 | }
66 | .album-cover{
67 | padding: 64px 16px 20px;
68 | background: rgba(0,0,0,.5);
69 | @include flex(center, flex-start);
70 | z-index: 5;
71 | position: relative;
72 | }
73 | .album-poster{
74 | width: 125px;
75 | height: 125px;
76 | object-fit: cover;
77 | background-size: cover;
78 | }
79 | .simple-info{
80 | margin-left: 10px;
81 | color: #fff;
82 | h4{
83 | font-size: 18px;
84 | font-weight: 400;
85 | line-height: 1.5;
86 | margin: 10px 0;
87 | }
88 | img{
89 | width: 24px;
90 | height: 24px;
91 | margin-right: 5px;
92 | border-radius: 50%;
93 | vertical-align: middle;
94 | }
95 | time{
96 | margin-top: 8px;
97 | font-size: 12px;
98 | }
99 | .singer-name{
100 | font-size: 14px;
101 | }
102 | }
103 | .album-box-bg{
104 | position: absolute;
105 | top: 0;
106 | left: 0;
107 | z-index: 1;
108 | width: 100%;
109 | height: 100%;
110 | object-fit: cover;
111 | filter: blur(36px);
112 | background-size: cover;
113 | transform: scale(1.1);
114 | }
115 |
116 | .navbar{
117 | height: 50px;
118 | background-color: #fff;
119 | letter-spacing: 3px;
120 | }
121 | .mint-navbar {
122 | background-color: #fff;
123 | text-align: center;
124 | display: flex;
125 | }
126 | .mint-tab-item {
127 | display: block;
128 | padding: 7px 0;
129 | flex: 1;
130 | text-decoration: none;
131 | padding: 17px 0;
132 | font-size: 15px;
133 | border-bottom: 3px solid #fff;
134 | &.is-selected {
135 | border-bottom: 3px solid #31c27c;
136 | color: #31c27c;
137 | // margin-bottom: -3px;
138 | }
139 | }
140 | .mint-tab-item-icon:empty {
141 | display: none;
142 | }
143 | .mint-tab-item-icon {
144 | width: 24px;
145 | height: 24px;
146 | margin: 0 auto 5px;
147 | }
148 |
149 | .mint-tab-item-label {
150 | font-size: 14px;
151 | color: inherit;
152 | line-height: 1;
153 | }
154 | .album-intr{
155 | padding: 10px 30px 50px;
156 | font-size: 14px;
157 | color: #777;
158 | text-indent: 2em;
159 | line-height: 2;
160 | }
161 | .play-all{
162 | @include flex(center, flex-start);
163 | padding: 10px;
164 | font-size: 15px;
165 | /deep/ .van-icon{
166 | font-size: 25px;
167 | margin-right: 5px;
168 | }
169 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/banners/banners.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
34 |
35 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/banners/getImgs.js:
--------------------------------------------------------------------------------
1 | import { httpGet } from '@/api/http.js';
2 |
3 | const getImgs = async () => {
4 | let res = await httpGet('/banner').catch(err => {
5 | return Promise.reject(err);
6 | })
7 | if(res.status === 200){
8 | return res.data.banners;
9 | }else{
10 | return Promise.reject(res.status);
11 | }
12 | }
13 |
14 | export default getImgs;
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/common.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
this is common
4 |
5 |
componentVal: {{value}}
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/footer/footer.scss:
--------------------------------------------------------------------------------
1 | @mixin flex-b{
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | }
6 | .copyright {
7 | margin-top: 1.5rem;
8 | padding: .8rem 0;
9 | text-align: center;
10 | font-size: 12px;
11 | color: rgba(26,26,26,1);
12 | }
13 | .copyright__media {
14 | width: 4rem;
15 | height: 1.1rem;
16 | margin: 0 auto .5rem;
17 | img{
18 | width: 100%;
19 | }
20 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/footer/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/header/headNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
42 |
43 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/header/header.scss:
--------------------------------------------------------------------------------
1 | @mixin flex-b{
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | }
6 | .top_operation_bar{
7 | // position: fixed;
8 | // top: 0;
9 | // left: 0;
10 | width: 100vw;
11 | height: 2.7rem;
12 | padding: 0 .7rem 0 .8rem;
13 | z-index: 100;
14 | box-shadow: 0 3px 25px rgba(0,0,0,.05);
15 | background-color: #fafafa;
16 | @include flex-b;
17 | }
18 | .Jacky_dialog{
19 | width: 320px;
20 | /deep/ {
21 | .van-dialog__confirm{
22 | color: #ff5e32;
23 | }
24 | }
25 | }
26 | .bar-l{
27 | @include flex-b;
28 | }
29 | .logo{
30 | width: 1.25rem;
31 | height: 1.25rem;
32 | margin-right: .3rem;
33 | }
34 | .title{
35 | font-size: .8rem;
36 | color: #000;
37 | font-weight: 400;
38 | overflow: hidden;
39 | white-space: nowrap;
40 | text-overflow: ellipsis;
41 | }
42 | .header-tips{
43 | font-size: .6rem;
44 | color: grey;
45 | }
46 | .bar-r{
47 | display: block;
48 | height: 1.25rem;
49 | line-height: 1.25rem;
50 | padding: 0 .85rem;
51 | text-align: center;
52 | font-size: .65rem;
53 | color: #575656;
54 | border: solid 1px #d8d8d8;
55 | border-radius: 999px;
56 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/header/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
Jacky Music
7 |
8 |
9 |
10 | 下载APP
11 |
12 |
13 |
14 |
38 |
39 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/home/modMv.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
![]()
7 |
8 | {{ numFormat(item.playCount) }}
9 |
10 |
11 | {{ item.name }}
12 |
13 |
14 |
15 |
16 |
17 |
37 |
38 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/home/modSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
![]()
7 |
8 | {{ numFormat(item.playCount) }}
9 |
10 |
11 | {{ item.name }}
12 |
13 |
14 |
15 |
16 |
17 |
61 |
62 |
63 |
66 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/home/modTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
35 |
36 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/mv/MvBlock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 | {{ numFormat(item.playCount) }}
7 |
8 |
9 |
10 |
{{ item.name }}
11 |
{{ item.artistName }}
12 |
13 |
14 |
15 |
16 |
51 |
52 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/mv/MvTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ info.main }}
4 | {{ info.other }}
5 |
6 |
7 |
8 |
40 |
41 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/mv/OneMv.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 | {{ numFormat(info.playCount) }}
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
47 |
48 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/playBar/playBar.scss:
--------------------------------------------------------------------------------
1 | .play-bar{
2 | position: fixed;
3 | bottom: 0;
4 | width: 100vw;
5 | height: 50px;
6 | left: 0;
7 | z-index: 99;
8 | background: #fff;
9 | border: {
10 | top: 1px solid #eee;
11 | }
12 | box-shadow: 0px -3px 7px rgba(0,0,0,.05);
13 | }
14 | .play-bottom{
15 | padding: 5px 10px;
16 | @include flex;
17 | }
18 |
19 | .play-poster{
20 | width: 40px;
21 | height: 40px;
22 | border-radius: 50%;
23 | margin-right: 5px;
24 | object-fit: cover;
25 | }
26 | .play-info{
27 | overflow: hidden;
28 | h4{
29 | font-size: 14px;
30 | font-weight: normal;
31 | color: #333;
32 | @include elp;
33 | }
34 | p{
35 | font-size: 12px;
36 | color: #aaa;
37 | @include elp;
38 | }
39 | }
40 | .pl-lf{
41 | @include flex(center, flex-start);
42 | flex: 1;
43 | overflow: hidden;
44 | }
45 | .play-pannel{
46 | position: fixed;
47 | left: 0;
48 | top: 0;
49 | height: 100vh;
50 | width: 100vw;
51 | z-index: 999;
52 | background: #fff;
53 | transform: translate3d(0, 102%, 0);
54 | transition: all .3s;
55 | }
56 | .show-pannel{
57 | transform: translate3d(0, -2%, 0);
58 | }
59 | .play-bg{
60 | position: absolute;
61 | height: 100vh;
62 | width: 100vw;
63 | top: 0;
64 | left: 0;
65 | background-color: #161824;
66 | background-position: 50%;
67 | background-repeat: no-repeat;
68 | background-size: auto 100%;
69 | transform: scale(1.5);
70 | transform-origin: center top;
71 | overflow: hidden;
72 | transition: opacity .3s linear;
73 | opacity: 1;
74 | filter: blur(2.5px);
75 | &::before{
76 | content: " ";
77 | position: absolute;
78 | left: 0;
79 | right: 0;
80 | bottom: 0;
81 | top: 0;
82 | background-color: rgba(0,0,0,.5);
83 | }
84 | }
85 |
86 | .play-wrap{
87 | padding-top: 70px;
88 | }
89 |
90 | .m-song-disc{
91 | width: 78vw;
92 | height: 78vw;
93 | margin: 0 auto;
94 | position: relative;
95 | border-radius: 50%;
96 | overflow: hidden;
97 | }
98 | .m-song-turn{
99 | border-radius: 50%;
100 | position: absolute;
101 | left: 0;
102 | right: 0;
103 | top: 0;
104 | bottom: 0;
105 | z-index: 2;
106 | background: url(https://s3.music.126.net/mobile-new/img/disc.png?d3bdd1080a72129346aa0b4b4964b75f=) no-repeat;
107 | background-size: contain;
108 | }
109 | .m-song-img {
110 | width: 184px;
111 | height: 184px;
112 | position: absolute;
113 | left: 50%;
114 | top: 50%;
115 | margin: -92px 0 0 -92px;
116 | >img{
117 | width: 100%;
118 | height: 100%;
119 | border-radius: 50%;
120 | transition: all .1s;
121 | object-fit: cover;
122 | }
123 | }
124 |
125 | .m-song-plybtn {
126 | position: absolute;
127 | width: 50px;
128 | height: 50px;
129 | left: 50%;
130 | top: 50%;
131 | -webkit-transform: translate(-50%,-50%);
132 | -ms-transform: translate(-50%,-50%);
133 | transform: translate(-50%,-50%);
134 | z-index: 10;
135 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAACoCAMAAABDlVWGAAABJlBMVEUAAAAAAAD////l5eX///9iYmKDg4Pn5+f///9YWFj////09PT////4+Pjt7e3///////9oaGhBQUH////////////////CwsIaGhr///8xMTEkJCT////7+/vp6en///////////////+srKyoqKienp58fHz////y8vKTk5P///8EBAT////////////////V1dW3t7f////////////////////v7++jo6N9fX3///////+UlJT////s7Oz////Nzc3///////+RkZGPj495eXkTExP////////29vb////k5OTPz882Njb////////////c3Nz///////9ycnJsbGz///9dXV3////////Q0ND///9QUFD///////////////////9FeiN6AAAAYXRSTlNmAP3c+oWT3ueB9vA19ealRId5EQbuurpu83RxD/nh05dfAquooo+M7JtzaSolE+vMspJ3Wj7w6KSQj6ucKeXNxLWnmpmObVYd8t3axXZRSt7TvbKLideCeSzHnn4V3Nh6YarbPAAABlRJREFUeNrU14lWEmEYh/GXcdj3HQTZRCkS913UNE2zbLd9Oc/930Q2LmVpwPAC03MD/M5835n/IC6dkqHnzcVoZvdkLp2HfHruZDcTXWw+DyVdOilAt6uNjI8782Ua1W2XjTShtdDSmzRdlH6zFKq5bKQBTS5H57gqXJrP1tsb7kShkBPJFQoJ90a7np0vhblqLrrc8R7oQ4vVPZOLIp8mp8flzsanJ59FuMjcqxZdHVKFxvYDWK1lPQXpooInu4ZVYD/m6pAWNLn0DSvvekJ6KLHuxerb0qyrQwrQViV/oZzKSc/lpi6sgUrLdSN16MyWARBcOBWbnS4EAYytj67r1KGtqMUseaSvPCWLGr14u+pDZysmwFhc+i4+BmBWZgcATTXTFvO9qOS3qOlmShu68g7A6xa13F6A7yuq0ORnA9jZENU2dgDjc1IPGvMB4QNR7+AJ4IspQVObBjDmF+Wur6qxmdKAbpeBoEcGlCcIlLf7hx7lOzxOlYeaP+oTmmoYQFsGWhswGql+oMUMEInLgItHgEzRPvS43OHYVY+/fGwXGvIBkzKU6oAvZA8aywOPZEg9AvIxO9AXAZg4lKF1OAGBF71DX5jw5EyG2NkTMJ/3Co2ZcM8tQ819D8xYb9CVgA2nijSw0gs0tArBzk59aRBWQ91Dj30QjssIiofBd9wttFiGiTMZSWcTUC52B629Bg5lRB0Cr2tdQRvAfRlZ94FGN9AjA+oywupgHHWGzuRhTEbaGORnOkFTZYj4ZaT5I1BOdYBuAnEZcXFg89/QmAFtGXltMGL/ghZ9I7+g19fUV/wHtAJBvzggfxAqd0NXDPCII/KAsXIXNPXOIQd/efjvUndAmxB2xMH/zB+G5u3Qr2k4EMd0AOmvt0IrsCMOagcqt0FbJkyLg5oGs3ULNApecVReiP4NnTHALY7KDcbHv6BbDno1XTUGW39CWwa8F4flB6P1B3TRgQ/UeqSLN6GzAQd83f1dHAKzN6AfoCQOrARLN6Andr9GpiITT9/KwPLAye/QGATFTusAxsOCDKogxH6D7sOC2GkNq+B9GVALsP8LWgzAqdjJ4LJXCRlIpxAoXkOrtteT6yYmH8gg8kL1GroHU/1CoTSQBZ6CvSto0oRc/1DM7GNRLwdm8hK6DF5RgMI9j6jnheVLaBTWdaAwPy7KrUP0Alqbg4QWlPCU6JaAuZoFDcGaqEHh6RdRLQIhC/oBsppQzAXVN1UWlixoBjyqUIhozr8H3vyE1lYhpwnVnv8CpH9CZyAi2lAIPtK8pDPn0Cp8UoWqz/8zqJ5DGzCpCdWf/0lonEMzMK0J1Z//acicQ30wrgnVn/9x8LkkCWFRherPfxiSEgKvLlR//r0QkmWY14Xqz/88LEsTsqpQ/fm3RlQWoa4L1Z//OixKFNq6UP35vw9RycDGMKAYD3P9vEhlF9y6UP35d8OuvISELlR//hPwUqxh0oTqz781TbIKueFB4em4rb/Mq5IfMpRn0nOPIS+ADBUatvcz/w/0B/X2VgMACMNQ9AMnWOADKwT/QuahyZYcDcte7e146X9W+vFmWmEzOeOJGfjMCmWOEubMYw7no7wizHPHvMuMAMFIOo5IxsiOjJDLSOOM2cDYN44hxliMjGnL2OAOWMCgGgz84uBEDKDFIG8ORLhzLPO2Y5kK6PpMdJiBsR28nQkMOBEMJtTixISY4JUTZWPCgVXevesgCINhGG7dZPLEIg7GRRsMTsaoMTEOnmLUuBgn+e//JuTHGt0aKT3y3QIFpj6vO9ctnbnAmr9PIysefn0E0PfhkjW92HFtnQHULn5AAG9aISZGF3NaQYxVrInRrcVYBW4H5vkPEPIfuIV5UGXhClHT8Qv9oXQ/MckoTfb+wVTGqK8ZUl9+4mmUnpGj03pOmzlH5y/w5w6ZiAil+G9qA0KpkfWMTxKsJ6530wel3try9CwjSscg27BTDcw321gHjzyuEjj9IbwP1hPeDqHo2abP0pn5XMR/NhTB/fyo2gv3q0gh1JSkEHCrUuMSG1VxCdxDOtfR1ZDr4AGUwIUACi5JJZMyx1RHUgY3kIn0LBuFQk2EFlv7J3t0+id7JPi86wlJsfm9FUVbQrZR1LrPmQ0hKZfSXC7Fzr5LeD7uGoYBQBCGV56PS2g5ewGy+NkUNbjr9gAAAABJRU5ErkJggg==) 0 0 no-repeat;
136 | background-size: contain;
137 | }
138 | .song-lrc{
139 | padding: 0 35px;
140 | margin-top: 25px;
141 | position: relative;
142 | z-index: 20;
143 | }
144 | .m-song-h2{
145 | text-align: center;
146 | font-size: 18px;
147 | line-height: 1.1;
148 | color: #fefefe;
149 | overflow: hidden;
150 | white-space: nowrap;
151 | text-overflow: ellipsis;
152 | font-weight: normal;
153 | }
154 | .m-song-gap {
155 | margin: 0 4px;
156 | }
157 | .m-song-autr {
158 | font-size: 16px;
159 | color: hsla(0,0%,100%,.6);
160 | }
161 | .lrc-wrap{
162 | position: relative;
163 | margin-top: 14px;
164 | }
165 | .lrc-scroll{
166 | font-size: 13px;
167 | height: 120px;
168 | overflow: hidden;
169 | -webkit-mask: -webkit-linear-gradient(top,#000,#000 70%,rgba(0,0,0,0));
170 | }
171 | .lrc-box{
172 | color: hsla(0,0%,100%,.6);
173 | text-align: center;
174 | transition: all .3s;
175 | p{
176 | line-height: 2;
177 | font-size: 13px;
178 | &.on{
179 | color: #fff;
180 | }
181 | }
182 | }
183 | .play-logo{
184 | position: absolute;
185 | top: 28px;
186 | left: 12px;
187 | color: #fff;
188 | }
189 | .option-area{
190 | position: absolute;
191 | bottom: 0;
192 | left: 0;
193 | width: 100vw;
194 | padding: 10px;
195 | // height: 50px;
196 |
197 | .time-bar{
198 | @include flex();
199 | time{
200 | color: #fff;
201 | font-size: 10px;
202 | transform: scale(0.8);
203 | }
204 | /deep/ .van-slider__button{
205 | width: 6px;
206 | height: 6px;
207 | transform: scaleY(2);
208 | }
209 | /deep/ .van-slider{
210 | height: 1px;
211 | transform: scaleY(.5);
212 | width: 82%;
213 | margin: auto;
214 | background-color: #aaa;
215 | }
216 | /deep/ .van-slider__bar{
217 | background-color: #fff;
218 | }
219 | }
220 | .option-bar{
221 | @include flex(center, space-between);
222 | color: #fff;
223 | padding: 11px 0 0px;
224 | font-size: 30px;
225 | /deep/ .van-icon{
226 | display: block;
227 | }
228 | }
229 | .rot{
230 | transform: rotate(180deg);
231 | }
232 | }
233 |
234 | .history-wrap{
235 | padding: 10px;
236 | font-size: 14px;
237 | .pop-title{
238 | text-align: center;
239 | line-height: 2;
240 | font-size: 16px;
241 | margin-bottom: 10px;
242 | // font-weight: normal;
243 | position: absolute;
244 | top: 10px;
245 | left: 50%;
246 | transform: translateX(-50%);
247 | background: #fff;
248 | z-index: 111;
249 | width: 100%;
250 | }
251 | em{
252 | font-style: normal;
253 | font-size: 12px;
254 | color: #999;
255 | }
256 | span{
257 | @include elp;
258 | }
259 | p{
260 | @include flex();
261 | line-height: 2.5;
262 | &.active{
263 | em, span{
264 | color: #dd001b;
265 | }
266 | }
267 | }
268 | .history-items{
269 | position: absolute;
270 | overflow-y: auto;
271 | width: 94%;
272 | top: 40px;
273 | bottom: 0;
274 | }
275 | }
276 |
277 | .voice-ctrl{
278 | position: relative;
279 | /deep/ .van-slider{
280 | position: absolute;
281 | height: 100px;
282 | bottom: 40px;
283 | left: 50%;
284 | transform: translateX(-50%);
285 | background-color: #1989fa;
286 | }
287 | /deep/ .van-slider__button{
288 | width: 15px;
289 | height: 15px;
290 | }
291 | /deep/ .van-slider__bar{
292 | background-color: #fff;
293 | }
294 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/playBar/playBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
{{ getAudioInfo.song }}
8 |
{{ getAudioInfo.singer }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
![]()
34 |
35 |
36 |
37 |
38 |
39 | {{ getAudioInfo.song }}
40 | -
41 | {{ getAudioInfo.singer }}
42 |
43 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
80 |
81 |
历史记录
82 |
83 |
84 |
85 |
86 | {{ item.song }} -
87 |
88 | {{ item.singer }}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
439 |
440 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/searchBar/searchBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
71 |
72 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/songItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ song }}
7 |
8 |
9 | {{ info }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
42 |
43 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/sort/sortPannel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
{{item.updateFrequency}}
7 |
8 |
{{item.name}}
9 |
10 |
11 |
12 |
13 |
39 |
40 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/sort/trackPannel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |
{{item.updateFrequency}}
8 |
9 |
10 |
11 |
{{i+1}}. {{trackFormat(items)}}
12 |
13 |
14 |
15 |
16 |
17 |
46 |
47 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/switchPannel/switchPannel.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
23 |
24 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/components/video/VideoComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ curQuality + 'P' }}
22 |
25 |
26 |
27 |
28 |
29 |
30 |
190 |
191 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './App.vue'
3 | import router from './router'
4 | import store from './store'
5 |
6 | import '@/tools/rem.js'
7 | // import Vant from 'vant';
8 | // import 'vant/lib/index.css';
9 | import fastClick from 'fastclick';
10 | fastClick.attach(document.body);
11 |
12 | // import VConsole from 'vconsole';
13 | if(process.env.NODE_ENV !== 'production') {
14 | // new VConsole();
15 | }
16 |
17 | router.afterEach((to, from) => {
18 | document.title = to.meta.title;
19 | store.dispatch('setNumber', Math.random());
20 | })
21 |
22 | import lazyPlugin from 'vue3-lazy'
23 |
24 | createApp(App).
25 | use(router)
26 | .use(store)
27 | .use(lazyPlugin, {
28 | loading: '@/assets/cover.png',
29 | })
30 | .mount('#app')
31 |
32 |
33 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 | import Home from '../views/Home.vue'
3 |
4 | const routes = [
5 | {
6 | path: '/',
7 | name: 'Home',
8 | component: Home,
9 | meta: {
10 | title: 'Jacky Music'
11 | },
12 | children: [
13 | {
14 | path: 'playList/:id',
15 | name: 'PlayList',
16 | component: () => import('@/views/playList.vue'),
17 | meta: {
18 | title: '歌单'
19 | }
20 | }
21 | ]
22 | },
23 | {
24 | path: '/singer',
25 | name: 'Singer',
26 | // route level code-splitting
27 | // this generates a separate chunk (about.[hash].js) for this route
28 | // which is lazy-loaded when the route is visited.
29 | component: () => import(/* webpackChunkName: "about" */ '@/views/singer.vue'),
30 | meta: {
31 | title: '歌手列表'
32 | },
33 | children: [
34 | {
35 | path: 'singerDetails',
36 | name: 'SingerDetails',
37 | component: () => import('@/views/singerDetails.vue'),
38 | meta: {
39 | title: '歌手详情'
40 | }
41 | }
42 | ]
43 | },
44 |
45 | {
46 | path: '/sort',
47 | name: 'Sort',
48 | component: () => import('@/views/sort.vue'),
49 | meta: {
50 | title: '排行榜'
51 | },
52 | children: [
53 | {
54 | path: 'details',
55 | name: 'SortDetails',
56 | component: () => import('@/views/sortDetails.vue'),
57 | meta: {
58 | title: '排行榜详情'
59 | }
60 | }
61 | ]
62 | },
63 |
64 | {
65 | path: '/search',
66 | name: 'Search',
67 | component: () => import('@/views/search.vue'),
68 | meta: {
69 | title: '搜索'
70 | }
71 | },
72 |
73 | {
74 | path: '/album/:id',
75 | name: 'Album',
76 | component: () => import('@/views/album.vue'),
77 | meta: {
78 | title: '专辑'
79 | }
80 | },
81 |
82 | {
83 | path: '/playSquare',
84 | name: 'PlaySquare',
85 | component: () => import('@/views/playSquare.vue'),
86 | meta: {
87 | title: '歌单广场'
88 | },
89 | children: [
90 | {
91 | path: 'playList/:id',
92 | name: 'PlaySquarePlayList',
93 | component: () => import('@/views/playList.vue'),
94 | meta: {
95 | title: '歌单'
96 | }
97 | }
98 | ]
99 | },
100 |
101 | {
102 | path: '/highqualitySquare',
103 | name: 'HighqualitySquare',
104 | component: () => import('@/views/highqualitySquare.vue'),
105 | meta: {
106 | title: '精选歌单广场'
107 | },
108 | children: [
109 | {
110 | path: 'playList/:id',
111 | name: 'HighqualitySquarePlayList',
112 | component: () => import('@/views/playList.vue'),
113 | meta: {
114 | title: '歌单'
115 | }
116 | }
117 | ]
118 | },
119 |
120 | {
121 | path: '/MvIdex',
122 | name: 'MvIndex',
123 | component: () => import('@/views/MvIndex.vue'),
124 | meta: {
125 | title: 'MV广场'
126 | }
127 | }
128 | ]
129 |
130 | const router = createRouter({
131 | history: createWebHashHistory(),
132 | routes
133 | })
134 |
135 | export default router
136 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vuex from 'vuex'
2 | import sessionStore from "@/tools/sessionStore.js";
3 |
4 | export default Vuex.createStore({
5 | state: {
6 | number: 0,
7 | audioInfo: {
8 | },
9 | audioHistory: [], //播放历史记录
10 | keep: ['Home', 'Sort', ''], //缓存页面
11 | },
12 | mutations: {
13 | setNumber(state, payload) {
14 | state.number = payload;
15 | },
16 | setAudioInfo(state, audioInfo) { //设置音频对象
17 | state.audioInfo = audioInfo;
18 | sessionStore.set('audioInfo', audioInfo);
19 | },
20 | setAudioHistory(state, history) { //设置播放历史
21 | state.audioHistory = history;
22 | sessionStore.set('audioHistory', history);
23 | },
24 | setKeep(state, keepArr) { //设置路由缓存
25 | state.keep = keepArr;
26 | sessionStore.set('keepArr', keepArr);
27 | }
28 | },
29 | actions: {
30 | setNumber({commit}, payload) {
31 | commit('setNumber', payload);
32 | },
33 | setAudioInfo({commit}, audioInfo) {
34 | commit('setAudioInfo', audioInfo);
35 | },
36 | setAudioHistory({commit}, history) {
37 | commit('setAudioHistory', history);
38 | },
39 | setKeep({commit}, keepArr) { //设置路由缓存
40 | commit('setKeep', keepArr);
41 | }
42 | },
43 | getters: {
44 | getNumber(state) {
45 | return state.number
46 | },
47 | getAudioInfo(state) {
48 | return state.audioInfo.song ? state.audioInfo : sessionStore.get('audioInfo');
49 | },
50 | getAudioHistory(state) {
51 | return state.audioHistory.length ? state.audioHistory : sessionStore.get('audioHistory');
52 | },
53 | getKeep(state) {
54 | return state.keep ? state.keep : sessionStore.get('keepArr');
55 | }
56 | },
57 | modules: {
58 | }
59 | });
60 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/tools/common.js:
--------------------------------------------------------------------------------
1 | import { Toast } from 'vant';
2 |
3 | export const numFormat = (num, basicNum = 10000) => {
4 | if(num < 10000) {
5 | return num;
6 | }else {
7 | return (num / basicNum).toFixed(1) + '万';
8 | }
9 | }
10 |
11 | export const artistsFormat = (artists) => {
12 | let str = [];
13 | artists.map(obj => {
14 | str.push(obj.name);
15 | })
16 | return str.join('/');
17 | }
18 |
19 | export const lockScroll = (e) => {
20 | e.stopPropagation();
21 | e.preventDefault();
22 | }
23 |
24 | export const loading = (msg = '加载中...', duration = 0) => {
25 | Toast.loading({
26 | message: msg,
27 | duration: duration,
28 | forbidClick: true,
29 | loadingType: 'spinner'
30 | })
31 | }
32 |
33 | export const loaded = () => {
34 | Toast.clear();
35 | }
36 |
37 | export const albumAndSinger = (al, ar) => { // 张学友 / 汤宝如 - 相思风雨中
38 | let arArr = [];
39 | ar.map((obj) => {
40 | arArr.push(obj.name);
41 | })
42 | return arArr.join(' / ') + ' - ' + al.name;
43 | }
44 |
45 | export const lazyLoadImg = (target, options) => { //图片懒加载
46 | let io = new IntersectionObserver((entries, observer) => {
47 | entries.forEach(entry => {
48 | if (entry.isIntersecting) {
49 | let src = entry.target.getAttribute('data-src');
50 | if (src) {
51 | entry.target.setAttribute('src', src);
52 | entry.target.removeAttribute('data-src');
53 | observer.disconnect();
54 | }
55 | }
56 | })
57 | }, options)
58 | io.observe(target);
59 | }
60 |
61 | import { getSongUrl } from '@/api/search.js';
62 |
63 | export async function toPlay(item, store, getAudioHistory) { //点击播放
64 | loading('歌曲下载中...');
65 | let res = await getSongUrl(item.id); //下载歌曲
66 | store.dispatch('setAudioInfo', { //更新store
67 | url: res.url,
68 | singer: item.ar[0].name,
69 | song: item.name,
70 | poster: item.al.picUrl,
71 | id: item.id
72 | }).then(() => {
73 | setTimeout(() => { loaded() }, 500)
74 | })
75 |
76 | if(getAudioHistory != null) {
77 | let tempHistory;
78 | if (getAudioHistory.value.length === undefined) { //没有记录
79 | tempHistory = [];
80 | } else {
81 | tempHistory = getAudioHistory.value;
82 | }
83 | let key = false;
84 | for (let i = 0; i < tempHistory.length; i++) {
85 | if (tempHistory[i].id == item.id) { //之前就存在过
86 | key = true;
87 | break;
88 | }
89 | }
90 | if (!key) {
91 | tempHistory.unshift({
92 | url: res.url,
93 | singer: item.ar[0].name,
94 | song: item.name,
95 | poster: item.al.picUrl,
96 | id: item.id
97 | })
98 | store.dispatch('setAudioHistory', tempHistory); //更新播放记录
99 | }
100 | }
101 | }
102 |
103 |
104 | export function playAll(his, targetGroup, store) { //播放全部
105 | /**
106 | * 第一步,把全部专辑里的歌曲list取出来,去重后加入历史记录
107 | * 第二步,将专辑第一首切换为当前播放,并更新store
108 | * 第三部,重新计算激活下标值
109 | */
110 | // console.log(albumObj.songs);
111 | // console.log(getAudioHistory);
112 | console.log(store)
113 | let historyArr = his.value || [], //历史数组
114 | songList = targetGroup, //专辑数组
115 | needAddArr = []; //需要往历史数组里添加的数组
116 | songList.map(item => {
117 | for(let i = 0; i < historyArr.length; i ++) {
118 | if(historyArr[i].id == item.id) { //有存在
119 | historyArr.splice(i, 1);
120 | i --;
121 | break;
122 | }
123 | }
124 | needAddArr.push({
125 | url: '',
126 | singer: item.ar[0].name,
127 | song: item.name,
128 | poster: item.al.picUrl,
129 | id: item.id
130 | });
131 | })
132 |
133 | let firstItem = songList[0];
134 | historyArr.unshift(...needAddArr);
135 | store.dispatch('setAudioHistory', historyArr)
136 | .then(() => {
137 | toPlay(firstItem, store);
138 | })
139 | }
140 |
141 | export const timeFormat = (time) => {
142 | time = new Date(time);
143 | return time.getFullYear() + '-' +
144 | (time.getMonth() + 1).toString().padStart(2, 0) + '-' +
145 | (time.getDate()).toString().padStart(2, 0)
146 | }
147 |
148 | export function timeStampFormat(timeStamp) { //320ms ==> 05:20
149 | let min = parseInt(timeStamp / 60),
150 | sec = timeStamp % 60;
151 |
152 | min = min.toString().padStart(2, '0');
153 | sec = sec.toString().padStart(2, '0');
154 |
155 | return min + ':' + sec;
156 | }
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/tools/rem.js:
--------------------------------------------------------------------------------
1 | (function (doc, win) { // 自适应
2 | var docEl = doc.documentElement,
3 | resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
4 | recalc = function () {
5 | var clientWidth = docEl.clientWidth;
6 | if (!clientWidth) return;
7 | docEl.style.fontSize = 20 * (clientWidth / 375) + 'px'; // 放下16个字
8 | };
9 | if (!doc.addEventListener) return;
10 | win.addEventListener(resizeEvt, recalc, false);
11 | doc.addEventListener('DOMContentLoaded', recalc, false);
12 | })(document, window);
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/tools/sessionStore.js:
--------------------------------------------------------------------------------
1 | const sessionStore = {
2 | get(key) {
3 | try{
4 | let res = sessionStorage.getItem(key);
5 | if(res == null) {
6 | return {};
7 | }else {
8 | res = JSON.parse(res);
9 | return res;
10 | }
11 | }catch(e) {
12 | return {};
13 | }
14 | },
15 | set(key, val) {
16 | sessionStorage.setItem(key, JSON.stringify(val));
17 | },
18 | remove(key) {
19 | sessionStorage.removeItem(key);
20 | },
21 | clear() {
22 | sessionStorage.clear();
23 | }
24 | }
25 |
26 | export default sessionStore;
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
119 |
120 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/MvIndex.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
36 |
37 |
46 |
47 |
48 |
49 |
215 |
216 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/album.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
![]()
11 |
12 |
{{ album.name }}
13 |
14 |
15 | {{ album.artist&&album.artist.name }}
16 |
17 |
18 |
19 |
20 |
![]()
21 |
22 |
34 |
35 |
36 |
37 | 播放全部
38 |
39 |
45 |
46 |
47 | {{ album.description }}
48 |
49 |
50 |
51 |
52 |
53 |
158 |
159 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/highqualitySquare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |

15 |
16 |
17 | {{ numFormat(item.playCount) }}
18 |
19 |
20 |
{{ item.name }}
21 |
22 |
23 |
点击加载更多
24 |
25 | 加载中...
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
196 |
197 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/playList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
![]()
10 |
11 |
{{sortDetails.name}}
12 |
13 |
14 | {{sortDetails.creator}}
15 |
16 |
17 |
18 |
19 |
![]()
20 |
21 |
33 |
34 |
35 |
36 | 播放全部
37 |
38 |
44 |
45 |
46 | {{ sortDetails.description }}
47 |
48 |
49 |
50 |
51 |
146 |
147 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/playSquare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |

15 |
16 |
17 | {{ numFormat(item.playCount) }}
18 |
19 |
20 |
{{ item.name }}
21 |
22 |
23 |
点击加载更多
24 |
25 | 加载中...
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
143 |
144 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
18 |
19 |
20 | 搜索“{{ searchVal }}”
21 |
22 | -
23 |
24 | {{ item.keyword }}
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 | {{ item.name }}
42 |
43 |
44 | {{ albumAndSinger(item.al, item.ar) }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
254 |
255 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/singer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.txt }}
6 |
7 |
8 | {{ item.txt }}
9 |
10 |
11 |
12 |
18 |
19 |
20 | {{ obj.name }} {{ obj.alias.length > 0?'(' + obj.alias[0] + ')':'' }}
21 |
22 |
23 |
24 |
25 |
28 |
33 |
34 |
35 |
36 |
159 |
160 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/singerDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
12 |
13 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 | {{ item.name }}
46 |
47 |
48 | {{ name }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |

69 |
70 |
{{ item.name }}
71 |
{{timeFormat(item.publishTime)}} {{item.size}}首
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
![]()
85 |
86 |
89 |
90 | {{ numFormat(item.playCount) }}
91 |
92 |
93 |
94 |
95 | {{ item.name }}
96 |
97 |
98 |
99 |
100 |
加载更多
101 |
102 | 加载中...
103 |
104 |
105 |
106 |
107 |
108 |
{{ name }}简介
109 |
110 | {{ descOfSinger.briefDesc }}
111 |
112 |
113 |
114 | {{ item.ti }}
115 | {{ item.txt }}
116 |
117 |
118 |
119 |
120 |
121 |
126 |
130 |
131 |
140 |
141 |
144 |
149 |
150 |
151 |
152 |
438 |
439 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/sort.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
榜单推荐
5 |
6 | 官方榜
7 |
8 | 特色榜
9 |
10 | 全球榜
11 |
12 | 地区榜
13 |
14 | 曲风榜
15 |
16 | 更多榜单
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
118 |
119 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/src/views/sortDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
{{sortDetails.name}}
13 |
14 |
15 | {{sortDetails.creator}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
35 |
36 |
37 |
38 | 播放全部
39 |
40 |
46 |
47 |
48 | {{ sortDetails.description }}
49 |
50 |
51 |
52 |
53 |
127 |
128 |
131 |
132 |
--------------------------------------------------------------------------------
/vue3NeteaseCloud/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | function resolve (dir) {
3 | return path.join(__dirname, dir)
4 | }
5 |
6 | module.exports = {
7 | lintOnSave: false,
8 | chainWebpack: (config)=>{
9 | config.resolve.alias
10 | .set('@', resolve('src'))
11 | .set('~', resolve('src'))
12 | },
13 | devServer: {
14 | port: 8080,
15 | open: false,
16 | proxy: {
17 | '/': { //这里最好有一个 /
18 | target: 'http://zhoup.top:7003/', // 后台接口域名
19 | ws: true, //如果要代理 websockets,配置这个参数
20 | secure: false, // 如果是https接口,需要配置这个参数
21 | changeOrigin: true, //是否跨域
22 | pathRewrite:{
23 | '^/':''
24 | }
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------