提交 9daebb95 编写于 作者: AresnLiang's avatar AresnLiang 提交者: 知心宝贝

更新README.md

上级 99e799c1
.DS_Store
node_modules node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
![](https://file.iviewui.com/cloud-ide/1024game-cover.png) # 网易云音乐(App版本)
## 一、大赛介绍 ## 一、基本介绍
代码开发随着云计算技术的不断发展,云 IDE 能够帮助开发者聚焦业务逻辑,简化应用开发难度,提升软件研发效率。云 IDE 应用挑战赛是由 CSDN 自主策划的轻量级挑战赛事。参赛者可在 CSDN 的云 IDE 上([https://ide.csdn.net](https://ide.csdn.net))自动拉取代码并在线运行、预览、提交代码,赛事所有的事项都可以在云 IDE 内一站式完成。比赛不对作品提交形式作限制,参赛者可以尽情发挥自己的想象力。
## 二、参赛对象
大赛面向全社会开放,个人、高等院校、科研单位、企业等人员均可报名参赛。 本项目使用`vue2+vue-cli`实现了一个简易的网易云音乐(**App版本**),实现了歌曲的首页推荐展示,歌曲的搜索,评论的展示以及歌曲的播放四个主要功能模块,主要用来练手,效果如下图所示:
说明:如有大赛主办方和技术支持单位(如 CSDN 员工)参赛,则自动放弃获奖资格。
## 三、赛事安排
| 赛程 | 时间 |
| ---- | ---- |
| 参赛报名及提交作品 | 2022.10.23 - 2022.11.06 |
| 作品评选 | 2022.11.07 - 2022.11.14 |
| 评选结果公布 | 2022.11.15 |
| 奖励发放 | 2022.11.16 - 2022.11.22 |
## 四、奖项设置 **音乐播放**
| 奖项 | 奖品 | 人数 | <img src="https://gitee.com/riskbaby/picgo/raw/master/blog/202210311601471.png" alt="5" style="zoom:67%;" />
| ---- | ---- | ---- |
| 一等奖 | **奖金 ¥5000 元** + 10核 10G 云实验环境会员 -1年| 1 |
| 二等奖 | **奖金 ¥2000 元** + 10核 10G 云实验环境会员 -1年 | 2 |
| 三等奖 | **奖金 ¥1000 元** + 10核 10G 云实验环境会员 -1年 | 5 |
| 优秀奖 | **奖金 ¥100 元** + 10核 10G 云实验环境会员 -1年 | 80 |
| 参与奖 | 10核 10G 云实验环境会员 -1个月 | 前10000名参赛者可得 |
注:一年10核10G云实验环境会员,非云服务器,是云容器实验环境,详细介绍[戳我查看](https://mydev.csdn.net/product/pod/new) ## 二、项目使用
## 五、参赛要求 ### 2.1 安装依赖
1. 开发者可根据自己情况选择业界知名开源项目进行自行开发,或自研可开源项目结合GitCode和Cloud IDE进行开发与适配,作品要求必须能通过 CSDN 的云 IDE 内置的预览功能,在云 IDE 内预览。(CSDN 的云IDE:[https://ide.csdn.net](https://ide.csdn.net))。作品可以是任何小应用(如外卖H5、个人主页);小游戏(如 2048、五子棋);各类特效(如 three.js、前端动画)等。 ```
npm i
```
2. 未满 18 周岁的报名者,请在报名前征得有法定监护权的监护人的同意。 ### 2.2 本地使用
3. 参赛作品不存在抄袭等行为。 ```
npm run serve
```
4. 除参与奖外,获奖人员,领奖时需实名认证,1 个实名认证用户仅可领取1份奖项。
## 六、报名及参赛流程
1. **进入 1024 云 IDE 应用挑战赛页面:[https://gitcode.net/cloud-ide/1024](https://gitcode.net/cloud-ide/1024)****Fork 该项目,即参与比赛** ### 2.3 打包上传
2. **参赛后,将 Fork 的项目在云 IDE 中打开,并在云IDE中完成您自己的作品,提交代码。** ```
npm run build
```
在云IDE提交代码,有2种操作方法,任选其一即可:
**方法一:**
> (1) 访问 [https://ide.csdn.net](https://ide.csdn.net) ,新建工作空间,仓库地址填写 克隆 的 Git 地址(操作方法:点击”克隆“,复制 ”通过SSH Clone项目“ 的链接) **github地址**[cloud-music](https://github.com/bosombaby/cloud-music)
>
> <img src="https://gitcode.net/cloud-ide/1024/-/raw/main/src/gitcode-ssh.png" width="400px" style="border: 6px solid #fff;border-radius: 2px;">
> <img src="https://file.iviewui.com/cloud-ide/cloud-ide-img-1.png" width="400px" style="border: 6px solid #fff;border-radius: 2px;">
>
> (2)填写 SSH 的 Git 地址,并将云 IDE 提供的 SSH Key (操作方法:[https://dev-ide.csdn.net/doc#git_commit](https://dev-ide.csdn.net/doc#git_commit) )绑定到自己的 GitCode SSH 密钥中 [https://gitcode.net/-/profile/keys](https://gitcode.net/-/profile/keys)
**在线预览:**:https://music.vrteam.top
**方法二:** ## 三、开发环境
> (1) 直接在 Fork 的 GitCode 域名前加 ide 进入,例如 https://gitcode.net/xxxxx/1024 修改为 https://idegitcode.net/xxxxx/1024 ### 2.1 代理服务器
> <br/>
> <img src="https://file.iviewui.com/cloud-ide/cloud-ide-img-2.png" width="400px" style="border: 6px solid #fff;border-radius: 2px;display:block">
>
> (2) 提交代码时,云 IDE 会让用户输入 CSDN 的账号和密码(即手机号+密码)
**3. 提交代码后,即可等待CSDN官方进行评选。** 2022.11.07 - 2022.11.14为作品评选时间,2022.11.15公布评选结果。 `api链接`:https://binaryify.github.io/NeteaseCloudMusicApi/#/![3](https://gitee.com/riskbaby/picgo/raw/master/blog/202210131612886.png)
<br/>Tips:如果您想要分享下 云IDE 的使用感受,欢迎投稿[ 云IDE测评征文 ](https://activity.csdn.net/creatActivity?id=10239)活动,还能获得额外奖品哦~
`总结`:根据文档在后端跑起来代理服务器
## 七、评选规则 ### 2.2 搭建前端项目
由评选委员会根据获得的 Star、作品复杂程度、易用性、新颖性等多角度评选。 接下来使用`yarn`代替`npm`,原因:https://blog.csdn.net/qq_39122387/article/details/109675680
1. Star 占比 20% #### 2.2.1 脚手架
a. 获得 Star 的数量,1 颗即为 1 分,最高 20 分封顶 ```js
vue create music-demo
```
2. 作品复杂程度占比 40% `总结`:默认选择`vue2``yarn`安装依赖的方式
a. 作品完整度 #### 2.2.2 项目依赖
b. 代码简洁,代码可读性强 **进入项目文件夹**
c. 架构清晰 ```
cd music-demo
```
d. 作品难度,如使用了一些高级 API,比如 WebGL、Canvas **第三方依赖**
3. 易用性占比 30% ```
yarn add axios vant@latest-v2 vue-router@3
```
a. 作品配套的说明,如果项目介绍、如果使用等 注意版本,这个是vue2的项目,对应的包有所变化
b. 交互体验,产品具有较好的交互体验,有足够的易用性 **生产依赖**
c. 可用,代码能在 CSDN 的云 IDE 上完整运行 ```
yarn add babel-plugin-import postcss postcss-pxtorem@5.1.1 -D
yarn add less less-loader@5.0.0 -D
```
4. 新颖性占比 10% **引入本地样式、图标**
a. 新颖性,使用了较为新颖的技术实现 ```js
// 字体图标
import '@/assets/fonts/TsangerYuYangT_W01_W01.ttf'
//自适应
import '@/mobile/flexible'
//初始化样式
import '@/styles/reset.css'
```
## 八、领奖规则 **在babel.config.js - 添加vant按需引入插件配置**
1. 获奖名单会在该评选结束后在该页面公布。 ```js
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
```
2. 工作人员将在活动结束后的7个工作日内,通过CSDN私信联系中奖用户,进行发奖,请您注意消息通知。 **postcss.config.js自定义自适应比例**
## 九、赛事交流 ```js
module.exports = {
plugins: {
'postcss-pxtorem': {
// 能够把所有元素的px单位转成Rem
// rootValue: 转换px的基准值。
// 例如一个元素宽是75px,则换成rem之后就是2rem。
rootValue: 37.5,
propList: ['*']
}
}
}
```
1、如果您想要分享下 云IDE 的使用感受,欢迎投稿[ 云IDE测评征文 ](https://activity.csdn.net/creatActivity?id=10239)活动,还能获得额外奖品哦~ `注意`:每一次改动`src`目录之外的内容需要**重启服务**
2、参赛选手官方交流群(仅作为选手之间讨论和交流)。扫码进入选手沟通群。大赛重要节点通知会在群内第一时间告知,请所有报名选手务必加群 ## 四、需求分析
<img src="https://file.iviewui.com/asd/cloud-ide/cloud-ide-2.png" width="200px" style="border: 6px solid #fff;border-radius: 2px;">
## 十、其他说明
若出现以下情况,将视为违规,大赛组委会有权取消用户参赛资格及获得的奖项。 <img src="https://gitee.com/riskbaby/picgo/raw/master/blog/202210131941184.png" alt="image-20210426212251154" style="zoom:50%;" />
1. 参赛报名信息虚假,或不符合大赛报名要求的参赛者。 ![1](https://gitee.com/riskbaby/picgo/raw/master/blog/202210161035039.png)
2. 参赛作品涉嫌抄袭,侵犯他人知识产权等行为。
3. 参赛期间或参赛作品发现或被举报认定存在的其他违法、违规行为。
法律允许范围内的,最终解释权归 CSDN 所有。 `App`:管理所有的组件
<div style="height:200px"></div> - `Layout`:管理首页和搜索
- `Home`:主页
- `SongItem`:负责音乐的布局
- `Search`:搜索页
- `SongItem`:负责音乐的布局
- `Play`:音乐播放
- `Comment`:评论页
`Play``Comment``Layout`平级,需要的时候直接调用就行
## 五、路由管理
```js
// 引入Vue
import Vue from 'vue'
// 引入路由
import VueRouter from "vue-router"
Vue.use(VueRouter)
// 引入组件
import Layout from '@/views/Layout'
import HomePage from '@/views/Home'
import SearchPage from '@/views/Search'
import Play from '@/views/Play'
import Comment from '@/views/Comment'
const routes = [
{
path: '/',
redirect:'/layout'
},
{
path: '/layout',
redirect: '/layout/home',
component: Layout,
children: [
{
path: 'home',
component: HomePage,
meta: {
title:'首页'
},
},
{
path: 'search',
component: SearchPage,
meta: {
title:'搜索'
},
}
]
},
{
path: '/play',
component:Play
},
{
path: '/comment',
component:Comment
}
]
const router = new VueRouter({ routes })
export default router
```
## 六、接口管理
```js
//所有api的管理者
import { recommendMusicAPI, newMusicAPI } from "@/api/Home";
import { hotSearchAPI, searchResultAPI } from "@/api/Search";
import { getSongByIdAPI, getLyricByIdAPI } from '@/api/Play'
import { getHotCommentAPI} from '@/api/Comment'
export { recommendMusicAPI,newMusicAPI,hotSearchAPI,searchResultAPI,getSongByIdAPI,getLyricByIdAPI,getHotCommentAPI}
```
## 七、难点分析
### 7.1 搜索页面
**防抖与节流**
`防抖`:像电梯门一样,每一次有人进来之后,电梯门就会重新计时,最终的记时完成,才会触发,相当于setTimeout
`节流`:像英雄的技能一样,触发一次之后要等待一段时间才能继续执行,相当于 setInterval
搜索歌曲避免多次发起请求,使用`防抖`实现
### 7.2 播放页面
`样式`:css的动画样式
`three.js`:打算引入`粒子特效`实现3d音乐效果的(后续需求,一开始未做好规划,不好加了)
### 7.3 评论页面
**评论的刷新与加载**
- vant组件库`List 列表`` PullRefresh `结合,List 组件通过 `loading``finished` 两个变量控制加载状态,当组件滚动到底部时,会触发 `load` 事件并将 `loading` 设置成 `true`。此时可以发起异步操作并更新数据,数据更新完毕后,将 `loading` 设置成 `false` 即可。若数据已全部加载完毕,则直接将 `finished` 设置成 `true` 即可
### 7.4 异步
`Promise`:将异步转换为同步`async await`(就近原则使用),等待数据返回之后再继续执行下面的代码
## 八、项目总结
这次主要对`vue2`的项目进行锻炼。学习了模块化处理流程,对待不同的功能模块要进行合理的分区,前期一定要做好项目规划处理,后续开始学习`vue3`以及`three.js`
\ No newline at end of file
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`,
// style: true
}, 'vant']
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="src/style.css">
</head>
<body>
<div class="app">
<img src="src/logo.svg" width="100px">
<h1>欢迎使用 Cloud IDE</h1>
官网:<a href="https://ide.csdn.net" target="_blank">https://ide.csdn.net</a>
</div>
</body>
</html>
\ No newline at end of file
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
{ {
"name": "cloud-ide-1024-game", "name": "music-demo",
"version": "1.0.0", "version": "0.1.0",
"description": "", "private": true,
"main": "index.js",
"scripts": { "scripts": {
"dev": "live-server" "serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}, },
"author": "Aresn",
"license": "MIT",
"dependencies": { "dependencies": {
"live-server": "^1.2.2" "axios": "^1.1.2",
} "core-js": "^3.8.3",
"vant": "^2.12.50",
"vue": "^2.6.14",
"vue-router": "3"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"babel-plugin-import": "^1.13.5",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"less": "^4.1.3",
"less-loader": "5.0.0",
"postcss": "^8.4.18",
"postcss-pxtorem": "5.1.1",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
} }
module.exports = {
plugins: {
'postcss-pxtorem': {
// 能够把所有元素的px单位转成Rem
// rootValue: 转换px的基准值。
// 例如一个元素宽是75px,则换成rem之后就是2rem。
rootValue: 37.5,
propList: ['*']
}
}
}
\ No newline at end of file
# preview.yml
autoOpen: true # 打开工作空间时是否自动开启所有应用的预览
apps:
- port: 3000 # 应用的端口
run: npm i && npm run dev # 应用的启动命令
command: # 使用此命令启动服务,且不执行run
root: ./ # 应用的启动目录
name: 1024云IDE挑战赛项目 # 应用名称
description: 1024云IDE挑战赛项目。 # 应用描述
autoOpen: true # 打开工作空间时是否自动开启预览(优先级高于根级 autoOpen
\ No newline at end of file
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>音乐播放器</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
}
</script scoped>
import { request } from "@/utils/request";
//获取热门评论
export const getHotCommentAPI = params => request({
url: 'comment/hot',
params
})
//首页推荐歌单请求
import { request } from "@/utils/request";
//推荐歌单
export const recommendMusicAPI = params => request({
url: '/personalized',
params
})
//最新音乐
export const newMusicAPI = params => request({
url: '/personalized/newsong',
params
})
\ No newline at end of file
import { request } from "@/utils/request";
// 播放页 - 获取歌曲详情
export const getSongByIdAPI = (id) => request({
url: `/song/detail?ids=${id}`,
})
// 播放页 - 获取歌词
export const getLyricByIdAPI = (id) => request({
url: `/lyric?id=${id}`,
})
//搜索页接口
import { request} from '@/utils/request'
//获取热门搜索
export const hotSearchAPI = params => request({
url: '/search/hot',
params
})
//获取搜索结果
export const searchResultAPI = params => request({
url: '/search',
params
})
//所有api的管理者
import { recommendMusicAPI, newMusicAPI } from "@/api/Home";
import { hotSearchAPI, searchResultAPI } from "@/api/Search";
import { getSongByIdAPI, getLyricByIdAPI } from '@/api/Play'
import { getHotCommentAPI} from '@/api/Comment'
export { recommendMusicAPI,newMusicAPI,hotSearchAPI,searchResultAPI,getSongByIdAPI,getLyricByIdAPI,getHotCommentAPI}
<template>
<van-cell center
:title="title"
:label="label"
@click="enterComment()"
>
<template #right-icon>
<van-icon name="play-circle-o" size="24" @click="playSong()"/>
</template>
</van-cell>
</template>
<script>
export default {
name:'SongItem',
props:{
title:String,
label:String,
id:Number
},
methods: {
playSong(){
this.$router.push({
path:'/play',
query:{
id:this.id
}
})
},
enterComment(){
this.$router.push({
path:'/comment',
query:{
id:this.id
}
})
}
},
}
</script>
<style>
</style>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="37px" height="28px" viewBox="0 0 37 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>开发云logo备份@40x</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="开发云logo备份@40x" transform="translate(2.000000, 2.000000)" stroke="#1890FF">
<path d="M22.8925411,9.29032948 L22.8925411,6.7248501 C22.8925411,3.01081795 19.8817231,-1.77635684e-15 16.167691,-1.77635684e-15 C12.4536588,-1.77635684e-15 9.44284085,3.01081795 9.44284085,6.7248501 L9.44284085,7.87271787 L9.44284085,7.87271787 L7.66157921,7.87271787 C3.43020585,7.87271787 0,11.3029237 0,15.5342971 C0,19.7656704 3.43020585,23.1958763 7.66157921,23.1958763 L23.6762383,23.1958763 L23.6762383,23.1958763" id="路径-28" stroke-width="4" stroke-linecap="round"></path>
<path d="M33.2652638,20.4086185 C33.0827033,20.2352897 32.8345087,20.1392132 32.5943359,20.1433691 C32.3478429,20.1482585 32.1246865,20.2509357 31.9659488,20.4335544 C30.544846,22.0690553 28.0684903,22.2455623 27.0696344,22.2455623 C25.2163178,22.2455623 23.798132,21.7732472 22.8544575,20.8423074 C21.9579423,19.9578168 21.4880498,18.6552834 21.4571774,16.9706441 C21.3852228,13.0654885 23.5839699,9.02587465 27.388082,9.02587465 C29.2051781,9.02587465 30.6155851,10.3220519 31.1374989,10.8794423 C31.3188438,11.0728177 31.5699556,11.1842958 31.8273877,11.1850292 C32.0860352,11.1921188 32.3245063,11.0764847 32.4864041,10.8845762 L32.6344458,10.7085581 C32.938308,10.3486991 33.0754106,9.88371809 33.0199862,9.39991297 C32.9645617,8.91195187 32.7209857,8.46579505 32.3347161,8.14431767 C31.4027098,7.36812705 29.8184937,6.44329897 27.5648083,6.44329897 C25.2231243,6.44329897 22.9103681,7.52654327 21.2191925,9.41555902 C19.4426923,11.4001623 18.4936698,14.1025281 18.5476357,17.0241829 C18.5906626,19.3679119 19.3554231,21.3390694 20.7597528,22.7249669 C22.2681249,24.2132972 24.4785403,25 27.152528,25 C30.2896017,25 32.2338338,24.066371 33.3124232,23.2830908 C33.7300514,22.9799486 33.9801908,22.5020108 33.9989087,21.9720009 C34.0173836,21.4397908 33.8005475,20.9180929 33.404068,20.5408765 L33.2652638,20.4086185 Z" id="Fill-9457" fill="#1890FF" fill-rule="nonzero"></path>
</g>
</g>
</svg>
\ No newline at end of file
import Vue from 'vue'
import App from '@/App.vue'
import router from '@/router'
// 字体图标
import '@/assets/fonts/TsangerYuYangT_W01_W01.ttf'
//自适应
import '@/mobile/flexible'
//初始化样式
import '@/styles/reset.css'
//引入vant组件库
import { NavBar,Tabbar, TabbarItem, Image as VanImage ,Cell, CellGroup, Icon,Search,Grid, GridItem,PullRefresh,List , Swipe, SwipeItem, Lazyload} from 'vant';
Vue.use(NavBar);
Vue.use(Tabbar);
Vue.use(TabbarItem);
Vue.use(VanImage);
Vue.use(Cell);
Vue.use(CellGroup);
Vue.use(Icon);
Vue.use(Search);
Vue.use(Grid);
Vue.use(GridItem);
Vue.use(PullRefresh);
Vue.use(List);
Vue.use(Swipe);
Vue.use(SwipeItem);
Vue.use(Lazyload);
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
// 首先是一个立即执行函数,执行时传入的参数是window和document
(function flexible (window, document) {
var docEl = document.documentElement // 返回文档的root元素
var dpr = window.devicePixelRatio || 1
// 获取设备的dpr,即当前设置下物理像素与虚拟像素的比值
// 调整body标签的fontSize,fontSize = (12 * dpr) + 'px'
// 设置默认字体大小,默认的字体大小继承自body
function setBodyFontSize () {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px'
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
// 设置root元素的fontSize = 其clientWidth / 10 + ‘px’
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
// 移动端的适配如何做
// (1): 所有的css单位, rem (vscode可以自动把px转成rem, pxtorem插件设置基准值37.5) - 1rem等于37.5px
// 原理: rem要根据html的font-size换算
// 目标: 网页宽度变小, html的font-size也要变小, ...网页变大, html的font-size变大.
// (2): flexible.js (专门负责当网页宽度改变, 会修改html的font-size)
setRemUnit()
// 当我们页面尺寸大小发生变化的时候,要重新设置下rem 的大小
window.addEventListener('resize', setRemUnit)
// pageshow 是我们重新加载页面触发的事件
window.addEventListener('pageshow', function(e) {
// e.persisted 返回的是true 就是说如果这个页面是从缓存取过来的页面,也需要从新计算一下rem 的大小
if (e.persisted) {
setRemUnit()
}
})
// 检测0.5px的支持,支持则root元素的class中有hairlines
if (dpr >= 2) {
var fakeBody = document.createElement('body')
var testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines')
}
docEl.removeChild(fakeBody)
}
}(window, document))
\ No newline at end of file
// 引入Vue
import Vue from 'vue'
// 引入路由
import VueRouter from "vue-router"
Vue.use(VueRouter)
// 引入组件
import Layout from '@/views/Layout'
import HomePage from '@/views/Home'
import SearchPage from '@/views/Search'
import Play from '@/views/Play'
import Comment from '@/views/Comment'
const routes = [
{
path: '/',
redirect:'/layout'
},
{
path: '/layout',
redirect: '/layout/home',
component: Layout,
children: [
{
path: 'home',
component: HomePage,
meta: {
title:'首页'
},
},
{
path: 'search',
component: SearchPage,
meta: {
title:'搜索'
},
}
]
},
{
path: '/play',
component:Play
},
{
path: '/comment',
component:Comment
}
]
const router = new VueRouter({ routes })
export default router
\ No newline at end of file
.app{
text-align: center;
}
.app img{
margin-top: 32px;
}
\ No newline at end of file
@cell-font-size:14px;
\ No newline at end of file
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, fieldset, input, textarea, p, blockquote, th, td {
padding: 0;
margin: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
fieldset, img {
border: 0;
}
address, caption, cite, code, dfn, em, strong, th, var {
font-weight: normal;
font-style: normal;
}
ol, ul {
list-style: none;
}
caption, th {
text-align: left;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
font-size: 100%;
}
q:before, q:after {
content: '';
}
abbr, acronym {
border: 0;
}
\ No newline at end of file
//默认请求配置
import axios from "axios";
export const request = axios.create({
method:'GET',
baseURL:'https://api.vrteam.top'
})
<template>
<div>
<van-nav-bar
title="评论"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<!-- 评论页 -->
<van-pull-refresh v-model="refreshing" success-text="刷新成功" @refresh="onRefresh">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="content">
<div v-for="hotObj in hotList" :key="hotObj.commentId" >
<div class="content-header">
<img :src="hotObj.user.avatarUrl" alt="">
<div class="author">{{hotObj.user.nickname}}</div>
<div class="agree">{{hotObj.likedCount}}人点赞</div>
</div>
<div class="content-main">
{{hotObj.content}}
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
import {getHotCommentAPI} from '@/api'
export default {
name:'Comment',
data(){
return {
loading: false,
finished: false,
refreshing: false,
page:1,
hotList:[]
}
},
methods: {
onClickLeft(){
this.$router.back()
},
async getHotList() {
const hotRes=await getHotCommentAPI({
id:this.$route.query.id,
type:0,
limit:20,
offset:(this.page-1)*20
})
this.hotList=[...this.hotList,...hotRes.data.hotComments]
// hotRes.data.hotComments.forEach(obj => {this.hotList.push(obj)});
this.refreshing=false
this.loading=false
},
// 下拉刷新
onRefresh(){
this.hotList=[]
this.page=1
this.getHotList()
},
//数据更新完毕
onLoad(){
// console.log('正在加载页数',this.page);
this.getHotList()
this.page++
this.loading=false
if(this.page>5){
this.finished=true
}
}
},
}
</script>
<style scoped>
.content-header{
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: true;
}
.content-header img{
width: 40px;
height: 40px;
border-radius:20px;
margin: 5px 10px;
}
.content-header .author{
flex: 1;
font-size: 17px;
font-weight: 700;
}
.content-header .agree{
font-size: 13px;
margin-right: 15px;
}
.content-main{
margin: 10px 15px 20px;
font-size: 14px;
text-indent: 1em;
color: #666;
word-wrap:break-word;
word-break:break-all;
}
</style>
\ No newline at end of file
<template>
<div>
<!-- 轮播图 -->
<p class="title">政治时事</p>
<van-swipe :autoplay="2000">
<van-swipe-item v-for="(image, index) in images" :key="index">
<img
width="100%"
height="150px"
:src="image"
/>
</van-swipe-item>
</van-swipe>
<!-- 推荐歌单 -->
<p class="title">推荐歌单</p>
<div>
<van-grid :border="false" :column-num="3">
<van-grid-item v-for="recommendObj in recommendList" :key="recommendObj.id">
<van-image
width="100%"
height="100"
:src="recommendObj.picUrl"
/>
<p class="song_name">{{recommendObj.name}}</p>
</van-grid-item>
</van-grid>
</div>
<!-- 最新音乐 -->
<p class="title">最新音乐</p>
<SongItem
v-for="newObj in newList"
:key="newObj.id"
:title="newObj.name"
:label="newObj.song.artists[0].name"
:id="newObj.id"
></SongItem>
</div>
</template>
<script>
import {recommendMusicAPI,newMusicAPI} from '@/api'
import SongItem from '@/components/SongItem'
export default {
name:'HomePage',
data(){
return {
recommendList:[],
newList: [],
images: [
'/swiper/first.png',
'/swiper/second.png',
'/swiper/thrid.png',
'/swiper/fourth.png',
]
}
},
async created() {
//推荐歌单
const res= await recommendMusicAPI({limit:6})
this.recommendList=res.data.result
//最新音乐
const newRes=await newMusicAPI({limit:20})
// console.log(newRes);
this.newList=newRes.data.result
},
components: { SongItem },
}
</script>
<style scoped>
.title {
padding: .2133rem 0.24rem;
background-color: #d43c33;
color: #fff;
font-size: 15px;
}
/* 推荐歌单 - 歌名 */
.song_name {
font-size: 0.346667rem;
padding: 0 0.08rem;
margin-bottom: 0.266667rem;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box; /** 对象作为伸缩盒子模型显示 **/
-webkit-box-orient: vertical; /** 设置或检索伸缩盒对象的子元素的排列方式 **/
-webkit-line-clamp: 2; /** 显示的行数 **/
overflow: hidden; /** 隐藏超出的内容 **/
}
</style>
\ No newline at end of file
<template>
<div>
<!-- 顶部导航栏 -->
<van-nav-bar
fixed
:title="activeTitle"
class="nav-title"
/>
<!-- 中间内容展示 -->
<div class="main">
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
<!-- 底部导航栏 -->
<van-tabbar route class="bottom">
<van-tabbar-item to="/layout/home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/layout/search" icon="search">搜索</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name:'Layout',
data(){
return {
}
},
computed:{
activeTitle(){
return this.$route.meta.title
}
}
}
</script>
<style scoped>
.nav-title{
background: url('../../assets/bg.jpg') no-repeat center;
}
.bottom{
padding: 15px 0;
}
.main{
padding-top: 45px;
padding-bottom: 20px;
}
</style>
\ No newline at end of file
<template>
<div class="play">
<!-- 模糊背景(靠样式设置), 固定定位 -->
<div
class="song-bg"
:style="`background-image: url(${
songInfo && songInfo.al && songInfo.al.picUrl
}?imageView&thumbnail=360y360&quality=75&tostatic=0);`"
></div>
<!-- 播放页头部导航 -->
<div class="header">
<van-icon
name="arrow-left"
size="20"
class="left-incon"
@click="$router.back()"
/>
</div>
<!-- 留声机 - 容器 -->
<div class="song-wrapper">
<!-- 留声机本身(靠css动画做旋转) -->
<div
class="song-turn ani"
:style="`animation-play-state:${playState ? 'running' : 'paused'}`"
>
<div class="song-img">
<!-- &&写法是为了防止报错, 有字段再继续往下访问属性 -->
<img
style="width: 100%"
:src="`${
songInfo && songInfo.al && songInfo.al.picUrl
}?imageView&thumbnail=360y360&quality=75&tostatic=0`"
alt=""
/>
</div>
</div>
<!-- 播放按钮 -->
<div class="start-box" @click="audioStart">
<span class="song-start" v-show="!playState"></span>
</div>
<!-- 播放歌词容器 -->
<div class="song-msg">
<!-- 歌曲名 -->
<h2 class="m-song-h2">
<span class="m-song-sname"
>{{ songInfo.name }}-{{
songInfo && songInfo.ar && songInfo.ar[0].name
}}</span
>
</h2>
<!-- 歌词部分-随着时间切换展示一句歌词 -->
<div class="lrcContent">
<p class="lrc">{{ curLyric }}</p>
</div>
</div>
<!-- 留声机 - 唱臂 -->
<div class="needle" :style="`transform: rotate(${needleDeg});`"></div>
</div>
<!-- 播放音乐真正的标签
看接口文档: 音乐地址需要带id去获取(但是有的歌曲可能404)
https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%e8%8e%b7%e5%8f%96%e9%9f%b3%e4%b9%90-url
-->
<audio
ref="audioMusic"
preload="true"
:src="`https://music.163.com/song/media/outer/url?id=${id}.mp3`"
></audio>
</div>
</template>
<script>
// 获取歌曲详情和 歌曲的歌词接口
import { getSongByIdAPI,getLyricByIdAPI } from '@/api'
import { Icon } from 'vant'
export default {
components: {
[Icon.name]: Icon,
},
name: 'play',
data() {
return {
playState: false, // 音乐播放状态(true暂停, false播放)
id: this.$route.query.id, // 上一页传过来的音乐id
songInfo: {}, // 歌曲信息
lyric: {}, // 歌词枚举对象(需要在js拿到歌词写代码处理后, 按照格式保存到这个对象)
curLyric: '', // 当前显示哪句歌词
lastLy: '' // 记录当前播放歌词
}
},
computed: {
needleDeg() { // 留声机-唱臂的位置属性
return this.playState ? '-7deg' : '-38deg'
}
},
methods: {
async getSong() { // 获取歌曲详情, 和歌词方法
const res = await getSongByIdAPI(this.id)
this.songInfo = res.data.songs[0]
// 获取-并调用_formatLyr方法, 处理歌词
const lyrContent = await getLyricByIdAPI(this.id)
const lyricStr = lyrContent.data.lrc.lyric
this.lyric = this._formatLyr(lyricStr)
// 初始化完毕先显示零秒歌词
this.curLyric = this.lyric[0]
},
_formatLyr(lyricStr) {
// 可以看network观察歌词数据是一个大字符串, 进行拆分.
let reg = /\[.+?\]/g //
let timeArr = lyricStr.match(reg) // 匹配所有[]字符串以及里面的一切内容, 返回数组
// console.log(timeArr); // ["[00:00.000]", "[00:01.000]", ......]
let contentArr = lyricStr.split(/\[.+?\]/).slice(1) // 按照[]拆分歌词字符串, 返回一个数组(下标为0位置元素不要,后面的留下所以截取)
// console.log(contentArr);
let lyricObj = {} // 保存歌词的对象, key是秒, value是显示的歌词
timeArr.forEach((item, index) => {
// 拆分[00:00.000]这个格式字符串, 把分钟数字取出, 转换成秒
let ms = item.split(':')[0].split('')[2] * 60
// 拆分[00:00.000]这个格式字符串, 把十位的秒拿出来, 如果是0, 去拿下一位数字, 否则直接用2位的值
let ss = item.split(':')[1].split('.')[0].split('')[0] === '0' ? item.split(':')[1].split('.')[0].split('')[1] : item.split(':')[1].split('.')[0]
// 秒数作为key, 对应歌词作为value
lyricObj[ms + Number(ss)] = contentArr[index]
})
// 返回得到的歌词对象(可以打印看看)
// console.log(lyricObj);
return lyricObj
},
audioStart() { // 播放按钮 - 点击事件
if (!this.playState) { // 如果状态为false
this.$refs.audioMusic.play() // 调用audio标签的内置方法play可以继续播放声音
} else {
this.$refs.audioMusic.pause() // 暂停audio的播放
}
this.playState = !this.playState // 点击设置对立状态
},
showLyric() {
// 监听播放audio进度, 切换歌词显示
this.$refs.audioMusic.addEventListener('timeupdate', () => {
let curTime = Math.floor(this.$refs.audioMusic.currentTime)
// 避免空白出现imageView
if (this.lyric[curTime]) {
this.curLyric = this.lyric[curTime]
this.lastLy = this.curLyric
} else {
this.curLyric = this.lastLy
}
})
}
},
mounted() {
this.getSong()
this.showLyric()
// console.log(this.$route.query.id);
}
}
</script>
<style scoped>
.header {
height: 50px;
}
.play {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.song-bg {
background-color: #161824;
background-position: 50%;
background-repeat: no-repeat;
background-size: auto 100%;
transform: scale(1.5);
transform-origin: center;
position: fixed;
left: 0;
right: 0;
top: 0;
height: 100%;
overflow: hidden;
z-index: 1;
opacity: 1;
filter: blur(25px); /*模糊背景 */
}
.song-bg::before{ /*纯白色的图片做背景, 歌词白色看不到了, 在背景前加入一个黑色半透明蒙层解决 */
content: " ";
background: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom:0;
}
.song-wrapper {
position: fixed;
width: 247px;
height: 247px;
left: 50%;
top: 50px;
transform: translateX(-50%);
z-index: 10001;
}
.song-turn {
width: 100%;
height: 100%;
background: url("./img/bg.png") no-repeat;
background-size: 100%;
}
.start-box {
position: absolute;
width: 156px;
height: 156px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
}
.song-start {
width: 56px;
height: 56px;
background: url("./img/start.png");
background-size: 100%;
}
.needle {
position: absolute;
transform-origin: left top;
background: url("./img/needle-ab.png") no-repeat;
background-size: contain;
width: 73px;
height: 118px;
top: -40px;
left: 112px;
transition: all 0.6s;
}
.song-img {
width: 154px;
height: 154px;
position: absolute;
left: 50%;
top: 50%;
overflow: hidden;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.m-song-h2 {
margin-top: 20px;
text-align: center;
font-size: 18px;
color: #fefefe;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.lrcContent {
margin-top: 50px;
}
.lrc {
font-size: 14px;
color: #fff;
text-align: center;
}
.left-incon {
position: absolute;
top: 10px;
left: 10px;
font-size: 24px;
z-index: 10001;
color: #fff;
}
.ani {
animation: turn 5s linear infinite;
}
@keyframes turn {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
</style>
\ No newline at end of file
<template>
<div>
<van-search
v-model="serachValue"
shape="round"
placeholder="请输入搜索关键词"
@keydown.enter="getSearchResults(serachValue)"
ref="search"
/>
<!-- 搜索下容器 -->
<div class="search_wrap" v-show="!resultList.length">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<ul class="hot_name_wrap">
<!-- 每个搜索关键词 -->
<li
class="hot_item"
v-for="(hotObj,index) in hotSearchList"
:key="index"
@click="getSearchValue(hotObj.first)"
>{{hotObj.first}}</li>
</ul>
</div>
<!-- 搜索结果 -->
<SongItem
v-show="resultList.length"
v-for="resultObj in resultList"
:key="resultObj.id"
:title="resultObj.name"
:label="resultObj.ar[0].name"
:id="resultObj.id"
></SongItem>
</div>
</template>
<script>
import {hotSearchAPI,searchResultAPI} from '@/api'
import SongItem from '@/components/SongItem'
export default {
name:'SearchPage',
data(){
return {
serachValue:'',
hotSearchList:[],
resultList:[],
timer:null,
}
},
async created() {
const hotRes=await hotSearchAPI({limit:12})
this.hotSearchList=hotRes.data.result.hots
},
methods: {
getSearchValue(value){
this.serachValue=value
clearTimeout(this.timer)
this.timer=setTimeout(async ()=>{
const serachRes=await searchResultAPI({
type:1,
keywords:this.serachValue
})
this.resultList=serachRes.data.result.songs
})
},
},
watch:{
serachValue(value){
clearTimeout(this.timer)
this.timer=setTimeout(async ()=>{
if(value==='') return this.resultList=[]
const serachRes=await searchResultAPI({
type:1,
keywords:value
})
this.resultList=serachRes.data.result.songs
},300)
//防抖 相当于setTimeout
//节流 相当于 setInterval
}
},
components:{SongItem}
}
</script>
<style scoped>
/* 深度选择器 */
.van-search >>> .van-field__control{
font-size: 14px !important;
}
/* 搜索容器的样式 */
.search_wrap {
padding: 0.266667rem;
}
/*热门搜索文字标题样式 */
.hot_title {
font-size: 0.32rem;
color: #666;
}
/* 热搜词_容器 */
.hot_name_wrap {
margin: 0.266667rem 0;
}
/* 热搜词_样式 */
.hot_item {
display: inline-block;
height: 0.853333rem;
margin-right: 0.213333rem;
margin-bottom: 0.213333rem;
padding: 0 0.373333rem;
font-size: 0.373333rem;
line-height: 0.853333rem;
color: #333;
border-color: #d3d4da;
border-radius: 0.853333rem;
border: 1px solid #d3d4da;
}
</style>
\ No newline at end of file
const { defineConfig } = require('@vue/cli-service')
const path = require('path')
const lessUrl=path.join(__dirname+'/src/styles/cover.less')
module.exports = defineConfig({
transpileDependencies: true,
//代码检查
lintOnSave: false,
css: {
loaderOptions: {
less: {
modifyVars: {
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
hack: `true; @import "${lessUrl}";`,
},
},
},
},
})
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册