Ver código fonte

'冲突暂存'‘

魏志佳 4 anos atrás
pai
commit
645c4f054b
51 arquivos alterados com 4504 adições e 77 exclusões
  1. 1 0
      .env
  2. 5 2
      package.json
  3. 4 0
      public/index.html
  4. 61 50
      src/App.vue
  5. 117 0
      src/api/music.js
  6. BIN
      src/assets/background/bg-1.jpg
  7. BIN
      src/assets/background/bg-2.jpg
  8. BIN
      src/assets/img/album_cover_player.png
  9. BIN
      src/assets/img/default.png
  10. BIN
      src/assets/img/player_cover.png
  11. BIN
      src/assets/img/wave.gif
  12. 230 0
      src/base/mm-dialog/mm-dialog.vue
  13. 53 0
      src/base/mm-icon/mm-icon.vue
  14. 86 0
      src/base/mm-loading/mm-loading.vue
  15. 34 0
      src/base/mm-no-result/mm-no-result.vue
  16. 178 0
      src/base/mm-progress/mm-progress.vue
  17. 39 0
      src/base/mm-toast/index.js
  18. 73 0
      src/base/mm-toast/mm-toast.vue
  19. 6 0
      src/components/layouts/TabLayout.vue
  20. 174 0
      src/components/lyric/lyric.vue
  21. 229 0
      src/components/mm-header/mm-header.vue
  22. 81 0
      src/components/music-btn/music-btn.vue
  23. 365 0
      src/components/music-list/music-list.vue
  24. 82 0
      src/components/volume/volume.vue
  25. 36 0
      src/config.js
  26. 10 2
      src/main.js
  27. 102 0
      src/store/actions.js
  28. 27 6
      src/store/getters.js
  29. 8 11
      src/store/index.js
  30. 15 0
      src/store/modules/music.js
  31. 12 1
      src/store/mutation-types.js
  32. 38 0
      src/store/mutations.js
  33. 98 0
      src/styles/index.less
  34. 13 0
      src/styles/mixin.less
  35. 141 0
      src/styles/reset.less
  36. 52 0
      src/styles/var.less
  37. 23 0
      src/utils/axiosMusic.js
  38. 53 1
      src/utils/mixin.js
  39. 70 0
      src/utils/song.js
  40. 136 0
      src/utils/storage.js
  41. 110 1
      src/utils/util.js
  42. 274 0
      src/views/modules/material/comment/comment.vue
  43. 67 0
      src/views/modules/material/details/details.vue
  44. 88 0
      src/views/modules/material/historyList/historyList.vue
  45. 97 0
      src/views/modules/material/mmPlayer.js
  46. 658 0
      src/views/modules/material/musicMaterial.vue
  47. 97 0
      src/views/modules/material/playList/playList.vue
  48. 176 0
      src/views/modules/material/search/search.vue
  49. 131 0
      src/views/modules/material/topList/topList.vue
  50. 142 0
      src/views/modules/material/userList/userList.vue
  51. 12 3
      vue.config.js

+ 1 - 0
.env

@@ -0,0 +1 @@
+VUE_APP_BASE_API_URL = http://localhost:3030

+ 5 - 2
package.json

@@ -61,7 +61,8 @@
     "vue-waterfall-easy": "^2.4.3",
     "vuedraggable": "^2.20.0",
     "vuex": "^3.0.1",
-    "vuex-class": "^0.3.1"
+    "vuex-class": "^0.3.1",
+    "vue-lazyload": "^1.3.3"
   },
   "devDependencies": {
     "@babel/polyfill": "^7.2.5",
@@ -78,7 +79,9 @@
     "less-loader": "^4.1.0",
     "node-sass": "^4.11.0",
     "sass-loader": "^7.0.1",
-    "vue-template-compiler": "^2.6.10"
+    "vue-template-compiler": "^2.6.10",
+    "style-resources-loader": "^1.3.2",
+    "vue-cli-plugin-style-resources-loader": "^0.1.3"
   },
   "eslintConfig": {
     "root": true,

+ 4 - 0
public/index.html

@@ -11,6 +11,10 @@
   <link href="<%= BASE_URL %>color.less">
   <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.2.5/polyfill.js"></script> -->
   <script src="<%= BASE_URL %>web-office-sdk-v1.1.1.umd.js"></script>
+  <link
+  rel="stylesheet"
+  href="//at.alicdn.com/t/font_1367495_eza6utwbiqn.css"
+/>
   <style>
     html,
     body,

+ 61 - 50
src/App.vue

@@ -2,67 +2,78 @@
   <a-locale-provider :locale="locale">
     <div id="app">
       <router-view />
+      <audio ref="mmPlayer"></audio>
     </div>
   </a-locale-provider>
 </template>
 <script>
-  import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
-  import enquireScreen from '@/utils/device'
-
-  export default {
-    data() {
-      return {
-        locale: zhCN,
+import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
+import enquireScreen from '@/utils/device'
+import { mapGetters, mapMutations, mapActions } from 'vuex'
+export default {
+  data() {
+    return {
+      locale: zhCN,
+    }
+  },
+  created() {
+    let that = this
+    enquireScreen((deviceType) => {
+      // tablet
+      if (deviceType === 0) {
+        that.$store.commit('TOGGLE_DEVICE', 'mobile')
+        that.$store.dispatch('setSidebar', false)
+      }
+      // mobile
+      else if (deviceType === 1) {
+        that.$store.commit('TOGGLE_DEVICE', 'mobile')
+        that.$store.dispatch('setSidebar', false)
+      } else {
+        that.$store.commit('TOGGLE_DEVICE', 'desktop')
+        that.$store.dispatch('setSidebar', true)
       }
-    },
-    created() {
-      let that = this
-      enquireScreen(deviceType => {
-        // tablet
-        if (deviceType === 0) {
-          that.$store.commit('TOGGLE_DEVICE', 'mobile')
-          that.$store.dispatch('setSidebar', false)
-        }
-        // mobile
-        else if (deviceType === 1) {
-          that.$store.commit('TOGGLE_DEVICE', 'mobile')
-          that.$store.dispatch('setSidebar', false)
-        } else {
-          that.$store.commit('TOGGLE_DEVICE', 'desktop')
-          that.$store.dispatch('setSidebar', true)
-        }
+    })
 
-      })
-    }
-  }
+    this.$nextTick(() => {
+      this.setAudioele(this.$refs.mmPlayer)
+    })
+  },
+  methods: {
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING',
+      setPlay: 'SET_PLAYLIST',
+      setCurrentIndex: 'SET_CURRENTINDEX',
+      setAudioele: 'SET_AUDIOELE',
+    }),
+  },
+}
 </script>
 <style>
-  #app {
-    height: 100%;
-  }
+#app {
+  height: 100%;
+}
 
-  body {
-    -webkit-user-select: text !important;
-  }
+body {
+  -webkit-user-select: text !important;
+}
 
-  * {
-    -webkit-user-select: text !important;
-    -moz-user-select: text !important;
-    -ms-user-select: text !important;
-    user-select: text !important;
-  }
+* {
+  -webkit-user-select: text !important;
+  -moz-user-select: text !important;
+  -ms-user-select: text !important;
+  user-select: text !important;
+}
 
-  .ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab {
-    height: 39px !important;
-  }
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab {
+  height: 39px !important;
+}
 
