{"id":15981090,"url":"https://github.com/yzh990918/mymusic","last_synced_at":"2025-07-18T19:33:47.419Z","repository":{"id":100746070,"uuid":"220614686","full_name":"yzh990918/mymusic","owner":"yzh990918","description":"一个让你意想不到的音乐app🎵🎵🎵","archived":false,"fork":false,"pushed_at":"2021-03-28T12:51:57.000Z","size":2471,"stargazers_count":4,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-06-14T08:07:55.644Z","etag":null,"topics":["ajax","json-api","lazy-loading","vue","vue-cli","vue-router","vuepress","vuex"],"latest_commit_sha":null,"homepage":"","language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yzh990918.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-09T08:40:15.000Z","updated_at":"2023-06-19T12:44:06.000Z","dependencies_parsed_at":"2023-06-09T12:45:13.148Z","dependency_job_id":null,"html_url":"https://github.com/yzh990918/mymusic","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/yzh990918/mymusic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yzh990918%2Fmymusic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yzh990918%2Fmymusic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yzh990918%2Fmymusic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yzh990918%2Fmymusic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yzh990918","download_url":"https://codeload.github.com/yzh990918/mymusic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yzh990918%2Fmymusic/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265820221,"owners_count":23833563,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ajax","json-api","lazy-loading","vue","vue-cli","vue-router","vuepress","vuex"],"created_at":"2024-10-08T00:40:49.498Z","updated_at":"2025-07-18T19:33:47.398Z","avatar_url":"https://github.com/yzh990918.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vue全家桶开发音乐app\n\n## 简介\n\n[![vue](https://img.shields.io/badge/vue-2.6.10-brightgreen.svg)](https://github.com/vuejs/vue)\n[![element-ui](https://img.shields.io/badge/element--ui-2.7.0-brightgreen.svg)](https://github.com/ElemeFE/element)\n\n\n\u003e项目是用vuecli 2.9构建 用vuecli 3.x的用户自行重构部分代码\n\n### 项目使用到的技术\n```markdown\nMVVM框架：Vue.js （版本2.x）\n状态管理：Vuex\n前端路由：Vue Router\n服务端通讯：axios、jsonp\n移动端滚动库：better-scroll\n构建工具：webpack 2.0\n源码：es6\n```\n### 项目基本目录\n\n```\n .\n├── README.md\n├── build\n│   ├── build.js\n│   ├── check-versions.js\n│   ├── dev-client.js\n│   ├── dev-server.js\n│   ├── utils.js\n│   ├── vue-loader.conf.js\n│   ├── webpack.base.conf.js\n│   ├── webpack.dev.conf.js\n│   └── webpack.prod.conf.js\n├── config\n│   ├── dev.env.js\n│   ├── index.js\n│   └── prod.env.js\n├── index.html\n├── package.json\n├── prod.server.js\n├── src\n│   ├── App.vue\n│   ├── api\n│   │   ├── config.js\n│   │   ├── rank.js\n│   │   ├── recommend.js\n│   │   ├── search.js\n│   │   ├── singer.js\n│   │   └── song.js\n│   ├── base\n│   │   ├── confirm\n│   │   │   └── confirm.vue\n│   │   ├── listview\n│   │   │   └── listview.vue\n│   │   ├── loading\n│   │   │   ├── loading.gif\n│   │   │   └── loading.vue\n│   │   ├── no-result\n│   │   │   ├── no-result.vue\n│   │   │   ├── no-result@2x.png\n│   │   │   └── no-result@3x.png\n│   │   ├── progress-bar\n│   │   │   └── progress-bar.vue\n│   │   ├── progress-circle\n│   │   │   └── progress-circle.vue\n│   │   ├── scroll\n│   │   │   └── scroll.vue\n│   │   ├── search-box\n│   │   │   └── search-box.vue\n│   │   ├── search-list\n│   │   │   └── search-list.vue\n│   │   ├── slider\n│   │   │   └── slider.vue\n│   │   ├── song-list\n│   │   │   ├── first@2x.png\n│   │   │   ├── first@3x.png\n│   │   │   ├── second@2x.png\n│   │   │   ├── second@3x.png\n│   │   │   ├── song-list.vue\n│   │   │   ├── third@2x.png\n│   │   │   └── third@3x.png\n│   │   ├── switches\n│   │   │   └── switches.vue\n│   │   └── top-\n│   │       └── top-.vue\n│   ├── common\n│   │   ├── fonts\n│   │   │   ├── music-icon.eot\n│   │   │   ├── music-icon.svg\n│   │   │   ├── music-icon.ttf\n│   │   │   └── music-icon.woff\n│   │   ├── image\n│   │   │   └── default.png\n│   │   ├── js\n│   │   │   ├── cache.js\n│   │   │   ├── config.js\n│   │   │   ├── dom.js\n│   │   │   ├── jsonp.js\n│   │   │   ├── mixin.js\n│   │   │   ├── singer.js\n│   │   │   ├── song.js\n│   │   │   └── util.js\n│   │   └── stylus\n│   │       ├── base.styl\n│   │       ├── icon.styl\n│   │       ├── index.styl\n│   │       ├── mixin.styl\n│   │       ├── reset.styl\n│   │       └── variable.styl\n│   ├── components\n│   │   ├── add-song\n│   │   │   └── add-song.vue\n│   │   ├── disc\n│   │   │   └── disc.vue\n│   │   ├── m-header\n│   │   │   ├── logo@2x.png\n│   │   │   ├── logo@3x.png\n│   │   │   └── m-header.vue\n│   │   ├── music-list\n│   │   │   └── music-list.vue\n│   │   ├── player\n│   │   │   └── player.vue\n│   │   ├── playlist\n│   │   │   └── playlist.vue\n│   │   ├── rank\n│   │   │   └── rank.vue\n│   │   ├── recommend\n│   │   │   └── recommend.vue\n│   │   ├── search\n│   │   │   └── search.vue\n│   │   ├── singer\n│   │   │   └── singer.vue\n│   │   ├── singer-detail\n│   │   │   └── singer-detail.vue\n│   │   ├── suggest\n│   │   │   └── suggest.vue\n│   │   ├── tab\n│   │   │   └── tab.vue\n│   │   ├── top-list\n│   │   │   └── top-list.vue\n│   │   └── user-center\n│   │       └── user-center.vue\n│   ├── main.js\n│   ├── router\n│   │   └── index.js\n│   └── store\n│       ├── actions.js\n│       ├── getters.js\n│       ├── index.js\n│       ├── mutation-types.js\n│       ├── mutations.js\n│       └── state.js\n└── static\n    ├── 1.png\n    ├── 2.png\n    ├── 3.png\n    ├── 4.png\n    └── 5.png\n\n\n```\n### 项目开发基本流程\n![](./images/2019-11-11-20-41-17.png)\n\n### 该项目包含到的基本组件库\n**基础组件**\n```bash\n1.confirm：确认对话框组件\n2.listview：通讯录列表组件\n3.loading：加载态组件\n4.no-result：无结果展示组件\n5.progress-bar：进度条组件\n6.progress-circle：圆形进度条组件\n7.scroll：移动端滚动组件\n8.search-box：搜索框组件\n9.search-list：搜索列表组件\n10.slider：轮播图组件\n11.switches：开关切换组件\n12.top-：顶部消息提示组件\n1.  song-list：歌曲列表组件\n```\n**业务组件**\n```bash\n1.add-song：添加歌曲到列表组件\n2.disc：歌单详情页组件\n3.m-header：页面头部组件\n4.music-list：歌曲列表页面组件\n5.player：播放器内核组件\n6.playlist：播放列表组件\n7.rank：排行榜页面组件\n8.recommend：推荐页面组件\n9.search：搜索页面组件\n10.singer：歌手页面组件\n11.singer-detail：歌手详情页组件\n12.suggest：搜索提示列表组件\n13.tab：顶部导航栏组件\n14.top-list：排行榜详情页组件\n15.user-center：用户中心页组件\n```\n\n## 初始化项目\n\n\n\u003e再开始开发项目之前，你必须要创建项目，并且安装好开发要用的依赖 插件等\n\n\n### 创建项目\n\n- 创建项目\n\n```bash\n$ vue init webpack XXXX\n```\n\n\n\u003evuecli3 开发 vue create XXXX\n\n\n### 初始化\n\n- 设计目录结构\n  ![](./images/2019-11-11-20-51-06.png)\n- 移动端初始化缩放\n\n```html\n\u003cmeta\n  name=\"viewport\"\n  content=\"width=device-width,initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no\"\n/\u003e\n```\n\n- 初始化样式 并且写好项目要用到的**icon 样式** 全局样式 背景图调用样式 等\n  \u003e  样式和字体图标的 svg 文件建议放在 common 里面\n\n  ![](./images/2019-11-11-20-56-47.png)\n  \u003cbr\u003e\n  reset.styl\n\n```stylus\n/**\n * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)\n * http://cssreset.com\n */\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video, input\n  margin: 0\n  padding: 0\n  border: 0\n  font-size: 100%\n  font-weight: normal\n  vertical-align: baseline\n\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, menu, nav, section\n  display: block\n\nbody\n  line-height: 1\n\nblockquote, q\n  quotes: none\n\nblockquote:before, blockquote:after,\nq:before, q:after\n  content: none\n\ntable\n  border-collapse: collapse\n  border-spacing: 0\n\n/* custom */\n\na\n  color: ##7e8c8d\n  -webkit-backface-visibility: hidden\n  text-decoration: none\n\nli\n  list-style: none\n\nbody\n  -webkit-text-size-adjust: none\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0)\n\n```\n\nicon.styl\n\n```stylus\n@font-face\n  font-family: 'music-icon'\n  src: url('../fonts/music-icon.eot?2qevqt')\n  src: url('../fonts/music-icon.eot?2qevqt##iefix') format('embedded-opentype'),\n          url('../fonts/music-icon.ttf?2qevqt') format('truetype'),\n          url('../fonts/music-icon.woff?2qevqt') format('woff'),\n          url('../fonts/music-icon.svg?2qevqt##music-icon') format('svg')\n  font-weight: normal\n  font-style: normal\n\n[class^=\"icon-\"], [class*=\" icon-\"]\n  /* use !important to prevent issues with browser extensions that change fonts */\n  font-family: 'music-icon' !important\n  speak: none\n  font-style: normal\n  font-weight: normal\n  font-variant: normal\n  text-transform: none\n  line-height: 1\n\n  /* Better Font Rendering =========== */\n  -webkit-font-smoothing: antialiased\n  -moz-osx-font-smoothing: grayscale\n\n.icon-ok:before\n  content: \"\\e900\"\n\n.icon-close:before\n  content: \"\\e901\"\n\n.icon-add:before\n  content: \"\\e902\"\n\n.icon-play-mini:before\n  content: \"\\e903\"\n\n.icon-playlist:before\n  content: \"\\e904\"\n\n.icon-music:before\n  content: \"\\e905\"\n\n.icon-search:before\n  content: \"\\e906\"\n\n.icon-clear:before\n  content: \"\\e907\"\n\n.icon-delete:before\n  content: \"\\e908\"\n\n.icon-favorite:before\n  content: \"\\e909\"\n\n.icon-not-favorite:before\n  content: \"\\e90a\"\n\n.icon-pause:before\n  content: \"\\e90b\"\n\n.icon-play:before\n  content: \"\\e90c\"\n\n.icon-prev:before\n  content: \"\\e90d\"\n\n.icon-loop:before\n  content: \"\\e90e\"\n\n.icon-sequence:before\n  content: \"\\e90f\"\n\n.icon-random:before\n  content: \"\\e910\"\n\n.icon-back:before\n  content: \"\\e911\"\n\n.icon-mine:before\n  content: \"\\e912\"\n\n.icon-next:before\n  content: \"\\e913\"\n\n.icon-dismiss:before\n  content: \"\\e914\"\n\n.icon-pause-mini:before\n  content: \"\\e915\"\n\n```\n\nvariable.styl\n\n```stylus\n// ! 颜色定义规范\n$color-background = ##222\n$color-background-d = rgba(0, 0, 0, 0.3)\n$color-highlight-background = ##333\n$color-dialog-background = ##666\n$color-theme = ##ffcd32\n$color-theme-d = rgba(255, 205, 49, 0.5)\n$color-sub-theme = ##d93f30\n$color-text = ##fff\n$color-text-d = rgba(255, 255, 255, 0.3)\n$color-text-l = rgba(255, 255, 255, 0.5)\n$color-text-ll = rgba(255, 255, 255, 0.8)\n// !字体定义规范\n$font-size-small-s = 10px\n$font-size-small = 12px\n$font-size-medium = 14px\n$font-size-medium-x = 16px\n$font-size-large = 18px\n$font-size-large-x = 22px\n\n```\n\nbase.styl\n\n```stylus\n@import \"variable.styl\"\n\nbody, html\n  line-height: 1\n  font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif, 'Droid Sans Fallback'\n  user-select: none\n  -webkit-tap-highlight-color: transparent\n  background: $color-background\n  color: $color-text\n```\n\nmixin.styl\n\n```stylus\n// 背景图片\nbg-image($url)\n  background-image: url($url + \"@2x.png\")\n  @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3)\n    background-image: url($url + \"@3x.png\")\n\n// 不换行\nno-wrap()\n  text-overflow: ellipsis\n  overflow: hidden\n  white-space: nowrap\n\n// 扩展点击区域\nextend-click()\n  position: relative\n  \u0026:before\n    content: ''\n    position: absolute\n    top: -10px\n    left: -10px\n    right: -10px\n    bottom: -10px\n```\n\n### 项目中需要用到的插件\n```js\n 发送ajax请求插件: \"axios\": \"^0.16.1\",\n 浏览器兼容性插件: \"babel-runtime\": \"^6.0.0\",\n 滚动屏幕插件:\"better-scroll\": \"^0.1.15\",\n 动画创建插件:\"create-keyframe-animation\": \"^0.1.0\",\n 解决移动端点击延迟300ms插件\"fastclick\": \"^1.0.6\",\n vuex相关插件\"good-storage\": \"^1.0.1\",\n \"js-base64\": \"^2.1.9\",\n 网络请求插件:\"jsonp\": \"0.2.1\",\n \"lyric-parser\": \"^1.0.1\",\n 预处理器:\"stylus\": \"^0.54.7\",\n \"stylus-loader\": \"^2.5.1\",\n \"vue\": \"^2.5.2\",\n 懒加载:\"vue-lazyload\": \"1.0.3\",\n 路由:\"vue-router\": \"^3.0.1\",\n 管理状态:\"vuex\": \"^2.3.1\"\n```\n### 路由配置\n```js\nexport default new Router({\n  routes: [{\n    // 配置默认根路径\n    path: '/',\n    redirect: '/Recommend'\n  }, {\n    path: '/Recommend',\n    name: 'Recommend',\n    component: Recommend\n  }, {\n    path: '/rank',\n    name: 'rank',\n    component: rank\n  },\n  {\n    path: '/singer',\n    name: 'singer',\n    component: singer\n  },\n  {\n    path: '/search',\n    name: 'search',\n    component: search\n  }\n  ]\n})\n```\n## 开发\n\n### Header组件\n\n![](./images/2019-11-11-21-12-54.png)\n\n结构代码\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"m-header\"\u003e\n    \u003cdiv class=\"icon\"\u003e\u003c/div\u003e\n    \u003cdiv class=\"text\"\u003eMagic music\u003c/div\u003e\n    \u003crouter-link tag=\"div\"\n                 class=\"user\"\n                 to=\"#\"\u003e\n      \u003ci class=\"icon-mine\"\u003e\u003c/i\u003e\n    \u003c/router-link\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n样式代码\n```stylus\n.m-header\n  position: relative\n  height: 44px\n  text-align: center\n  color: $color-theme\n  font-size: 0\n  .icon\n    display: inline-block\n    vertical-align: top\n    margin-top: 6px\n    margin-right: 9px\n    width: 30px\n    height: 32px\n    bg-image('logo')\n    background-size: 30px 32px\n  .text\n    display: inline-block\n    vertical-align: top\n    line-height: 44px\n    font-size: $font-size-large\n  .user\n    position: absolute\n    right: 0\n    top: 0\n    .icon-mine\n      display: block\n      padding: 12px\n      font-size: 20px\n      color: $color-theme\n```\n\n\n### Tab组件\n\n实现路由(推荐-歌手-排行-搜索)的切换 并且添加基本样式\n\u003cbr\u003e\n代码很简单 所以直接贴\n\n```vue\n\u003c!-- router-link 组成的tab栏 --\u003e\n\u003ctemplate\u003e\n  \u003cdiv class=\"tab\"\u003e\n    \u003crouter-link tag=\"div\"\n                 class=\"tab-item\"\n                 to=\"/recommend\"\u003e\n      \u003cspan class=\"tab-link\"\u003e推荐\u003c/span\u003e\n    \u003c/router-link\u003e\n    \u003crouter-link tag=\"div\"\n                 class=\"tab-item\"\n                 to=\"/singer\"\u003e\n      \u003cspan class=\"tab-link\"\u003e歌手\u003c/span\u003e\n    \u003c/router-link\u003e\n    \u003crouter-link tag=\"div\"\n                 class=\"tab-item\"\n                 to=\"/rank\"\u003e\n      \u003cspan class=\"tab-link\"\u003e排行\n      \u003c/span\u003e\n    \u003c/router-link\u003e\n    \u003crouter-link tag=\"div\"\n                 class=\"tab-item\"\n                 to=\"/search\"\u003e\n      \u003cspan class=\"tab-link\"\u003e搜索\u003c/span\u003e\n    \u003c/router-link\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript\u003e\n\nexport default {\n  name: 'tab',\n  props: [''],\n  data () {\n    return {\n\n    }\n  },\n\n  components: {},\n\n  computed: {},\n\n  beforeMount () { },\n\n  mounted () { },\n\n  methods: {},\n\n  watch: {}\n\n}\n\n\u003c/script\u003e\n\u003cstyle lang='stylus' scoped\u003e\n@import '~@/common/stylus/variable.styl'\n.tab\n  display: flex\n  height: 44px\n  line-height: 44px\n  font-size: $font-size-medium\n  /* flex布局使得不管屏幕大小如何 四个tab都平均分布 */\n  .tab-item\n    flex: 1\n    text-align: center\n    .tab-link\n      padding-bottom: 5px\n      color: $color-text-l\n    \u0026.router-link-active\n      .tab-link\n        color: $color-theme\n        border-bottom: 2px solid $color-theme\n\u003c/style\u003e\n\n```\n\n\u003e点击路由时显示的样式  vue自动会给路由链接套上一个router-link-active的class，所以只需要给这个选择器下的span添加样式即可 还要实现一个下边框哦\n\n\n### Recommend页面\n该页面由轮播 推荐歌单页面 歌单详情页面组成\n#### 轮播\n- 使用vue-awesome-swiper第三方插件构成轮播图\n轮播模板\n相关插件地址 [vue-awesome-swiper](https://github.com/surmon-china/vue-awesome-swiper)\n```js\n\u003ctemplate\u003e\n  \u003cdiv class=\"recommend\"\u003e\n    \u003cdiv class=\"recommend-content\"\u003e\n      \u003cdiv class=\"slide-wrapper\"\u003e\n        \u003cswiper :options=\"swiperOption\"\n                ref=\"mySwiper\"\u003e\n          \u003c!-- slides --\u003e\n          \u003cswiper-slide v-for=\"(item,index) of recommends\"\n                        :key=\"index\"\u003e\n            \u003ca :href=\"item.linkUrl\"\u003e\n              \u003cimg width=\"100%\"\n                   :src=\"item.picUrl\"\n                   alt=\"\"\u003e\n            \u003c/a\u003e\n          \u003c/swiper-slide\u003e\n          \u003c!-- Optional controls --\u003e\n          \u003cdiv class=\"swiper-pagination\"\n               slot=\"pagination\"\u003e\u003c/div\u003e\n        \u003c/swiper\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"recommend-list\"\u003e\n        \u003ch1 class=\"list-title\"\u003e热门歌单推荐\u003c/h1\u003e\n        \u003cul\u003e\u003c/ul\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n.....\ndata () {\n    return {\n      recommends: [],\n      swiperOption: {\n        // 使用轮播控制dots 轮回轮播 三秒自动跳转\n        pagination: '.swiper-pagination',\n        loop: true,\n        autoplay: 3000,\n        autoplayDisableOnInteraction: false\n      }\n\n    }\n```\n\n\n\u003e 在使用组件前  最好全局注册\n\n![](./images/2019-11-13-00-03-47.png)\n\n\u003cbr\u003e\n\n#### 自定义轮播组件(slider)\n**文件目录**\n\u003cbr/\u003e\n![](./images/2019-11-14-23-10-07.png)\n\n\n\u003e书写自定义组件前安装好better-scroll插件 项目依赖\n##### 第一步.写好基本dom结构和样式\n```html\n\u003ctemplate\u003e\n  \u003cdiv class=\"slider\"\n       ref=\"slider\"\u003e\n    \u003cdiv class=\"slider-group\"\n         ref=\"sliderGroup\"\u003e\n      \u003cslot\u003e\u003c/slot\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"dots\"\u003e\n      \u003cspan v-for=\"(item,index) of dots\"\n            :key=\"index\"\n            :class=\"{active:currentpageindex===index}\"\n            class=\"dots-item\"\u003e\n      \u003c/span\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n```stylus\n\u003cstyle lang='stylus' scoped\u003e\n@import '~@/common/stylus/variable.styl'\n.slider\n  min-height: 1px\n  .slider-group\n    position: relative\n    overflow: hidden\n    white-space: nowrap\n    .slide-item\n      // * 轮播图slidergroup的样式\n      float: left\n      box-sizing: border-box\n      overflow: hidden\n      text-align: center\n      a\n        display: block\n        width: 100%\n        overflow: hidden\n        text-decoration: none\n      img\n        display: block\n        width: 100%\n  .dots\n    position: absolute\n    top: 130.075px\n    right: 0\n    left: 0\n    bottom: 12px\n    text-align: center\n    font-size: 0\n    height: 8px\n    .dots-item\n      display: inline-block\n      margin: 0 4px\n      width: 8px\n      height: 8px\n      border-radius: 50%\n      background: $color-text-l\n      \u0026.active\n        width: 20px\n        border-radius: 5px\n        background: $color-text-ll\n\u003c/style\u003e\n\n```\n##### 第二步.定义好slider组件的属性\n```js\n props: {\n    loop: {\n      // *循环轮播\n      type: Boolean,\n      default: true\n    },\n    autoPlay: {\n      // *自动轮播\n      type: Boolean,\n      default: true\n    },\n    interval: {\n      // *轮播间隔\n      type: Number,\n      default: 3000\n    }\n\n```\n##### 第三步.计算出slider的宽度 然后再根据此设置sliderGroup的宽度\n```js\n _setSliderwidth (isResize) {\n      this.children = this.$refs.sliderGroup.children\n      // better-scroll 会多复制两份\n      let width = 0\n      // 拿到slide宽度\n      let sliderwidth = this.$refs.slider.clientWidth\n      // 计算slidegroup的宽度\n      for (let i = 0; i \u003c this.children.length; i++) {\n        let child = this.children[i]\n        // 添加dom样式\n        addClass(child, 'slide-item')\n        // child的宽度\n        child.style.width = sliderwidth + 'px'\n        // 总宽度\n        width += sliderwidth\n      }\n      if (this.loop \u0026\u0026 !isResize) {\n        // 循环切换 width加两倍sliderwidth\n        width += 2 * sliderwidth\n      }\n      // 设置slider-group宽度\n      this.$refs.sliderGroup.style.width = width + 'px'\n    }\n```\n\n\u003e 其中为每个slider的children添加样式时采用的是调用模块\n,通过正则表达式判断是否有该class,然后再将class添加到dom上。\n\n模块代码dom.js为\n```js\nexport function hasClass (el, className) {\n  let reg = new RegExp('(^|\\\\s)' + className + '(\\\\s|$)')\n  // 判断是否有class\n  return reg.test(el.className)\n}\n\nexport function addClass (el, className) {\n  if (hasClass(el, className)) {\n    return\n  }\n  // 空白间隔开\n  let newClass = el.className.split(' ')\n  newClass.push(className)\n  // 连接成新class\n  el.className = newClass.join(' ')\n}\n\n```\n##### 第四步.初始化slider\n\n```js\n _initSlider () {\n      this.slider = new BScroll(this.$refs.slider, {\n        scrollX: true,\n        scrollY: false,\n        // !惯性 循环 轮播间隔 可以点击\n        momentum: false,\n        snap: true,\n        snapLoop: this.loop,\n        snapThreshold: 0.3,\n        snapSpeed: 300\n      })\n```\n##### 第五步.添加下方的dots控制器\n\n```js\n data () {\n    return {\n      dots: [],\n      currentpageindex: 0\n\n    }\n  },\n\n  _initdots () {\n      this.dots = new Array(this.children.length)\n    }\n```\n##### 第六步.计算出轮播下标 实现dots的样式\n**监听slider的scrollEnd事件 然后获取下标**\n```js\n // 获取当前下标 滚动派发scrollEnd事件 \n      this.slider.on('scrollEnd', () =\u003e {\n        // pageX 当前pageindex\n        let pageIndex = this.slider.getCurrentPage().pageX\n        if (this.loop) {\n          // 默认第一个元素会添加拷贝 所以要减掉一\n          pageIndex -= 1\n        }\n        this.currentpageindex = pageIndex\n        if (this.autoPlay) {\n          //! 轮播前必须清除 不然跳到第二章图片会stop 然后因为settimeout只执行一次 所以再执行一遍_play()\n          clearTimeout(this.timer)\n          this._play()\n        }\n      })\n    },\n```\n**获取到下标后 给dom设置active样式**\n\n```html\n \u003cdiv class=\"dots\"\u003e\n      \u003cspan v-for=\"(item,index) of dots\"\n            :key=\"index\"\n            :class=\"{active:currentpageindex===index}\"\n            class=\"dots-item\"\u003e\n      \u003c/span\u003e\n    \u003c/div\u003e\n```\n##### 第七步.添加自动轮播\n**利用better-scroll的goToPage接口实现**\n```js\n_play () {\n      let pageIndex = this.currentpageindex + 1\n      if (this.loop) {\n        pageIndex += 1\n      }\n      this.timer = setTimeout(() =\u003e {\n        // x下标 y下标 每个轮播切换动画300ms 设置时间间隔\n        this.slider.goToPage(pageIndex, 0, 300)\n      }, this.interval)\n    }\n```\n\n\u003e 注意:此时的轮播图轮播到第二张图还是不会继续自动轮播,这是因为slier的autoplay是设置了setTimeout函数 所以必须在初始化时清除掉timer,然后再重新计算一遍,还需要监听到窗口变化然后重新计算宽度,传一个参数控制是否重新计算\n```js\n  if (this.autoPlay) {\n          //! 轮播前必须清除 不然跳到第二章图片会stop 然后因为settimeout只执行一次 所以再执行一遍_play()\n          clearTimeout(this.timer)\n          this._play()\n        }\n\n....\n mounted () {\n    setTimeout(() =\u003e {\n      this._setSliderwidth()\n      this._initdots()\n      this._initSlider()\n      if (this.autoPlay) {\n        this._play()\n      }\n    }, 20)\n    // 监听到窗口发生改变就重新计算宽度\n    window.addEventListener('resize', () =\u003e {\n      if (!this.slider) {\n\n      }\n      this._setSliderwidth(true)\n      this.slider.refresh()\n    })\n  },\n```\n请求recommends是一个异步请求,它加载出资源有延时,所以再slot渲染到父组件前可能recommeds是一个空数组,会导致样式添加失效\n**解决方法**:在slider组件的父dom上绑定v-if控制\n\n```html\n   \u003cdiv v-if=\"recommends.length\"\n           class=\"slider-wrapper\"\u003e\n        \u003cslider\u003e\n          \u003cdiv v-for=\"(item,index) in recommends\"\n               :key=\"index\"\u003e\n            \u003ca :href=\"item.linkUrl\"\u003e\n              \u003cimg :src=\"item.picUrl\"\n                   alt=\"\"\u003e\n            \u003c/a\u003e\n          \u003c/div\u003e\n        \u003c/slider\u003e\n      \u003c/div\u003e\n```\n**综上,slide组件算是完成了,自己写插件组件还是挺爽的,很有利于造轮子的能力提升**\n\n### 推荐歌单页面\n\n#### 基本结构开发 数据获取\ndom结构\n```html\n \u003cdiv class=\"recommend-list\"\n             ref=\"recommendscroll\"\u003e\n          \u003ch1 class=\"list-title\"\u003e热门歌单推荐\u003c/h1\u003e\n          \u003cul\u003e\n            \u003cli v-for=\"(item,index) in recommendsList\"\n                :key=\"index\"\n                class=\"item\"\u003e\n              \u003cdiv class=\"icon\"\u003e\n                \u003cimg width=\"60\"\n                     height=\"60\"\n                     v-lazy=\"item.imgurl\"\n                     alt=\"\"\u003e\n              \u003c/div\u003e\n              \u003cdiv class=\"text\"\u003e\n                \u003ch2 class=\"name\"\n                    v-html=\"item.creator.name\"\u003e\u003c/h2\u003e\n                \u003cp class=\"desc\"\n                   v-html=\"item.dissname\"\u003e\u003c/p\u003e\n              \u003c/div\u003e\n            \u003c/li\u003e\n          \u003c/ul\u003e\n        \u003c/div\u003e\n```\n这个recommendList获取的数据不是用jsonp抓取的,而是修改devserve代理,利用axios发送ajax数据获取\n代码如下:\n首先修改webpac-dev-config.js\n```js\n// !模拟数据需要引入的依赖\nvar express = require('express')\nvar axios = require('axios')\nvar app = express()\nvar apiRoutes = express.Router()\napp.use('/api', apiRoutes)\n\n  before(app) {\n            app.get('api/getRecommendlist', (req, res) =\u003e {\n                var url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'\n                axios.get(url, {\n                    headers: {\n                        referer: 'https://c.y.qq.com/',\n                        host: 'c.y.qq.com'\n                    },\n                    params: req.query()\n\n                }).then((response) =\u003e {\n                  // response是获取到的qq音乐\n                  的response\n                    res.json(response.data)\n                }).catch((e) =\u003e {\n                    console.log(e)\n                })\n            })\n        }\n```\n返回回调获取数据\n```js\nexport function getRecommendlist () {\n  const url = '/api/getRecommendlist'\n  const data = Object.assign({}, commonParams, {\n    platform: 'yqq',\n    hostUin: 0,\n    sin: 0,\n    ein: 29,\n    sortId: 5,\n    needNewCode: 0,\n    categoryId: 10000000,\n    rnd: Math.random(),\n    format: 'json'\n  })\n  return axios.get(url, {\n    params: data\n  }).then((res) =\u003e {\n    return Promise.resolve(res.data)\n  })\n}\n```\n**css样式**:运用flex布局\n```stylus\n .recommend-list\n      .list-title\n        height: 65px\n        line-height: 65px\n        text-align: center\n        font-size: $font-size-medium\n        color: $color-theme\n      .item\n        display: flex\n        box-sizing: border-box\n        align-items: center\n        padding: 0 20px 20px 20px\n        .icon\n          flex: 0 0 60px\n          width: 60px\n          padding-right: 20px\n        .text\n          display: flex\n          flex-direction: column\n          justify-content: center\n          flex: 1\n          line-height: 20px\n          overflow: hidden\n          font-size: $font-size-medium\n          .name\n            margin-bottom: 10px\n            color: $color-text\n          .desc\n            color: $color-text-d\n```\n#### scroll组件开发\n由于better-scroll组件运用起来太命令化,所以自己动手造轮子 开发scroll组件\n##### 1.定义scroll的属性\n```js\n props: {\n    probeType: {\n      // 监听滚动事件类型\n      type: Number,\n      default: 1\n    },\n    click: {\n      type: Boolean,\n      default: true\n    },\n    data: {\n      type: Array,\n      default: null\n    }\n  },\n```\n##### 2.初始化scroll并代理内置better-scroll方法\n```js\n methods: {\n    _initscroll () {\n      if (!this.$refs.wrapper) {\n        return\n      }\n      this.scroll = new BSroll(this.$refs.wrapper, {\n        probeType: this.probeType,\n        click: this.click\n      })\n    },\n    enable () {\n      // 启用better-scroll\n      this.scroll \u0026\u0026 this.scroll.enable()\n    },\n    disable () {\n      // 启用better-scroll\n      this.scroll \u0026\u0026 this.scroll.disable()\n    },\n    refresh () {\n      this.scroll \u0026\u0026 this.scroll.refresh()\n    }\n\n  },\n```\nscroll的点击事件会和fastClick的点击冲突,解决办法是在dom的父容器上加一个needsclick的class\n##### 监听到传过来的数据就重新计算高度\n```js\nwatch: {\n    data () {\n      // 监听到data变化 重新refresh计算高度\n      setTimeout(() =\u003e {\n        this.refresh()\n      }, 20)\n    }\n  }\n\n```\n\n##### 数据懒加载\n使用vue-lazyload插件\n```js\n// 传入默认图片参数\nVue.use(VueLazyload, {\n  loading: require('./common/image/default.png')\n})\n然后直接在img标签上将:src替换为v-lazy就OK了\n\n```\n解决bug:等待数据渲染dom之后才能重新计算高度 且要等到完全加载再进行计算\n示例代码\n给image添加load事件\n```js\n    loadimage () {\n      if (!this.checkLoaded) {\n        // !图片加载完 重新计算 第一张图片加载就计算高度 后面不需要重新计算\n        this.$refs.scroll.refresh()\n        this.checkLoaded = true\n      }\n    }\n\n```\n##### 实现效果\n![](./images/2019-11-17-12-41-51.png)\n\n\n\n\n### singer组件\n你通过jsonp拿到的singer数据并不是符合vue调用的jsonp格式\n你必须要封装字母表类排序的对象数组\n#### 你拿到的jsonp数据\n![](./images/2019-11-19-17-23-45.png)\n#### 你要做的事情\n- 定义一个map对象:里面包含了**热门歌手list对象**(**title,items[]**),还有**歌手list对象**(**title,items**)\n```js\nlet map={\n  hot:{\n    title:'热门，\n    items:[]\n  }\n}\n```\n- 创建Singer类,后面需要往items数组中push该对象\n```js\nexport default class Singer{\nconstructor({id,name}){\n  this.id=id\n  this.name=name\n  this.avtar=`ttps://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`\n}\n}\n\n```\n\n- 循环遍历,封装数据\n\n```js\n  list.forEach((item,index)=\u003e{\n    // 热门歌手区\n    if(index\u003c10){\n      map.hot.items.push(new Singer({\n        id:item.Fsinger_mid\n        name:item.Fsinger_name\n      }))\n    }\n    // 歌手区\n    const key=item.Findex\n    if(!map[key]){\n      map[key]={\n        title:key,\n        items:[]\n      }\n    }\n    map[key].items.push(new Singer({\n      id:item.Fsinger_mid\n      name:item.Fsinger_name\n    }))\n\n  })\n\n```\n**这样数据就会变成如下形式**\n以字母顺序排列的map对象\n![](./images/2019-11-19-18-08-15.png)\n\n#### 对数据进行排序\n \n我们只是用map对象盛满了hot对象和map[key]对象,然而vue遍历对象会是一个无序的结果,所以我们还是要将\n对象经过过排序装入数组,得到一个满意的对象数组\n\n```js\nlet hot=[]\nlet singers=[]\nfor(let key in map){\n  let value=map[key]\n  if(value.title.match(/[a-zA-Z]/)){\n    // 如果匹配的是字母\n    singers.push(value)\n  }else if(value.title === '热门'){\n    hot.push(value)\n  }\n}\n....\nreturn hot.concat(singers)\n\n```\n\n**Get到预期的效果**:\n![](./images/2019-11-19-19-57-52.png)\n\n\n\n### 实现列表和索引表\n\n#### 列表\n- dom结构：通常是scroll\u003eul\u003eli\u003eh2,ul\u003eli\u003eimg,name\n```html\n \u003cul\u003e\n      \u003cli v-for=\"(group,index) in data\"\n          :key=\"index\"\n          class=\"list-group\"\u003e\n        \u003ch2 class=\"title\"\u003e{{group.title}}\u003c/h2\u003e\n        \u003cul\u003e\n          \u003cli v-for=\"(item,index) in group.items\"\n              :key=\"index\"\n              class=\"list-group-item\"\u003e\n            \u003cimg v-lazy=\"item.avatar\"\n                 class=\"avatar\"\n                 alt=\"\"\u003e\n            \u003cspan class=\"name\"\u003e{{item.name}}\u003c/span\u003e\n          \u003c/li\u003e\n        \u003c/ul\u003e\n\n      \u003c/li\u003e\n    \u003c/ul\u003e\n\n```\n- css样式:listview相对定位,定义容器为当前屏幕,对应的list-group-item是一个flex布局 ,元素垂直居中对齐\n```stylus\n.listview\n  position: relative\n  width: 100%\n  height: 100%\n  overflow: hidden\n  background: $color-background\n  .list-group\n    padding-bottom: 20px\n    .title\n      height: 30px\n      line-height: 30px\n      padding-left: 20px\n      font-size: $font-size-small\n      color: $color-text-l\n      background: $color-highlight-background\n    .list-group-item\n      display: flex\n      align-items: center\n      padding: 20px 0 0 30px\n      .avatar\n        width: 50px\n        height: 50px\n        border-radius: 50%\n      .name\n        margin-left: 20px\n        color: $color-text-l\n        font-size: $font-size-medium\n```\n#### 索引表\n- 首先索引表里面的数据是需要截取group.title的第一个字符串 所以这里会对数组进行处理\n```js\n// 计算属性中定义\n shortcutlist () {\n      return this.data.map((group) =\u003e {\n        // *map方法 得到一个经过处理后的数组 热门区 字母索引区 substr拿第一个字符\n        return group.title.substr(0, 1)\n      })\n    }\n```\n\nArray.map(callback)方法是返回一个处理过后的数组,\nsubstr(a):取对应下标之后的元素\nsubstr(a,b):取下标为a-b的元素\nsubstring(a):截取对应下标元素\nsubstring(a,b):截取a-b下标的元素\n\n- dom结构\n\n```html\n\u003cdiv class=\"list-shortcut\"\u003e\n      \u003cul\u003e\n        \u003cli v-for=\"(item,index) of shortcutlist\"\n            :key=\"index\"\n            class=\"item\"\n            :data-index=\"index\"\n            @touchStart.stop.prevent=\"OnshortcutTouchstart\"\n            @touchStop.stop.prevent=\"OnshortcutTouchstop\"\u003e{{item}}\u003c/li\u003e\n      \u003c/ul\u003e\n\n    \u003c/div\u003e\n```\n- 对应样式:1.垂直居中 2.绝对定位靠右  3.对应容器样式\n```stylus\n  .list-shortcut\n    position: absolute\n    z-index: 30\n    right: 0\n    top: 50%\n    padding: 20px 0\n    text-align: center\n    border-radius: 10px\n    transform: translateY(-50%)\n    background: $color-background-d\n    font-family: Helvetica\n    .item\n      padding: 3px\n      line-height: 1\n      color: $color-text-l\n      font-size: $font-size-small\n```\n\n通常是使一个容器垂直方向居中的做法是：\nposiition:absolute\ntop:50%\nmargin-top:-height\n但是这是在知道容器高度的前提下，我们在不知道元素高度的前提下使用transform平移的方法来进行处理\nposiition:absolute\ntop:50%\ntransform:translateY(-50%)\n\n\n\n![](./images/2019-11-25-23-50-55.png)\n\n\n### 索引列表联动实现\n#### 功能分析\n\n- \u003ch4\u003e点击右侧shortcut区域,能够实现左侧歌手列表区的滚动到相应的标题\u003c/h4\u003e\n- \u003ch4\u003e滑动右侧的shrotcut区域,能够实现左侧滚动 左侧滚动也能映射右边的shortcut区域高亮\u003c/h4\u003e\n- \u003ch4\u003e歌手列表区有一个固定标题，帮助读者查询当前区域\u003c/h4\u003e\n\u003cbr/\u003e\n\n \njs的touchstart()和touchmove能够有效地帮助实现功能\n\n\n#### 点击滚屏实现\n 业务分析\n1. 获取到右侧索引区域的下标 触发一个touchstart事件\u003cbr/\u003e\n2. 封装原先定义好的scroll组件,添加scrollTo和scrollToElemnet事件\u003cbr/\u003e\n3. 设置两个ref,分别是scroll父dom和group子dom,调用scrollToElement Api\n\n\n#### 获取右侧索引区域下标\n  推荐\n推荐封装一个方法来获取dom中的变量的值\n\n**dom.js**\n```js\nexport function getData(el,name,val){\n// shortcut区域定义data-name变量 记录index等属性\n  const prefix='data-'\n  if(val){\n    return el.setAttribute(prefix + name,val)\n  }\n  return el.getAttribute(prefix + name)\n}\n```\n**获取到了index之后 对scroll组件代理两个滚动方法**\n```js\nmethods:{\n...\nscrollTo(){\n  // 通过apply改变this指向为this.scroll这个betterscroll的实例中 其实就是betterscroll的这个方法的参数修改为this.scroll\n  this.scroll \u0026\u0026 this.scrollTo.apply(this.scroll,argument)\n}\nscrollToElement(){\n  this.scroll \u0026\u0026 this.scrollToElement.apply(this.scroll,argument)\n}\n}\n\n```\n**调用scrollToElement事件**\n```js\nlet touchstartindex=getData(el.target,'index')\nthis.$refs.scorll.scrollToElement(this.$refs.listgroup[touchstartindex])\n```\n#### 滑动滚屏实现\n\n\n获取touchstart记录的y值和touchmove记录的y值,计算二者之间差值,然后除以锚点的高度[padding+高度],计算出锚点数量,滑动到位置的下标就等于刚开始的touchstart的下标+锚点数量\n\n\n\n```js\n onshortcutTouchstart(e){\n   let touchstartindex=getData(el.target,'index')\n   let firstTouch=e.touches[0]\n   this.touch.y1=firstTouch.pageY\n   this.touch.startindex=toustartindex\n }\n onshortcutTouchmove(e){\n  let firstTouch=e.touched[0]\n  this.touch.y2=firstTouch.pageY\n  let moveindex=Math.floor((this.touch.y2-this,touch.y1)/18)+parseInt(this.touch.startindex)\n  _scrollToElement(moveindex)\n\n }\n```\n\n此处应该连续滚屏事件 给scroll传一个probeType:3\n经过处理后就可以实现滑动右侧索引区域左侧跟着滚动了\n\n\n#### 滑动左侧区域实现右侧高亮显示\n 业务逻辑\n1. 首先要监听scroll的scroll事件,然后算出左侧滚屏的位置\n2. 算出滚屏的位置后，计算出各个group的高度，存入listheight数组中\n3. 根据这个高度数组，监听scrollY的变化，计算出currentindex的值 就是当前是哪一个title \n4. 根据currentindex确定是哪一个index需要加高亮的样式\n\n##### 步骤一 监听scroll事件\n首先要给scroll传入一个listenscroll的boolean变量，然后在初始化的时候确定是否调用监听的事件，然后向父组件传一个scroll事件\n```js\nprops:{\n...\nlistenscroll:{\n  type: Boolean,\n  default: true\n}\nmethods:{\n_initscroll(){\n  ...\n  if(this.listenscroll){\n    let me = this //这里要确保this指向vue实例\n    this.scroll.on('scroll',(pos)=\u003e{\n      me.$emit('scroll',pos)\n    })\n  }\n}\n}\n}\n\n```\n 注意\n**定义好后父组件在created钩子中定义listenscroll为true传入子组件**\n\n##### 步骤二 计算scrollY的值和各个Group的高度\n计算这两个高度之前，先想一下为什么要计算出滚屏的高度，和各个group的高度\u003cbr\u003e\n`scrollY:用来计算滚屏的位置,后面通过watch监听这个滚屏的变化就能计算出当前的title`\u003cbr\u003e\n`listheight:确定各个group的高度,一个上限 一个下限确定当前的currentindex的值`\u003cbr\u003e\n```js\nscroll组件绑定处理scroll事件\n@scroll=\"handlescroll\"\nmethods：{\n  handlescroll(pos){\n    this.scrollY=pos.y\n    // scroll.Y是一个负数 顶部部分是会大于0\n  }\n}\n\n```\n计算出各个group的高度\n```js\n_calculateheight(){\n  const singerlist=this.$refs.listgroup\n  let height=0\n  this.listheight.push(height)\n  for(let i=0 ;i\u003csingerlist.length;i++){\n    let item=singerlist[i].clientHeight\n    height+=item\n    this.listheight.push(height)\n  }\n}\n```\n##### 步骤三 监听data,scrollY计算出currentindex\n每当data变化时，重新计算高度\n```js\nwatch:{\n  data(){\n    setTimeout(()=\u003e{\n      this._calculateheight()\n    },20)\n  }\n}\n\n```\n计算currentindex之前,想一下之前做过的外卖项目\n之前计算出currentindex是这样做的:\n```js\ncurrentindex(){\n  for(let i=0;i\u003cthis.listheight.length;i++){\n    let height1=this.listheight[i]\n    let height2=this.listheight[i+1]\n    if(!height || (this.scorllY\u003e=height1 \u0026\u0026 this.scorllY\u003cheight2\u003e)){\n      return i;\n    }\n  }\n  return 0;\n}\n```\n\n但是你会发现,当滚屏滚动到顶部或者底步的时候scrollY的值可能永远取不到那个范围，会出现高亮bug，所以进行进一步改善。\n\n```js\nscrollY(newY){\n  \u003c!-- 滚屏顶部 --\u003e\n  if(newY \u003e 0){\n    this.currentindex=0\n  }\n  // 中间部分\n  for(let i=0;i\u003clistheight.length-1;i++){\n    let height1 = listHeight[i]\n        let height2 = listHeight[i + 1]\n        //  listheight的元素比索引表元素多一个 listHeight 0~23 右侧 0~22\n        if (-newY \u003e= height1 \u0026\u0026 -newY \u003c height2) { // !height2表示列表的最后一项\n          this.currentindex = i\n          this.diff = height2 + newY\n          // console.log(this.currentIndex)\n          return\n        }\n  }\n  // 底部 由于listgroup的元素比shorcut的元素多一个 所以就应该减去一个\n  this.currentindex = listheight.length-2\n}\n\n```\n这些工作做完之后,在shortcut上绑定动态样式即可显示高亮\n\n##### Fixedtitle\n\u003e 为了达到体验优化的最大化,上下滑动列表固定一个title区块,当数据加载成功时,title的值跟着list滚动而滚动,watch监听能够帮助到你\n\n- 确认业务逻辑(我们要做的就是把fixedtitle显示出来,可以通过计算属性吧fixedtitle内容计算出，然后应用在dom)\u003cbr/\u003e\ndom结构\n```html\n    \u003cdiv class=\"fixedtitle\"\n         v-show=\"fixedtitle\"\n         ref=\"fixed\"\u003e\n      \u003ch1 class=\"title\"\u003e{{fixedtitle}}\u003c/h1\u003e\n    \u003c/div\u003e\n```\n```stylus\n  .fixedtitle\n    position: absolute\n    left: 0\n    top: 0\n    width: 100%\n    .title\n      height: 30px\n      line-height: 30px\n      padding-left: 20px\n      font-size: $font-size-small\n      color: $color-text-l\n      background: $color-highlight-background\n```\n- 计算出fixedtitle的值,v-show控制显示\n```js\ncomputed:{\n  fixedtitle(){\n    if(this.scrollY\u003e0){\n      return ''\n    }\n    return this.data[this.currentindex]?this.data[this.currentindex].title:''\n  }\n}\n\n```\n  建议\n这样显示会有一个小bug,就是当滑动到标题交替处会出现标题重叠的现象，影响体验,可以通过计算出diff距离的大小将上一个title平移上去\n\n```js\n// 这里是计算滚动位置距离上限的差值\nthis.diff=height2+newY\nwatch:{\ndiff(newval){\n  let fixedTop=(newval \u003e 0 \u0026\u0026 newcal \u003ctitleHeight ) ? newval-titleHight : 0\n  if(this.fixedTop === fixedTop){\n    return \n    // 避免重复计算\n  }\n  this.fixedTop=fixedTop\n  this.$refs.fixed.style.transform='translate3d(0,`${fixedTop}px`,0)'\n}\n}\n\n```\n---------\n综上,singer的listview组件开发完成,读者可以借鉴listview里面才用的经验去处理一系列的**索引表**问题,[star](https://github.com/251205668/mymusic)别忘了\n\n\n\n\n\n\n### 歌手详情页\n\n#### :crystal_ball:封装数据\n\n 分析\n我们拿到的数据里面如下图,歌手详情页面的 hotsongs 是我们想要的数据 `name`是歌曲名 `al`专辑相关 `ar`歌曲演唱的歌手 还有就是歌手名 mvid 等等\n\n![QQ截图20191228225755.png](https://i.loli.net/2019/12/28/YfydjNwxLZvcFgi.png)\n\n\u003e 以后遇到这种需要封装数据到一个对象 就应该用工厂模式去实例一个类 然后遍历数据添加对象到初始化数组里即可\n\n创建 song 类\n\n```js\nexport default class Song {\n  constructor({ id, singer, name, album, image, mv }) {\n    this.id = id\n    this.singer = singer\n    this.name = name\n    this.album = album\n    this.image = image\n    this.mv = mv\n  }\n}\n```\n\n工厂模式创建实例函数\n\n```js\nexport function createSong(music) {\n  return new Song({\n    // 根据api修改参数\n    id: music.id,\n    singer: filterSinger(music.ar),\n    name: music.name,\n    album: music.al.name, //专辑名\n    image: music.al.picUrl,\n    mv: music.mv,\n  })\n}\n```\n\n 注意\n这里的歌手不止一个,这种情况就定义函数返回一个分隔开歌手数据的数组\n\n```js\nfunction filterSinger(singer) {\n  let ret = []\n  ret = singer.map((item) =\u003e {\n    return item.name\n  })\n  return ret.join('/')\n}\n```\n\n\n\n事先做好的事情都准备好了，开始处理数据\n\n```js\n_normalizeSong(list){\n  let ret=[]\n  list.forEach((item)=\u003e{\n    ret.push(createSong(item))\n  })\n  return ret\n}\n```\n\n~~## :mega:总结~~\n~~这样就完成了所有的封装数据任务,后续要完成的事情就是利用数据完成 singerDetail 组件,加油！~~\n\n#### :boom:详情页开发\n\n##### :minidisc:singerdetail\n\n\n首先解决上一次开发遗留下的问题:处理 song 数据然后向 list 数组中添加工厂对象 forEach 会报错,原因是请求数据是异步加载 页面刚没渲染 item 就没有值 就会报错\n\n**解决办法：**\n\n```js\n// 定义一个监控data变量\ndata(){\n  return {\n    listdetail:[],\n    node:null\n\n  }\n}\nmethods:{\n  getSingerdetail(this.singer.id).then((res)=\u003e{\n    this.node=res.data.hotsongs\n  })\n},\nwatch:{\n  node(val){\n    this.listdetail=this._normalize(val)\n  }\n}\n\n\n```\n\n`watch`监听到数据把数据置于代理变量中 然后传给子组件 就可以解决异步加载的问题\n\n数据解决完毕后,开发 dom\n\n```vue\n\u003ctemplate\u003e\n  \u003ctransition name=\"change\"\u003e\n    \u003cdiv class=\"singer-detail\"\u003e\n      \u003c!-- musiclist音乐列表组件 --\u003e\n      \u003cmusiclist\n        :songs=\"listdetail\"\n        :title=\"title\"\n        :bgImage=\"bgimage\"\n      \u003e\u003c/musiclist\u003e\n    \u003c/div\u003e\n  \u003c/transition\u003e\n\u003c/template\u003e\n```\n\n这样点击链接跳转到子路由的太干了，设置一个进场动画\n\n```stylus\n.change-enter-active,.change-leave-active\n  transition: all 0.3s\n.change-enter,.change-leave-to\n  //向由往左划 入场相反\n  transform:translate3d(100%,0,0)\n```\n\n由此联想到设置加载中的时候也用到了 translateY 设置居中显示\n总结一下页面居中技巧\n\n\n1. 已知元素的宽高,设置元素决定定位居中显示\n\n```css\nposition:absolute;\ntop:50%;\nleft:50%;\nmargin-left:-50%width\nmargin-top:-50%height\n```\n\n2. 未知宽高,设置元素居中\n\n```css\nposition:absolute;\ntop:50%\nleft:50%\ntransform:translate3d(-50%,-50%,0)\n```\n\n3. 利用 flex 布局进行垂直居中\n\n```css\ndisplay:flex\n//横轴\njustify-content:center\n纵轴\naligin-items:center\n```\n\n\n\n##### :dvd:mmusiclist\n\n`musiclist`包括`back`图标 `bg-image`背景图 `filter`蒙层 `play-wrapper`随机播放按钮 `songlist`歌曲列表组件 `loading`加载组件\ndom 结构\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"music-list\"\u003e\n    \u003cdiv class=\"back\" @click=\"back\"\u003e\n      \u003ci class=\"icon-back\"\u003e\u003c/i\u003e\n    \u003c/div\u003e\n    \u003ch1 class=\"title\" v-html=\"title\"\u003e\u003c/h1\u003e\n    \u003cdiv class=\"bg-image\" :style=\"bgimage\" ref=\"image\"\u003e\n      \u003cdiv class=\"filter\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"play-wrapper\" ref=\"playwrapper\" v-show=\"songs.length\"\u003e\n        \u003cdiv class=\"play-button\"\u003e\n          \u003ci class=\"icon-play\"\u003e\u003c/i\u003e\n          \u003cspan class=\"text\"\u003e随机播放热门歌曲\u003c/span\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"layer\" ref=\"layer\"\u003e\u003c/div\u003e\n    \u003cscroll\n      :probeType=\"probetype\"\n      :listenScroll=\"true\"\n      @scroll=\"scroll\"\n      :data=\"songs\"\n      class=\"list\"\n      ref=\"list\"\n    \u003e\n      \u003cdiv class=\"songlist-wrapper\"\u003e\n        \u003csonglist :songs=\"songs\"\u003e\u003c/songlist\u003e\n      \u003c/div\u003e\n    \u003c/scroll\u003e\n    \u003cdiv v-show=\"!songs.length\" class=\"loading-container\"\u003e\n      \u003cloading\u003e\u003c/loading\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n\u003ch3\u003e样式书写\u003c/h3\u003e\n\n```stylus\n.music-list\n  position:fixed\n  top:0\n  right:0\n  left:0\n  bottom:0\n  z-index:100\n  background-color:$color-background\n  .back\n    position:absolute\n    top 0\n    left 6px\n    z-index 50\n    .icon-back\n      display block\n      font-size $font-size-large-x\n      padding 10px\n      color $color-theme\n  .title\n    position:absolute\n    top 0\n    left 10%\n    width 80%\n    line-height 40px\n    font-size $font-size-large\n    color $color-text\n    text-align center\n    z-index 40\n    no-wrap()\n  .bg-image\n    position relative\n    width 100%\n    height 0\n    padding-top 70%\n    transform-origin top\n    background-size cover\n    .play-wrapper\n      position absolute\n      bottom 20px\n      z-index 50\n      width 100%\n      .play-button\n        box-sizing border-box\n        width 9.685rem\n        padding 7px 0\n        margin 0 auto\n        text-align center\n        border 1px solid $color-theme\n        color $color\n        .icon-play\n          display inline-block\n          vertical-align middle\n          margin-right 6px\n          font-size $font-size-medium-x\n        .text\n          display inline-block\n          vertical-aligin middle\n          font-size $font-size-small\n    .filter\n      position absolute\n      top 0\n      left 0\n      width 100%\n      height 100%\n      background rgba(7,17,27,0.4)\n  .layer\n    //除了bg-image的屏幕内容 用于计算高度\n    position relative\n    height 100%\n    background $color-background\n  .list\n    position fixed\n    top 0\n    bottom 0\n    width 100%\n    background $color-background\n    .songlist-wrapper\n      padding 20px 30px\n  .loading-container\n    position absolute\n    width 100%\n    top 60%\n    transform translateY(-50%)\n\n```\n\n样式书写完成后,开始书写逻辑部分\n\n- bgimage 用 computed 获取\n\n```js\ncomputed:{\n  bgimage() {\n    return `background-image:url(${this.bgimage})`\n  }\n}\n```\n\n- soglist 开发\n\n直接贴代码,比较简单\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"songlist\"\u003e\n    \u003cul\u003e\n      \u003cli v-for=\"(item, index) in songs\" :key=\"index\" class=\"item\"\u003e\n        \u003cdiv class=\"content\"\u003e\n          \u003ch2 class=\"name\"\u003e\n            {{ item.name }}\n          \u003c/h2\u003e\n          \u003cp class=\"desc\"\u003e{{ getdesc(item) }}\u003c/p\u003e\n        \u003c/div\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript\u003e\nexport default {\n  name: 'songlist',\n  props: {\n    songs: {\n      type: Array,\n      // default: []\n    },\n  },\n  data() {\n    return {}\n  },\n\n  components: {},\n\n  computed: {\n    desc() {\n      return this.songs.singer + this.songs.album\n    },\n  },\n\n  beforeMount() {},\n\n  mounted() {\n    console.log(this.songs)\n  },\n\n  methods: {\n    getdesc(song) {\n      return `${song.singer}-${song.album}`\n    },\n  },\n\n  watch: {},\n}\n\u003c/script\u003e\n\u003cstyle lang=\"stylus\" scoped\u003e\n@import '~@/common/stylus/variable.styl'\n@import '~@/common/stylus/mixin.styl'\n.songlist\n  .item\n    font-size: $font-size-medium\n    box-sizing: border-box\n    align-items: center\n    display: flex\n    height: 64px\n    .content\n      line-height: 20px\n      overflow: hidden\n      .name\n        no-wrap()\n        color: $color-text\n      .desc\n        no-wrap()\n        margin-top: 4px\n        color: $color-text-d\n\u003c/style\u003e\n```\n\n- 滚动 songlist 设置\n\n1. 向 scroll 组件传值(listenScroll:true,probeType:3 监听 scroll 事件,data 数据)\n\n```js\ndata() {\n  return {\n    proveType:3,\n    scrollY:0\n  }\n},\ncreated(){\n  this.listenScroll=true\n}\n```\n\n2. 设置 songlist 滚动到一定位置不动 且文字盖过图片 上面留一个 bg-image 一小部分\n    实现想法\n   先发现图片会消失不见 dom 方法获取到 bg-image 的高度 然后给 songlist 设置高度\n   通过监听到`scroll`事件 获取到实时的位置 然后监听`scrollY`的值 固定最大偏移量 然后监听过程中拿到偏移量 设置`layer`的向上偏移 layer 的作用(这时需要一个位于歌单列表下方的图层（`bg-layer`），当歌单列表向上滑动的时候 filter 也跟着向上滑动，**盖住下方的图像**) 然后 如果到达到顶部(`val\u003ctranslateY`)时 设置图像区域的高度为 40px\n   其他情况就和之前相似 这个过程中 bg-image 的 index 都是 0 如果是顶部位置 就是 10\n   \n   这个讲起来有点复杂 接下来一步一步来\n\n- 监听 scroll\n\n```js\nscroll(pos){\n  this.scrollY=pos.y\n}\n```\n\n- 设置图片的高度撑开\n\n```js\nmounted() {\n  this.imageHeight=this.$refs.image.clientHeight\n  //获取组件的dom 后面$el\n  this.$refs.list.$el.style.top=`${this.imageHeight}px`\n}\n```\n\n- layer 偏移一定量 固定不动\n\n```js\nthis.minTranslateY= -this.imageHeight +TITLE_HEIGHT\nwatch() {\n  scrollY(val){\n    let zIndex=0\n    let translateY=Math.max(this.minTranslateY,val)\n    this.$refs.layer.style['transform']=`translate3d(0,${translateY}px,0)`\n    this.$refss.layer.style['webkitTransform']=`translate3d(0,${translate3Y}px,0)`\n\n    if(val \u003c translateY){\n      \u003c!-- 顶部 --\u003e\n      z-index = 10\n      this.$refs.image.style.paddingTop = 0\n      this.$refs.image.style.height = '40px'\n      this.$refs.playwrapper.style.display='none'\n    }else {\n      this.$refs.image.style.paddingTop = '7-%'\n      this.$refs.image.style.height = 0\n      this.$refs.playwrapper.style.display=''\n    }\n    this.$refs.image.index = zIndex\n\n  }\n}\n```\n\n- 实现向下拉图片 图片放大效果\n\n```js\nlet scale=1.0\nconst present=Math.abs(val / this.imageHeight) //这样就可以是图片层的高度变为1+放大的高度\nif(val \u003e 0){\n  scale=1 + present\n  z-Index = 10\n}\n\n····\nthis.$refs.image.style[`transform`]=`scale(${scale})`\n```\n\n综上 歌手详情页就开发完成 使用到了 scroll 组件 根据高度盖住图层 知道了 index 的重要性 后面开发开发中最难的组件-播放起组件 加油！！\n\n### 播放器组件\n\n#### 定义播放器底层数据\n开发前先思考 播放器可能需要这几个数据 \n- playing 暂停或者播放\n- fullScreen 控制播放器是否全屏显示\n- playlist 播放列表\n- sequenceList 顺序播放模式歌曲列表\n- mode 播放模式\n- currentIndex 当前播放歌曲的下标(可以通过此下标得到currentSong:state.playlist[state.currentIndex])\n\nOK,我们开始书写vuex相关\n 定义公共配置\nconfig.js\n```js\nexport const playMode ={\n  sequence:0,\n  loop:1,\n  random:2\n}\n```\n\n\u003ch3\u003estate.js\u003c/h3\u003e\n\n```js\nimport {playMode} from 'config.js'\nconst state ={\n  singer:{},\n  playing:false,\n  fullScreen:false,\n  playlist:[],\n  sequencelist:[],\n  mode:playMode.sequence,\n  currentIndex:-1\n}\nexport default state\n```\n\u003ch3\u003emutations-type 定义函数名\u003c/h3\u003e\n\n```js\nexport const SET_PLAYING='SET_PLAYING'\nexport const SET_FULLSCREEN='SET_FULLSCREEN'\nexport const SET_PLAYLIST='SET_PLAYLIST'\nexport const SET_SEQUENCE='SET_SEQUENCE'\nexport const SET_MODE='SET_MODE'\nexport const SET_CURRENTINDEX='SET_CURRENTINDEX'\n```\n\u003ch3\u003emutation.js 修改数据\u003c/h3\u003e\n\n```js\nconst mutations ={\n  [types.SET_PLAYING](state,flag) {\n    state.playing=flag\n  },\n  [types.SET_FULLSCREEN](state,flag) {\n    state.fullScreen=flag\n  },\n  [types.SET_PLAYLIST](state,list) {\n    state.playlist=list\n  },\n  [types.SET_SEQUENCE](state,list) {\n    state.sequence=list\n  },\n  [types.SET_MODE](state,mode) {\n    state.mode=mode\n  },\n  [types.SETCURRENTINDEX](state,index) {\n    state.currentIndex=index\n  }\n}\nexport default mutations\n```\n\n\u003ch3\u003egetters.js\u003c/h3\u003e\n\n```js\n+export const playing = state =\u003e state.playing\n\nexport const fullScreen = state =\u003e state.fullScreen\n\nexport const playlist = state =\u003e state.playlist\n\nexport const sequencelist = state =\u003e state.sequencelist\n\nexport const mode = state =\u003e state.mode\n\nexport const currentIndex = state =\u003e state.currentIndex\n\nexport const currentSong = (state) =\u003e {\n  return state.playlist[state.currentIndex] || {}\n}\n```\n\n#### 定义完数据之后先把play组件的基本代码书写 \n**基本dom结构**\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"player\" v-show=\"playlist.length\"\u003e\n    \u003ctransition name=\"normal-player\"\u003e\n\u003cdiv class=\"normal-player\" v-show=\"fullScreen\"\u003e\n  \u003c!-- 背景图 --\u003e\n  \u003cdiv class=\"background\"\u003e\n    \u003cimg width=\"100%\" height=\"100%\" :src=\"currentSong.image\" alt=\"\"\u003e\n  \u003c/div\u003e\n  \u003c!-- 顶部标题 --\u003e\n  \u003cdiv class=\"top\"\u003e\n    \u003cdiv class=\"back\" @click=\"back\" \u003e\n      \u003ci class=\"icon-back\" \u003e\u003c/i\u003e\n    \u003c/div\u003e\n    \u003ch1 class=\"title\" v-html=\"currentSong.name\"\u003e\n    \u003c/h1\u003e\n    \u003ch2 class=\"subtitle\" v-html=\"currentSong.singer\" @click=\"tosinger\"\u003e\u003c/h2\u003e\n  \u003c/div\u003e\n  \u003c!-- 中间部分cd唱片 歌曲歌词 --\u003e\n  \u003cdiv class=\"middle\"\u003e\n    \u003cdiv class=\"middle-l\"\u003e\n      \u003cdiv class=\"cd-wrapper\"\u003e\n        \u003cdiv class=\"cd\" \u003e\n          \u003cimg class=\"image\" :src=\"currentSong.image\" alt=\"\"\u003e\n        \u003c/div\u003e\n        \u003cdiv class=\"play-lyric-wrapper\"\u003e\n          \u003cdiv class=\"playing-lyric\"\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"middle-r\"\u003e\n      \u003cdiv class=\"lyric-wrapper\"\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  \u003c!-- 底部进度条 歌曲操作 --\u003e\n  \u003cdiv class=\"bottom\"\u003e\n    \u003c!-- 滑块条 --\u003e\n    \u003cdiv class=\"dot-wrapper\"\u003e\n    \u003c/div\u003e\n    \u003c!-- 进度条 --\u003e\n    \u003cdiv class=\"progress-wrapper\"\u003e\n      \u003cdiv class=\"progress-bar-wrapper\"\u003e\u003c/div\u003e\n    \u003c/div\u003e\n    \u003c!-- 操作区 --\u003e\n    \u003cdiv class=\"operators\"\u003e\n      \u003cdiv class=\"icon i-left\"\u003e\n        \u003ci class=\"icon-sequence\"\u003e\u003c/i\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"icon i-left\"\u003e\n        \u003ci class=\"icon-prev\"\u003e\u003c/i\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"icon i-center\"\u003e\n        \u003ci class=\"icon-play\"\u003e\u003c/i\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"icon i-right\"\u003e\n        \u003ci class=\"icon-next\"\u003e\u003c/i\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"icon i-right\"\u003e\n        \u003ci class=\"icon icon-not-favorite\"\u003e\u003c/i\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n \u003c/transition\u003e\n \u003ctransition name=\"min-player\"\u003e\n\u003cdiv class=\"mini-player\" @click=\"open\" v-show=\"!fullScreen\"\u003e\n  \u003cdiv class=\"image\"\u003e\n    \u003cimg class=\"\"  width=\"40\" height=\"40\" :src=\"currentSong.image\"  \u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"text\"\u003e\n    \u003ch2 class=\"name\" v-html=\"currentSong.name\"\u003e\u003c/h2\u003e\n    \u003cp class=\"singername\" v-html=\"currentSong.singer\"\u003e\u003c/p\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"control\"\u003e\n    \u003ci class=\"icon-mini icon-play-mini\"\u003e\u003c/i\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"control\"\u003e\n    \u003ci class=\"icon-playlist\"\u003e\u003c/i\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n \u003c/transition\u003e\n\u003caudio autoplay muted ref=\"audio\" @canplay=\"getDuration\" :src=\"songsUrl\"\u003e\u003c/audio\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n```\n\u003e plaer分为全屏播放器和迷你播放器,全屏播放器里面有(模糊背景图 顶层(back图标 歌曲名 歌手名) 中间层(唱片图 歌词区 右划歌词区 进度条 dot区) 底层(操作区)) 迷你播放器(左(歌曲图) 中间(歌曲名 歌手名) 右边(歌曲暂停播放图标 进度 歌曲列表) )\n\n\n- 迷你播放器flex布局最好,横轴改为纵轴 垂直方向居中 然后中间desc flex:1 其他都分配好宽度最好  \n- 全屏播放器建议cd和操作区的样式难些 多练习 \n\n\n```stylus\n.player\n  .normal-player\n    position fixed\n    left 0\n    right 0\n    bottom 0\n    top 0\n    z-index 150\n    background $color-background\n    .background\n      //高斯模糊背景图\n      position absolute\n      left 0\n      top 0\n      width 100%\n      height 100%\n      z-index -1\n      opacity 0.6\n      filter:blur(20px)\n    .top\n      position relative\n      margin-bottom 25px\n      .back\n        position absolute\n        left 6px\n        top 0\n        z-index 50\n        .icon-back\n          padding 9px\n          display block\n          font-size $font-size-large-x\n          color $color-theme\n          transform rotate(-90deg)\n      .title\n        width 70%\n        margin 0 auto\n        line-height 40px\n        text-align center\n        no-wrap()\n        font-size #font-size-large\n        color $color-text\n      .subtitle\n        line-height 20px\n        text-align center\n        font-size $font-size-medium\n        color $color-text\n    .middle\n      position fixed\n      wdith 100%\n      top 80px\n      bottom 170px\n      white-space nowrap\n      font-size 0\n      .middle-l\n        display inline-block\n        vertical-align top\n        position relative\n        width 100%\n        height 0\n        padding 80%\n        .cd-wrapepr\n          position absolute\n          left 10%\n          top 0\n          width 100%\n          height 100%\n          .cd\n            width 100%\n            height 100%\n            box-sizing border-box\n            border 10px solid rgba(255,255,255,0.1)\n            border-radius 50%\n            .image\n              position absolute\n              left 0\n              top 0\n              width 100%\n              height 100%\n              border-radius 50%\n               .playing-lyric-wrapper\n            width: 80%\n            margin: 30px auto 0 auto\n            overflow: hidden\n            text-align: center\n            .playing-lyric\n              height: 20px\n              line-height: 20px\n              font-size: $font-size-medium\n              color: $color-text-l\n        .middle-r\n          display: inline-block\n          vertical-align: top\n          width: 100%\n          height: 100%\n          overflow: hidden\n          .lyric-wrapper\n            width: 80%\n            margin: 0 auto\n            overflow: hidden\n            text-align: center\n            .text\n              line-height: 32px\n              color: $color-text-l\n              font-size: $font-size-medium\n              \u0026.current\n                color: $color-text\n      .bottom\n        position: absolute\n        bottom: 50px\n        width: 100%\n        .dot-wrapper\n          text-align: center\n          font-size: 0\n          .dot\n            display: inline-block\n            vertical-align: middle\n            margin: 0 4px\n            width: 8px\n            height: 8px\n            border-radius: 50%\n            background: $color-text-l\n            \u0026.active\n              width: 20px\n              border-radius: 5px\n              background: $color-text-ll\n        .progress-wrapper\n          display: flex\n          align-items: center\n          width: 80%\n          margin: 0px auto\n          padding: 10px 0\n          .time\n            color: $color-text\n            font-size: $font-size-small\n            flex: 0 0 30px\n            line-height: 30px\n            width: 30px\n            \u0026.time-l\n              text-align: left\n            \u0026.time-r\n              text-align: right\n          .progress-bar-wrapper\n            flex: 1\n        .operators\n          display: flex\n          align-items: center\n          .icon\n            flex: 1\n            color: $color-theme\n            \u0026.disable\n              color: $color-theme-d\n            i\n              font-size: 30px\n          .i-left\n            text-align: right\n          .i-center\n            padding: 0 20px\n            text-align: center\n            i\n              font-size: 40px\n          .i-right\n            text-align: left\n          .icon-favorite\n            color: $color-sub-theme\n      \u0026.normal-enter-active, \u0026.normal-leave-active\n        transition: all 0.4s\n        .top, .bottom\n          transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)\n      \u0026.normal-enter, \u0026.normal-leave-to\n        opacity: 0\n        .top\n          transform: translate3d(0, -100px, 0)\n        .bottom\n          transform: translate3d(0, 100px, 0)\n    .mini-player\n      display: flex\n      align-items: center\n      position: fixed\n      left: 0\n      bottom: 0\n      z-index: 180\n      width: 100%\n      height: 60px\n      background: $color-highlight-background\n      \u0026.mini-enter-active, \u0026.mini-leave-active\n        transition: all 0.4s\n      \u0026.mini-enter, \u0026.mini-leave-to\n        opacity: 0\n      .icon\n        flex: 0 0 40px\n        width: 40px\n        padding: 0 10px 0 20px\n        img\n          border-radius: 50%\n          \u0026.play\n            animation: rotate 10s linear infinite\n          \u0026.pause\n            animation-play-state: paused\n      .text\n        display: flex\n        flex-direction: column\n        justify-content: center\n        flex: 1\n        line-height: 20px\n        overflow: hidden\n        .name\n          margin-bottom: 2px\n          no-wrap()\n          font-size: $font-size-medium\n          color: $color-text\n        .desc\n          no-wrap()\n          font-size: $font-size-small\n          color: $color-text-d\n      .control\n        flex: 0 0 30px\n        width: 30px\n        padding: 0 10px\n        .icon-play-mini, .icon-pause-mini, .icon-playlist\n          font-size: 30px\n          color: $color-theme-d\n        .icon-mini\n          font-size: 32px\n          position: absolute\n          left: 0\n          top: 0\n```\n在写两个交互动画\n打开normal播放器或者mini播放器\n```stylus\n.normal-player-enter-active,.normal-player-leave-active\n  translate all 0.4s\n  .top,.bottom\n  //贝赛尔曲线定义环动 \n    transition all 0.4s cubic-bezier(0.86,0.18,0.82,1.32)\n.normal-player-enter,.normal-player-leave-active\n  opcity 0\n  //定义top初始向上100px bottom初始向下100px 动画自下而上 自上而下 渐隐渐现\n  .top\n    transform translate3d(0,-100px,0)\n  .bottom \n    transform translate3d(0,100px,0)\n\n\u0026.min-player-enter-active,\u0026.min-player-leave-active\n  transition all 0.4s\n\u0026.min-player-enter,\u0026.min-player-leave-active\n  opcity 0\n```\n#### 逻辑部分书写\n\n书写完player组件,由于他跟路由没啥关系,而且很多组件会用到,所以定义在app.vue中，莫忘记注册\n\n我们需要控制player的显示与隐藏,不然会盖住singer-detail\n\n```js\ncomputed:{\n  ...mapGetters([\n    'fullScreen',\n    'playlist',\n    'currentSong'\n  ])\n}\n \u003cdiv class=\"player\" v-show=\"playlist.length \u003e 0\"\u003e\n \u003cdiv class=\"normal-player\" v-show=\"fullScreen\"\u003e\n \u003cdiv class=\"mini-player\" v-show=\"!fullScreen\"\u003e     \n```\n这样就不会默认显示了,接下来我们去写songlist的事件\n```vue\n\u003cul\u003e\n      \u003cli @click=\"selectItem(song, index)\" v-for=\"(song, index) in songs\" :key=\"index\" class=\"item\"\u003e\n          \u003cdiv class=\"content\"\u003e\n              \u003ch2 class=\"name\"\u003e{{song.name}}\u003c/h2\u003e\n              \u003cp class=\"desc\"\u003e{{getDesc(song)}}\u003c/p\u003e\n          \u003c/div\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \n  .....\n  methods:{\n    selectItem(item,index) {\n      this.$emit('select',item,index)\n    }\n  }\n```\n父组件music-list接受事件 因为这里我们需要将点击的歌曲信息传入vuex并修改 发现需要修改多个mutation，所以定义action.js\n\n```js\nimport * as types from './mutations-type'\nexport const selectPlay = function ({commit, state}, {list, index}) {\n  commit(types.SET_SEQUENCE, list)\n  commit(types.SET_PLAYLIST, list)\n  commit(types.SET_CURRENTINDEX, index)\n  commit(types.SET_FULLSCREEN, true)\n  commit(types.SET_PLAYING, true)\n}\n```\n定义好了批量操作 传入数据修改mutation\n```js\n@select=\"selectItem\"\n...\nmethods:{\n  ...mapActions([\n    'selectplay'\n  ])\n  selectItem(item,index) {\n  this.selectPlay(\n    {\n      list:this.songs,\n      index\n    }\n  )\n\n  }\n}\n```\n\n接下来取出currentSong 挂到dom上,这里不详细介绍。\n我们在点击歌曲后需要将fullScreen置为true,但是不能直接去修改getters的数据,需要进行mutations修改\n\n```js\n...mapMutations(\n{\n  setfullScreen:'SET_FULLSCREEN'\n})\n\nmethods:{\n  back() {\n    this.setfullScreen(false)\n  }\n  \n  play() {\n    this.setfullScreen(true)\n  }\n}\n```\n这样就可以图标控制隐藏显示 或者迷你播放器打开 添加点击事件即可 下一节将介绍播放器核心部分\n\n\n#### 播放器飞入左下角动画\n\nvue的js钩子可以帮助我们实现这个动画\n- @enter=\"\"\n- @after-enter=\"\"\n- @leave=\"\"\n- @after-leave=\"\"\n\n\n首先引入第三方创建动画库`create-keyframe-animation`\n具体思路:\n1. 计算两个圆心的坐标,根据坐标计算出水平竖直偏移量\n2. 定义并使用动画\n3. 清除动画\n\n\u003ch3\u003e定义函数计算出图片之间偏移量和缩放\u003c/h3\u003e\n\n```js\n_getPosAndScale () {\n  // * 要做一个大图片从mini播放器飞到大图层cdWrapper的动画 使用到create-keyframe-animation第三方插键\n  // ! 思路：1.计算出偏移横纵坐标 2.横轴偏移(屏幕宽度/2-min播放器左侧偏移) 3.纵轴偏移(屏幕高度-大图层paddingTop-min播放器圆心距底部位置-cdwrapper的高度/2) 4.书写动画\n  const targetWidth = 40//图片宽度\n  const paddingLeft = 40//mini圆心左翩移\n  const paddingBottom = 30//底部距圆心距离\n  const paddingTop = 80 //顶部距圆心距离\n  const width = window.innerWidth * 0.8 //整个cd-wrapper的宽度\n  const scale = targetWidth / width//原始缩放比例\n   const x = -(window.innerWidth / 2 - paddingLeft)\n      const y = window.innerHeight - paddingTop - width / 2 - paddingBottom\n      return {\n        x,\n        y,\n        scale\n      }\n\n}\n```\n\u003ch3\u003e定义动画\u003c/h3\u003e\n\n```js\n//import之后\n//进入时\nenter(el,done) {\n  const {x,y,scale} =this._getPosAndScale()\n  // 定义动画\n  let animation = {\n    // 底部飞到大图片 然后从60到100 scale先1.1然后回调\n    0：{\n      transform:`translate3d(${x}px,${y}px,scale(${scale}))`\n    },\n    60: {\n      transform: `translate3d(0,0,0) scale(1.1)`\n\n    },\n    100: {\n      transform :`translate3d(0,0,0) scale(1)`\n  }\n}\n// 注册animation\nanimations.registerAnimation({\n  name:'move',\n  animation, //定义好的动画\n  presets: {// 预设参数\n  duration:400,\n  easing:'linear'\n  } \n}),\n// 运行animation\nanimations.runAnimation(this.$refs.cdWrapper,'move',done)\n}\n\n// 进入后清除\nafterEnter () {\n  // 清除animation\n  animations.unregisterAnimation('move') \n  this.$refs.cdWrapper.style.animation = ''\n}\n```\n关闭窗口时 原理也是相似的\n\n```js\nleave (el,done) {\n  const {x, y, scale} = this._getPosAndScale()\n  this.$refs.cdWrapper.style['transform'] = `translate3d(${x}px,${y}px,0) scale(${scale})`\n  this.$refs.cdWrapper.style.transition = 'all 0.4s'\n  // 监听transitionend事件 之后执行afterLeave\n  this.$refs.cdWrapper.addEventListener('transitionend', done)\n\n  //清除transition transform\n   afterLeave () {\n      this.$refs.cdWrapper.style['transform'] = ''\n      this.$refs.cdWrapper.style.transition = ''\n    },\n```\n这样cd的打开关闭都会有一个效果不错的动画效果\n\n\n\n### 💙播放器部分\n\n歌曲的播放功能,主要是利用 html5 的 audio 标签实现\n\n```vue\n\u003caudio\n  autoplay\n  muted\n  ref=\"audio\"\n  @canplay=\"getDuration\"\n  @error=\"error\"\n  :src=\"songsUrl\"\n  @timeupdate=\"UpdateTime\"\n\u003e\u003c/audio\u003e\n```\n\n#### 💜获取歌曲 url\n\n\n由于这个 webapp 使用到的 api 是网易云的 api，所以这里我们需要调用获取歌曲的 url\n\n\n```js\nexport function getMusic(musicId) {\n  const url = HOST + `/music/url?id=${musicId}`\n  return axios.get(url)\n}\n```\n\n**play 组件里面调用 api**\n\n```js\n currentSong (newSong, oldSong) {\n  //  如果两首歌曲的id都一样 就不刷新currentSong\n      if (newSong.id === oldSong.id) {\n        return\n      }\n      getMusic(this.currentSong.id).then((res) =\u003e {\n        this.songsUrl = res.data.data[0].url\n      })\n      // 如果有歌词在播放 这个Lyric对象停止播放\n      if (this.currentLyric) {\n        this.currentLyric.stop()\n      }\n      this._getLyric(this.currentSong.id)\n    },\n```\n\n#### 💚播放器逻辑部分\n\n**之后当歌曲 ready 的时候，才能点下一首歌，设置一个标志位控制加载成功**\n\n```js\ndata () {\n  return {\n    songReady: false\n  }\n}\n```\n\n**添加一个@canPlay 函数 当其 songReady 为 true 才能**\n\n```js\n   getDuration () {\n      // 获取到歌曲\n      this.duration = this.$refs.audio.duration\n      this.songsTime = this.$refs.audio.duration\n      // 可以播放 songReady置为true\n      this.songReady = true\n    }\n```\n\n**写好了 songReady 之后,回到 next 和 prev 函数控制歌曲的上一首下一首 点击播放歌曲 togglePlaying()**\n\n```js\nnext () {\n  if(!this.songReady) {\n    return\n  }\n  let index = this.currentIndex + 1\n  if(index === this.playlist.length) {\n    // 当切到最后一首歌\n    index = 0\n  }\n  this.setCurrentIndex(index)\n  // 切换到写一首歌时保证播放状态\n  if(!this.playing) {\n    this.togglePlaying()\n    this.songReady = false //重置\n  }\n....\nprev () {\n  if(!this.songReady) {\n    return\n  }\n  let index = this.currentIndex - 1\n  if(index === -1) {\n    // 第一首歌 index切换到最后一首歌的index\n    index = this.playlist.length\n  }\n  this.currentIndex(index)\n  if(!this.playing) {\n    this.togglePlaying()\n    this.songReady = false\n  }\n}\n....\ntoggleplaying () {\n   if (!this.songReady) {\n        return\n      }\n      this.setPlaying(!this.playing)\n      if (this.currentLyric) {\n        this.currentLyric.togglePlay()\n      }\n}\n}\n\n```\n\n**当按钮不能点击时 就是歌曲没有 songReady 的时候给按钮一个 disable 属性**\n\n```js\ndisableCls () {\n  return this.songReady ? '':'disable'\n}\n```\n\n**播放器进度条 dom**\n\n```vue\n\u003c!-- 进度条 --\u003e\n\u003cdiv class=\"progress-wrapper\"\u003e\n      \u003cspan class=\"time time-l\"\u003e{{format(currentTime)}}\u003c/span\u003e\n      \u003cdiv class=\"progress-bar-wrapper\"\u003e\n        \u003cprogressbar :currentTime=\"currentTime\" :songsTime=\"songsTime\" :precent=\"precent\" @percentChangeEnd=\"onProgressBarChange\"\u003e\u003c/progressbar\u003e\n      \u003c/div\u003e\n      \u003cspan class=\"time time-r\"\u003e{{format(songsTime)}}\u003c/span\u003e\n    \u003c/div\u003e\n```\n\n**歌曲在播放时会派发 timeupdate 事件**\n\n```js\nupdateTime (e) {\n  this.currentTime = e.target.currentTime\n}\n```\n\n**但是这个时间是小数点的 需要进行格式化**\n\n```js\nformat (inteval) {\n  inteval = inteval | 0\n  let minute = inteval /60 | 0\n  let second = this._pad(inteval % 60)\n  return `${minute}:${second}`\n}\n\n// 当秒的字符串长度小于于2 就在前面补0\n_pad(num,n=2) {\n  let len = num.toString().length\n  while(len \u003c 2) {\n    num = '0' + num\n    len++\n  }\n  return num\n}\n\n```\n\n调用格式化时间 然后挂在 dom 上\n\n\u003ch3\u003e进度条组件\u003c/h3\u003e\n在play组件上的样式\n\n\u003e flex布局,垂直居中,水平也居中,上下有两个padding 左右span文字对齐方式不同\n\n```stylus\n.progress-wrapper\n  display flex\n  align-items center\n  width 80%\n  margin 0 auto\n  padding 10px 0\n  .time\n    color:$color-text\n    font-size $font-size-small\n    flex 0 0 30px\n    line-height 1.875rem\n    width 1.875rem\n    \u0026.time-l\n      text-align left\n    \u0026.time-r\n      text-align right\n  .progress-bar-wrapper\n    flex 1\n```\n\nprogress-bar 组件\n\ndom 结构：\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"progress-bar\" ref=\"progressbar\" @click=\"progressClick\"\u003e\n    \u003c!-- 总进度条 --\u003e\n    \u003cdiv class=\"bar-inner\"\u003e\n      \u003c!-- 进度条左侧 --\u003e\n      \u003cdiv class=\"progress\" ref=\"progress\"\u003e\u003c/div\u003e\n      \u003c!-- 进度条按钮 --\u003e\n      \u003cdiv class=\"progress-btn-wrapper\"\u003e\n        \u003cdiv\n          class=\"progress-btn\"\n          ref=\"progressbtn\"\n          @touchstart.prevent=\"progressTouchStart\"\n          @touchmove.prevent=\"progressTouchMove\"\n          @touchend.prevent=\"progressTouchEnd\"\n        \u003e\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n**进度条样式**\n\n```stylus\n.progress-bar\n  height 30px\n  .bar-inner\n    position relative\n    top 13px\n    height 4px\n    background rgba(0,0,0,0.3)\n    .progress\n      position absolute\n      height 100%\n      background $color-theme\n    .progress-btn-wrapper\n      position absolute\n      left -8px\n      top -13px\n      width 30px\n      height 30px\n      .progress-btn\n        position relative\n        top 7px\n        left 7px\n        box-sizing border-box\n        wdith 16px\n        height 16px\n        border 3px solid $color-text\n        border-radius 50%\n        background $color-theme\n```\n\n**对应的效果图**\n\n![QQ截图20200118213149.png](https://i.loli.net/2020/01/18/yAONqK5FQR9wYL3.png)\n\n**挂到 play.vue 上**\n\n```vue\n\u003cdiv class=\"progress-bar-wrapper\"\u003e\n        \u003cprogressbar :currentTime=\"currentTime\" :songsTime=\"songsTime\" :precent=\"precent\" @percentChangeEnd=\"onProgressBarChange\"\u003e\u003c/progressbar\u003e\n      \u003c/div\u003e\n```\n\nprecent 对 progress-bar 组件进度条有用\n\n```js\ncomputed :{\n  precent () {\n    return this.currentTime / this.songTime\n  }\n }\n```\n\n使进度条睡着歌曲进度而改变\n\n```js\nwatch:{\n  currentTime (val) {\n    if(val \u003e 0) {\n      // 总进度条长度\n      const progressTotalWidth =this.$refs.progressbar.clientWidth -16\n      // 左侧progress偏移\n      cost precentx = val /this.somngTime\n      const progressWidth = progressTotalWidth * precentx\n      this.$refs.progress.style.width = `${progressWidth}px`\n      this.$refs.progressbtn.style[transform] = `translate3d(${progressWidth}px,0,0)`\n    }\n  }\n}\n```\n\n 进度条拖拽\n实现方法:\n\n- 定义好偏移函数\n- 使用 js 的三个钩子(`touchstart` `touchmove` `touchend`) 对进度条实现偏移\n- 向外派发事件 将当前 precent 转发到 play 组件\n  \n\n\u003ch3\u003e定义偏移方法\u003c/h3\u003e\n\n```js\n_offset (offsetWidth) {\n  this.$refs.progress.style.width = `${offsetWidth}px`\n  this.$refs.progressbtn.style[transform] = `translate3d(${offsetWidth}px,0,0)`\n}\n```\n\n```js\ncreated() {\n  this.touch = {}\n}\n// 记录第一次触屏位置 和初始进度条偏移\nprogressTouchStart (e) {\nthis.touch.initiated = true\nthis.touch.startX = e.touches[0].pageX\nthis.touch.left = this.$refs.progress.clientWidth\n},\n// 记录拖动偏移量,现在偏移量等于初始偏移量+拖动偏移 但是不能呢个超过进度条的总宽度\nprogressTouchMove(e) {\n if (!this.touch.initiated) {\n        return\n      }\n      const moveWidth = e.touches[0].pageX - this.touch.startX\n      const offsetWidth = Math.min(this.$refs.progressbar.clientWidth - 16, Math.max(0, this.touch.left + moveWidth))\n      this._triggerPrecent()\n      this._offset(offsetWidth)\n},\n// 初始化为false 将拖动信息转发出去\nprogressTouchEnd (e) {\nthis.touch.initiated = false\nconst barWidth = this.$refs.progressbar.clientWidth-16\nconst precent = this.$refs.progress.clientWidth / barWidth\nthis.$emit('percentChangeEnd',precent)\n},\n\n _triggerPrecent () {\n      // 派发当前precent\n      const progressTotalWidth = this.$refs.progressbar.clientWidth - 16\n      const precent = this.$refs.progress.clientWidth / progressTotalWidth\n      this.$emit('precentChange', precent)\n    }\n```\n\n**play 组件上监听转发事件 **\n\n```js\nonProgressBarChange (precent) {\n  // 控制播放时间\n  this.$refs.audio.currentTime = this.songTime * precent\n  if(!this.playing) {\n    // 如果拖动进度条暂停了 就继续播放\n    this.togglePlaying()\n  }\n  if(this.currentLyric) {\n    // 寻找歌词\n    this.currentLyric.seek(this.$refs.audio.currentTime * 1000)\n  }\n}\n```\n\n实现效果图\n\n![GIF.gif](https://i.loli.net/2020/01/19/a1WbrhY7UJPOQsV.gif)\n\n添加完了拖动事件，接下来添加点击变换进度条事件.\n\n原理也是一样，也是计算 `progressgress` 偏移和 `progressbtn` 偏移，然后派发 precent 出去\n\n\n```js\n // 点击进度条事件\n    progressClick (e) {\n      //  ! e.pageX 获取到的位置有bug\n      const rect = this.$refs.progressbar.getBoundingClientRect()\n      // rect.left 元素距离左边的距离\n      // e.pageX 点击距离左边的距离\n      const offsetWidth = e.pageX - rect.left\n      // console.log(rect, e.pageX)\n      this._offset(offsetWidth)\n      const barWidth = this.$refs.progressbar.clientWidth - 16\n      const percent = this.$refs.progress.clientWidth / barWidth\n      this.$emit('percentChangeEnd', percent)\n    },\n```\n\n bug\n此处如果直接使用 `e.pageX` 记录元素距左边的位置会有 bug,我们这里使用 `getBoundClientRect()`相对视口位置 bar 相对浏览器的左侧视口位置 偏移量就等于 `e.pageX-rect`\n\n这样我们就完成了进度条的点击和拖拽进度效果\n\n接着我们要给 `min-player` 也制作一个圆形播放进度条效果\n同样我们创建 `progress-circle` 基础组件\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv class=\"progresscircle\"\u003e\n    \u003c!-- wdith height 是svg圈的大小 stroke-dasharray是描边总周长 dashOffset描边偏移量  --\u003e\n    \u003csvg\n      :width=\"radius\"\n      :height=\"radius\"\n      viewBox=\"0 0 100 100\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    \u003e\n      \u003ccircle\n        class=\"progress-background\"\n        r=\"50\"\n        cx=\"50\"\n        cy=\"50\"\n        fill=\"transparent\"\n      /\u003e\n      \u003ccircle\n        class=\"progress-bar\"\n        r=\"50\"\n        cx=\"50\"\n        cy=\"50\"\n        fill=\"transparent\"\n        :stroke-dasharray=\"dashArray\"\n        :stroke-dashoffset=\"dashOffset\"\n      /\u003e\n    \u003c/svg\u003e\n    \u003cslot\u003e\u003c/slot\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript\u003e\nexport default {\n  name: 'progresscircle',\n  props: {\n    radius: {\n      type: Number,\n      default: 100\n    },\n    precent: {\n      type: Number,\n      default: 0\n    }\n  },\n  data() {\n    return {\n      dashArray: 314\n    }\n  },\n\n  components: {},\n\n  computed: {\n    dashOffset() {\n      return (1 - this.precent) * this.dashArray\n    }\n  },\n\n  beforeMount() {},\n\n  mounted() {},\n\n  methods: {},\n\n  watch: {}\n}\n\u003c/script\u003e\n\u003cstyle lang=\"stylus\" scoped\u003e\n@import '~@/common/stylus/variable.styl'\n .progresscircle\n    position: relative\n    circle\n      stroke-width: 6px\n      transform-origin: center\n      \u0026.progress-background\n        transform: scale(0.9)\n        stroke: $color-theme-d\n      \u0026.progress-bar\n        transform: scale(0.9) rotate(-90deg)\n        stroke: $color-theme\n\u003c/style\u003e\n```\n\n\u003ch3\u003e组件接受两个参数\u003c/h3\u003e\n\n- radius radius 用来控制 svg 图的宽高 我们这里 radius 传入 32\n- precent svg 图下 progress-bar 有两个参数\n  - **stroke-dasharray**描边周长 这里应该是 314(2*3.14*50)\n  - **stroke-dashoffset** 外层有颜色描边的长度 这里我们用(1-precent)\\*dasharray 来表示当前进度\n\n#### 💛播放器模式切换\n\n一开始播放模式我们是写死的 icon,接下来就不用写死了\n\n```vue\n\u003cdiv class=\"icon i-left\" @click=\"changeMode\"\u003e\n        \u003ci :class=\"iconMode\" \u003e\u003c/i\u003e\n      \u003c/div\u003e\n.... iconMode () { return this.mode === playMode.sequence ? 'icon-sequence' :\nthis.mode === playMode.loop ? 'icon-loop' : 'icon-random' }\n```\n\n更换模式的实现，模式以共有三种所以我们设置模式 3 个里面循环\n\n```js\nchangeMode () {\n  const mode = (this.mode + 1)%3\n  this.setMode(mode)\n  let list =null\n  if(this..mode === playMode.random) {\n    list = shuffle(this.sequencelist)\n     this.resetCurrentIndex(list)\n        this.setsplaylist(list)\n  }else {\n        list = this.sequencelist\n        this.resetCurrentIndex(list)\n        this.setsplaylist(list)\n      }\n}\n\n.....\n洗牌函数(遍历数组用array[i]去和随机的min到max下标的数祖元素去交换 这样数组就会很乱)\n\nfunction getRandomInt(min,max){\n  // 保证取到min-max的随机数\n  return Math.floor(Math.random()*(max - min + 1) + min)\n}\nfunction shuffle(arr){\n  let _arr = arr.slice()\n  for(let i= 0; i\u003c_arr.length;i++){\n    let j= getRandomInt(0,i)\n    let t = _arr[i]\n    _arr[i] = _arr[j]\n    _arr[j] = t\n  }\n  return _arr\n}\n\n```\n\n 注意\n这里用到的定义的两个方法\n\n```js\nresetCurrentIndex(list){\n// 找到当前歌曲id相同的list元素索引 这样歌曲就不会再切换了模式之后 list改变 currentSong改变 当前歌曲被切换了\nlet index1 = list.finIndex((item)=\u003e{\n  return itlem.id === this.currentSong.id\n})\nthis.setCurrentindex(index1)\n}\n\n// 然后我们在监听currentSong时这样去写\ncurrentSong(newSomng,oldSong){\n  if(newSong.id === oldSong.id){\n    return\n  }\n}\n```\n\n\n\n\u003ch3\u003e接下来就是播放器的一些问题解决\u003c/h3\u003e\n\n1. 歌曲播放到末尾 自动切换下一首\n\n```js\n// 监听ended事件\n End () {\n      if (this.mode === playMode.loop) {\n        this.loop()\n      } else {\n        this.next()\n      }\n    }\n```\n\n2. 循环播放模式设置\n\n```JS\nloop() {\n  setTimeout(()=\u003e{\n    this.$refs.audio.currentTime = 0\n    this.$refs.audio.play()\n  },1000)\n  if(this.currentLyric) {\n    this.currentLyric,seek(0)\n  }\n}\n```\n\n3. 实现封面随机播放\n\n```js\n// 添加点击事件\nrandom() {\n  this.randomPlay({\n    list:this.songs\n  })\n}\n\n\n// 这里需要设置actions\nexport const randomPlay = function ({commit}, {list}) {\n  commit(types.SET_PLAY_MODE, playMode.random)\n  commit(types.SET_SEQUENCE, list)\n  let randomlist = shuffle(list)\n  commit(types.SET_CURRENTINDEX, 0)\n  commit(types.SET_PLAYLIST, randomlist)\n  commit(types.SET_FULLSCREEN, true)\n  commit(types.SET_PLAYING, true)\n}\n\n```\n\n#### ❤️播放器歌词部分\n\n获取歌词部分\n\n```js\nimport axios from 'axios'\nexport function getLyric(id) {\n  const url = `http://neteasemusic.yangxiansheng.top/lyric?id=${id}`\n  return axios.get(url)\n}\n```\n\nmethods 里面定义好获取歌词方法\n\n```js\n _getLyric (id) {\n      getLyric(id).then((res) =\u003e {\n        // 调用第三方库 将currentLyric转化为一个time-text对象\n        this.currentLyric = new Lyric(res.data.lrc.lyric, this.handleLyric)\n        if (this.playing) {\n          this.currentLyric.play()\n        }\n      })\n    },\n```\n\n\u003e 这里使用到了第三方库`Lyric-parser` 这个插件的作用就是讲 currentLyric 转化为一个 time-text 对象 便于使用\n\n在 watch 里面监听 currentSong 调用\n\n```js\ncurrentSong(newSong,oldSong) {\n  if(newSong.id === oldSong.id) {\n    return\n  }\n  getMusic(this.currentSong.id).then((res)=\u003e{\n    this.songurl= res.data.data[0].url\n  })\n  if(this.currentSong.id) {\n    this.currentLyric.stop()\n  }\n  this._getLyric(this.currentSong.id)\n}\n```\n\n这样就可以实现歌词的自动跳转\n\n![GIF.gif](https://i.loli.net/2020/01/19/nskZdBEUqrLKuyC.gif)\n\n我们要实现歌词到了第五行后面自动向下滚动\n\n```js\nhandleLyric({lineNum,txt}) {\n  // 当前行\n  this.currentLineNum = lineNum\n  if(lineNum \u003e 5){\n    let LineEl = this.$refs.lyricLine[lineNum-5]\n    this.$refs.lyricList.scrollToElement(LineEl,500)\n  }else{\n    this.$refs.lyricList.scrollTo(0,0,1000)\n  }\n  // 记录歌词\n  this.currentLyrictxt = txt\n}\n```\n\n实现歌词左右滑动 切换(同样使用到了 touch 事件)\n\n```vue\n\u003cScroll\n  class=\"middle-r\"\n  ref=\"lyricList\"\n  :data=\"currentLyric \u0026\u0026 currentLyric.lines\"\n\u003e\n\u003cdiv class=\"lyric-wrapper\"\u003e\n  \u003cdiv v-if=\"currentLyric\"\u003e\n    \u003cp ref=\"lyricLine\"\n        class=\"text\"\n        :class=\"{'current':currentLineNum === index}\"\n        v-for=\"(line,index) in currentLyric.lines\" :key=\"index\"\u003e{{line.txt}}\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003c/Scroll\u003e\n```\n滑动处理\n```js\n    midleTouchStart (e) {\n      // 设置标志位已经初始化\n      this.touch.initiated = true\n      this.touch.startX = e.touches[0].pageX\n      this.touch.startY = e.touches[0].pageY\n    },\n    midleTouchMove (e) {\n      if (!this.touch.initiated) {\n        return\n      }\n      // 拿到差值\n      const deltaX = e.touches[0].pageX - this.touch.startX\n      const deltaY = e.touches[0].pageY - this.touch.startY\n      // 歌词纵向滚动 当纵向偏移大于左右偏移的时候 我们不应该左右移动\n      if (Math.abs(deltaY) \u003e Math.abs(deltaX)) {\n        return\n      }\n      // 拿到滚动过程中middle-r距离左右两侧的屏幕的差值\n      const left = this.currentShow === 'cd' ? 0 : -window.innerWidth\n      const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX))\n      this.touch.percent = Math.abs(offsetWidth / window.innerWidth)\n      // lyricList是一个Vue(scroll)组件，是无法直接造作它的dom的，用$el来代替它的dom\n      this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`\n      this.$refs.lyricList.$el.style[transitionDuration] = 0\n      // 透明度的渐变\n      this.$refs.middleL.style.opacity = 1 - this.touch.percent\n      this.$refs.middleL.style[transitionDuration] = 0\n    },\n    middleTouchEnd () {\n      // 用来决定停在哪个位置\n      let offsetWidth\n      let opacity\n      if (this.currentShow === 'cd') {\n        // 从右向左滑，只需要滑10%就行\n        if (this.touch.percent \u003e 0.1) {\n          offsetWidth = -window.innerWidth // 最终停止的位置\n          opacity = 0\n          this.currentShow = 'lyric' // 改变dot的css样式\n        } else {\n          offsetWidth = 0 // 否则就回到原来的位置\n          opacity = 1\n        }\n      } else { // 从右向左滑，看的是0.9\n        if (this.touch.percent \u003c 0.9) {\n          offsetWidth = 0\n          this.currentShow = 'cd'\n          opacity = 1\n        } else {\n          offsetWidth = -window.innerWidth\n          opacity = 0\n        }\n      }\n```\ndot区域\n```vue\n    \u003cdiv class=\"dot-wrapper\"\u003e\n    \u003cspan class=\"dot\" :class=\"{'active':currentShow === 'cd'}\"\u003e\u003c/span\u003e\n    \u003cspan class=\"dot\" :class=\"{'active':currentShow === 'lyric'}\"\u003e\u003c/span\u003e\n    \u003c/div\u003e\n```\n\n\n后面发现一个问题 就是iphone无法播放出声音，这是因为iphone设备或者微信浏览器对用户流量进行监控 不允许用户在浏览器上对audio进行播放 对此我采用了监听`touchstart`事件对audio标签进行播放\n\n```js\n firstPlay () {\n      this.$refs.audio.play()\n    },\n```\n#### mixin方式解决滚动bug\n我们发现实现后的滚动条的组件 滚动到底部几乎不能被看到,这是因为背mini播放器阻挡住了，所以我们需要给这些组件的scroll重新refresh，然后给组件设置mini播放器高的bottom\n\n\u003ch3\u003e定义mixin\u003c/h3\u003e\n通过mixin的方式引入代码到组件是一个很方便的方法，这些代码会被复用，然后merge到组件的原来的钩子上\n\n```js\nimport {mapGetters} from 'vuex'\nexport const playlistMixin ={\n  computed:{\n    ...mapGetters([\n      'playlist'\n    ])\n  },\n  mounted() {\n    this.handlePlaylist(this.playlist)\n  },\n  // keep-alive的方法\n  activated () {\n    this.handlePlaylistMixin(this.playlist)\n  },\n  watch:{\n    playlist(newval){\n      this.handlePlaylist(newval)\n    }\n  },\n  methods:{\n    handlePlaylist() {\n      thorw new Error(' methods must be define')\n    }\n  }\n}\n```\n\n\n#### 🌟总结\n综上,播放器组件全部写完，里面包含了player组件 progress-bar组件 progress-circle组件，使用到了vuex实现状态管理，学习到了样式处理 播放状态管理 以及模式切换涉及到的知识等等,接下来就是开发榜单组件，加油~~~\n\n附上效果图\n\n![GIF.gif](https://i.loli.net/2020/01/19/TpaQiOYkyRcSsVr.gif)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyzh990918%2Fmymusic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyzh990918%2Fmymusic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyzh990918%2Fmymusic/lists"}