-  .ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active {
-    height: 39px !important;
-    border-bottom:1px solid #1890ff !important;
-  }
-  .layout .header {
- background-color:#0B2040  !important;
-  background:#0B2040  !important;
-  
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active {
+  height: 39px !important;
+  border-bottom: 1px solid #1890ff !important;
+}
+.layout .header {
+  background-color: #0b2040 !important;
+  background: #0b2040 !important;
 }
 </style>

+ 117 - 0
src/api/music.js

@@ -0,0 +1,117 @@
+import axios from '@/utils/axiosMusic'
+import { defaultLimit } from '@/config'
+import { formatTopSongs } from '@/utils/song'
+
+axios.defaults.baseURL = process.env.VUE_APP_BASE_API_URL
+
+// 排行榜列表
+export function getToplistDetail() {
+  return axios.get('/toplist/detail')
+}
+
+// 推荐歌单
+export function getPersonalized() {
+  return axios.get('/personalized')
+}
+
+// 歌单详情
+export function getPlaylistDetail(id) {
+  return new Promise((resolve, reject) => {
+    axios
+      .get('/playlist/detail', {
+        params: { id }
+      })
+      .then(({ playlist }) => playlist)
+      .then(playlist => {
+        const { trackIds, tracks } = playlist
+        // 过滤完整歌单 如排行榜
+        if (tracks.length === trackIds.length) {
+          playlist.tracks = formatTopSongs(playlist.tracks)
+          resolve(playlist)
+          return
+        }
+        // 限制歌单详情最大 500
+        const ids = trackIds
+          .slice(0, 500)
+          .map(v => v.id)
+          .toString()
+        getMusicDetail(ids).then(({ songs }) => {
+          playlist.tracks = formatTopSongs(songs)
+          resolve(playlist)
+        })
+      })
+  })
+}
+
+// 搜索
+export function search(keywords, page = 0, limit = defaultLimit) {
+  return axios.get('/search', {
+    params: {
+      offset: page * limit,
+      limit: limit,
+      keywords
+    }
+  })
+}
+
+// 热搜
+export function searchHot() {
+  return axios.get('/search/hot')
+}
+
+// 获取用户歌单详情
+export function getUserPlaylist(uid) {
+  return axios.get('/user/playlist', {
+    params: {
+      uid
+    }
+  })
+}
+
+// 获取歌曲详情
+export function getMusicDetail(ids) {
+  return axios.get('/song/detail', {
+    params: {
+      ids
+    }
+  })
+}
+
+// 获取音乐是否可以用
+export function getCheckMusic(id) {
+  return axios.get('/check/music', {
+    params: {
+      id
+    }
+  })
+}
+
+// 获取音乐地址
+export function getMusicUrl(id) {
+  return axios.get('/song/url', {
+    params: {
+      id
+    }
+  })
+}
+
+// 获取歌词
+export function getLyric(id) {
+  const url = '/lyric'
+  return axios.get(url, {
+    params: {
+      id
+    }
+  })
+}
+
+// 获取音乐评论
+export function getComment(id, page, limit = defaultLimit) {
+  return axios.get('/comment/music', {
+    params: {
+      offset: page * limit,
+      limit: limit,
+      id
+    }
+  })
+}

BIN
src/assets/background/bg-1.jpg


BIN
src/assets/background/bg-2.jpg


BIN
src/assets/img/album_cover_player.png


BIN
src/assets/img/default.png


BIN
src/assets/img/player_cover.png


BIN
src/assets/img/wave.gif


+ 230 - 0
src/base/mm-dialog/mm-dialog.vue

@@ -0,0 +1,230 @@
+<template>
+  <!--对话框-->
+  <transition name="mm-dialog-fade">
+    <div v-show="dialogShow" class="mm-dialog-box">
+      <div class="mm-dialog-wrapper">
+        <div class="mm-dialog-content">
+          <div class="mm-dialog-head" v-text="headText"></div>
+          <slot>
+            <!-- eslint-disable-next-line vue/no-v-html -->
+            <div class="mm-dialog-text" v-html="bodyText"></div>
+          </slot>
+          <div class="mm-dialog-btns">
+            <div
+              v-if="dialogType!=='alert'"
+              class="mm-btn-cancel"
+              @click="cancel"
+              v-text="cancelBtnText"
+            ></div>
+            <slot name="btn"></slot>
+            <div
+              class="mm-btn-confirm"
+              @click="confirm"
+              v-text="confirmBtnText"
+            ></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'MmDialog',
+  props: {
+    // type:confirm、alert
+    type: {
+      type: String,
+      default: 'confirm'
+    },
+    // 标题文本
+    headText: {
+      type: String,
+      default: '提示'
+    },
+    // 内容文本(可以是html)
+    bodyText: {
+      type: String,
+      default: ''
+    },
+    // 取消按钮文本
+    cancelBtnText: {
+      type: String,
+      default: '取消'
+    },
+    // 确定按钮文本
+    confirmBtnText: {
+      type: String,
+      default: '确定'
+    },
+    // Dialog 是否插入至 body 元素下
+    appendToBody: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      dialogShow: false // 是否显示对话框
+    }
+  },
+  computed: {
+    dialogType() {
+      return this.type.toLowerCase()
+    }
+  },
+  watch: {
+    dialogShow(val) {
+      if (val && this.appendToBody) {
+        document.body.appendChild(this.$el)
+      }
+    }
+  },
+  mounted() {
+    if (this.dialogShow && this.appendToBody) {
+      document.body.appendChild(this.$el)
+    }
+  },
+  destroyed() {
+    if (this.appendToBody && this.$el && this.$el.parentNode) {
+      this.$el.parentNode.removeChild(this.$el)
+    }
+  },
+  methods: {
+    // 显示事件
+    show() {
+      this.dialogShow = true
+    },
+    // 隐藏事件
+    hide() {
+      this.dialogShow = false
+    },
+    // 取消事件
+    cancel() {
+      this.hide()
+      this.$emit('cancel')
+    },
+    // 确定事件
+    confirm() {
+      this.hide()
+      this.$emit('confirm')
+    }
+  }
+}
+</script>
+
+<style lang="less">
+@dialog-prefix-cls: mm-dialog;
+
+.@{dialog-prefix-cls}-box {
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 1996;
+  background-color: @dialog_bg_color;
+  user-select: none;
+  &.@{dialog-prefix-cls}-fade-enter-active {
+    animation: mm-dialog-fadein 0.3s;
+    .@{dialog-prefix-cls}-content {
+      animation: mm-dialog-zoom 0.3s;
+    }
+  }
+  .@{dialog-prefix-cls}-wrapper {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1996;
+    .@{dialog-prefix-cls}-content {
+      width: 420px;
+      border-radius: 5px;
+      background: @dialog_content_bg_color;
+      @media (max-width: 767px) {
+        width: 270px;
+        border-radius: 10px;
+        text-align: center;
+      }
+      .@{dialog-prefix-cls}-head {
+        padding: 15px;
+        padding-bottom: 0;
+        font-size: @font_size_large;
+        color: @text_color_active;
+      }
+      .@{dialog-prefix-cls}-text {
+        padding: 20px 15px;
+        line-height: 22px;
+        font-size: @font_size_medium;
+        color: @dialog_text_color;
+      }
+      .@{dialog-prefix-cls}-btns {
+        display: flex;
+        align-items: center;
+        padding: 0 15px 10px;
+        text-align: center;
+        color: @dialog_text_color;
+        @media (min-width: 768px) {
+          justify-content: flex-end;
+          div {
+            display: block;
+            padding: 8px 15px;
+            border-radius: 3px;
+            border: 1px solid @btn_color;
+            font-size: @font_size_medium;
+            cursor: pointer;
+            &:not(:nth-of-type(1)) {
+              margin-left: 10px;
+            }
+            &.mm-btn-confirm {
+              border-style: @btn_color_active;
+            }
+            &:hover {
+              color: @text_color_active;
+              border: 1px solid @btn_color_active;
+            }
+          }
+        }
+        @media (max-width: 767px) {
+          & {
+            padding: 0;
+            justify-content: center;
+            div {
+              flex: 1;
+              line-height: 22px;
+              padding: 10px 0;
+              border-top: 1px solid @dialog_line_color;
+              font-size: @font_size_large;
+              &:not(:nth-of-type(1)) {
+                border-left: 1px solid @dialog_line_color;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@keyframes mm-dialog-fadein {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+
+@keyframes mm-dialog-zoom {
+  0% {
+    transform: scale(0);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+</style>

+ 53 - 0
src/base/mm-icon/mm-icon.vue

@@ -0,0 +1,53 @@
+<!-- icon 组件 -->
+<script>
+export default {
+  name: 'MmIcon',
+  props: {
+    type: {
+      type: String,
+      required: true
+    },
+    size: {
+      type: Number,
+      default: 16
+    }
+  },
+  methods: {
+    getIconCls() {
+      return `icon-${this.type}`
+    },
+    getIconStyle() {
+      return { fontSize: this.size + 'px' }
+    },
+    onClick(e) {
+      this.$emit('click', e)
+    }
+  },
+  render() {
+    const Icon = (
+      <i
+        onClick={this.onClick}
+        class={`iconfont ${this.getIconCls()}`}
+        style={this.getIconStyle()}
+      />
+    )
+    return Icon
+  }
+}
+</script>
+
+<style lang="less">
+.iconfont {
+  display: inline-block;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  line-height: 1;
+  vertical-align: baseline;
+  text-transform: none;
+  speak: none;
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</style>

+ 86 - 0
src/base/mm-loading/mm-loading.vue

@@ -0,0 +1,86 @@
+<template>
+  <!--加载动画-->
+  <div
+    v-show="value"
+    class="mm-loading"
+    :style="{backgroundColor:loadingBgColor}"
+  >
+    <div class="mm-loading-content">
+      <svg class="circular" viewBox="25 25 50 50">
+        <circle class="path" cx="50" cy="50" r="20" fill="none" />
+      </svg>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MmLoading',
+  props: {
+    // 是否显示
+    value: {
+      type: Boolean,
+      default: true
+    },
+    // 加载动画背景颜色
+    loadingBgColor: {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.mm-loading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 1996;
+  background: @load_bg_color;
+  .mm-loading-content {
+    position: absolute;
+    top: 50%;
+    width: 100%;
+    transform: translateY(-50%);
+    text-align: center;
+    .circular {
+      height: 50px;
+      width: 50px;
+      animation: loading-rotate 2s linear infinite;
+      .path {
+        animation: loading-dash 1.5s ease-in-out infinite;
+        stroke-dasharray: 90, 150;
+        stroke-dashoffset: 0;
+        stroke-width: 2;
+        stroke: @text_color;
+        stroke-linecap: round;
+      }
+    }
+  }
+}
+
+//动画函数
+@keyframes loading-rotate {
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes loading-dash {
+  0% {
+    stroke-dasharray: 1, 200;
+    stroke-dashoffset: 0;
+  }
+  50% {
+    stroke-dasharray: 90, 150;
+    stroke-dashoffset: -40px;
+  }
+  100% {
+    stroke-dasharray: 90, 150;
+    stroke-dashoffset: -120px;
+  }
+}
+</style>

+ 34 - 0
src/base/mm-no-result/mm-no-result.vue

@@ -0,0 +1,34 @@
+<template>
+  <!--暂无数据提示-->
+  <div class="mm-no-result">
+    <p class="mm-no-result-text">{{ title }}</p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MmNoResult',
+  props: {
+    // 无数据提示文本
+    title: {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.mm-no-result {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  &-text {
+    margin-top: 30px;
+    font-size: @font_size_medium;
+    color: @text_color;
+  }
+}
+</style>

+ 178 - 0
src/base/mm-progress/mm-progress.vue

@@ -0,0 +1,178 @@
+<template>
+  <!--进度条拖动-->
+  <div ref="mmProgress" class="mmProgress" @click="barClick">
+    <div class="mmProgress-bar"></div>
+    <div ref="mmPercentProgress" class="mmProgress-outer"></div>
+    <div ref="mmProgressInner" class="mmProgress-inner">
+      <div
+        class="mmProgress-dot"
+        @mousedown="barDown"
+        @touchstart.prevent="barDown"
+      ></div>
+    </div>
+  </div>
+</template>
+
+<script>
+const dotWidth = 10
+export default {
+  name: 'MmProgress',
+  props: {
+    // 进度值一
+    percent: {
+      type: [Number],
+      default: 0
+    },
+    // 进度值二(歌曲缓冲进度用)
+    percentProgress: {
+      type: [Number],
+      default: 0
+    }
+  },
+  data() {
+    return {
+      move: {
+        status: false, // 是否可拖动
+        startX: 0, // 记录最开始点击的X坐标
+        left: 0 // 记录当前已经移动的距离
+      }
+    }
+  },
+  watch: {
+    percent(newPercent) {
+      if (newPercent >= 0 && !this.move.status) {
+        const barWidth = this.$refs.mmProgress.clientWidth - dotWidth
+        const offsetWidth = newPercent * barWidth
+        this.moveSilde(offsetWidth)
+      }
+    },
+    percentProgress(newValue) {
+      let offsetWidth = this.$refs.mmProgress.clientWidth * newValue
+      this.$refs.mmPercentProgress.style.width = `${offsetWidth}px`
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.bindEvents()
+      const barWidth = this.$refs.mmProgress.clientWidth - dotWidth
+      const offsetWidth = this.percent * barWidth
+      this.moveSilde(offsetWidth)
+    })
+  },
+  beforeDestroy() {
+    this.unbindEvents()
+  },
+  methods: {
+    // 添加绑定事件
+    bindEvents() {
+      document.addEventListener('mousemove', this.barMove)
+      document.addEventListener('mouseup', this.barUp)
+
+      document.addEventListener('touchmove', this.barMove)
+      document.addEventListener('touchend', this.barUp)
+    },
+    // 移除绑定事件
+    unbindEvents() {
+      document.removeEventListener('mousemove', this.barMove)
+      document.removeEventListener('mouseup', this.barUp)
+
+      document.removeEventListener('touchmove', this.barMove)
+      document.removeEventListener('touchend', this.barUp)
+    },
+    // 点击事件
+    barClick(e) {
+      let rect = this.$refs.mmProgress.getBoundingClientRect()
+      let offsetWidth = Math.min(
+        this.$refs.mmProgress.clientWidth - dotWidth,
+        Math.max(0, e.clientX - rect.left)
+      )
+      this.moveSilde(offsetWidth)
+      this.commitPercent(true)
+    },
+    // 鼠标按下事件
+    barDown(e) {
+      this.move.status = true
+      this.move.startX = e.clientX || e.touches[0].pageX
+      this.move.left = this.$refs.mmProgressInner.clientWidth
+    },
+    // 鼠标/触摸移动事件
+    barMove(e) {
+      if (!this.move.status) {
+        return false
+      }
+      e.preventDefault()
+      let endX = e.clientX || e.touches[0].pageX
+      let dist = endX - this.move.startX
+      let offsetWidth = Math.min(
+        this.$refs.mmProgress.clientWidth - dotWidth,
+        Math.max(0, this.move.left + dist)
+      )
+      this.moveSilde(offsetWidth)
+      this.commitPercent()
+    },
+    // 鼠标/触摸释放事件
+    barUp(e) {
+      if (this.move.status) {
+        this.commitPercent(true)
+        this.move.status = false
+      }
+    },
+    // 移动滑块
+    moveSilde(offsetWidth) {
+      this.$refs.mmProgressInner.style.width = `${offsetWidth}px`
+    },
+    // 修改 percent
+    commitPercent(isEnd = false) {
+      const { mmProgress, mmProgressInner } = this.$refs
+      const lineWidth = mmProgress.clientWidth - dotWidth
+      const percent = mmProgressInner.clientWidth / lineWidth
+      this.$emit(isEnd ? 'percentChangeEnd' : 'percentChange', percent)
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.mmProgress {
+  position: relative;
+  padding: 5px;
+  user-select: none;
+  cursor: pointer;
+  overflow: hidden;
+  .mmProgress-bar {
+    height: 2px;
+    width: 100%;
+    background: @bar_color;
+  }
+  .mmProgress-outer {
+    position: absolute;
+    top: 50%;
+    left: 5px;
+    display: inline-block;
+    width: 0;
+    height: 2px;
+    margin-top: -1px;
+    background: rgba(255, 255, 255, 0.2);
+  }
+  .mmProgress-inner {
+    position: absolute;
+    top: 50%;
+    left: 5px;
+    display: inline-block;
+    width: 0;
+    height: 2px;
+    margin-top: -1px;
+    background: @line_color;
+    .mmProgress-dot {
+      position: absolute;
+      top: 50%;
+      right: -5px;
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background-color: @dot_color;
+      transform: translateY(-50%);
+    }
+  }
+}
+</style>

+ 39 - 0
src/base/mm-toast/index.js

@@ -0,0 +1,39 @@
+import TempToast from './mm-toast.vue'
+
+let instance
+let showToast = false
+let time // 存储toast显示状态
+const mmToast = {
+  install(Vue, options = {}) {
+    let opt = TempToast.data() // 获取组件中的默认配置
+    Object.assign(opt, options) // 合并配置
+    Vue.prototype.$mmToast = (message, position) => {
+      if (showToast) {
+        clearTimeout(time)
+        instance.vm.visible = showToast = false
+        document.body.removeChild(instance.vm.$el)
+        // return;// 如果toast还在,则不再执行
+      }
+      if (message) {
+        opt.message = message // 如果有传message,则使用所传的message
+      }
+      if (position) {
+        opt.position = position // 如果有传type,则使用所传的type
+      }
+      let TempToastConstructor = Vue.extend(TempToast)
+      instance = new TempToastConstructor({
+        data: opt
+      })
+      instance.vm = instance.$mount()
+      document.body.appendChild(instance.vm.$el)
+      instance.vm.visible = showToast = true
+
+      time = setTimeout(function() {
+        instance.vm.visible = showToast = false
+        document.body.removeChild(instance.vm.$el)
+      }, opt.duration)
+    }
+  }
+}
+
+export default mmToast

+ 73 - 0
src/base/mm-toast/mm-toast.vue

@@ -0,0 +1,73 @@
+<template>
+  <!--弹出层提示-->
+  <transition name="toast-fade">
+    <div v-show="visible" class="mm-toast" :class="positionClasss">{{ message }}</div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: 'MmToast',
+  data() {
+    return {
+      position: 'center', // 默认显示位置
+      message: '', // 默认显示文本
+      duration: 1500, // 显示时间, 毫秒
+      visible: false // 是否显示
+    }
+  },
+  computed: {
+    positionClasss() {
+      return 'mm-toast-' + this.position
+    }
+  }
+}
+</script>
+
+<style lang="less">
+@prefix-cls: mm-toast;
+
+.@{prefix-cls} {
+  position: fixed;
+  left: 50%;
+  z-index: 1996;
+  max-width: 80%;
+  box-sizing: border-box;
+  border-radius: 5px;
+  padding: 10px 20px;
+  overflow: hidden;
+  text-align: center;
+  min-height: 40px;
+  line-height: 20px;
+  font-size: 14px;
+  color: #fff;
+  background: rgba(0, 0, 0, 0.5);
+  user-select: none;
+  transform: translateX(-50%);
+  &&-top {
+    top: 10%;
+  }
+  &&-center {
+    top: 50%;
+    margin-top: -20px;
+  }
+  &&-bottom {
+    bottom: 10%;
+  }
+}
+
+.toast-fade-enter {
+  opacity: 0;
+  transform: translate3d(-50%, -10px, 0);
+}
+
+.toast-fade-enter-active {
+  will-change: transform;
+  transition: all 0.2s;
+}
+
+.toast-fade-enter-to {
+  opacity: 1;
+  transform: translate3d(-50%, 0, 0);
+}
+</style>

+ 6 - 0
src/components/layouts/TabLayout.vue

@@ -24,6 +24,7 @@
 </template>
 
 <script>
+  import { mapActions } from 'vuex'
   import GlobalLayout from '@/components/page/GlobalLayout'
   import Contextmenu from '@/components/menu/Contextmenu'
   import {
@@ -169,6 +170,10 @@
         let waitRouter = this.pageList[index]
         this.$router.push(Object.assign({}, waitRouter));
         // this.changeTitle(waitRouter.meta.title)
+        console.log(key)
+        if(key!='/material/musicMaterial'){
+          this.clearPlayList()
+        }
       },
       'multipage': function (newVal) {
         if (this.reloadFlag) {
@@ -187,6 +192,7 @@
       // update-end-author:sunjianlei date:20191223 for: 修复从单页模式切换回多页模式后首页不居第一位的 BUG
     },
     methods: {
+      ...mapActions([ 'clearPlayList']),
       // update-begin-author:sunjianlei date:20191223 for: 修复从单页模式切换回多页模式后首页不居第一位的 BUG
       // 将首页添加到第一位
       addIndexToFirst() {

+ 174 - 0
src/components/lyric/lyric.vue

@@ -0,0 +1,174 @@
+<template>
+  <div>
+    <!--封面-->
+    <dl class="music-info">
+      <dt>
+        <img :src="musicPicUrl" />
+      </dt>
+      <template v-if="currentMusic.id">
+        <dd>歌曲名:{{ currentMusic.name }}</dd>
+        <dd>歌手名:{{ currentMusic.singer }}</dd>
+        <dd>专辑名:{{ currentMusic.album }}</dd>
+      </template>
+      <template v-else>
+        <dd>mmPlayer在线音乐播放器</dd>
+        <dd>
+          <a class="hover" target="_blank" href="https://github.com/maomao1996">
+            <mm-icon type="github" :size="14" />&nbsp;茂茂
+          </a>
+        </dd>
+      </template>
+    </dl>
+    <!--歌词-->
+    <div ref="musicLyric" class="music-lyric">
+      <div class="music-lyric-items" :style="lyricTop">
+        <p v-if="!currentMusic.id">还没有播放音乐哦!</p>
+        <p v-else-if="nolyric">暂无歌词!</p>
+        <template v-else-if="lyric.length > 0">
+          <p v-for="(item, index) in lyric" :key="index" :class="{ on: lyricIndex === index }">
+            {{ item.text }}
+          </p>
+        </template>
+        <p v-else>歌词加载失败!</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+export default {
+  name: 'Lyric',
+  props: {
+    // 歌词数据
+    lyric: {
+      type: Array,
+      default: () => [],
+    },
+    // 是否无歌词
+    nolyric: {
+      type: Boolean,
+      default: false,
+    },
+    // 当前歌词下标
+    lyricIndex: {
+      type: Number,
+      default: 0,
+    },
+  },
+  data() {
+    return {
+      top: 0, // 歌词居中
+    }
+  },
+  computed: {
+    musicPicUrl() {
+      return this.currentMusic.id
+        ? `${this.currentMusic.image}?param=300y300`
+        : require('../../assets/img/player_cover.png')
+    },
+    lyricTop() {
+      return `transform :translate3d(0, ${-34 * (this.lyricIndex - this.top)}px, 0)`
+    },
+    ...mapGetters(['currentMusic']),
+  },
+  mounted() {
+    window.addEventListener('resize', () => {
+      clearTimeout(this.resizeTimer)
+      this.resizeTimer = setTimeout(() => this.clacTop(), 60)
+    })
+    this.$nextTick(() => this.clacTop())
+  },
+  methods: {
+    // 计算歌词居中的 top值
+    clacTop() {
+      this.$nextTick(() => {
+        const dom = this.$refs.musicLyric
+        if (dom) {
+          const { display = '' } = window.getComputedStyle(dom)
+          if (display === 'none') {
+            return
+          }
+          const height = dom.offsetHeight
+          this.top = Math.floor(height / 34 / 2)
+        }
+      })
+    },
+  },
+}
+</script>
+
+<style lang="less" scoped>
+.music-info {
+  padding-bottom: 20px;
+  text-align: center;
+  font-size: @font_size_medium;
+  dt {
+    position: relative;
+    width: 186px;
+    height: 186px;
+    margin: 0 auto 15px;
+    &:after {
+      content: '';
+      position: absolute;
+      left: 9px;
+      top: 0;
+      width: 201px;
+      height: 180px;
+      background: url('~@/assets/img/album_cover_player.png') 0 0 no-repeat;
+    }
+    img {
+      vertical-align: middle;
+      width: 186px;
+      height: 186px;
+    }
+  }
+  dd {
+    height: 30px;
+    line-height: 30px;
+    .no-wrap();
+  }
+}
+
+/*歌词部分*/
+.music-lyric {
+  position: absolute;
+  top: 315px;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  overflow: hidden;
+  text-align: center;
+  mask-image: linear-gradient(
+    to bottom,
+    rgba(255, 255, 255, 0) 0,
+    rgba(255, 255, 255, 0.6) 15%,
+    rgba(255, 255, 255, 1) 25%,
+    rgba(255, 255, 255, 1) 75%,
+    rgba(255, 255, 255, 0.6) 85%,
+    rgba(255, 255, 255, 0) 100%
+  );
+  .music-lyric-items {
+    text-align: center;
+    line-height: 34px;
+    font-size: @font_size_small;
+    transform: translate3d(0, 0, 0);
+    transition: transform 0.6s ease-out;
+    .no-wrap();
+    .on {
+      color: @lyric_color_active;
+    }
+  }
+}
+
+// 当屏幕小于 960 时
+@media (max-width: 960px) {
+  .music-info {
+    display: none;
+  }
+  .music-lyric {
+    top: 0;
+  }
+}
+</style>

+ 229 - 0
src/components/mm-header/mm-header.vue

@@ -0,0 +1,229 @@
+<template>
+  <!--头部-->
+  <header class="mm-header">
+    <h1 class="header">
+      <a href="https://github.com/maomao1996/Vue-mmPlayer" target="_blank">
+        mmPlayer 在线音乐播放器
+      </a>
+    </h1>
+    <dl class="user">
+      <template v-if="user.userId">
+        <router-link class="user-info" to="/music/userlist" tag="dt">
+          <img class="avatar" :src="`${user.avatarUrl}?param=50y50`" />
+          <span>{{ user.nickname }}</span>
+        </router-link>
+        <dd class="user-btn" @click="openDialog(2)">退出</dd>
+      </template>
+      <dd v-else class="user-btn" @click="openDialog(0)">登录</dd>
+    </dl>
+    <!--登录-->
+    <mm-dialog
+      ref="loginDialog"
+      head-text="登录"
+      confirm-btn-text="登录"
+      cancel-btn-text="关闭"
+      @confirm="login"
+    >
+      <div class="mm-dialog-text">
+        <input
+          v-model.trim="uidValue"
+          class="mm-dialog-input"
+          type="number"
+          autofocus
+          placeholder="请输入您的网易云 UID"
+          @keyup.enter="login"
+        />
+      </div>
+      <div slot="btn" @click="openDialog(1)">帮助</div>
+    </mm-dialog>
+    <!--帮助-->
+    <mm-dialog
+      ref="helpDialog"
+      head-text="登录帮助"
+      confirm-btn-text="去登录"
+      cancel-btn-text="关闭"
+      @confirm="openDialog(0)"
+    >
+      <div class="mm-dialog-text">
+        <p>
+          1、
+          <a target="_blank" href="https://music.163.com">
+            点我(https://music.163.com)
+          </a>
+          打开网易云音乐官网
+        </p>
+        <p>2、点击页面右上角的“登录”</p>
+        <p>3、点击您的头像,进入我的主页</p>
+        <p>4、复制浏览器地址栏 /user/home?id= 后面的数字(网易云 UID)</p>
+      </div>
+    </mm-dialog>
+    <!--退出-->
+    <mm-dialog
+      ref="outDialog"
+      body-text="确定退出当前用户吗?"
+      @confirm="out"
+    />
+  </header>
+</template>
+
+<script>
+import { getUserPlaylist } from '@api/music.js'
+import { mapGetters, mapActions } from 'vuex'
+import MmDialog from '@base/mm-dialog/mm-dialog'
+import { toHttps } from '@/utils/util'
+
+export default {
+  name: 'MmHeader',
+  components: {
+    MmDialog
+  },
+  data() {
+    return {
+      user: {}, // 用户数据
+      uidValue: '' // 记录用户 UID
+    }
+  },
+  computed: {
+    ...mapGetters(['uid'])
+  },
+  created() {
+    this.uid && this._getUserPlaylist(this.uid)
+  },
+  methods: {
+    // 打开对话框
+    openDialog(key) {
+      switch (key) {
+        case 0:
+          this.$refs.loginDialog.show()
+          break
+        case 1:
+          this.$refs.loginDialog.hide()
+          this.$refs.helpDialog.show()
+          break
+        case 2:
+          this.$refs.outDialog.show()
+          break
+        case 3:
+          this.$refs.loginDialog.hide()
+          break
+      }
+    },
+    // 退出登录
+    out() {
+      this.user = {}
+      this.setUid(null)
+      this.$mmToast('退出成功!')
+    },
+    // 登录
+    login() {
+      if (this.uidValue === '') {
+        this.$mmToast('UID 不能为空')
+        this.openDialog(0)
+        return
+      }
+      this.openDialog(3)
+      this._getUserPlaylist(this.uidValue)
+    },
+    // 获取用户数据
+    _getUserPlaylist(uid) {
+      getUserPlaylist(uid).then(({ playlist = [] }) => {
+        this.uidValue = ''
+        if (playlist.length === 0 || !playlist[0].creator) {
+          this.$mmToast(`未查询找 UID 为 ${uid} 的用户信息`)
+          return
+        }
+        const creator = playlist[0].creator
+        this.setUid(uid)
+        creator.avatarUrl = toHttps(creator.avatarUrl)
+        this.user = creator
+        setTimeout(() => {
+          this.$mmToast(`${this.user.nickname} 欢迎使用 mmPlayer`)
+        }, 200)
+      })
+    },
+    ...mapActions(['setUid'])
+  }
+}
+</script>
+
+<style lang="less">
+.mm-header {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 60px;
+  @media (max-width: 768px) {
+    background: @header_bg_color;
+  }
+  .header {
+    text-align: center;
+    line-height: 60px;
+    color: @text_color_active;
+    font-size: @font_size_large;
+    @media (max-width: 768px) {
+      padding-left: 15px;
+      text-align: left;
+    }
+  }
+  .user {
+    position: absolute;
+    top: 50%;
+    right: 15px;
+    line-height: 30px;
+    text-align: right;
+    transform: translateY(-50%);
+    &-info {
+      float: left;
+      margin-right: 15px;
+      cursor: pointer;
+      .avatar {
+        width: 30px;
+        height: 30px;
+        border-radius: 90px;
+        vertical-align: middle;
+      }
+      span {
+        margin-left: 10px;
+        color: @text_color_active;
+      }
+    }
+    &-btn {
+      float: left;
+      cursor: pointer;
+      &:hover {
+        color: @text_color_active;
+      }
+    }
+    @media (max-width: 768px) {
+      &-info {
+        margin-right: 10px;
+        span {
+          display: none;
+        }
+      }
+    }
+  }
+}
+.mm-dialog-text {
+  text-align: left;
+  .mm-dialog-input {
+    width: 100%;
+    height: 40px;
+    box-sizing: border-box;
+    padding: 0 15px;
+    border: 1px solid @btn_color;
+    outline: 0;
+    background: transparent;
+    color: @text_color_active;
+    font-size: @font_size_medium;
+    box-shadow: 0 0 1px 0 #fff inset;
+    &::placeholder {
+      color: @text_color;
+    }
+  }
+  a:hover {
+    color: #d43c33;
+  }
+}
+</style>

+ 81 - 0
src/components/music-btn/music-btn.vue

@@ -0,0 +1,81 @@
+<template>
+  <!--选项-->
+  <div class="music-btn">
+    
+    <span @click="$emit('changeList', 'playList')" :class="{hoverSpan:formComponent=='playList'}">正在播放</span>
+    <span @click="$emit('changeList', 'topList')" :class="{hoverSpan:formComponent=='topList'}">推荐</span>
+    <span @click="$emit('changeList', 'search')" :class="{hoverSpan:formComponent=='search'}">搜索</span>
+    <!-- <span @click="$emit('changeList','userList')">
+      我的歌单
+    </span>
+    <span class="show-960" @click="$emit('onClickLyric')">歌词</span>
+    <span @click="$emit('changeList','historyList')">我听过的</span> -->
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'music-btn',
+  props:{
+    formComponent:{
+      type:String,
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.music-btn {
+  width: 100%;
+  height: 60px;
+  font-size: 0;
+  white-space: nowrap;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+  .hoverSpan{
+    border-color: #fff;
+    color: #fff;
+  }
+  span {
+    display: inline-block;
+    height: 40px;
+    box-sizing: border-box;
+    margin-right: 8px;
+    padding: 0 23px;
+    border: 1px solid @btn_color;
+    color: @btn_color;
+    border-radius: 2px;
+    font-size: 14px;
+    line-height: 40px;
+    overflow: hidden;
+    cursor: pointer;
+    &:nth-last-of-type(1) {
+      margin: 0;
+    }
+    &:hover,
+    &.active {
+      border-color: @btn_color_active;
+      color: @btn_color_active;
+    }
+  }
+  @media (min-width: 960px) {
+    span.show-960 {
+      display: none;
+    }
+  }
+  @media (max-width: 960px) {
+    span.show-960 {
+      display: inline-block;
+    }
+  }
+  @media (max-width: 768px) {
+    height: 50px;
+    span {
+      height: 35px;
+      padding: 0 10px;
+      margin-right: 6px;
+      line-height: 35px;
+    }
+  }
+}
+</style>

+ 365 - 0
src/components/music-list/music-list.vue

@@ -0,0 +1,365 @@
+<template>
+  <!--歌曲列表-->
+  <div class="musicList">
+    <template v-if="list.length > 0">
+      <div class="list-item list-header">
+        <span class="list-name">歌曲</span>
+        <span class="list-artist">歌手</span>
+        <span v-if="listType === 1" class="list-time">时长</span>
+        <span v-else class="list-album">专辑</span>
+      </div>
+      <div ref="listContent" class="list-content" @scroll="listScroll($event)">
+        <div
+          v-for="(item, index) in list"
+          :key="item.id"
+          class="list-item"
+          :class="{ on: playing && currentMusic.id === item.id }"
+          @dblclick="selectItem(item, index, $event)"
+        >
+          <span class="list-num" v-text="index + 1"></span>
+          <div class="list-name">
+            <span>{{ item.name }}</span>
+            <div class="list-menu">
+              <mm-icon
+                class="hover"
+                :type="getPlayIconType(item)"
+                :size="40"
+                @click.stop="selectItem(item, index)"
+              />
+            </div>
+          </div>
+          <span class="list-artist">{{ item.singer }}</span>
+          <span v-if="listType === 1" class="list-time">
+            {{ item.duration % 3600 | format }}
+            <!-- <mm-icon
+              class="hover list-menu-icon-del"
+              type="delete-mini"
+              :size="40"
+              @click.stop="deleteItem(item, index)"
+            /> -->
+            <a class="hover list-menu-icon-del" style="font-size:16px" @click.stop="deleteItem(item, index)">下载</a>
+          </span>
+          <span v-else class="list-album">{{ item.album }}
+            <a class="hover list-menu-icon-del" style="font-size:16px;float:right;margin-right:10px" @click.stop="deleteItem(item, index)">下载</a>
+          </span>
+        </div>
+        <slot name="listBtn"></slot>
+      </div>
+    </template>
+    <mm-no-result v-else title="弄啥呢,怎么啥也没有!!!" />
+  </div>
+</template>
+
+<script>
+// import {getCheckMusic} from '@api/music.js'
+import { mapGetters, mapMutations } from 'vuex'
+import { format } from '@/utils/util'
+import MmNoResult from '@base/mm-no-result/mm-no-result'
+
+const musicUrl = 'https://music.163.com/song/media/outer/url'
+export default {
+  name: 'MusicList',
+  components: {
+    MmNoResult
+  },
+  filters: {
+    format
+  },
+  props: {
+    // 歌曲数据
+    list: {
+      type: Array,
+      default: () => []
+    },
+    /**
+     *  0:显示专辑栏目(默认)
+     *  1:显示时长栏目
+     */
+    listType: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      lockUp: true // 是否锁定滚动加载事件,默认锁定
+    }
+  },
+  computed: {
+    ...mapGetters(['playing', 'currentMusic'])
+  },
+  watch: {
+    list(newList, oldList) {
+      if (this.listType !== 2) {
+        return
+      }
+      if (newList.length !== oldList.length) {
+        this.lockUp = false
+      } else if (
+        newList[newList.length - 1].id !== oldList[oldList.length - 1].id
+      ) {
+        this.lockUp = false
+      }
+    }
+  },
+  activated() {
+    this.scrollTop &&
+      this.$refs.listContent &&
+      (this.$refs.listContent.scrollTop = this.scrollTop)
+  },
+  methods: {
+    // 滚动事件
+    listScroll(e) {
+      const scrollTop = e.target.scrollTop
+      this.scrollTop = scrollTop
+      if (this.listType !== 2 || this.lockUp) {
+        return
+      }
+      const { scrollHeight, offsetHeight } = e.target
+      if (scrollTop + offsetHeight >= scrollHeight - 50) {
+        this.lockUp = true // 锁定滚动加载
+        this.$emit('pullUp') // 触发滚动加载事件
+      }
+    },
+    // 回到顶部
+    scrollTo() {
+      this.$refs.listContent.scrollTop = 0
+    },
+    // 播放暂停事件
+    selectItem(item, index, e) {
+      if (e && /list-menu-icon-del/.test(e.target.className)) {
+        return
+      }
+      if (this.currentMusic.id && item.id === this.currentMusic.id) {
+        this.setPlaying(!this.playing)
+        return
+      }
+
+      /**
+       * 为了修复 safari、 ios 微信、安卓 UC 无法播放问题,暂时移除接口校验直接播放
+       */
+      this.$emit('select', item, index) // 触发点击播放事件
+
+      // getMusicUrl(item.id)
+      // .then(res => {
+      //     if (!res.data.data[0].url) {
+      //         this.$mmToast('当前音乐无法播放,请播放其他音乐')
+      //     } else {
+      //         this.$emit('select', item, index)//触发点击播放事件
+      //     }
+      // });
+      // getCheckMusic(item.id)
+      // .then(res => {
+      //     if (res.data.message !== 'ok') {
+      //         this.$mmToast('当前音乐无法播放,请播放其他音乐')
+      //     } else {
+      //         this.$emit('select', item, index)//触发点击播放事件
+      //     }
+      // }).catch(error => {
+      //     this.$mmToast(error.response.data.message)
+      // })
+    },
+    // 获取播放状态 type
+    getPlayIconType({ id: itemId }) {
+      const {
+        playing,
+        currentMusic: { id }
+      } = this
+      return playing && id === itemId ? 'pause-mini' : 'play-mini'
+    },
+    // 删除事件
+    deleteItem(item, index) {
+      // this.$emit('del', index) // 触发删除事件
+      // console.log(item, index)
+      let downloadElement = document.createElement('a')
+      downloadElement.href = musicUrl + '?id=' + item.id + '.mp3'
+      downloadElement.download = item.name
+      downloadElement.target = '_blank'
+      document.body.appendChild(downloadElement)
+      downloadElement.click()
+      document.body.removeChild(downloadElement)
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING'
+    })
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.list-header {
+  border-bottom: 1px solid @list_head_line_color;
+  color: @text_color_active;
+
+  .list-name {
+    padding-left: 40px;
+    user-select: none;
+  }
+}
+
+.list-content {
+  width: 100%;
+  height: calc(~'100% - 120px');
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.list-no {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  color: @text_color;
+}
+
+.list-item {
+  display: flex;
+  width: 100%;
+  height: 50px;
+  border-bottom: 1px solid @list_item_line_color;
+  line-height: 50px;
+  overflow: hidden;
+
+  &.list-item-no {
+    justify-content: center;
+    align-items: center;
+  }
+
+  &.on {
+    color: #fff;
+
+    .list-num {
+      font-size: 0;
+      background: url('~@/assets/img/wave.gif') no-repeat center center;
+    }
+  }
+
+  &:hover {
+    .list-name {
+      padding-right: 80px;
+
+      .list-menu {
+        display: block;
+      }
+    }
+  }
+
+  &:not([class*='list-header']):hover {
+    .list-name {
+      padding-right: 80px;
+
+      .list-menu {
+        display: block;
+      }
+    }
+
+    .list-time {
+      font-size: 0;
+
+      .list-menu-icon-del {
+        display: block;
+      }
+    }
+  }
+
+  .list-num {
+    display: block;
+    width: 30px;
+    margin-right: 10px;
+    text-align: center;
+  }
+
+  .list-name {
+    position: relative;
+    flex: 1;
+    box-sizing: border-box;
+
+    & > span {
+      text-overflow: ellipsis;
+      overflow: hidden;
+      display: -webkit-box;
+      -webkit-line-clamp: 1;
+      -webkit-box-orient: vertical;
+    }
+
+    small {
+      margin-left: 5px;
+      font-size: 12px;
+      color: rgba(255, 255, 255, 0.5);
+    }
+
+    /*hover菜单*/
+
+    .list-menu {
+      display: none;
+      position: absolute;
+      top: 50%;
+      right: 10px;
+      height: 40px;
+      font-size: 0;
+      transform: translateY(-50%);
+    }
+  }
+
+  .list-artist,
+  .list-album {
+    display: block;
+    width: 300px;
+    .no-wrap();
+    @media (max-width: 1440px) {
+      width: 200px;
+    }
+    @media (max-width: 1200px) {
+      width: 150px;
+    }
+  }
+
+  .list-time {
+    display: block;
+    width: 60px;
+    position: relative;
+
+    .list-menu-icon-del {
+      display: none;
+      position: absolute;
+      top: 50%;
+      left: 0;
+      transform: translateY(-50%);
+    }
+  }
+}
+
+@media (max-width: 960px) {
+  .list-item .list-name {
+    padding-right: 70px;
+  }
+}
+
+@media (max-width: 768px) {
+  .list-item {
+    .list-name .list-menu {
+      display: block;
+    }
+
+    .list-artist,
+    .list-album {
+      width: 20%;
+    }
+  }
+}
+
+@media (max-width: 640px) {
+  .list-item {
+    .list-artist {
+      width: 80px;
+    }
+
+    .list-album,
+    .list-time {
+      display: none;
+    }
+  }
+}
+</style>

+ 82 - 0
src/components/volume/volume.vue

@@ -0,0 +1,82 @@
+<!-- 音量控制组件 -->
+<template>
+  <div class="volume">
+    <mm-icon
+      class="pointer volume-icon"
+      :type="getVolumeIconType()"
+      :size="30"
+      @click="handleToggleVolume"
+    />
+    <div class="volume-progress-wrapper">
+      <mm-progress
+        :percent="volumeProgress"
+        @percentChange="handleVolumeChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import MmProgress from '@base/mm-progress/mm-progress'
+
+export default {
+  name: 'Volume',
+  components: {
+    MmProgress
+  },
+  props: {
+    volume: {
+      type: Number,
+      required: true
+    }
+  },
+  computed: {
+    volumeProgress() {
+      return this.volume
+    },
+    isMute: {
+      get() {
+        return this.volumeProgress === 0
+      },
+      set(newMute) {
+        const volume = newMute ? 0 : this.lastVolume
+        if (newMute) {
+          this.lastVolume = this.volumeProgress
+        }
+        this.handleVolumeChange(volume)
+      }
+    }
+  },
+  methods: {
+    getVolumeIconType() {
+      return this.isMute ? 'volume-off' : 'volume'
+    },
+    // 是否静音
+    handleToggleVolume() {
+      this.isMute = !this.isMute
+    },
+    handleVolumeChange(percent) {
+      this.$emit('volumeChange', percent)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.volume {
+  display: flex;
+  align-items: center;
+  width: 150px;
+  &-icon {
+    margin-right: 5px;
+    color: #fff;
+  }
+  &-progress-wrapper {
+    flex: 1;
+  }
+  @media (max-width: 768px) {
+    top: 2px;
+    width: 36px;
+  }
+}
+</style>

+ 36 - 0
src/config.js

@@ -0,0 +1,36 @@
+// 版本号
+export const VERSION = process.env.VUE_APP_VERSION
+
+/**
+ * 默认歌单ID (正在播放列表)
+ * 默认云音乐热歌榜 https://music.163.com/#/discover/toplist?id=3778678
+ * 如需要修改自定义歌单的请修改
+ * @type {number}
+ */
+export const defaultSheetId = 3778678
+
+// 默认分页数量
+export const defaultLimit = 30
+
+// 默认背景图(可引入网络图或本地静态图)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+const req = require.context('./assets/background', false)
+const BG_ARR = requireAll(req)
+export const defaultBG = BG_ARR[Math.floor(Math.random() * BG_ARR.length)]
+
+// 默认音量
+export const defaultVolume = 0.8
+
+/**
+ * 播放模式
+ * listLoop: 列表循环
+ * order:顺序
+ * loop: 单曲循环
+ * random: 随机
+ */
+export const playMode = {
+  listLoop: 0,
+  order: 1,
+  random: 2,
+  loop: 3
+}

+ 10 - 2
src/main.js

@@ -37,7 +37,12 @@ import 'k-form-design/styles/k-form-design.less'
 // import 'k-form-design/lib/k-form-design.css'
 Vue.use(KFormDesign)
 import 'ant-design-vue/dist/antd.less';
-
+import '@/styles/index.less'
+import VueLazyload from 'vue-lazyload'
+Vue.use(VueLazyload, {
+  preLoad: 1,
+  loading: require('@assets/img/default.png')
+})
 Vue.prototype.$md5 = md5;
 import {
   ACCESS_TOKEN,
@@ -73,7 +78,10 @@ import {
 import * as WPS from './assets/jwps.es6'
 Vue.prototype.wps = WPS;
 
-
+import Icon from '@base/mm-icon/mm-icon'
+import mmToast from '@base/mm-toast'
+Vue.component(Icon.name, Icon)
+Vue.use(mmToast)
 
 Vue.prototype.$getDay = getDay
 Vue.prototype.allWeeks = allWeeks

+ 102 - 0
src/store/actions.js

@@ -0,0 +1,102 @@
+import {
+  clearHistoryList,
+  setHistoryList,
+  removeHistoryList,
+  setMode,
+  setUserId
+} from '@/utils/storage'
+import * as types from './mutation-types'
+
+function findIndex(list, music) {
+  return list.findIndex(item => {
+    return item.id === music.id
+  })
+}
+
+
+export default {
+  // 设置播放列表
+  setPlaylist: function ({ commit }, { list }) {
+    commit(types.SET_PLAYLIST, list)
+    commit(types.SET_ORDERLIST, list)
+  },
+  // 选择播放(会更新整个播放列表)
+  selectPlay: function ({ commit }, { list, index }) {
+    commit(types.SET_PLAYLIST, list)
+    commit(types.SET_ORDERLIST, list)
+    commit(types.SET_CURRENTINDEX, index)
+    commit(types.SET_PLAYING, true)
+  },
+  // 选择播放(会插入一条到播放列表)
+  selectAddPlay: function ({ commit, state }, music) {
+    let list = [...state.playlist]
+    // 查询当前播放列表是否有代插入的音乐,并返回其索引值
+    let index = findIndex(list, music)
+    // 当前播放列表有待插入的音乐时,直接改变当前播放音乐的索引值
+    if (index > -1) {
+      commit(types.SET_CURRENTINDEX, index)
+    } else {
+      list.unshift(music)
+      commit(types.SET_PLAYLIST, list)
+      commit(types.SET_ORDERLIST, list)
+      commit(types.SET_CURRENTINDEX, 0)
+    }
+    commit(types.SET_PLAYING, true)
+  },
+  // 清空播放列表
+  clearPlayList: function ({ commit }) {
+    commit(types.SET_PLAYING, false)
+    commit(types.SET_CURRENTINDEX, -1)
+    commit(types.SET_PLAYLIST, [])
+    commit(types.SET_ORDERLIST, [])
+  },
+
+  // 删除正在播放列表中的歌曲
+  removerPlayListItem: function (
+    { commit, state },
+    { list, index }
+  ) {
+    let currentIndex = state.currentIndex
+    if (index < state.currentIndex || list.length === state.currentIndex) {
+      currentIndex--
+      commit(types.SET_CURRENTINDEX, currentIndex)
+    }
+    commit(types.SET_PLAYLIST, list)
+    commit(types.SET_ORDERLIST, list)
+    if (!list.length) {
+      commit(types.SET_PLAYING, false)
+    } else {
+      commit(types.SET_PLAYING, true)
+    }
+  },
+  // 设置播放历史
+  setHistory: function ({ commit }, music) {
+    commit(types.SET_HISTORYLIST, setHistoryList(music))
+  },
+
+  // 删除播放历史
+  removeHistory: function ({ commit }, music) {
+    commit(types.SET_HISTORYLIST, removeHistoryList(music))
+  },
+  // 清空播放历史
+  clearHistory: function ({ commit }) {
+    commit(types.SET_HISTORYLIST, clearHistoryList())
+  },
+  // 设置播放模式
+  setPlayMode: function ({ commit }, mode) {
+    commit(types.SET_PLAYMODE, setMode(mode))
+  },
+  // 设置网易云用户UID
+  setUid: function ({ commit }, uid) {
+    commit(types.SET_UID, setUserId(uid))
+  }
+}
+
+
+
+
+
+
+
+
+

+ 27 - 6
src/store/getters.js

@@ -5,17 +5,38 @@ const getters = {
   theme: state => state.app.theme,
   color: state => state.app.color,
   token: state => state.user.token,
-  avatar: state => {state.user.avatar = Vue.ls.get(USER_INFO).avatar; return state.user.avatar},
+  avatar: state => { state.user.avatar = Vue.ls.get(USER_INFO).avatar; return state.user.avatar },
   username: state => state.user.username,
-  nickname: state => {state.user.realname = Vue.ls.get(USER_INFO).realname; return state.user.realname},
+  nickname: state => { state.user.realname = Vue.ls.get(USER_INFO).realname; return state.user.realname },
   welcome: state => state.user.welcome,
   permissionList: state => state.user.permissionList,
-  userInfo: state => {state.user.info = Vue.ls.get(USER_INFO); return state.user.info},
+  userInfo: state => { state.user.info = Vue.ls.get(USER_INFO); return state.user.info },
   addRouters: state => state.permission.addRouters,
-  enhanceJs:(state) => (code) => {
-    state.enhance.enhanceJs[code] = Vue.ls.get(ENHANCE_PRE+code);
+  enhanceJs: (state) => (code) => {
+    state.enhance.enhanceJs[code] = Vue.ls.get(ENHANCE_PRE + code);
     return state.enhance.enhanceJs[code]
-  }
+  },
+
+  // audio元素
+  audioEle: state => state.audioEle,
+  // 播放模式
+  mode: state => state.mode,
+  // 播放状态
+  playing: state => state.playing,
+  // 播放列表
+  playlist: state => state.playlist,
+  // 顺序列表
+  orderList: state => state.orderList,
+  // 当前音乐索引
+  currentIndex: state => state.currentIndex,
+  // 当前音乐
+  currentMusic: state => {
+    return state.playlist[state.currentIndex] || {}
+  },
+  // 播放历史列表
+  historyList: state => state.historyList,
+  // 网易云用户UID
+  uid: state => state.uid
 
 }
 

+ 8 - 11
src/store/index.js

@@ -5,8 +5,10 @@ import app from './modules/app'
 import user from './modules/user'
 import permission from './modules/permission'
 import enhance from './modules/enhance'
+import music from './modules/music'
 import getters from './getters'
-
+import mutations from './mutations'
+import actions from './actions'
 Vue.use(Vuex)
 
 export default new Vuex.Store({
@@ -14,16 +16,11 @@ export default new Vuex.Store({
     app,
     user,
     permission,
-    enhance
-  },
-  state: {
-
-  },
-  mutations: {
-
-  },
-  actions: {
-
+    enhance,
   },
+  state: music,
+  mutations,
+  actions,
   getters
+  
 })

+ 15 - 0
src/store/modules/music.js

@@ -0,0 +1,15 @@
+import { playMode } from '@/config'
+import { getHistoryList, getMode, getUserId } from '@/utils/storage'
+
+const state = {
+  audioEle: null, // audio元素
+  mode: Number(getMode()) || playMode.listLoop, // 播放模式,默认列表循环
+  playing: false, // 播放状态
+  playlist: [], // 播放列表
+  orderList: [], // 顺序列表
+  currentIndex: -1, // 当前音乐索引
+  historyList: getHistoryList() || [], // 播放历史列表
+  uid: getUserId() || null // 网易云用户UID
+}
+
+export default state

+ 12 - 1
src/store/mutation-types.js

@@ -21,4 +21,15 @@ export const INDEX_MAIN_PAGE_PATH =  "/dashboard/analysis"
 export const CONTENT_WIDTH_TYPE = {
   Fluid: 'Fluid',
   Fixed: 'Fixed'
-}
+}
+
+
+
+export const SET_AUDIOELE = 'SET_AUDIOELE' // 修改audio元素
+export const SET_PLAYMODE = 'SET_PLAYMODE' // 修改播放模式
+export const SET_PLAYING = 'SET_PLAYING' // 修改播放状态
+export const SET_PLAYLIST = 'SET_PLAYLIST' // 修改播放列表
+export const SET_ORDERLIST = 'SET_ORDERLIST' // 修改顺序列表
+export const SET_CURRENTINDEX = 'SET_CURRENTINDEX' // 修改当前音乐索引
+export const SET_HISTORYLIST = 'SET_HISTORYLIST' // 修改播放历史列表
+export const SET_UID = 'SET_UID' // 修改网易云用户UID

+ 38 - 0
src/store/mutations.js

@@ -0,0 +1,38 @@
+import * as types from './mutation-types'
+
+const mutations = {
+  // 修改audio元素
+  [types.SET_AUDIOELE](state, audioEle) {
+    state.audioEle = audioEle
+  },
+  // 修改播放模式
+  [types.SET_PLAYMODE](state, mode) {
+    state.mode = mode
+  },
+  // 修改播放状态
+  [types.SET_PLAYING](state, playing) {
+    state.playing = playing
+  },
+  // 修改播放列表
+  [types.SET_PLAYLIST](state, playlist) {
+    state.playlist = playlist
+  },
+  // 修改顺序列表
+  [types.SET_ORDERLIST](state, orderList) {
+    state.orderList = orderList
+  },
+  // 修改当前音乐索引
+  [types.SET_CURRENTINDEX](state, currentIndex) {
+    state.currentIndex = currentIndex
+  },
+  // 修改播放历史列表
+  [types.SET_HISTORYLIST](state, historyList) {
+    state.historyList = historyList
+  },
+  // 修改网易云用户UID
+  [types.SET_UID](state, uid) {
+    state.uid = uid
+  }
+}
+
+export default mutations

+ 98 - 0
src/styles/index.less

@@ -0,0 +1,98 @@
+@import 'reset';
+@import 'var';
+html,
+body,
+#app {
+  width: 100%;
+  height: 100%;
+  // overflow: hidden;
+}
+
+body {
+  min-width: 320px;
+  // font-family: Arial;
+}
+
+// #app {
+//   position: relative;
+// }
+
+.cover-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+//浮动
+.fl {
+  float: left;
+}
+
+.fr {
+  float: right;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+.hover {
+  color: @text_color;
+  cursor: pointer;
+  &:hover {
+    color: @text_color_active;
+  }
+}
+
+.text-left {
+  text-align: left;
+}
+
+.clearfix {
+  &:after {
+    display: block;
+    content: '';
+    clear: both;
+  }
+}
+
+//滚动条
+::-webkit-scrollbar {
+  /*滚动条整体部分,其中的属性有width,height,background,border(就和一个块级元素一样)等*/
+  background-color: rgba(0, 0, 0, 0.3);
+  width: 5px; //纵向滚动条
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-button {
+  /*滚动条两端的按钮。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
+  display: none;
+}
+
+::-webkit-scrollbar-track {
+  /*外层轨道。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
+  display: none;
+  //background-color: rgba(255, 255, 255, 0.1);
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-track-piece {
+  /*内层轨道,滚动条中间部分(除去)。*/
+  //background-color: rgba(255, 255, 255, .1);
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+  /*滚动条里面可以拖动的那部分*/
+  background-color: rgba(255, 255, 255, 0.5);
+  border-radius: 10px;
+}
+
+::-webkit-scrollbar-corner {
+  border-radius: 10px;
+}
+
+::-webkit-resizer {
+  /*定义右下角拖动块的样式*/
+  border-radius: 10px;
+}

+ 13 - 0
src/styles/mixin.less

@@ -0,0 +1,13 @@
+// 显示省略号
+.no-wrap() {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.flex-center(@direction: row) {
+  display: flex;
+  flex-direction: @direction;
+  justify-content: center;
+  align-items: center;
+}

+ 141 - 0
src/styles/reset.less

@@ -0,0 +1,141 @@
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font: inherit;
+  vertical-align: baseline;
+  -webkit-tap-highlight-color: transparent;
+}
+
+/* HTML5 display-role reset for older browsers */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+  display: block;
+}
+
+body {
+  line-height: 1;
+}
+
+ol,
+ul {
+  list-style: none;
+}
+
+blockquote,
+q {
+  quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+  content: '';
+  content: none;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+a {
+  text-decoration: none;
+  color: inherit;
+}
+
+input[type='number']::-webkit-inner-spin-button,
+input[type='number']::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+}

+ 52 - 0
src/styles/var.less

@@ -0,0 +1,52 @@
+//字体颜色定义规范
+@text_color: rgba(255, 255, 255, 0.6);
+@text_color_active: #fff; //重点部分
+
+//active颜色
+@active_color: #fff;
+
+//遮罩层颜色
+@mask_color: rgba(0, 0, 0, 0.4);
+
+//loading背景颜色
+@load_bg_color: rgba(0, 0, 0, 0.2);
+
+//header背景颜色
+@header_bg_color: rgba(0, 0, 0, 0.3);
+
+//search-head
+@search_bg_coloe: rgba(0, 0, 0, 0.2);
+
+//dialog相关
+@dialog_bg_color: rgba(0, 0, 0, 0.5);
+@dialog_content_bg_color: rgba(0, 0, 0, 0.6);
+@dialog_text_color: rgba(255, 255, 255, 0.7);
+@dialog_line_color: rgba(0, 0, 0, 0.35);
+
+//btn相关
+@btn_color: rgba(255, 255, 255, 0.6);
+@btn_color_active: #fff;
+
+//歌词高亮颜色
+@lyric_color_active: #40ce8f;
+
+//进度条
+@bar_color: rgba(255, 255, 255, 0.15);
+@line_color: #fff;
+@dot_color: #fff;
+
+//列表
+@list_head_line_color: rgba(255, 255, 255, 0.8);
+@list_item_line_color: rgba(255, 255, 255, 0.1);
+
+//评论
+@comment_head_line_color: rgba(255, 255, 255, 0.8);
+@comment_item_line_color: rgba(255, 255, 255, 0.1);
+@comment_replied_line_color: rgba(255, 255, 255, 0.3);
+
+//字体大小定义规范
+@font_size_small: 12px;
+@font_size_medium: 14px;
+@font_size_medium_x: 16px;
+@font_size_large: 18px;
+@font_size_large_x: 22px;

+ 23 - 0
src/utils/axiosMusic.js

@@ -0,0 +1,23 @@
+import axios from 'axios'
+import Vue from 'vue'
+
+const request = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API_URL
+})
+
+request.interceptors.response.use(
+  response => {
+    window.response = response
+
+    if (response.status === 200 && response.data.code === 200) {
+      return response.data
+    }
+    return Promise.reject(response)
+  },
+  e => {
+    Vue.prototype.$mmToast(e.message)
+    throw e
+  }
+)
+
+export default request

+ 53 - 1
src/utils/mixin.js

@@ -37,4 +37,56 @@ const mixinDevice = {
   }
 }
 
-export {mixin, mixinDevice}
+export {mixin, mixinDevice}
+
+
+
+
+
+import { mapGetters, mapMutations, mapActions } from 'vuex'
+
+/**
+ * 歌曲列表
+ */
+export const listMixin = {
+  computed: {
+    ...mapGetters(['playing', 'currentMusic'])
+  },
+  methods: {
+    selectItem(item, index) {
+      if (item.id === this.currentMusic.id && this.playing) {
+        this.setPlaying(false)
+      } else {
+        this.selectPlay({
+          list: this.list,
+          index
+        })
+      }
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING'
+    }),
+    ...mapActions(['selectPlay'])
+  }
+}
+
+/**
+ * loading状态
+ * @type {{data(): *, methods: {_hideLoad(): void}}}
+ */
+export const loadMixin = {
+  data() {
+    return {
+      mmLoadShow: true // loading状态
+    }
+  },
+  methods: {
+    _hideLoad() {
+      let timer
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        this.mmLoadShow = false
+      }, 200)
+    }
+  }
+}

+ 70 - 0
src/utils/song.js

@@ -0,0 +1,70 @@
+import { toHttps } from './util'
+
+function filterSinger(singers) {
+  let arr = []
+  singers.forEach(item => {
+    arr.push(item.name)
+  })
+  return arr.join('/')
+}
+
+export class Song {
+  constructor({ id, name, singer, album, image, duration, url }) {
+    this.id = id
+    this.name = name
+    this.singer = singer
+    this.album = album
+    this.image = image
+    this.duration = duration
+    this.url = url
+  }
+}
+
+export function createPlayList(music) {
+  return new Song({
+    id: music.id,
+    name: music.name,
+    singer: music.artists.length > 0 && filterSinger(music.artists),
+    album: music.album.name,
+    image: toHttps(music.album.picUrl) || null,
+    duration: music.duration / 1000,
+    url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
+  })
+}
+
+export function createTopList(music) {
+  return new Song({
+    id: music.id,
+    name: music.name,
+    singer: music.ar.length > 0 && filterSinger(music.ar),
+    album: music.al.name,
+    image: toHttps(music.al.picUrl),
+    duration: music.dt / 1000,
+    url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
+  })
+}
+
+// 歌曲数据格式化
+const formatSongs = function formatPlayList(list) {
+  let Songs = []
+  list.forEach(item => {
+    const musicData = item
+    if (musicData.id) {
+      Songs.push(createPlayList(musicData))
+    }
+  })
+  return Songs
+}
+
+export const formatTopSongs = function formatTopList(list) {
+  let Songs = []
+  list.forEach(item => {
+    const musicData = item
+    if (musicData.id) {
+      Songs.push(createTopList(musicData))
+    }
+  })
+  return Songs
+}
+
+export default formatSongs

+ 136 - 0
src/utils/storage.js

@@ -1,3 +1,5 @@
+
+import { defaultVolume } from '@/config'
 /**
  * Set storage
  *
@@ -76,3 +78,137 @@ export const clearAll = () => {
   window.localStorage.clear()
 }
 
+
+
+//音乐库方法
+const _storage = window.localStorage
+const storage = {
+  get(key, data = []) {
+    if (_storage) {
+      return _storage.getItem(key)
+        ? Array.isArray(data)
+          ? JSON.parse(_storage.getItem(key))
+          : _storage.getItem(key)
+        : data
+    }
+  },
+  set(key, val) {
+    if (_storage) {
+      _storage.setItem(key, val)
+    }
+  },
+  clear(key) {
+    if (_storage) {
+      _storage.removeItem(key)
+    }
+  }
+}
+
+/**
+ * 播放历史
+ * @type    HISTORYLIST_KEY:key值
+ *          HistoryListMAX:最大长度
+ */
+const HISTORYLIST_KEY = '__mmPlayer_historyList__'
+const HistoryListMAX = 200
+// 获取播放历史
+export function getHistoryList() {
+  return storage.get(HISTORYLIST_KEY)
+}
+
+// 更新播放历史
+export function setHistoryList(music) {
+  let list = storage.get(HISTORYLIST_KEY)
+  const index = list.findIndex(item => {
+    return item.id === music.id
+  })
+  if (index === 0) {
+    return list
+  }
+  if (index > 0) {
+    list.splice(index, 1)
+  }
+  list.unshift(music)
+  if (HistoryListMAX && list.length > HistoryListMAX) {
+    list.pop()
+  }
+  storage.set(HISTORYLIST_KEY, JSON.stringify(list))
+  return list
+}
+
+// 删除一条播放历史
+export function removeHistoryList(music) {
+  storage.set(HISTORYLIST_KEY, JSON.stringify(music))
+  return music
+}
+
+// 清空播放历史
+export function clearHistoryList() {
+  storage.clear(HISTORYLIST_KEY)
+  return []
+}
+
+/**
+ * 播放模式
+ * @type    MODE_KEY:key值
+ *          HistoryListMAX:最大长度
+ */
+const MODE_KEY = '__mmPlayer_mode__'
+// 获取播放模式
+export function getMode() {
+  return storage.get(MODE_KEY, null)
+}
+// 修改播放模式
+export function setMode(mode) {
+  storage.set(MODE_KEY, mode)
+  return mode
+}
+
+/**
+ * 网易云用户uid
+ * @type USERID_KEY:key值
+ */
+const USERID_KEY = '__mmPlayer_userID__'
+// 获取用户uid
+export function getUserId() {
+  return Number(storage.get(USERID_KEY, null))
+}
+// 修改用户uid
+export function setUserId(uid) {
+  storage.set(USERID_KEY, uid)
+  return uid
+}
+
+/**
+ * 版本号
+ * @type VERSION_KEY:key值
+ */
+const VERSION_KEY = '__mmPlayer_version__'
+// 获取版本号
+export function getVersion() {
+  let version = storage.get(VERSION_KEY, null)
+  return Array.isArray(version) ? null : version
+}
+// 修改版本号
+export function setVersion(version) {
+  storage.set(VERSION_KEY, version)
+  return version
+}
+
+/**
+ * 音量
+ * @type VOLUME_KEY:key值
+ */
+const VOLUME_KEY = '__mmPlayer_volume__'
+// 获取音量
+export function getVolume() {
+  const volume = storage.get(VOLUME_KEY, defaultVolume)
+  return Number(volume)
+}
+// 修改音量
+export function setVolume(volume) {
+  storage.set(VOLUME_KEY, volume)
+  return volume
+}
+
+

+ 110 - 1
src/utils/util.js

@@ -506,4 +506,113 @@ export const transformTozTreeFormat = (sNodes, setting={}) => {
     } else {
         return [sNodes];
     }
-}
+}
+
+//音乐库方法
+// 随机排序数组/洗牌函数 https://github.com/lodash/lodash/blob/master/shuffle.js
+function copyArray(source, array) {
+  let index = -1
+  const length = source.length
+  array || (array = new Array(length))
+  while (++index < length) {
+    array[index] = source[index]
+  }
+  return array
+}
+
+export const randomSortArray = function shuffle(array) {
+  const length = array == null ? 0 : array.length
+  if (!length) {
+    return []
+  }
+  let index = -1
+  const lastIndex = length - 1
+  const result = copyArray(array)
+  while (++index < length) {
+    const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
+    const value = result[rand]
+    result[rand] = result[index]
+    result[index] = value
+  }
+  return result
+}
+
+// 防抖函数
+export function debounce(func, delay) {
+  let timer
+  return function(...args) {
+    if (timer) {
+      clearTimeout(timer)
+    }
+    timer = setTimeout(() => {
+      func.apply(this, args)
+    }, delay)
+  }
+}
+
+// 补0函数
+export function addZero(s) {
+  return s < 10 ? '0' + s : s
+}
+
+// 歌词解析
+const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g
+export function parseLyric(lrc) {
+  const lines = lrc.split('\n')
+  const lyric = []
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i]
+    const result = timeExp.exec(line)
+    if (!result) {
+      continue
+    }
+    const text = line.replace(timeExp, '').trim()
+    if (text) {
+      lyric.push({
+        time: (result[1] * 6e4 + result[2] * 1e3 + (result[3] || 0) * 1) / 1e3,
+        text
+      })
+    }
+  }
+  return lyric
+}
+
+// 时间格式化
+export function format(value) {
+  let minute = Math.floor(value / 60)
+  let second = Math.floor(value % 60)
+  return `${addZero(minute)}:${addZero(second)}`
+}
+
+/**
+ * https://github.com/videojs/video.js/blob/master/src/js/utils/promise.js
+ * Silence a Promise-like object.
+ *
+ * This is useful for avoiding non-harmful, but potentially confusing "uncaught
+ * play promise" rejection error messages.
+ *
+ * @param  {Object} value
+ *         An object that may or may not be `Promise`-like.
+ */
+export function isPromise(v) {
+  return v !== undefined && v !== null && typeof v.then === 'function'
+}
+
+export function silencePromise(value) {
+  if (isPromise(value)) {
+    value.then(null, () => {})
+  }
+}
+
+// 判断 string 类型
+export function isString(v) {
+  return typeof v === 'string'
+}
+
+// http 链接转化成 https
+export function toHttps(url) {
+  if (!isString(url)) {
+    return url
+  }
+  return url.replace('http://', 'https://')
+}

+ 274 - 0
src/views/modules/material/comment/comment.vue

@@ -0,0 +1,274 @@
+<template>
+  <!--评论-->
+  <div class="comment">
+    <mm-loading v-model="mmLoadShow" />
+    <dl
+      v-if="hotComments.length > 0"
+      class="comment-list"
+      @scroll="listScroll($event)"
+    >
+      <!--精彩评论-->
+      <dt class="comment-title">精彩评论</dt>
+      <dd
+        v-for="item in hotComments"
+        :key="item.commentId"
+        class="comment-item"
+      >
+        <a
+          target="_blank"
+          :href="`https://music.163.com/#/user/home?id=${item.user.userId}`"
+        >
+          <img
+            :src="`${item.user.avatarUrl}`"
+            class="comment-item-pic"
+          />
+          <h2 class="comment-item-title">{{ item.user.nickname }}</h2>
+        </a>
+        <p class="comment-item-disc">{{ item.content }}</p>
+        <div class="comment-item-opt">
+          <span class="comment-opt-date">{{ item.time | format }}</span>
+          <span class="comment-opt-liked">
+            <mm-icon type="good" />
+            {{ item.likedCount }}
+          </span>
+        </div>
+      </dd>
+      <!--最新评论-->
+      <dt class="comment-title">最新评论({{ total }})</dt>
+      <dd
+        v-for="item in commentList"
+        :key="item.commentId"
+        class="comment-item"
+      >
+        <a
+          class="comment-item-pic"
+          target="_blank"
+          :href="`https://music.163.com/#/user/home?id=${item.user.userId}`"
+        >
+                   <!-- v-lazy="`${item.user.avatarUrl}?param=50y50`" -->
+          <img
+ 
+            :src="`${item.user.avatarUrl}`"
+            class="cover-img"
+          />
+        </a>
+        <h2 class="comment-item-title">
+          <a
+            target="_blank"
+            :href="`https://music.163.com/#/user/home?id=${item.user.userId}`"
+          >
+            {{ item.user.nickname }}
+          </a>
+        </h2>
+        <p class="comment-item-disc">{{ item.content }}</p>
+        <div
+          v-for="beReplied in item.beReplied"
+          :key="beReplied.user.userId"
+          class="comment-item-replied"
+        >
+          <a
+            target="_blank"
+            :href="
+              `https://music.163.com/#/user/home?id=${beReplied.user.userId}`
+            "
+          >
+            {{ beReplied.user.nickname }}
+          </a>
+          :{{ beReplied.content }}
+        </div>
+        <div class="comment-item-opt">
+          <span class="comment-opt-date">{{ item.time | format }}</span>
+          <span v-if="item.likedCount > 0" class="comment-opt-liked">
+            <mm-icon type="good" />
+            {{ item.likedCount }}
+          </span>
+        </div>
+      </dd>
+    </dl>
+  </div>
+</template>
+
+<script>
+import { getComment } from '@api/music.js'
+import { addZero } from '@/utils/util'
+import MmLoading from '@base/mm-loading/mm-loading'
+import { loadMixin } from '@/utils/mixin'
+
+export default {
+  name: 'Comment',
+  components: {
+    MmLoading
+  },
+  filters: {
+    // 格式化时间
+    format(time) {
+      let formatTime
+      const date = new Date(time)
+      const dateObj = {
+        year: date.getFullYear(),
+        month: date.getMonth(),
+        date: date.getDate(),
+        hours: date.getHours(),
+        minutes: date.getMinutes()
+      }
+      const newTime = new Date()
+      const diff = newTime.getTime() - time
+      if (newTime.getDate() === dateObj.date && diff < 60000) {
+        formatTime = '刚刚'
+      } else if (
+        newTime.getDate() === dateObj.date &&
+        diff > 60000 &&
+        diff < 3600000
+      ) {
+        formatTime = `${Math.floor(diff / 60000)}分钟前`
+      } else if (
+        newTime.getDate() === dateObj.date &&
+        diff > 3600000 &&
+        diff < 86400000
+      ) {
+        formatTime = `${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
+      } else if (newTime.getDate() !== dateObj.date && diff < 86400000) {
+        formatTime = `昨天${addZero(dateObj.hours)}:${addZero(dateObj.minutes)}`
+      } else if (newTime.getFullYear() === dateObj.year) {
+        formatTime = `${dateObj.month + 1}月${dateObj.date}日`
+      } else {
+        formatTime = `${dateObj.year}年${dateObj.month + 1}月${dateObj.date}日`
+      }
+      return formatTime
+    }
+  },
+  mixins: [loadMixin],
+  data() {
+    return {
+      lockUp: true, // 是否锁定滚动加载事件,默认锁定
+      page: 0, // 分页
+      hotComments: [], // 精彩评论
+      commentList: [], // 最新评论
+      total: null // 评论总数
+    }
+  },
+  watch: {
+    commentList(newList, oldList) {
+      if (newList.length !== oldList.length) {
+        this.lockUp = false
+      }
+    }
+  },
+  created() {
+    this.initData()
+  },
+  methods: {
+    // 初始化数据
+    initData() {
+      getComment(this.$route.params.id, this.page).then(res => {
+        this.hotComments = res.hotComments
+        this.commentList = res.comments
+        this.total = res.total
+        this.lockUp = true
+        this._hideLoad()
+      })
+    },
+    // 列表滚动事件
+    listScroll(e) {
+      if (this.lockUp) {
+        return
+      }
+      const { scrollTop, scrollHeight, offsetHeight } = e.target
+      if (scrollTop + offsetHeight >= scrollHeight - 100) {
+        this.lockUp = true // 锁定滚动加载
+        this.page += 1
+        this.pullUp() // 触发滚动加载事件
+      }
+    },
+    // 滚动加载事件
+    pullUp() {
+      getComment(this.$route.params.id, this.page).then(({ comments }) => {
+        this.commentList = [...this.commentList, ...comments]
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.comment {
+  position: relative;
+  transform: translate3d(0, 0, 0);
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  .comment-list {
+    height: 100%;
+    padding: 0 10px;
+    overflow-x: hidden;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+    .comment-title {
+      height: 34px;
+      line-height: 34px;
+      padding: 10px 0;
+      color: @text_color_active;
+      border-bottom: 1px solid @comment_head_line_color;
+    }
+    .comment-item {
+      position: relative;
+      padding: 15px 0 15px 55px;
+      border-bottom: 1px solid @comment_item_line_color;
+      .comment-item-pic {
+        display: block;
+        position: absolute;
+        left: 0;
+        top: 20px;
+        width: 38px;
+        height: 38px;
+        border-radius: 50%;
+        overflow: hidden;
+      }
+      .comment-item-title {
+        height: 20px;
+        margin-bottom: 6px;
+        font-weight: 400;
+        .no-wrap();
+        color: @text_color_active;
+      }
+      .comment-item-disc {
+        overflow: hidden;
+        word-break: break-all;
+        word-wrap: break-word;
+        line-height: 25px;
+        text-align: justify;
+        color: @text_color;
+        img {
+          position: relative;
+          vertical-align: middle;
+          top: -2px;
+        }
+      }
+      .comment-item-replied {
+        padding: 8px 19px;
+        margin-top: 10px;
+        line-height: 20px;
+        border: 1px solid @comment_replied_line_color;
+        a {
+          color: @text_color_active;
+        }
+      }
+      .comment-item-opt {
+        margin-top: 10px;
+        line-height: 25px;
+        text-align: right;
+        overflow: hidden;
+        .comment-opt-date {
+          float: left;
+          line-height: 28px;
+        }
+        .comment-opt-liked {
+          display: inline-block;
+          height: 20px;
+          line-height: 20px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 67 - 0
src/views/modules/material/details/details.vue

@@ -0,0 +1,67 @@
+<template>
+  <!--歌单详情-->
+  <div class="details">
+    <mm-loading v-model="mmLoadShow" />
+    <music-list :list="list" @select="selectItem" />
+  </div>
+</template>
+
+<script>
+import { mapActions } from 'vuex'
+import { getPlaylistDetail } from '@api/music.js'
+import MmLoading from '@base/mm-loading/mm-loading'
+import MusicList from '@/components/music-list/music-list'
+import { loadMixin } from '@/utils/mixin'
+
+export default {
+  name: 'Detail-all',
+  components: {
+    MmLoading,
+    MusicList,
+  },
+  mixins: [loadMixin],
+  props: {
+    detailId: {
+      type: [String, Number],
+    },
+  },
+  data() {
+    return {
+      list: [], // 列表
+    }
+  },
+  mounted() {
+    // 获取歌单详情
+    this.$nextTick(() => {
+      getPlaylistDetail(this.detailId).then((playlist) => {
+        console.log(playlist)
+        // document.title = `${playlist.name} - mmPlayer在线音乐播放器`
+        this.list = playlist.tracks
+        this._hideLoad()
+      })
+    })
+  },
+  methods: {
+    // 播放暂停事件
+    selectItem(item, index) {
+      this.selectPlay({
+        list: this.list,
+        index,
+      })
+    },
+    ...mapActions(['selectPlay']),
+  },
+}
+</script>
+
+<style lang="less" scoped>
+.details {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  .musicList {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 88 - 0
src/views/modules/material/historyList/historyList.vue

@@ -0,0 +1,88 @@
+<template>
+  <!--我听过的(播放历史)-->
+  <div class="historyList">
+    <music-list
+      :list="historyList"
+      :list-type="1"
+      @select="selectItem"
+      @del="deleteItem"
+    >
+      <div slot="listBtn" class="list-btn">
+        <span @click="$refs.dialog.show()">清空列表</span>
+      </div>
+    </music-list>
+    <mm-dialog
+      ref="dialog"
+      body-text="是否清空播放历史列表"
+      confirm-btn-text="清空"
+      @confirm="clearList"
+    />
+  </div>
+</template>
+
+<script>
+import { mapGetters, mapMutations, mapActions } from 'vuex'
+import MusicList from '@/components/music-list/music-list'
+import MmDialog from '@base/mm-dialog/mm-dialog'
+
+export default {
+  name: 'HistoryList',
+  components: {
+    MusicList,
+    MmDialog
+  },
+  computed: {
+    ...mapGetters(['historyList', 'playing', 'currentMusic'])
+  },
+  methods: {
+    // 清空列表事件
+    clearList() {
+      this.clearHistory()
+      this.$mmToast('列表清空成功')
+    },
+    // 播放事件
+    selectItem(item, index) {
+      this.selectPlay({
+        list: this.historyList,
+        index
+      })
+    },
+    // 删除事件
+    deleteItem(index) {
+      let list = [...this.historyList]
+      list.splice(index, 1)
+      this.removeHistory(list)
+      this.$mmToast('删除成功')
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING'
+    }),
+    ...mapActions(['selectPlay', 'clearHistory', 'removeHistory'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.historyList {
+  width: 100%;
+  height: 100%;
+  .musicList {
+    width: 100%;
+    height: 100%;
+    .list-btn {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 50px;
+      span {
+        padding: 5px 20px;
+        cursor: pointer;
+        user-select: none;
+        &:hover {
+          color: @text_color_active;
+        }
+      }
+    }
+  }
+}
+</style>

+ 97 - 0
src/views/modules/material/mmPlayer.js

@@ -0,0 +1,97 @@
+import { playMode } from '@/config'
+
+// 重试次数
+let retry = 1
+
+const mmPlayerMusic = {
+  initAudio(that) {
+    const ele = that.audioEle
+    // 音频缓冲事件
+    ele.onprogress = () => {
+      try {
+        if (ele.buffered.length > 0) {
+          const duration = that.currentMusic.duration
+          let buffered = 0
+          ele.buffered.end(0)
+          buffered =
+            ele.buffered.end(0) > duration ? duration : ele.buffered.end(0)
+          that.currentProgress = buffered / duration
+        }
+      } catch (e) {}
+    }
+    // 开始播放音乐
+    ele.onplay = () => {
+      let timer
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        that.musicReady = true
+      }, 100)
+    }
+    // 获取当前播放时间
+    ele.ontimeupdate = () => {
+      that.currentTime = ele.currentTime
+    }
+    // 当前音乐播放完毕
+    ele.onended = () => {
+      if (that.mode === playMode.loop) {
+        that.loop()
+      } else {
+        that.next()
+      }
+    }
+    // 音乐播放出错
+    ele.onerror = () => {
+      if (retry === 0) {
+        let toastText = '当前音乐为VIP歌曲不可播放,已自动播放下一曲'
+        if (that.playlist.length === 1) {
+          toastText = '没有可播放的音乐哦~'
+        }
+        that.$mmToast(toastText)
+        that.next(true)
+      } else {
+        // eslint-disable-next-line no-console
+        console.log('重试一次')
+        retry -= 1
+        ele.url = that.currentMusic.url
+        ele.load()
+      }
+      // console.log('播放出错啦!')
+    }
+    // 音乐进度拖动大于加载时重载音乐
+    ele.onstalled = () => {
+      ele.load()
+      that.setPlaying(false)
+      let timer
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        that.setPlaying(true)
+      }, 10)
+    }
+    // 将能播放的音乐加入播放历史
+    ele.oncanplay = () => {
+      retry = 1
+      if (
+        that.historyList.length === 0 ||
+        that.currentMusic.id !== that.historyList[0].id
+      ) {
+        that.setHistory(that.currentMusic)
+      }
+    }
+    // 音频数据不可用时
+    ele.onstalled = () => {
+      ele.load()
+      that.setPlaying(false)
+      let timer
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        that.setPlaying(true)
+      }, 10)
+    }
+    // 当音频已暂停时
+    ele.onpause = () => {
+      that.setPlaying(false)
+    }
+  }
+}
+
+export default mmPlayerMusic

+ 658 - 0
src/views/modules/material/musicMaterial.vue

@@ -0,0 +1,658 @@
+<template>
+  <div class="music">
+    <div class="music-content">
+      <div class="music-left">
+          <!-- {{$store.state.playlist}} -->
+        <music-btn @onClickLyric="handleOpenLyric" @changeList="changeList" :formComponent="formComponent"/>
+        <!-- <keep-alive>
+          <router-view v-if="$route.meta.keepAlive" class="music-list" />
+        </keep-alive>
+        <router-view
+          v-if="!$route.meta.keepAlive"
+          :key="$route.path"
+          class="music-list"
+        /> -->
+        <component :is="formComponent" v-if="showFormComponent"  @changeList="changeList" :detailId="detailId"></component>
+      </div>
+      <div class="music-right" :class="{ show: lyricVisible }">
+        <div class="close-lyric" @click="handleCloseLyric">关闭歌词</div>
+        <lyric ref="lyric" :lyric="lyric" :nolyric="nolyric" :lyric-index="lyricIndex" />
+      </div>
+    </div>
+
+    <!--播放器-->
+    <div class="music-bar" :class="{ disable: !musicReady || !currentMusic.id }" style="position:relative;z-index:100">
+      <div class="music-bar-btns">
+        <mm-icon class="pointer" type="prev" :size="36" title="上一曲 Ctrl + Left" @click="prev" />
+        <div class="control-play pointer" title="播放暂停 Ctrl + Space" @click="play">
+          <mm-icon :type="playing ? 'pause' : 'play'" :size="36" />
+        </div>
+        <mm-icon class="pointer" type="next" :size="36" title="下一曲 Ctrl + Right" @click="next" />
+      </div>
+      <div class="music-music">
+        <div class="music-bar-info">
+          <template v-if="currentMusic && currentMusic.id">
+            {{ currentMusic.name }}
+            <span>- {{ currentMusic.singer }}</span>
+          </template>
+          <!-- <template v-else>欢迎使用mmPlayer在线音乐播放器</template> -->
+        </div>
+        <div v-if="currentMusic.id" class="music-bar-time">
+          {{ currentTime | format }}/{{ currentMusic.duration % 3600 | format }}
+        </div>
+        <mm-progress
+          class="music-progress"
+          :percent="percentMusic"
+          :percent-progress="currentProgress"
+          @percentChange="progressMusic"
+          @percentChangeEnd="progressMusicEnd"
+        />
+      </div>
+
+      <!-- 播放模式 -->
+      <mm-icon
+        class="icon-color pointer mode"
+        :type="getModeIconType()"
+        :title="getModeIconTitle()"
+        :size="30"
+        @click="modeChange"
+      />
+
+      <!-- 评论 -->
+      <!-- <mm-icon class="icon-color pointer comment" type="comment" :size="30" @click="openComment" /> -->
+
+      <!-- 音量控制 -->
+      <!-- <div class="music-bar-volume" title="音量加减 [Ctrl + Up / Down]">
+        <volume :volume="volume" @volumeChange="volumeChange" />
+      </div> -->
+    </div>
+    <!-- <audio ref="mmPlayer"></audio> -->
+    <!--遮罩-->
+    <!-- :style="{ backgroundImage: picUrl }" -->
+    <div class="mmPlayer-bg" style="background:#0b2040"></div>
+    <!-- <div class="mmPlayer-mask"></div> -->
+  </div>
+</template>
+
+<script>
+import { getLyric, getPlaylistDetail } from '@api/music.js'
+import mmPlayerMusic from './mmPlayer'
+import { randomSortArray, parseLyric, format, silencePromise } from '@/utils/util'
+import { playMode, defaultBG } from '@/config'
+import { getVolume, setVolume } from '@/utils/storage'
+import { mapGetters, mapMutations, mapActions } from 'vuex'
+
+import MmProgress from '@base/mm-progress/mm-progress'
+import MusicBtn from '@/components/music-btn/music-btn'
+import Lyric from '@/components/lyric/lyric'
+import Volume from '@/components/volume/volume'
+
+import playList from './playList/playList'
+import userList from './userList/userList'
+import topList from './topList/topList'
+import detailsAll from './details/details'
+import historyList from './historyList/historyList'
+import search from './search/search'
+import comment from './comment/comment'
+import { defaultSheetId } from '@/config'
+
+export default {
+  name: 'Music',
+  components: {
+    MmProgress,
+    MusicBtn,
+    Lyric,
+    Volume,
+    playList,
+    userList,
+    topList,
+    detailsAll,
+    historyList,
+    search,
+    comment,
+  },
+  filters: {
+    // 时间格式化
+    format,
+  },
+  data() {
+    const volume = getVolume()
+    return {
+      showFormComponent: true,
+      formComponent: null,
+      musicReady: false, // 是否可以使用播放器
+      currentTime: 0, // 当前播放时间
+      currentProgress: 0, // 当前缓冲进度
+      lyricVisible: false, // 移动端歌词显示
+      lyric: [], // 歌词
+      nolyric: false, // 是否有歌词
+      lyricIndex: 0, // 当前播放歌词下标
+      isMute: false, // 是否静音
+      volume, // 音量大小
+      detailId:null
+    }
+  },
+  computed: {
+    picUrl() {
+      return this.currentMusic.id && this.currentMusic.image
+        ? `url(${this.currentMusic.image}?param=300y300)`
+        : `url(${defaultBG})`
+    },
+    percentMusic() {
+      const duration = this.currentMusic.duration
+      return this.currentTime && duration ? this.currentTime / duration : 0
+    },
+    ...mapGetters([
+      'audioEle',
+      'mode',
+      'playing',
+      'playlist',
+      'orderList',
+      'currentIndex',
+      'currentMusic',
+      'historyList',
+    ]),
+  },
+  watch: {
+    currentMusic(newMusic, oldMusic) {
+      if (!newMusic.id) {
+        this.lyric = []
+        return
+      }
+      if (newMusic.id === oldMusic.id) {
+        return
+      }
+      this.audioEle.src = newMusic.url
+      // 重置相关参数
+      this.lyricIndex = this.currentTime = this.currentProgress = 0
+      silencePromise(this.audioEle.play())
+      this.$nextTick(() => {
+        this._getLyric(newMusic.id)
+      })
+    },
+    playing(newPlaying) {
+      const audio = this.audioEle
+      this.$nextTick(() => {
+        newPlaying ? silencePromise(audio.play()) : audio.pause()
+        this.musicReady = true
+      })
+    },
+    currentTime(newTime) {
+      if (this.nolyric) {
+        return
+      }
+      let lyricIndex = 0
+      for (let i = 0; i < this.lyric.length; i++) {
+        if (newTime > this.lyric[i].time) {
+          lyricIndex = i
+        }
+      }
+      this.lyricIndex = lyricIndex
+    },
+    $route() {
+      this.lyricVisible = false
+    },
+    formComponent(n, o) {
+      setTimeout(() => {
+        this.showFormComponent = true
+      }, 100)
+    },
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.formComponent = "playList"
+      mmPlayerMusic.initAudio(this)
+      this.initKeyDown()
+      this.volumeChange(this.volume)
+    })
+  },
+  created() {
+    // 获取正在播放列表
+    // getPlaylistDetail(defaultSheetId).then((playlist) => {
+    //   const list = playlist.tracks.slice(0, 100)
+    //   console.log(playlist.tracks)
+    //   this.setPlaylist({ list })
+    // })
+
+    // this.$nextTick(() => {
+    //   this.setAudioele(this.$refs.mmPlayer)
+    // })
+  },
+  methods: {
+    //切换歌单
+    changeList(list,id) {
+      this.showFormComponent = false
+      this.formComponent = list
+      if(id){
+        this.detailId = id
+      }else{
+        this.detailId = null
+      }
+    },
+    // 按键事件
+    initKeyDown() {
+      document.onkeydown = (e) => {
+        switch (e.ctrlKey && e.keyCode) {
+          case 32: // 播放暂停Ctrl + Space
+            this.play()
+            break
+          case 37: // 上一曲Ctrl + Left
+            this.prev()
+            break
+          case 38: // 音量加Ctrl + Up
+            let plus = Number((this.volume += 0.1).toFixed(1))
+            if (plus > 1) {
+              plus = 1
+            }
+            this.volumeChange(plus)
+            break
+          case 39: // 下一曲Ctrl + Right
+            this.next()
+            break
+          case 40: // 音量减Ctrl + Down
+            let reduce = Number((this.volume -= 0.1).toFixed(1))
+            if (reduce < 0) {
+              reduce = 0
+            }
+            this.volumeChange(reduce)
+            break
+          case 79: // 切换播放模式Ctrl + O
+            this.modeChange()
+            break
+        }
+      }
+    },
+    // 上一曲
+    prev() {
+      if (!this.musicReady) {
+        return
+      }
+      if (this.playlist.length === 1) {
+        this.loop()
+      } else {
+        let index = this.currentIndex - 1
+        if (index < 0) {
+          index = this.playlist.length - 1
+        }
+        this.setCurrentIndex(index)
+        if (!this.playing && this.musicReady) {
+          this.setPlaying(true)
+        }
+        this.musicReady = false
+      }
+    },
+    // 播放暂停
+    play() {
+      if (!this.musicReady) {
+        return
+      }
+      this.setPlaying(!this.playing)
+    },
+    // 下一曲
+    // 当 flag 为 true 时,表示上一曲播放失败
+    next(flag = false) {
+      if (!this.musicReady) {
+        return
+      }
+      const {
+        playlist: { length },
+      } = this
+      if ((length - 1 === this.currentIndex && this.mode === playMode.order) || (length === 1 && flag)) {
+        this.setCurrentIndex(-1)
+        this.setPlaying(false)
+        return
+      }
+      if (length === 1) {
+        this.loop()
+      } else {
+        let index = this.currentIndex + 1
+        if (index === length) {
+          index = 0
+        }
+        if (!this.playing && this.musicReady) {
+          this.setPlaying(true)
+        }
+        this.setCurrentIndex(index)
+        this.musicReady = false
+      }
+    },
+    // 循环
+    loop() {
+      this.audioEle.currentTime = 0
+      silencePromise(this.audioEle.play())
+      this.setPlaying(true)
+      if (this.lyric.length > 0) {
+        this.lyricIndex = 0
+      }
+    },
+    // 修改音乐显示时长
+    progressMusic(percent) {
+      this.currentTime = this.currentMusic.duration * percent
+    },
+    // 修改音乐进度
+    progressMusicEnd(percent) {
+      this.audioEle.currentTime = this.currentMusic.duration * percent
+    },
+    // 切换播放顺序
+    modeChange() {
+      const mode = (this.mode + 1) % 4
+      this.setPlayMode(mode)
+      if (mode === playMode.loop) {
+        return
+      }
+      let list = []
+      switch (mode) {
+        case playMode.listLoop:
+        case playMode.order:
+          list = this.orderList
+          break
+        case playMode.random:
+          list = randomSortArray(this.orderList)
+          break
+      }
+      this.resetCurrentIndex(list)
+    //   this.setPlay(list)
+    },
+    // 修改当前歌曲索引
+    resetCurrentIndex(list) {
+      const index = list.findIndex((item) => {
+        return item.id === this.currentMusic.id
+      })
+      this.setCurrentIndex(index)
+    },
+    // 打开音乐评论
+    openComment() {
+      if (!this.currentMusic.id) {
+        this.$mmToast('还没有播放歌曲哦!')
+        return false
+      }
+      this.$router.push(`/music/comment/${this.currentMusic.id}`)
+    },
+    // 修改音量大小
+    volumeChange(percent) {
+      percent === 0 ? (this.isMute = true) : (this.isMute = false)
+      this.volume = percent
+      this.audioEle.volume = percent
+      setVolume(percent)
+    },
+    // 获取播放模式 icon
+    getModeIconType() {
+      return {
+        [playMode.listLoop]: 'loop',
+        [playMode.order]: 'sequence',
+        [playMode.random]: 'random',
+        [playMode.loop]: 'loop-one',
+      }[this.mode]
+    },
+    // 获取播放模式 title
+    getModeIconTitle() {
+      const key = 'Ctrl + O'
+      return {
+        [playMode.listLoop]: `列表循环 ${key}`,
+        [playMode.order]: `顺序播放 ${key}`,
+        [playMode.random]: `随机播放 ${key}`,
+        [playMode.loop]: `单曲循环 ${key}`,
+      }[this.mode]
+    },
+    // 查看歌词
+    handleOpenLyric() {
+      this.lyricVisible = true
+      this.$nextTick(() => {
+        this.$refs.lyric.clacTop()
+      })
+    },
+    // 关闭歌词
+    handleCloseLyric() {
+      this.lyricVisible = false
+    },
+    // 获取歌词
+    _getLyric(id) {
+      getLyric(id).then((res) => {
+        if (res.nolyric) {
+          this.nolyric = true
+        } else {
+          this.nolyric = false
+          this.lyric = parseLyric(res.lrc.lyric)
+        }
+        silencePromise(this.audioEle.play())
+      })
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING',
+      setPlay: 'SET_PLAYLIST',
+      setCurrentIndex: 'SET_CURRENTINDEX',
+      setAudioele: 'SET_AUDIOELE'
+    }),
+    ...mapActions(['setHistory', 'setPlayMode','setPlaylist']),
+  },
+}
+</script>
+
+<style lang="less">
+.music {
+  padding: 75px 25px 25px 25px;
+  width: 100%;
+  max-width: 1750px;
+  margin: 0 auto;
+  height: 100%;
+  box-sizing: border-box;
+  overflow: hidden;
+  position: relative;
+  .music-content {
+    display: flex;
+    width: 100%;
+    height: calc(~'80vh - 80px');
+    position: relative;
+    z-index: 10;
+    .music-left {
+      flex: 1;
+      height: 100%;
+      overflow: hidden;
+      .music-list {
+        height: calc(~'100% - 60px');
+      }
+    }
+    .music-right {
+      position: relative;
+      width: 310px;
+      margin-left: 10px;
+      .close-lyric {
+        position: absolute;
+        top: 0;
+        z-index: 1;
+        cursor: pointer;
+      }
+    }
+  }
+
+  /*底部mmPlayer-bar*/
+  .music-bar {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 80px;
+    box-sizing: border-box;
+    padding-bottom: 15px;
+    color: #fff;
+    &.disable {
+      pointer-events: none;
+      opacity: 0.6;
+    }
+    .icon-color {
+      color: #fff;
+    }
+    .music-bar-btns {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 180px;
+      .control-play {
+        .flex-center;
+        border-radius: 50%;
+        width: 40px;
+        height: 40px;
+        color: #fff;
+        background-color: rgba(255, 255, 255, 0.3);
+        text-align: center;
+
+        .icon-bofang101 {
+          transform: translateX(2px);
+        }
+      }
+    }
+
+    .flex-center;
+
+    .btn-prev {
+      width: 19px;
+      min-width: 19px;
+      height: 20px;
+      background-position: 0 -30px;
+    }
+    .btn-play {
+      width: 21px;
+      min-width: 21px;
+      height: 29px;
+      margin: 0 50px;
+      background-position: 0 0;
+      &.btn-play-pause {
+        background-position: -30px 0;
+      }
+    }
+    .btn-next {
+      width: 19px;
+      min-width: 19px;
+      height: 20px;
+      background-position: 0 -52px;
+    }
+    .music-music {
+      position: relative;
+      width: 100%;
+      flex: 1;
+      box-sizing: border-box;
+      padding-left: 40px;
+      font-size: @font_size_small;
+      color: @text_color_active;
+      .music-bar-info {
+        height: 15px;
+        padding-right: 80px;
+        line-height: 15px;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        display: -webkit-box;
+        -webkit-line-clamp: 1;
+        -webkit-box-orient: vertical;
+      }
+      .music-bar-time {
+        position: absolute;
+        top: 0;
+        right: 5px;
+      }
+    }
+    .mode,
+    .comment,
+    .music-bar-volume {
+      margin-left: 20px;
+    }
+
+    // 音量控制
+    .volume-wrapper {
+      margin-left: 20px;
+      width: 150px;
+    }
+  }
+
+  /*遮罩*/
+  .mmPlayer-mask,
+  .mmPlayer-bg {
+    position: absolute;
+    top: 0;
+    right: 0;
+    left: 0;
+    bottom: 0;
+  }
+
+  .mmPlayer-mask {
+    z-index: -1;
+    background-color: @mask_color;
+  }
+
+  .mmPlayer-bg {
+    z-index: 0;
+    background-repeat: no-repeat;
+    background-size: cover;
+    background-position: 50%;
+    filter: blur(20px);
+    opacity: 0.7;
+    transition: all 0.8s;
+    transform: scale(1.1);
+  }
+
+  @media (min-width: 960px) {
+    .close-lyric {
+      display: none;
+    }
+  }
+
+  //当屏幕小于960时
+  @media (max-width: 960px) {
+    .music-right {
+      display: none;
+      &.show {
+        display: block;
+        margin-left: 0;
+        width: 100%;
+      }
+    }
+  }
+  //当屏幕小于768时
+  @media (max-width: 768px) {
+    & {
+      padding: 75px 15px 5px 15px;
+    }
+
+    .music-content .music-left {
+      .music-list {
+        font-size: @font_size_medium;
+      }
+    }
+
+    .music-bar {
+      .music-bar-info span,
+      .music-bar-volume .mmProgress {
+        display: none;
+      }
+    }
+  }
+  //当屏幕小于520时
+  @media (max-width: 520px) {
+    .music-bar {
+      position: relative;
+      flex-direction: column;
+      .music-bar-btns {
+        width: 60%;
+        margin-top: 15px;
+        order: 2;
+      }
+      .music-music {
+        padding-left: 0;
+        order: 1;
+      }
+      & > i.mode {
+        position: absolute;
+        top: 40px;
+        left: 5px;
+        margin: 0;
+      }
+      .comment {
+        position: absolute;
+        top: 40px;
+        right: 5px;
+      }
+      .music-bar-volume {
+        display: none;
+      }
+    }
+  }
+}
+</style>

+ 97 - 0
src/views/modules/material/playList/playList.vue

@@ -0,0 +1,97 @@
+<template>
+  <!--正在播放-->
+  <div class="playList">
+    <music-list :list="list" :list-type="1" @select="selectItem" @del="deleteItem">
+      <div slot="listBtn" class="list-btn">
+        <span @click="$refs.dialog.show()">清空列表</span>
+      </div>
+    </music-list>
+    <mm-dialog ref="dialog" body-text="是否清空正在播放列表" confirm-btn-text="清空" @confirm="clearList" />
+  </div>
+</template>
+
+<script>
+import { mapGetters, mapMutations, mapActions } from 'vuex'
+import MusicList from '@/components/music-list/music-list'
+import MmDialog from '@base/mm-dialog/mm-dialog'
+import { getLyric, getPlaylistDetail } from '@api/music.js'
+import { defaultSheetId } from '@/config'
+export default {
+  name: 'PlayList',
+  components: {
+    MusicList,
+    MmDialog,
+  },
+  data() {
+    return {
+      show: false,
+      list: [],
+    }
+  },
+  computed: {
+    ...mapGetters(['playing', 'playlist', 'currentMusic']),
+  },
+  created() {
+    this.$nextTick(() => {
+      getPlaylistDetail(defaultSheetId).then((playlist) => {
+        this.list = playlist.tracks.slice(0, 100)
+        var list = this.list
+        this.setPlaylist({ list })
+      })
+    })
+  },
+  methods: {
+    // 清空列表事件
+    clearList() {
+      this.clearPlayList()
+      this.$mmToast('列表清空成功')
+    },
+    // 播放暂停事件
+    selectItem(item, index) {
+      if (item.id !== this.currentMusic.id) {
+        this.setCurrentIndex(index)
+        this.setPlaying(true)
+      }
+    },
+    // 删除事件
+    deleteItem(index) {
+      let list = [...this.playlist]
+      list.splice(index, 1)
+      this.removerPlayListItem({ list, index })
+      this.$mmToast('删除成功')
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING',
+      setCurrentIndex: 'SET_CURRENTINDEX',
+      clearPlaylist: 'CLEAR_PLAYLIST',
+    }),
+    ...mapActions(['removerPlayListItem', 'clearPlayList', 'setPlaylist']),
+  },
+}
+</script>
+
+<style lang="less">
+.playList {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  .musicList {
+    width: 100%;
+    height: 100%;
+    .list-btn {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 50px;
+      span {
+        padding: 5px 20px;
+        cursor: pointer;
+        user-select: none;
+        &:hover {
+          color: @text_color_active;
+        }
+      }
+    }
+  }
+}
+</style>

+ 176 - 0
src/views/modules/material/search/search.vue

@@ -0,0 +1,176 @@
+<template>
+  <!--搜索-->
+  <div class="search">
+    <mm-loading v-model="mmLoadShow" />
+    <div class="search-head">
+      <span
+        v-for="(item, index) in Artists"
+        :key="index"
+        @click="clickHot(item.first)"
+      >
+        {{ item.first }}
+      </span>
+      <input
+        v-model.trim="searchValue"
+        class="search-input"
+        type="text"
+        placeholder="音乐/歌手"
+        @keyup.enter="onEnter"
+      />
+    </div>
+    <music-list
+      ref="musicList"
+      :list="list"
+      :list-type="2"
+      @select="selectItem"
+      @pullUp="pullUpLoad"
+    />
+  </div>
+</template>
+
+<script>
+import { mapGetters, mapActions, mapMutations } from 'vuex'
+import { search, searchHot, getMusicDetail } from '@api/music.js'
+import formatSongs from '@/utils/song'
+import MmLoading from '@base/mm-loading/mm-loading'
+import MusicList from '@/components/music-list/music-list'
+import { loadMixin } from '@/utils/mixin'
+import { toHttps } from '@/utils/util'
+
+export default {
+  name: 'Search',
+  components: {
+    MmLoading,
+    MusicList
+  },
+  mixins: [loadMixin],
+  data() {
+    return {
+      searchValue: '', // 搜索关键词
+      Artists: [], // 热搜数组
+      list: [], // 搜索数组
+      page: 0, // 分页
+      lockUp: true // 是否锁定上拉加载事件,默认锁定
+    }
+  },
+  computed: {
+    ...mapGetters(['playing', 'currentMusic'])
+  },
+  watch: {
+    list(newList, oldList) {
+      if (newList.length !== oldList.length) {
+        this.lockUp = false
+      } else if (
+        newList[newList.length - 1].id !== oldList[oldList.length - 1].id
+      ) {
+        this.lockUp = false
+      }
+    }
+  },
+  created() {
+    // 获取热搜
+    searchHot().then(({ result }) => {
+      this.Artists = result.hots.slice(0, 5)
+      this.mmLoadShow = false
+    })
+  },
+  methods: {
+    // 点击热搜
+    clickHot(name) {
+      this.searchValue = name
+      this.onEnter()
+    },
+    // 搜索事件
+    onEnter() {
+      if (this.searchValue.replace(/(^\s+)|(\s+$)/g, '') === '') {
+        this.$mmToast('搜索内容不能为空!')
+        return
+      }
+      this.mmLoadShow = true
+      this.page = 0
+      if (this.list.length > 0) {
+        this.$refs.musicList.scrollTo()
+      }
+      search(this.searchValue).then(({ result }) => {
+        this.list = formatSongs(result.songs)
+        this._hideLoad()
+      })
+    },
+    // 滚动加载事件
+    pullUpLoad() {
+      this.page += 1
+      search(this.searchValue, this.page).then(({ result }) => {
+        if (!result.songs) {
+          this.$mmToast('没有更多歌曲啦!')
+          return
+        }
+        this.list = [...this.list, ...formatSongs(result.songs)]
+      })
+    },
+    // 播放歌曲
+    async selectItem(music) {
+      try {
+        const image = await this._getMusicDetail(music.id)
+        music.image = toHttps(image)
+        this.selectAddPlay(music)
+      } catch (error) {
+        this.$mmToast('哎呀,出错啦~')
+      }
+    },
+    // 获取歌曲详情
+    _getMusicDetail(id) {
+      return getMusicDetail(id).then(res => res.songs[0].al.picUrl)
+    },
+    ...mapMutations({
+      setPlaying: 'SET_PLAYING'
+    }),
+    ...mapActions(['selectAddPlay'])
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.search {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  .search-head {
+    display: flex;
+    padding: 10px 15px;
+    overflow: hidden;
+    background: @search_bg_coloe;
+    span {
+      line-height: 40px;
+      margin-right: 15px;
+      cursor: pointer;
+      &:hover {
+        color: @text_color_active;
+      }
+      @media (max-width: 640px) {
+        & {
+          display: none;
+        }
+      }
+    }
+    .search-input {
+      flex: 1;
+      height: 40px;
+      box-sizing: border-box;
+      padding: 0 15px;
+      border: 1px solid @btn_color;
+      outline: 0;
+      background: transparent;
+      color: @text_color_active;
+      font-size: @font_size_medium;
+      box-shadow: 0 0 1px 0 #fff inset;
+      &::placeholder {
+        color: @text_color;
+      }
+    }
+  }
+  .musicList {
+    width: 100%;
+    height: calc(~'100% - 50px');
+  }
+}
+</style>

+ 131 - 0
src/views/modules/material/topList/topList.vue

@@ -0,0 +1,131 @@
+<template>
+  <!--排行榜-->
+  <div class="topList">
+    <mm-loading v-model="mmLoadShow" />
+    <template v-if="!mmLoadShow">
+      <div class="topList-head">云音乐特色榜</div>
+      <div class="topList-content">
+        <div v-for="(item, index) in list" :key="index" class="list-item" :title="item.name + item.updateFrequency">
+          <!-- :to="{ path: `/music/details/${item.id}` }" -->
+          <div class="topList-item" @click="$emit('changeList','detailsAll',item.id)">
+            <div class="topList-img">
+              <!-- v-lazy="`${item.coverImgUrl}?param=300y300`" -->
+              <img :src="`${item.coverImgUrl}`" class="cover-img" />
+            </div>
+            <h3 class="name">{{ item.name }}</h3>
+          </div>
+        </div>
+      </div>
+      <div class="topList-head">热门歌单</div>
+      <div class="topList-content">
+        <div v-for="(item, index) in hotList" :key="index" class="list-item" :title="item.name + item.updateFrequency">
+          <!-- :to="{ path: `/music/details/${item.id}` }" -->
+          <div class="topList-item"  @click="$emit('changeList','detailsAll',item.id)">
+            <div class="topList-img">
+              <!-- v-lazy="`${item.picUrl}?param=300y300`" -->
+              <img :src="`${item.picUrl}`" class="cover-img" />
+            </div>
+            <h3 class="name">{{ item.name }}</h3>
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { getToplistDetail, getPersonalized } from '@api/music.js'
+import MmLoading from '@base/mm-loading/mm-loading'
+import { loadMixin } from '@/utils/mixin'
+
+export default {
+  name: 'TopList',
+  components: {
+    MmLoading,
+  },
+  mixins: [loadMixin],
+  data() {
+    return {
+      list: [], // 云音乐特色榜
+      hotList: [], // 热门歌单
+    }
+  },
+  created() {
+    Promise.all([getToplistDetail(), getPersonalized()])
+      .then(([topList, hotList]) => {
+        this.list = topList.list.filter((v) => v.ToplistType)
+        this.hotList = hotList.result.slice()
+        this._hideLoad()
+      })
+      .catch((e) => {
+        console.log(e)
+      })
+  },
+}
+</script>
+
+<style lang="less" scoped>
+.topList {
+  position: relative;
+  width: 100%;
+  height: calc(~'100% - 120px');  
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  &-head {
+    width: 100%;
+    height: 34px;
+    line-height: 34px;
+    font-size: @font_size_large;
+    color: @text_color_active;
+  }
+  &-content {
+    overflow: hidden;
+  }
+  .list-item {
+    float: left;
+    width: calc(~'100% / 7');
+    .topList-item {
+      width: 130px;
+      text-align: center;
+      cursor: pointer;
+      margin: 0 auto 20px;
+      &:hover {
+        color: #fff;
+      }
+      .name {
+        height: 30px;
+        line-height: 30px;
+        font-size: @font_size_medium;
+        .no-wrap();
+      }
+      @media (max-width: 1100px) {
+        width: 80%;
+      }
+    }
+    @media (max-width: 1500px) {
+      width: calc(~'100% / 6');
+    }
+    @media (max-width: 1400px), (max-width: 960px) {
+      width: calc(~'100% / 5');
+    }
+    @media (max-width: 1280px), (max-width: 768px) {
+      width: calc(~'100% / 4');
+    }
+    @media (max-width: 540px) {
+      width: calc(~'100% / 3');
+    }
+    .topList-img {
+      position: relative;
+      padding-top: 100%;
+      width: 100%;
+      height: 0;
+      .cover-img {
+        position: absolute;
+        top: 0;
+        left: 0;
+      }
+    }
+  }
+}
+</style>

+ 142 - 0
src/views/modules/material/userList/userList.vue

@@ -0,0 +1,142 @@
+<template>
+  <!--我的歌单-->
+  <div class="userList">
+    <mm-loading v-model="mmLoadShow" />
+    <template v-if="list.length > 0">
+      <div
+        v-for="item in formatList"
+        :key="item.id"
+        class="list-item"
+        :title="item.name"
+      >
+        <router-link
+          :to="{ path: `/music/details/${item.id}` }"
+          tag="div"
+          class="userList-item"
+        >
+        <!-- v-lazy="`${item.coverImgUrl}?param=200y200`" -->
+          <img 
+           :src="`${item.coverImgUrl}`"
+           class="cover-img" />
+          <h3 class="name">{{ item.name }}</h3>
+        </router-link>
+      </div>
+    </template>
+    <mm-no-result v-else title="啥也没有哦,快去登录看看吧!" />
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+import { getUserPlaylist } from '@api/music.js'
+import { loadMixin } from '@/utils/mixin'
+
+import MmLoading from '@base/mm-loading/mm-loading'
+import MmNoResult from '@base/mm-no-result/mm-no-result'
+
+export default {
+  name: 'UserList',
+  components: {
+    MmLoading,
+    MmNoResult
+  },
+  mixins: [loadMixin],
+  data() {
+    return {
+      list: [] // 列表
+    }
+  },
+  computed: {
+    formatList() {
+      return this.list.filter(item => item.trackCount > 0)
+    },
+    ...mapGetters(['uid'])
+  },
+  watch: {
+    uid(newUid) {
+      if (newUid) {
+        this.mmLoadShow = true
+        this._getUserPlaylist(newUid)
+      } else {
+        this.list = []
+      }
+    }
+  },
+  created() {
+    if (!this.uid) {
+      this.mmLoadShow = false
+    } else {
+      this._getUserPlaylist(this.uid)
+    }
+  },
+  activated() {
+    if (this.uid && this.list.length === 0) {
+      this.mmLoadShow = true
+      this._getUserPlaylist(this.uid)
+    } else if (!this.uid && this.list.length !== 0) {
+      this.list = []
+    }
+  },
+  methods: {
+    // 获取我的歌单详情
+    _getUserPlaylist(uid) {
+      getUserPlaylist(uid).then(res => {
+        if (res.playlist.length === 0) {
+          return
+        }
+        this.list = res.playlist
+        this._hideLoad()
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.userList {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  &-head {
+    height: 100px;
+  }
+  .list-item {
+    float: left;
+    width: calc(~'100% / 7');
+    .userList-item {
+      width: 130px;
+      text-align: center;
+      cursor: pointer;
+      margin: 0 auto 20px;
+      &:hover {
+        color: #fff;
+      }
+      .name {
+        height: 30px;
+        line-height: 30px;
+        font-size: @font_size_medium;
+        .no-wrap();
+      }
+      @media (max-width: 1100px) {
+        width: 80%;
+      }
+    }
+    @media (max-width: 1500px) {
+      width: calc(~'100% / 6');
+    }
+    @media (max-width: 1400px), (max-width: 960px) {
+      width: calc(~'100% / 5');
+    }
+    @media (max-width: 1280px), (max-width: 768px) {
+      width: calc(~'100% / 4');
+    }
+    @media (max-width: 540px) {
+      width: calc(~'100% / 3');
+    }
+  }
+}
+</style>

+ 12 - 3
vue.config.js

@@ -27,6 +27,7 @@ module.exports = {
   chainWebpack: (config) => {
     config.resolve.alias
       .set('@$', resolve('src'))
+      .set('@base', resolve('src/base'))
       .set('@api', resolve('src/api'))
       .set('@assets', resolve('src/assets'))
       .set('@comp', resolve('src/components'))
@@ -34,6 +35,15 @@ module.exports = {
       .set('@layout', resolve('src/layout'))
       .set('@static', resolve('src/static'))
   },
+  pluginOptions: {
+    'style-resources-loader': {
+      preProcessor: 'less',
+      patterns: [
+        resolve('src/styles/var.less'),
+        resolve('src/styles/mixin.less')
+      ]
+    }
+  },
   css: {
     loaderOptions: {
       less: {
@@ -59,9 +69,8 @@ module.exports = {
     proxy: {
       '/jeecg-boot': {
         //  target: 'http://192.168.1.8:8080', //请求本地 需要jeecg-boot后台项目  蒙蒙
-
         // target: 'http://192.168.0.59:8088', //请求本地 需要jeecg-boot后台项目  英豪
-        target: 'http://192.168.1.72:8088', //请求本地 需要jeecg-boot后台项目  英豪
+        // target: 'http://192.168.1.72:8088', //请求本地 需要jeecg-boot后台项目  英豪
 
         // target: 'http://192.168.2.115:8080', //请求本地 需要jeecg-boot后台项目  祚云
 
@@ -72,7 +81,7 @@ module.exports = {
         // target: 'http://192.168.1.94:8081', //请求本地 需要jeecg-boot后台项目  赵西安
 
 
-        //  target: 'http://api.tjyourong.com.cn', //请求本地 需要jeecg-boot后台项目
+         target: 'http://api.tjyourong.com.cn', //请求本地 需要jeecg-boot后台项目
         // target: 'https://trac.tjyourong.com.cn', //请求本地 需要jeecg-boot后台项目
         // target: 'http://39.106.184.70:8088/', //请求本地 需要jeecg-boot后台项目
          // target: 'http://adsp.tjyourong.com.cn/', //请求本地 需要jeecg-boot后台项目