├── .env ├── .env.prod ├── src ├── api │ ├── index.ts │ ├── mode.ts │ └── http │ │ ├── login.ts │ │ └── http.ts ├── assets │ ├── image │ │ ├── cd.png │ │ ├── logo.png │ │ ├── cd-wrap.png │ │ └── login_m.png │ ├── font │ │ ├── fonticon │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ ├── iconfont.css │ │ │ ├── iconfont.json │ │ │ └── demo.css │ │ └── fonts.css │ ├── css │ │ ├── router.css │ │ ├── element-plus.css │ │ ├── main.css │ │ └── flex.css │ ├── ts │ │ └── elementPlus.ts │ └── scss │ │ ├── animation.scss │ │ └── main.scss ├── models │ ├── index.ts │ ├── search.ts │ ├── mv.ts │ ├── singer.ts │ └── detail.ts ├── components │ ├── common │ │ ├── Empty.vue │ │ ├── svgIcon │ │ │ ├── svg │ │ │ │ └── close.svg │ │ │ ├── SvgIcon.vue │ │ │ └── svgBuilder.js │ │ ├── loading │ │ │ ├── Loading.vue │ │ │ └── LoadingIcon.vue │ │ ├── ContentBox.vue │ │ ├── LoadScroll.vue │ │ ├── ListModule.vue │ │ └── TalkList.vue │ ├── home-page │ │ ├── Footer.vue │ │ ├── Main.vue │ │ └── Nav.vue │ ├── singer │ │ ├── SingerMsg.vue │ │ ├── SingerSheet.vue │ │ └── SingerAlbum.vue │ ├── lyric │ │ ├── Lyric.vue │ │ └── Scroll.vue │ ├── discover-music │ │ └── Carousel.vue │ ├── mv │ │ ├── MvList.vue │ │ └── MvSheet.vue │ ├── new-disc │ │ └── NewSongSheet.vue │ ├── song-sheet │ │ ├── SongSheetCard.vue │ │ ├── BoutiqueSongSheetCard.vue │ │ └── SongList.vue │ ├── rank │ │ └── RankShow.vue │ └── search │ │ └── SearchMusic.vue ├── App.vue ├── util │ ├── index.ts │ ├── mv.ts │ ├── useStorage.ts │ ├── storage.ts │ ├── scroll.ts │ ├── way.ts │ ├── song.ts │ ├── debounce.ts │ └── time.ts ├── env.d.ts ├── main.ts ├── router │ ├── index.ts │ └── routes.ts ├── views │ ├── home-page │ │ └── HomePage.vue │ ├── Login.vue │ ├── rank │ │ └── RankList.vue │ ├── mv │ │ ├── Mv.vue │ │ └── MvSort.vue │ ├── discover-music │ │ └── DiscoverMusic.vue │ ├── search │ │ └── SearchPage.vue │ ├── song-sheet │ │ └── BoutiqueSongSheet.vue │ └── singer │ │ ├── Singer.vue │ │ └── SingerDetails.vue ├── directives │ └── custom-directives.ts └── store │ └── play.ts ├── public ├── head.png └── assets │ └── images │ ├── ava.jpeg │ ├── test │ └── avatar.jpeg │ └── singer │ └── singerback.png ├── auto-imports.d.ts ├── tsconfig.node.json ├── .gitignore ├── vercel.json ├── tsconfig.json ├── index.html ├── package.json ├── vite.config.ts ├── README.md └── components.d.ts /.env: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=https://api-chiko-music.vercel.app/ -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http/api'; 2 | export * from './http/login'; -------------------------------------------------------------------------------- /public/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/public/head.png -------------------------------------------------------------------------------- /src/assets/image/cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/image/cd.png -------------------------------------------------------------------------------- /src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/image/logo.png -------------------------------------------------------------------------------- /public/assets/images/ava.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/public/assets/images/ava.jpeg -------------------------------------------------------------------------------- /src/assets/image/cd-wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/image/cd-wrap.png -------------------------------------------------------------------------------- /src/assets/image/login_m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/image/login_m.png -------------------------------------------------------------------------------- /public/assets/images/test/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/public/assets/images/test/avatar.jpeg -------------------------------------------------------------------------------- /src/assets/font/fonticon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/font/fonticon/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/font/fonticon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/font/fonticon/iconfont.woff -------------------------------------------------------------------------------- /src/assets/font/fonticon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/src/assets/font/fonticon/iconfont.woff2 -------------------------------------------------------------------------------- /public/assets/images/singer/singerback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen-ziwen/chiko-music/HEAD/public/assets/images/singer/singerback.png -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // 统一暴露出去 3 | export * from './detail'; 4 | export * from './search'; 5 | export * from './singer'; 6 | export * from './mv'; -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | // We suggest you to commit this file into source control 3 | declare global { 4 | 5 | } 6 | export {} 7 | -------------------------------------------------------------------------------- /src/components/common/Empty.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": [ 8 | "vite.config.ts", 9 | ] 10 | } -------------------------------------------------------------------------------- /src/api/mode.ts: -------------------------------------------------------------------------------- 1 | export let isDev: boolean = true; // 默认为开发环境 2 | 3 | if (import.meta.env.MODE === 'development') { 4 | isDev = true; 5 | } else if (import.meta.env.MODE === 'production') { 6 | isDev = false; 7 | } -------------------------------------------------------------------------------- /src/assets/css/router.css: -------------------------------------------------------------------------------- 1 | .router-style { 2 | display: inline-block; 3 | padding: 0 20px; 4 | height: 50px; 5 | background-color: rgb(248, 78, 78); 6 | line-height: 50px; 7 | color: whitesmoke !important; 8 | } -------------------------------------------------------------------------------- /src/components/common/svgIcon/svg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | // 统一暴露方法 2 | export * from './debounce'; // 防抖节流方法 3 | export * from './storage'; // 本地存储方法 4 | export * from './scroll'; // 滚动方法 5 | export * from './way'; // 一些简单的小方法集合 6 | export * from './song'; // 歌曲列表加工 7 | export * from './mv'; // mv列表加工 8 | export * from './time'; // 集合时间的函数 9 | export * from './useStorage'; // 歌曲搜索记录 -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | 11 | declare module 'lyric-parser'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/assets/ts/elementPlus.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from '@/App.vue' 3 | const app = createApp(App); 4 | 5 | // 使用了按需自动引入插件 所以这边就不需要写了 不过还是留下来 以后可能需要写 6 | import { 7 | ElButton, ElCard, ElRow, 8 | } from 'element-plus'; 9 | 10 | 11 | // app.component('el-button', ElButton); 12 | // app.component('el-row', ElRow); 13 | // app.component('el-card', ElCard) -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true, 3 | "rewrites": [ 4 | { 5 | "source": "/(.*)", 6 | "destination": "/" 7 | } 8 | ], 9 | "headers": [ 10 | { 11 | "source": "/assets/(.*)", 12 | "headers": [ 13 | { 14 | "key": "Cache-Control", 15 | "value": "max-age=31536000, immutable" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import 'element-plus/theme-chalk/el-message.css'; 6 | import directives from './directives/custom-directives'; 7 | 8 | const app = createApp(App); 9 | 10 | for (let item in directives) { 11 | app.directive(item, directives[item]); 12 | } 13 | 14 | app.use(router).use(createPinia()).mount('#app'); 15 | 16 | -------------------------------------------------------------------------------- /src/components/home-page/Footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, } from "vue-router"; 2 | import { routes } from './routes'; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: routes, 7 | }); 8 | 9 | router.beforeEach((to, from, next) => { 10 | document.title = to.meta.title; 11 | // chrome 12 | document.body.scrollTop = 0 13 | // firefox 14 | document.documentElement.scrollTop = 0 15 | // safari 16 | window.pageYOffset = 0 17 | next(); 18 | }); 19 | 20 | export default router; -------------------------------------------------------------------------------- /src/models/search.ts: -------------------------------------------------------------------------------- 1 | export interface TagSearch { 2 | albums: T; 3 | artists: T; 4 | playlists: T; 5 | songs: T; 6 | mv: T; 7 | } 8 | 9 | export interface SearchHotDetailType { 10 | alg: string; 11 | content?: string; 12 | iconType: number; 13 | iconUrl: string; 14 | score: number; 15 | searchWord: string; 16 | source: number; 17 | url: string; 18 | } 19 | 20 | export interface SearchSuggestType { 21 | albums?: { name: string }[]; 22 | artists?: { name: string }[]; 23 | songs?: { name: string }[]; 24 | playlists?: { name: string }[] 25 | } -------------------------------------------------------------------------------- /src/views/home-page/HomePage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/models/mv.ts: -------------------------------------------------------------------------------- 1 | export interface ArtMV { 2 | artist: string; 3 | artistName: string; 4 | duration: string; 5 | id: number; 6 | imgurl: string; 7 | imgurl16v9: string; 8 | name: string; 9 | playCount: number; 10 | publishTime: string; 11 | status: boolean; 12 | } 13 | 14 | export interface MvDetailType { 15 | desc: string; 16 | name: string; 17 | videoGroup: { id: string, name: string }[]; 18 | publishTime: number; 19 | playCount: number; 20 | subCount: number; 21 | } 22 | export interface VideoInfoType { 23 | isLike: number; 24 | likeCount: number; 25 | shareCount: number; 26 | commentCount: number; 27 | commentTotal: number; 28 | } -------------------------------------------------------------------------------- /src/components/common/loading/Loading.vue: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /src/util/mv.ts: -------------------------------------------------------------------------------- 1 | import { changetime, changeNum } from '@/util'; 2 | export interface MvType { 3 | id: number; 4 | imgurl: string; 5 | artistName: string; 6 | duration: string; 7 | playCount: string; 8 | info: string; 9 | } 10 | export function useMv(MvList: any[]) { 11 | const newMvList: MvType[] = []; 12 | for (let i = 0; i < MvList.length; i++) { 13 | const MvInfo: MvType = { 14 | id: MvList[i].id, 15 | info: MvList[i].name, 16 | imgurl: MvList[i].cover || MvList[i].imgurl, 17 | artistName: MvList[i].artistName, 18 | duration: changetime(MvList[i].duration), 19 | playCount: changeNum(MvList[i].playCount) as string, 20 | } 21 | newMvList.push(MvInfo); 22 | } 23 | return newMvList; 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "esnext", 14 | "dom" 15 | ], 16 | "isolatedModules": false, 17 | "baseUrl": "./", 18 | "paths": { 19 | "@/*": [ 20 | "./src/*" 21 | // @/路径会直接指定到src文件夹 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.d.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue", 30 | "components.d.ts", //添加这一行,不然这个文件会报错 31 | ], 32 | "references": [ 33 | { 34 | "path": "./tsconfig.node.json" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 千子音乐 9 | 10 | 11 | 12 |
13 | 14 | 15 | 39 | 40 | -------------------------------------------------------------------------------- /src/util/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "./storage"; 2 | 3 | function findDeletSearch(list: string[], fun: any) { 4 | const index = list.findIndex(fun); 5 | if (index !== -1) { 6 | list.splice(index, 1); 7 | } 8 | } 9 | 10 | class useSearchStorage extends useStorage { 11 | deleteSingleSearch(storage: string, key: string) { 12 | const keylist = this.get(storage); 13 | findDeletSearch(keylist, (item: any) => item === key); 14 | this.set(storage, keylist); 15 | } 16 | 17 | addSingleSearch(storage: string, val?: string) { 18 | if (!this.get(storage)) { 19 | this.set(storage, []); 20 | } else { 21 | val && this.set(storage, [...new Set([val, ...this.get(storage)])]); 22 | } 23 | } 24 | } 25 | 26 | const searchStorage = new useSearchStorage(); 27 | 28 | export { searchStorage }; -------------------------------------------------------------------------------- /src/assets/scss/animation.scss: -------------------------------------------------------------------------------- 1 | // 进入 2 | .pop-enter-active { 3 | transition: all .25s ease-in-out; 4 | } 5 | 6 | .pop-leave-active { 7 | transition: all .35s linear; 8 | } 9 | 10 | .pop-enter-from { 11 | transform: translateY(25px); 12 | opacity: 0; 13 | } 14 | 15 | .pop-leave-to { 16 | opacity: 0; 17 | } 18 | 19 | 20 | 21 | //出现时弹出 22 | .pop-enter-active, 23 | .pop-leave-active { 24 | transition: all 0.5s ease-in-out; 25 | } 26 | 27 | .pop-enter-from, 28 | .pop-leave-to { 29 | transform: translateY(80px); 30 | } 31 | 32 | // 进入离开 33 | .slide-fade-enter-active { 34 | transition: all 0.5s ease; 35 | } 36 | 37 | .slide-fade-leave-active { 38 | transition: all 0.3s cubic-bezier(0.5, 0.3, 0.2, 0.5); 39 | } 40 | 41 | .slide-fade-enter, 42 | .slide-fade-leave-to { 43 | opacity: 0; 44 | transform: translateY(30px); 45 | } -------------------------------------------------------------------------------- /src/models/singer.ts: -------------------------------------------------------------------------------- 1 | 2 | // 歌手列表 3 | export interface SingerListType { 4 | albumSize: number; 5 | alias: string[]; 6 | briefDesc: string; 7 | followed: boolean; 8 | id: number; 9 | imglvlId: number; 10 | imglvlId_str: string; 11 | imglvlUrl: string; 12 | musicSize: number; 13 | name: string; 14 | picId: number; 15 | picId_str: string; 16 | picUrl: string; 17 | topicPerson: number; 18 | trans: string; 19 | } 20 | 21 | // 歌手详情 22 | export interface SingerDetail { 23 | id?: number; 24 | cover: string; 25 | name: string; 26 | albumSize: number; 27 | musicSize: number; 28 | mvSize: number; 29 | briefDesc: string; 30 | identities: string[]; 31 | } 32 | 33 | // 歌手专辑 34 | export interface SingerAlbumType { 35 | picUrl: string; 36 | publishTime: number; 37 | name: string; 38 | subType: string; 39 | id: number; 40 | } -------------------------------------------------------------------------------- /src/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import './animation.scss'; 2 | $width: 1240px; 3 | $color: rgb(254, 236, 239); 4 | $margin_I: 25px 0px 25px 15px; 5 | 6 | @mixin _flex($justify, $align) { 7 | display: flex; 8 | justify-content: $justify; 9 | align-items: $align; 10 | } 11 | 12 | 13 | /* el-image样式 */ 14 | @mixin _imgslot($width, $height, $size) { 15 | display: flex; 16 | justify-content: center; 17 | min-width: $width; 18 | min-height: $height; 19 | align-items: center; 20 | background: var(--el-fill-color-light); 21 | color: var(--el-text-color-secondary); 22 | font-size: $size; 23 | } 24 | 25 | @mixin bannerSlot($w, $ow, $oh) { 26 | height: calc($w * $oh / $ow); 27 | } 28 | 29 | .pagination { 30 | width: 100%; 31 | @include _flex(center, center); 32 | margin-top: 15px; 33 | } 34 | 35 | @keyframes rotate { 36 | 0% { 37 | transform: rotateZ(-360deg) 38 | } 39 | 40 | 100% { 41 | transform: rotateZ(0deg) 42 | } 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chiko-music", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "vite --port 3001", 7 | "build": "vite build --mode prod", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@better-scroll/core": "^2.5.1", 12 | "@better-scroll/mouse-wheel": "^2.5.1", 13 | "@element-plus/icons-vue": "^1.1.4", 14 | "@types/node": "^17.0.23", 15 | "axios": "^0.26.1", 16 | "dayjs": "^1.11.1", 17 | "element-plus": "^2.2.20", 18 | "lyric-parser": "^1.0.1", 19 | "pinia": "^2.0.12", 20 | "vue": "^3.3.8", 21 | "vue-router": "^4.0.14" 22 | }, 23 | "devDependencies": { 24 | "@types/lodash-es": "^4.17.7", 25 | "@vitejs/plugin-vue": "^2.2.0", 26 | "sass": "^1.50.0", 27 | "sass-loader": "^12.6.0", 28 | "typescript": "^4.5.4", 29 | "unplugin-auto-import": "^0.6.6", 30 | "unplugin-vue-components": "^0.18.4", 31 | "vite": "^2.8.0", 32 | "vue-tsc": "^0.29.8" 33 | } 34 | } -------------------------------------------------------------------------------- /src/directives/custom-directives.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from "vue-demi"; 2 | export default { 3 | // 当点击的元素不在绑定元素内部时 触发回调函数 4 | 'closeOutside': { 5 | mounted(el: any, binding: any) { 6 | el.oustsideEvent = (event: any) => { 7 | // 在内部点击时 不反应 8 | if (el == event.target || el.contains(event.target)) { 9 | return; 10 | } 11 | // 在外部点击时 触发回调函数 12 | if (binding.value && typeof binding.value == 'function') { 13 | binding.value(); 14 | } 15 | }; 16 | document.addEventListener('click', el.oustsideEvent); 17 | }, 18 | unmounted(el: any) { 19 | document.removeEventListener('click', el.oustsideEvent); 20 | delete el.oustsideEvent; 21 | } 22 | }, 23 | // 改变背景色 24 | 'color': { 25 | mounted(el: HTMLElement, binding: any) { 26 | el.style.background = binding.value; 27 | }, 28 | } 29 | } as Record; -------------------------------------------------------------------------------- /src/components/home-page/Main.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 45 | -------------------------------------------------------------------------------- /src/components/common/svgIcon/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 26 | -------------------------------------------------------------------------------- /src/util/storage.ts: -------------------------------------------------------------------------------- 1 | 2 | // localStorage 缓存数据 3 | export class useStorage { 4 | public readonly set: (name: string, data: any) => void; 5 | public readonly get: (name: string) => any; 6 | public readonly remove: (name: string) => void; 7 | public readonly clear: () => void; 8 | constructor() { 9 | this.set = (name, data) => { 10 | let cloneData = data; 11 | if (typeof data === "object" && data !== null) { 12 | cloneData = JSON.stringify(cloneData); 13 | } 14 | window.localStorage.setItem(name, cloneData); 15 | }; 16 | this.get = (name) => { 17 | const data = window.localStorage.getItem(name); 18 | if (data) { 19 | return JSON.parse(data); 20 | } 21 | return data; 22 | }; 23 | this.remove = (name) => { 24 | window.localStorage.removeItem(name); 25 | } 26 | this.clear = () => { 27 | window.localStorage.clear(); 28 | } 29 | } 30 | } 31 | 32 | const storage = new useStorage(); 33 | export { storage }; 34 | 35 | -------------------------------------------------------------------------------- /src/assets/css/element-plus.css: -------------------------------------------------------------------------------- 1 | /* el-loading */ 2 | .el-loading-mask { 3 | background-color: #ffffff; 4 | } 5 | 6 | .el-loading-spinner .path { 7 | stroke: #4a4a4a; 8 | } 9 | 10 | .el-loading-spinner .el-loading-text { 11 | color: #4a4a4a; 12 | } 13 | 14 | .el-dropdown-menu__item .el-icon-user { 15 | margin-right: 2px; 16 | } 17 | 18 | /* el-pagination */ 19 | .el-pagination.is-background .el-pager li:not(.is-disabled).is-active { 20 | background-color: pink !important; 21 | cursor: pointer; 22 | } 23 | 24 | .el-pagination { 25 | --el-pagination-hover-color: pink !important; 26 | } 27 | 28 | /* el-dropdown */ 29 | .el-dropdown-menu__item:not(.is-disabled) { 30 | color: rgb(248, 103, 103); 31 | } 32 | 33 | .el-dropdown-menu__item:not(.is-disabled):focus { 34 | background-color: rgb(254, 236, 239); 35 | color: rgb(248, 103, 103); 36 | } 37 | 38 | /* el-slider */ 39 | .el-slider__bar { 40 | background-color: #F08080 !important; 41 | } 42 | 43 | .el-slider__button { 44 | border: 2px solid #F08080 !important; 45 | width: 17px; 46 | height: 17px; 47 | } 48 | 49 | .el-tooltip__trigger:focus-visible { 50 | outline: none; 51 | } -------------------------------------------------------------------------------- /src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import "./flex.css"; 2 | @import "./router.css"; 3 | @import "./element-plus.css"; 4 | 5 | :root { 6 | --main-margin: 10px 15px; 7 | --max-width:1240px; 8 | } 9 | * { 10 | margin: 0px; 11 | padding: 0px 12 | } 13 | 14 | i { 15 | font-style: normal 16 | } 17 | 18 | a { 19 | text-decoration: none; 20 | } 21 | 22 | li { 23 | list-style: none; 24 | } 25 | 26 | 27 | html, 28 | body { 29 | width: 100vw; 30 | overflow-y: auto !important; 31 | overflow-x: hidden; 32 | } 33 | 34 | ::-webkit-scrollbar { 35 | width: 0px; 36 | } 37 | 38 | ::-webkit-scrollbar-track { 39 | border-radius: 8px; 40 | } 41 | 42 | ::-webkit-scrollbar-thumb { 43 | border-radius: 8px; 44 | background: #ccc; 45 | } 46 | 47 | ::-webkit-scrollbar-thumb:hover { 48 | background: #aaa; 49 | } 50 | 51 | ::-webkit-scrollbar-thumb:active { 52 | background: #999; 53 | } 54 | 55 | /* 子模块的盒子全部用这个样式 */ 56 | .container { 57 | width: var(--max-width); 58 | height: auto; 59 | margin: 0 auto; 60 | } 61 | 62 | .text-hidden { 63 | white-space: nowrap; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import Components from 'unplugin-vue-components/vite' 5 | // unplugin-vue-components这个插件可以自动导入组件,不需要import导入,elementPlus也用到了它。 6 | import { svgBuilder } from './src/components/common/svgIcon/svgBuilder' 7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 8 | // https://vitejs.dev/config/ 9 | import { resolve } from 'path' //用来自定义相对路径 10 | export default defineConfig({ 11 | plugins: [vue(), 12 | svgBuilder("./src/components/common/svgIcon/svg/"), 13 | // 引入ElementPlus配置 14 | AutoImport({ 15 | resolvers: [ElementPlusResolver()], 16 | }), 17 | Components({ 18 | resolvers: [ElementPlusResolver()], 19 | dts: true, 20 | deep: true, // 深度匹配 ,对于子路由也有效果 21 | dirs: ['src/components'] // 在这两个文件下的组件会自动引入 22 | }), 23 | ], 24 | resolve: { 25 | alias: { 26 | '@': resolve(__dirname, './src') //vite 的相对路径的重新配置 27 | } 28 | }, 29 | css: { 30 | preprocessorOptions: { 31 | scss: { 32 | additionalData: '@import "@/assets/scss/main.scss";', 33 | }, 34 | }, 35 | }, 36 | 37 | server: { 38 | open: true //编译成功自动打开浏览器 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/assets/css/flex.css: -------------------------------------------------------------------------------- 1 | /* flex 简写 */ 2 | 3 | .flex-layout { 4 | display: flex; 5 | } 6 | 7 | .flex-center { 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .flex-acenter { 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .flex-jcenter { 19 | display: flex; 20 | justify-content: center; 21 | } 22 | 23 | 24 | .flex-start { 25 | display: flex; 26 | justify-content: flex-start; 27 | align-items: center; 28 | } 29 | 30 | .flex-between { 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | } 35 | 36 | .flex-row { 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | } 41 | 42 | .flex-column { 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | .flex-between { 48 | display: flex; 49 | justify-content: space-between; 50 | } 51 | 52 | .flex-shrink { 53 | flex-shrink: 0; 54 | } 55 | 56 | .flex-grow-1 { 57 | flex-grow: 1; 58 | } 59 | 60 | .flex-grow-2 { 61 | flex-grow: 2; 62 | } 63 | 64 | .flex-grow-3 { 65 | flex-grow: 3; 66 | } 67 | 68 | .flex-grow-4 { 69 | flex-grow: 4; 70 | } 71 | 72 | .flex-wrap { 73 | flex-wrap: wrap; 74 | } 75 | 76 | .flex-nowrap { 77 | flex-wrap: nowrap; 78 | } -------------------------------------------------------------------------------- /src/util/scroll.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 将屏幕滚动到顶部 4 | export function scrollTop() { 5 | let timer: number | undefined = undefined; 6 | return (delay: number = 15, step: number = 35) => { 7 | if (timer) { 8 | clearInterval(timer) 9 | } 10 | timer = window.setInterval(() => { 11 | document.documentElement.scrollTop -= step; 12 | if (document.documentElement.scrollTop <= 0) { 13 | document.documentElement.scrollTop = 0; 14 | clearInterval(timer); 15 | } 16 | }, delay) 17 | } 18 | } 19 | 20 | // 将屏幕滚动到顶部 类写法 21 | export class ScrollTop { 22 | private timer: number | undefined; 23 | public readonly scroll: (delay?: number, step?: number) => void; 24 | constructor() { 25 | this.timer = undefined; 26 | this.scroll = (delay = 15, step = 35) => { 27 | if (this.timer) { 28 | clearInterval(this.timer) 29 | } 30 | this.timer = window.setInterval(() => { 31 | document.documentElement.scrollTop -= step; 32 | if (document.documentElement.scrollTop <= 0) { 33 | document.documentElement.scrollTop = 0; 34 | clearInterval(this.timer); 35 | } 36 | }, delay) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/util/way.ts: -------------------------------------------------------------------------------- 1 | // 对图片的大小进行处理 2 | export function imgurl(url: string, param: string = "100") { 3 | if (!url) { 4 | return '/assets/images/ava.jpeg'; 5 | } 6 | return `${url}?param=${param}y${param}` 7 | } 8 | 9 | // 随机播放,这样索引可能重复 10 | export function randomNum(min: number, max: number) { 11 | let num = Math.floor((Math.random() * (max - min)) + min); 12 | return num; 13 | } 14 | 15 | // 最多显示三个歌手名 16 | export function changeData(msg: { name: string }[]) { 17 | return msg.map(item => item.name).slice(0, 3).join("\n"); 18 | } 19 | 20 | export function changeNum(num: number, point: number = 0) { 21 | let numStr = num.toString() 22 | // 十万以内直接返回 23 | if (numStr.length < 6) { 24 | return numStr 25 | } 26 | //大于8位数是亿 27 | else if (numStr.length > 8) { 28 | let decimal = numStr.substring( 29 | numStr.length - 8, 30 | numStr.length - 8 + point 31 | ) 32 | return parseFloat(parseInt(num / 100000000 + '') + '.' + decimal) + '亿' 33 | } 34 | //大于6位数是十万 (以10W分割 10W以下全部显示) 35 | else if (numStr.length > 5) { 36 | let decimal = numStr.substring( 37 | numStr.length - 4, 38 | numStr.length - 4 + point 39 | ) 40 | return parseFloat(parseInt(num / 10000 + '') + '.' + decimal) + '万' 41 | } 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /src/util/song.ts: -------------------------------------------------------------------------------- 1 | 2 | import { formatSecond } from '@/util'; 3 | 4 | interface SongList { 5 | id: number; 6 | singer: string; 7 | name: string; 8 | album: string; 9 | duration: number; 10 | image: string; 11 | url: string; 12 | playCount: string | number; 13 | score: string | number; 14 | idx: number; 15 | } 16 | 17 | export const useSong = (songList: any[]) => { 18 | const newSongList: SongList[] = []; 19 | for (let i = 0; i < songList.length; i++) { 20 | const songMsg: SongList = { 21 | id: songList[i].id, 22 | singer: filterSinger(songList[i].ar || songList[i].artists), 23 | name: songList[i].name, 24 | album: songList[i].al ? songList[i].al.name : songList[i].album.name, 25 | duration: formatSecond(songList[i].dt || songList[i].duration), 26 | image: songList[i].al ? songList[i].al.picUrl : songList[i].album.artist.img1v1Url, 27 | url: `https://music.163.com/song/media/outer/url?id=${songList[i].id}.mp3`, 28 | playCount: songList[i].playCount || "", 29 | score: songList[i].score || "", 30 | idx: i + 1, 31 | } 32 | newSongList.push(songMsg); 33 | } 34 | return newSongList; 35 | } 36 | 37 | function filterSinger(singer: { name: string }[]) { 38 | if (!singer) { 39 | return ''; 40 | } 41 | return singer.map((item: any) => item.name).join('/'); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/common/ContentBox.vue: -------------------------------------------------------------------------------- 1 | 9 | 31 | -------------------------------------------------------------------------------- /src/models/detail.ts: -------------------------------------------------------------------------------- 1 | 2 | // 精品歌单 creator 3 | interface AvatarDetail { 4 | identityLevel: number, 5 | identityIconUrl: string 6 | } 7 | export interface Creator { 8 | avatarUrl: string; 9 | userId: number; 10 | nickname: string; 11 | avatarDetail: AvatarDetail; 12 | } 13 | // 歌曲列表 14 | export interface SongList { 15 | id: number; 16 | singer: string; 17 | name: string; 18 | album: string; 19 | duration: number; 20 | image: string; 21 | url: string; 22 | playCount: string | number; 23 | score: string | number; 24 | idx: number; 25 | } 26 | 27 | // 歌单列表 28 | export interface RecommendList { 29 | [x: string]: any; 30 | alg: string; 31 | canDislike: boolean | null; 32 | highQuality: boolean; 33 | id: number; 34 | name: string; 35 | picUrl: string; 36 | coverImgUrl: string; 37 | playCount: number; 38 | trackCount: number; 39 | songList: SongList[]; 40 | updateTime: number; 41 | trackNumberUpdateTime: number; 42 | copywriter: string; 43 | creator: Creator; 44 | type: number; 45 | 46 | } 47 | 48 | export interface TopArtists { 49 | name: string; 50 | alias: string[]; 51 | id: number; 52 | picUrl: string; 53 | img1v1Url: string; 54 | musicSize: number; 55 | } 56 | 57 | export interface Newsong { 58 | id: number; 59 | name: string; 60 | picUrl: string; 61 | song: { 62 | artists: []; 63 | duration: number; 64 | } 65 | } -------------------------------------------------------------------------------- /src/components/singer/SingerMsg.vue: -------------------------------------------------------------------------------- 1 | 10 | 27 | -------------------------------------------------------------------------------- /src/components/common/LoadScroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 49 | -------------------------------------------------------------------------------- /src/components/lyric/Lyric.vue: -------------------------------------------------------------------------------- 1 | 17 | 33 | -------------------------------------------------------------------------------- /src/api/http/login.ts: -------------------------------------------------------------------------------- 1 | import http from "./http"; 2 | 3 | /** 4 | * @function 手机登陆 5 | * @param phone 手机号码 6 | * @param password 登陆密码 7 | * @param countrycode 国家码 8 | * @param md5_password md5 密码加密 9 | * @param captcha 验证码 // 可发送验证码登陆 10 | */ 11 | export function phoneGet(phone: number, captcha: number, countrycode?: number, md5_password?: string) { 12 | return http.get('/login/cellphone', { phone, captcha, countrycode, md5_password }) 13 | } 14 | 15 | export function phonePost(phone: number, captcha: number, countrycode?: number, md5_password?: string) { 16 | return http.post('/login/cellphone', { phone, captcha, countrycode, md5_password }) 17 | } 18 | 19 | /** 20 | * @function 发送验证码 21 | * @param phone 手机号码 22 | * @param ctcode 国家编号 23 | */ 24 | export function captchaSend(phone: number, ctcode?: number) { 25 | return http.get('/captcha/sent', { phone, ctcode }) 26 | } 27 | 28 | /** 29 | * @function 验证验证码 30 | * @param phone 手机号码 31 | * @param captcha 验证码 32 | * @param ctcode 国家编号 33 | */ 34 | export function captchaVerify(phone: number, captcha: number, ctcode?: number) { 35 | return http.get('/captcha/verify', { phone, captcha, ctcode }) 36 | } 37 | 38 | // 二维码登陆 39 | 40 | export function loginKey(timestamp: number) { 41 | return http.get('/login/qr/key', { timestamp }); 42 | } 43 | 44 | export function loginQrCreate(key: string, qrimg: boolean, timestamp: number) { 45 | return http.get('/login/qr/create', { key, qrimg, timestamp }); 46 | } 47 | 48 | export function loginQrCheck(key: string, timestamp: number) { 49 | return http.get('/login/qr/check', { key, timestamp }); 50 | } 51 | 52 | // 登录状态 53 | 54 | export function isLogins() { 55 | return http.get('/login/status'); 56 | } 57 | 58 | export function loginOut() { 59 | return http.get('/logout'); 60 | } -------------------------------------------------------------------------------- /src/components/common/ListModule.vue: -------------------------------------------------------------------------------- 1 | 9 | 30 | -------------------------------------------------------------------------------- /src/store/play.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import type { SongList } from "@/models/detail"; 3 | import { storage } from "@/util"; 4 | 5 | 6 | export const playState = { 7 | listloop: 0, 8 | loop: 1, 9 | random: 2, 10 | } 11 | 12 | export const usePlay = defineStore({ 13 | id: 'playing', 14 | state: () => { 15 | return { 16 | // 底部音乐播放器是否显示 17 | barShow: false, 18 | songUrl: '', 19 | // 是否登陆 20 | isLogin: false, 21 | // 歌单id 22 | sheetId: 0, 23 | // 用户信息 24 | userInfo: null as (null | {}), 25 | // 歌手信息 26 | singer: {}, 27 | // 是否播放 28 | playing: false, 29 | // 音乐三个模式:列表循环,单曲循环,随机 30 | playType: playState.listloop, 31 | // 音乐列表,随机有用,当点击列表时,会把音乐url都传进来 32 | playList: [] as SongList[], 33 | // 当前播放歌曲在列表中的索引值 34 | currentindex: -1, 35 | } 36 | }, 37 | actions: { 38 | // 根据索引播放对应歌曲 39 | selectPlay(list: SongList[], idx: number) { 40 | this.$state.playList = list; 41 | this.$state.currentindex = idx; 42 | this.$state.playing = true; 43 | }, 44 | // 播放全部 45 | playAll(list: SongList[]) { 46 | this.selectPlay(list, 0); 47 | this.$state.playType = playState.listloop; 48 | }, 49 | loginOut() { 50 | this.$state.userInfo = null; 51 | this.$state.isLogin = false; 52 | } 53 | }, 54 | getters: { 55 | currentPlay(state) { 56 | return state.playList[state.currentindex] || {}; // 拿到当前索引值的歌曲 57 | }, 58 | 59 | getIsLogin(state): boolean { 60 | return state.isLogin || storage.get("isLogin"); 61 | }, 62 | 63 | getUserInfo(state) { 64 | return state.userInfo || (storage.get("loginInfo") || {}); 65 | } 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /src/api/http/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosRequestConfig } from "axios"; 2 | interface API { 3 | get(url: string, params?: any): Promise 4 | post(url: string, params?: any): Promise 5 | } 6 | 7 | const instance = axios.create({ 8 | baseURL: import.meta.env.VITE_API_BASE_URL as string, // 默认请求地址 9 | timeout: 8000, // 请求超时八秒 10 | withCredentials: true, // 让cookie可以跨域请求 11 | // params: { realIP: "58.23.138.35" }, // 添加公共的参数,就是每个接口都需要的参数 12 | }); 13 | 14 | instance.interceptors.request.use( 15 | function (config: AxiosRequestConfig) { 16 | // request拦截器 可以拿到请求报文,然后给请求标头设置Authorization(授权)为token值,这样每次请求都会带上这个token用于服务器验证身份。 17 | // config.headers.Authorization = window.sessionStorage.getItem('token'); // 将token加入到请求头 18 | return config; 19 | }, 20 | function (error) { 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | instance.interceptors.response.use( 26 | function (response) { 27 | if (response.status >= 200 && response.status < 300) { 28 | return Promise.resolve(response) 29 | } 30 | else if (response.status >= 300 && response.status < 400) { 31 | } 32 | else if (response.status >= 400) { 33 | return Promise.reject(response) 34 | } 35 | }, 36 | function (error) { 37 | return Promise.reject(error); 38 | } 39 | ); 40 | 41 | const http: API = { 42 | get: function (url, params) { 43 | return new Promise((resolve, reject) => { 44 | instance.get(url, { params: params }).then(res => { 45 | resolve(res.data) 46 | }).catch(err => { 47 | reject(err.data) 48 | }) 49 | }) 50 | }, 51 | post: function (url, params) { 52 | return new Promise((resolve, reject) => { 53 | instance.post(url, params).then(res => { 54 | resolve(res.data) 55 | }).catch(err => { 56 | reject(err.data) 57 | }) 58 | }) 59 | } 60 | } 61 | 62 | export default http; -------------------------------------------------------------------------------- /src/components/common/loading/LoadingIcon.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | -------------------------------------------------------------------------------- /src/util/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 防抖,规定时间内多次只触发一次 3 | * @param {Function} fun 传入一个需要防抖的函数 4 | * @param {number} delay 规定时间 5 | * @param {boolean} immediate 触发的第一次是否立即执行 6 | * @return {Function} 返回一个函数 7 | */ 8 | export class Debounce { 9 | private timeout: number | null; 10 | constructor() { 11 | this.timeout = null; 12 | } 13 | use(fun: Function, delay: number, immediate = false): Function { 14 | return (...args: any) => { 15 | if (this.timeout) { 16 | clearTimeout(this.timeout); 17 | } 18 | 19 | if (immediate) { 20 | if (!this.timeout) { 21 | fun.apply(this, args) 22 | } 23 | this.timeout = window.setTimeout(() => { 24 | this.timeout = null; 25 | }, delay); 26 | return; 27 | } 28 | this.timeout = window.setTimeout(() => { 29 | fun.apply(this, args); 30 | this.timeout = null; 31 | }, delay); 32 | } 33 | }; 34 | cancel() { 35 | if (this.timeout) { 36 | clearTimeout(this.timeout) 37 | } 38 | this.timeout = null; 39 | } 40 | } 41 | /** 42 | * @class 节流,每间隔一段时间执行一次 43 | * @param {Function} fun 传入一个需要节流的函数 44 | * @param {number} delay 间隔的时间 45 | * @param {boolean} immediate 触发的第一次是否立即执行 46 | * @return {Function} 返回一个函数 47 | */ 48 | export class Throttle { 49 | public use: (fun: Function, delay: number, immediate?: boolean) => Function; 50 | constructor() { 51 | this.use = (fun, delay, immediate = false) => { 52 | let tiemout: NodeJS.Timeout | undefined = undefined; 53 | let flag = true; 54 | return (...args: any) => { 55 | if (immediate) { 56 | fun.apply(this, args); 57 | immediate = false; 58 | return; 59 | } 60 | if (!flag) { 61 | return; 62 | } 63 | flag = false; 64 | tiemout = setTimeout(() => { 65 | fun.apply(this, args); 66 | flag = true; 67 | clearTimeout(tiemout as NodeJS.Timeout); 68 | }, delay) 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/components/common/svgIcon/svgBuilder.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, readdirSync } from "fs"; 2 | 3 | let idPerfix = ""; 4 | const svgTitle = /+].*?)>/; 5 | const clearHeightWidth = /(width|height)="([^>+].*?)"/g; 6 | 7 | const hasViewBox = /(viewBox="[^>+].*?")/g; 8 | 9 | const clearReturn = /(\r)|(\n)/g; 10 | 11 | function findSvgFile(dir) { 12 | const svgRes = []; 13 | const dirents = readdirSync(dir, { 14 | withFileTypes: true, 15 | }); 16 | for (const dirent of dirents) { 17 | if (dirent.isDirectory()) { 18 | svgRes.push(...findSvgFile(dir + dirent.name + "/")); 19 | } else { 20 | const svg = readFileSync(dir + dirent.name) 21 | .toString() 22 | .replace(clearReturn, "") 23 | .replace(svgTitle, ($1, $2) => { 24 | let width = 0; 25 | let height = 0; 26 | let content = $2.replace(clearHeightWidth, (s1, s2, s3) => { 27 | if (s2 === "width") { 28 | width = s3; 29 | } else if (s2 === "height") { 30 | height = s3; 31 | } 32 | return ""; 33 | }); 34 | if (!hasViewBox.test($2)) { 35 | content += `viewBox="0 0 ${width} ${height}"`; 36 | } 37 | return ``; 41 | }) 42 | .replace("", ""); 43 | svgRes.push(svg); 44 | } 45 | } 46 | return svgRes; 47 | } 48 | 49 | export const svgBuilder = (path, perfix = "icon") => { 50 | if (path === "") return; 51 | idPerfix = perfix; 52 | const res = findSvgFile(path); 53 | return { 54 | name: "svg-transform", 55 | transformIndexHtml(html) { 56 | return html.replace( 57 | "", 58 | ` 59 | 60 | 61 | ${res.join("")} 62 | 63 | ` 64 | ); 65 | }, 66 | }; 67 | }; -------------------------------------------------------------------------------- /src/components/discover-music/Carousel.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/rank/RankList.vue: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/common/TalkList.vue: -------------------------------------------------------------------------------- 1 | 22 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 千子音乐 2 | 3 | ## 项目介绍 4 | - 千子音乐是一个基于 `Vue3 + TypeScript` 的音乐网站,实现了首页、排行榜、歌单、歌手、MV 等功能。 5 | 6 | - 该项目始于 `2022` 年,当时的我对 `Vue3` 并不算熟悉,在浏览 `github` 的过程中看到了 `NeteaseCloudMusicApi` 这个开源接口项目,于是便有了使用这个接口配合 `Vue3` 实现一个练手项目的想法。 7 | 8 | - 部署该网站的服务器在 `2023` 年就已经过期,目前为了能正常访问,已经将项目迁移到了 `Vercel` 上,访问速度相对会比较慢。(无所谓啦,能访问就行,几年前的练手项目而已) 9 | 10 | ## 数据来源 11 | [NeteaseCloudMusicApi](https://github.com/chen-ziwen/NeteaseCloudMusicApi) 12 | ## 预览地址 13 | [千子音乐](http://chiko-music.vercel.app/) 14 | ## 部分截图展示 15 | ### 首页 16 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/30ede7f4-afe5-44ed-a8be-99174532eefa) 17 | ### 排行榜 18 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/9568f480-be76-4179-ad36-02a516b6524d) 19 | ### 歌单 20 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/398e770e-0168-49eb-b3b2-8519ccd68d46) 21 | ![image](https://github.com/user-attachments/assets/ced53d8a-2ab5-4467-a608-5f292db7364c) 22 | ### 歌手 23 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/a926b99a-b62d-4bcb-ad0a-5468b071dd61) 24 | ![image](https://github.com/user-attachments/assets/5b9c37c8-559b-47f8-baab-6d55c6c37fc1) 25 | ### MV 26 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/ebd391df-28d1-435c-bb79-6e057b7d1185) 27 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/98b94415-b884-463f-8679-5bd4c3d3d97d) 28 | ![image](https://github.com/chen-ziwen/chiko_music/assets/85820568/7df66a6c-0360-4424-9c79-5ee204a7c067) 29 | 30 | ## 目录结构 31 | ```plaintext 32 | chiko_music 33 | │ 34 | ├── public 35 | │ └── assets 36 | │ └── images 37 | │ ├── singer 38 | │ └── test 39 | └── src 40 | ├── api 41 | │ └── http 42 | ├── assets 43 | │ ├── css 44 | │ ├── font 45 | │ │ └── fonticon 46 | │ ├── image 47 | │ ├── scss 48 | │ └── ts 49 | ├── components 50 | │ ├── common 51 | │ │ ├── loading 52 | │ │ └── svgIcon 53 | │ │ └── svg 54 | │ ├── discover-music 55 | │ ├── home-page 56 | │ ├── lyric 57 | │ ├── mv 58 | │ ├── new-disc 59 | │ ├── progress-bar 60 | │ ├── rank 61 | │ ├── search 62 | │ ├── singer 63 | │ └── song-sheet 64 | ├── directives 65 | ├── models 66 | ├── router 67 | ├── store 68 | ├── util 69 | └── views 70 | ├── discover-music 71 | ├── home-page 72 | ├── mv 73 | ├── rank 74 | ├── search 75 | ├── singer 76 | └── song-sheet 77 | ``` 78 | ## 安装运行 79 | ```git 80 | $ git clone git@github.com:chen-ziwen/NeteaseCloudMusicApi.git 81 | $ cd NeteaseCloudMusicApi 82 | $ npm install 83 | $ node app.js 84 | $ git clone git@github.com:chen-ziwen/chiko-music.git 85 | $ cd chiko-music 86 | $ npm install 87 | $ npm start 88 | ``` 89 | ## 打包上线 90 | ```git 91 | $ npm run build 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /src/components/lyric/Scroll.vue: -------------------------------------------------------------------------------- 1 | 6 | 108 | 109 | -------------------------------------------------------------------------------- /src/util/time.ts: -------------------------------------------------------------------------------- 1 | 2 | //将音乐播放时间的时间戳转变为分秒。 3 | export function changetime(msg: number) { 4 | let second = msg / 1000; 5 | let minute = ~~(second / 60); 6 | let over = (second % 60).toFixed(0); //保留0位小数 7 | let sminute = minute.toString(); 8 | let sover = over.toString(); 9 | if (minute < 10) sminute = '0' + minute.toString(); 10 | if (+over < 10) sover = '0' + over.toString(); 11 | return sminute + ':' + sover; 12 | } 13 | 14 | // 将时间转化为 时分秒 15 | export function judegtime(time: number) { 16 | let s = '' 17 | const second = time % 60; 18 | const minute = Math.floor(time / 60 % 60); 19 | const hours = Math.floor(time / 3600); 20 | 21 | if (hours >= 1) { 22 | s += hours + '小时' 23 | } 24 | if (minute >= 1) { 25 | s += minute + '分钟'; 26 | } 27 | if (minute > 0 || hours > 0) { 28 | if (second >= 1) { 29 | s += second + '秒' 30 | } 31 | } else { 32 | s += second + '秒' 33 | } 34 | } 35 | 36 | export function formatZero(num: number | string, len: number) { 37 | if (String(num).length > len) return num 38 | return (Array(len).join("0") + num).slice(-len) 39 | }; 40 | 41 | // 秒转00:00 42 | export function formatSecondTime(interval: number) { 43 | interval = interval | 0 44 | const m = (interval / 60) | 0 45 | const s = interval % 60 46 | return `${formatZero(m, 2)}:${formatZero(s, 2)}` 47 | }; 48 | 49 | // 转换成秒 50 | export function formatSecond(time: number) { 51 | time = ~~time; 52 | var secondTime 53 | if (time < 10) { 54 | secondTime = '00:0' + time 55 | } else if (time < 60) { 56 | secondTime = '00:' + time 57 | } else { 58 | var m = ~~((time % (1000 * 60 * 60)) / (1000 * 60)) 59 | var s = ~~((time % (1000 * 60)) / 1000) 60 | secondTime = Number(m * 60 + s) 61 | } 62 | return secondTime as number; 63 | } 64 | 65 | // 日期格式化 66 | export function dateFormat(str: number | string, type: string) { 67 | let date = new Date(str) 68 | let year = date.getFullYear() 69 | let month = formatZero(date.getMonth() + 1, 2) 70 | let day = formatZero(date.getDate(), 2) 71 | let hour = formatZero(date.getHours(), 2) 72 | let minute = formatZero(date.getMinutes(), 2) 73 | let seconds = formatZero(date.getSeconds(), 2) 74 | if (type == 'YYYY-MM-DD') { 75 | return `${year}-${month}-${day}` 76 | } else if (type == 'YYYY-MM-DD HH:MM:SS') { 77 | return `${year}-${month}-${day} ${hour}:${minute}:${seconds}` 78 | } else if (type == 'MM/DD HH:MM:SS') { 79 | return `${month}/${day} ${hour}:${minute}:${seconds}` 80 | } 81 | }; 82 | 83 | // 获取当前时间前后N天前后日期 84 | export function getDateBefore(dayCount: number) { 85 | var date = new Date() 86 | date.setDate(date.getDate() + dayCount) 87 | let year = date.getFullYear() 88 | let month = formatZero(date.getMonth() + 1, 2) 89 | let day = formatZero(date.getDate(), 2) 90 | return `${year}-${month}-${day}` 91 | }; 92 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | 5 | declare module 'vue' { 6 | export interface GlobalComponents { 7 | BoutiqueSongSheetCard: typeof import('./src/components/song-sheet/BoutiqueSongSheetCard.vue')['default'] 8 | Carousel: typeof import('./src/components/discover-music/Carousel.vue')['default'] 9 | ContentBox: typeof import('./src/components/common/ContentBox.vue')['default'] 10 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 11 | ElCarousel: typeof import('element-plus/es')['ElCarousel'] 12 | ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] 13 | ElDialog: typeof import('element-plus/es')['ElDialog'] 14 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 15 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 16 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 17 | ElIcon: typeof import('element-plus/es')['ElIcon'] 18 | ElImage: typeof import('element-plus/es')['ElImage'] 19 | ElPagination: typeof import('element-plus/es')['ElPagination'] 20 | ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] 21 | ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] 22 | ElSlider: typeof import('element-plus/es')['ElSlider'] 23 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 24 | ElTabs: typeof import('element-plus/es')['ElTabs'] 25 | Empty: typeof import('./src/components/common/Empty.vue')['default'] 26 | Footer: typeof import('./src/components/home-page/Footer.vue')['default'] 27 | ListModule: typeof import('./src/components/common/ListModule.vue')['default'] 28 | Loading: typeof import('./src/components/common/loading/Loading.vue')['default'] 29 | LoadingIcon: typeof import('./src/components/common/loading/LoadingIcon.vue')['default'] 30 | LoadScroll: typeof import('./src/components/common/LoadScroll.vue')['default'] 31 | Lyric: typeof import('./src/components/lyric/Lyric.vue')['default'] 32 | Main: typeof import('./src/components/home-page/Main.vue')['default'] 33 | MvList: typeof import('./src/components/mv/MvList.vue')['default'] 34 | MvSheet: typeof import('./src/components/mv/MvSheet.vue')['default'] 35 | Nav: typeof import('./src/components/home-page/Nav.vue')['default'] 36 | NewSongSheet: typeof import('./src/components/new-disc/NewSongSheet.vue')['default'] 37 | ProgressBar: typeof import('./src/components/progress-bar/ProgressBar.vue')['default'] 38 | RankShow: typeof import('./src/components/rank/RankShow.vue')['default'] 39 | Scroll: typeof import('./src/components/lyric/Scroll.vue')['default'] 40 | SearchMusic: typeof import('./src/components/search/SearchMusic.vue')['default'] 41 | SingerAlbum: typeof import('./src/components/singer/SingerAlbum.vue')['default'] 42 | SingerMsg: typeof import('./src/components/singer/SingerMsg.vue')['default'] 43 | SingerSheet: typeof import('./src/components/singer/SingerSheet.vue')['default'] 44 | SongList: typeof import('./src/components/song-sheet/SongList.vue')['default'] 45 | SongSheetCard: typeof import('./src/components/song-sheet/SongSheetCard.vue')['default'] 46 | SvgIcon: typeof import('./src/components/common/svgIcon/SvgIcon.vue')['default'] 47 | TalkList: typeof import('./src/components/common/TalkList.vue')['default'] 48 | } 49 | } 50 | 51 | export { } 52 | -------------------------------------------------------------------------------- /src/components/singer/SingerSheet.vue: -------------------------------------------------------------------------------- 1 | 22 | 37 | -------------------------------------------------------------------------------- /src/views/mv/Mv.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 111 | 112 | -------------------------------------------------------------------------------- /src/views/discover-music/DiscoverMusic.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/singer/SingerAlbum.vue: -------------------------------------------------------------------------------- 1 | 21 | 30 | -------------------------------------------------------------------------------- /src/assets/font/fonticon/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3613962 */ 3 | src: url('iconfont.woff2?t=1661586971000') format('woff2'), 4 | url('iconfont.woff?t=1661586971000') format('woff'), 5 | url('iconfont.ttf?t=1661586971000') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-chaxun:before { 17 | content: "\e606"; 18 | } 19 | 20 | .icon-bofang:before { 21 | content: "\e607"; 22 | } 23 | 24 | .icon-danquxunhuan:before { 25 | content: "\e608"; 26 | } 27 | 28 | .icon-changpian:before { 29 | content: "\e609"; 30 | } 31 | 32 | .icon-erji:before { 33 | content: "\e60a"; 34 | } 35 | 36 | .icon-huatong:before { 37 | content: "\e60b"; 38 | } 39 | 40 | .icon-jianshao:before { 41 | content: "\e60c"; 42 | } 43 | 44 | .icon-jingyin:before { 45 | content: "\e60d"; 46 | } 47 | 48 | .icon-liebiaoxunhuan:before { 49 | content: "\e60e"; 50 | } 51 | 52 | .icon-shanchu:before { 53 | content: "\e60f"; 54 | } 55 | 56 | .icon-shengyin:before { 57 | content: "\e610"; 58 | } 59 | 60 | .icon-kuaitui:before { 61 | content: "\e611"; 62 | } 63 | 64 | .icon-suiji:before { 65 | content: "\e612"; 66 | } 67 | 68 | .icon-xiai:before { 69 | content: "\e613"; 70 | } 71 | 72 | .icon-zanting:before { 73 | content: "\e614"; 74 | } 75 | 76 | .icon-zhongzhi:before { 77 | content: "\e615"; 78 | } 79 | 80 | .icon-jinzhi:before { 81 | content: "\e616"; 82 | } 83 | 84 | .icon-liebiao:before { 85 | content: "\e617"; 86 | } 87 | 88 | .icon-shezhi:before { 89 | content: "\e618"; 90 | } 91 | 92 | .icon-shuaxin:before { 93 | content: "\e619"; 94 | } 95 | 96 | .icon-zengjia:before { 97 | content: "\e61a"; 98 | } 99 | 100 | .icon-kuaijin:before { 101 | content: "\e61b"; 102 | } 103 | 104 | .icon-yinle:before { 105 | content: "\e61c"; 106 | } 107 | 108 | .icon-pinghengqi:before { 109 | content: "\e61d"; 110 | } 111 | 112 | .icon-shangyitiao:before { 113 | content: "\e61e"; 114 | } 115 | 116 | .icon-xiayitiao:before { 117 | content: "\e61f"; 118 | } 119 | 120 | .icon-qiyong:before { 121 | content: "\e63a"; 122 | } 123 | 124 | .icon-nan:before { 125 | content: "\e682"; 126 | } 127 | 128 | .icon-nv:before { 129 | content: "\e683"; 130 | } 131 | 132 | .icon-pinglun:before { 133 | content: "\e684"; 134 | } 135 | 136 | .icon-shangchuan:before { 137 | content: "\e685"; 138 | } 139 | 140 | .icon-shuaxin1:before { 141 | content: "\e686"; 142 | } 143 | 144 | .icon-sousuo:before { 145 | content: "\e687"; 146 | } 147 | 148 | .icon-xiaoxizhongxin:before { 149 | content: "\e688"; 150 | } 151 | 152 | .icon-wodeguanzhu:before { 153 | content: "\e689"; 154 | } 155 | 156 | .icon-youjiantou:before { 157 | content: "\e68a"; 158 | } 159 | 160 | .icon-zuojiantou:before { 161 | content: "\e68b"; 162 | } 163 | 164 | .icon-zengjia1:before { 165 | content: "\e68c"; 166 | } 167 | 168 | .icon-fuzhi-01:before { 169 | content: "\e620"; 170 | } 171 | 172 | .icon-dianzan-shixin:before { 173 | content: "\e621"; 174 | } 175 | 176 | .icon-dianzan-xiankuang:before { 177 | content: "\e622"; 178 | } 179 | 180 | .icon-sousuo1:before { 181 | content: "\e623"; 182 | } 183 | 184 | .icon-pinglun1:before { 185 | content: "\e624"; 186 | } 187 | 188 | .icon-zanting1:before { 189 | content: "\e625"; 190 | } 191 | 192 | .icon-bofang1:before { 193 | content: "\e626"; 194 | } 195 | 196 | .icon-jingyin1:before { 197 | content: "\e627"; 198 | } 199 | 200 | .icon-shangyiji:before { 201 | content: "\e628"; 202 | } 203 | 204 | .icon-xiayiji:before { 205 | content: "\e629"; 206 | } 207 | 208 | .icon-yinliang:before { 209 | content: "\e62a"; 210 | } 211 | 212 | .icon-User:before { 213 | content: "\e62b"; 214 | } 215 | 216 | .icon-Headset:before { 217 | content: "\e62c"; 218 | } 219 | 220 | -------------------------------------------------------------------------------- /src/components/mv/MvList.vue: -------------------------------------------------------------------------------- 1 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/mv/MvSheet.vue: -------------------------------------------------------------------------------- 1 | 30 | 46 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import home from '@/views/home-page/HomePage.vue'; 2 | 3 | const loginRouter = { 4 | path: '/login', 5 | name: 'login', 6 | meta: { 7 | title: '登陆', 8 | isLogin: true, 9 | keepAlive: false 10 | }, 11 | component: () => import('@/views/Login.vue'), 12 | } 13 | 14 | const mainRouter = [ 15 | { 16 | path: '/', 17 | name: 'home', 18 | redirect: { name: 'discover' }, 19 | meta: { 20 | title: '首页', 21 | keepAlive: true 22 | }, 23 | component: home, 24 | children: [ 25 | { 26 | path: 'discover', 27 | name: 'discover', 28 | meta: { 29 | title: '发现音乐', 30 | keepAlive: true, 31 | }, 32 | component: () => import('@/views/discover-music/DiscoverMusic.vue') 33 | }, 34 | { 35 | path: 'singer', 36 | name: 'singer', 37 | meta: { 38 | title: '歌手', 39 | keepAlive: true 40 | }, 41 | component: () => import('@/views/singer/Singer.vue') 42 | }, 43 | { 44 | path: 'singerdetails', 45 | name: 'singerdetails', 46 | meta: { 47 | title: '歌手详情', 48 | keepAlive: true 49 | }, 50 | component: () => import('@/views/singer/SingerDetails.vue') 51 | }, 52 | { 53 | path: 'songsheet', 54 | name: 'songsheet', 55 | meta: { 56 | title: '歌单', 57 | keepAlive: true 58 | }, 59 | component: () => import('@/views/song-sheet/SongSheet.vue') 60 | }, 61 | { 62 | path: 'sheetlist', 63 | name: 'sheetlist', 64 | meta: { 65 | title: '歌单详情', 66 | keepAlive: false, 67 | }, 68 | component: () => import('@/views/song-sheet/SheetList.vue') 69 | }, 70 | { 71 | path: 'albumlist', 72 | name: 'albumlist', 73 | meta: { 74 | title: '专辑详情', 75 | keepAlive: true, 76 | }, 77 | component: () => import('@/views/singer/AlbumList.vue') 78 | }, 79 | { 80 | path: 'boutiquesongsheet', 81 | name: 'boutiquesongsheet', 82 | meta: { 83 | title: '精品歌单', 84 | keepAlive: true, 85 | }, 86 | component: () => import('@/views/song-sheet/BoutiqueSongSheet.vue') 87 | }, 88 | { 89 | path: 'ranklist', 90 | name: 'ranklist', 91 | meta: { 92 | title: '排行榜', 93 | keepAlive: true 94 | }, 95 | component: () => import('@/views/rank/RankList.vue') 96 | 97 | }, 98 | { 99 | path: 'mv', 100 | name: 'mv', 101 | meta: { 102 | title: 'MV', 103 | keepAlive: true 104 | }, 105 | component: () => import('@/views/mv/Mv.vue') 106 | }, 107 | { 108 | path: 'mvdetail', 109 | name: 'mvdetail', 110 | meta: { 111 | title: 'MV详情', 112 | keepAlive: true 113 | }, 114 | component: () => import('@/views/mv/MvDetail.vue') 115 | }, 116 | { 117 | path: 'mvsort', 118 | name: 'mvsort', 119 | meta: { 120 | title: 'MV分类', 121 | keepAlive: true 122 | }, 123 | component: () => import('@/views/mv/MvSort.vue') 124 | }, 125 | { 126 | path: 'searchpage', 127 | name: 'searchpage', 128 | meta: { 129 | title: '资源搜索', 130 | keepAlive: false 131 | }, 132 | component: () => import('@/views/search/SearchPage.vue') 133 | }, 134 | ] 135 | }, 136 | 137 | ] 138 | 139 | export const routes = [loginRouter, ...mainRouter]; -------------------------------------------------------------------------------- /src/components/new-disc/NewSongSheet.vue: -------------------------------------------------------------------------------- 1 | 30 | 53 | -------------------------------------------------------------------------------- /src/views/search/SearchPage.vue: -------------------------------------------------------------------------------- 1 | 25 | 118 | -------------------------------------------------------------------------------- /src/assets/font/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | /* Project id 3613962 */ 4 | src: url('//at.alicdn.com/t/c/font_3613962_ey9cbrfngp.woff2?t=1679136355239') format('woff2'), 5 | url('//at.alicdn.com/t/c/font_3613962_ey9cbrfngp.woff?t=1679136355239') format('woff'), 6 | url('//at.alicdn.com/t/c/font_3613962_ey9cbrfngp.ttf?t=1679136355239') format('truetype'); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-cuowu:before { 18 | content: "\e641"; 19 | } 20 | 21 | .icon-zhengque:before { 22 | content: "\e665"; 23 | } 24 | 25 | .icon-music_play:before { 26 | content: "\e642"; 27 | } 28 | 29 | .icon-zhuanji:before { 30 | content: "\e631"; 31 | } 32 | 33 | .icon-lajitong:before { 34 | content: "\e632"; 35 | } 36 | 37 | .icon-gedan:before { 38 | content: "\e636"; 39 | } 40 | 41 | .icon-yinfu:before { 42 | content: "\e711"; 43 | } 44 | 45 | .icon-gedan1:before { 46 | content: "\e6bf"; 47 | } 48 | 49 | .icon-xinxi:before { 50 | content: "\e630"; 51 | } 52 | 53 | .icon-MV:before { 54 | content: "\e62f"; 55 | } 56 | 57 | .icon-huangguan1:before { 58 | content: "\e653"; 59 | } 60 | 61 | .icon-add:before { 62 | content: "\e664"; 63 | } 64 | 65 | .icon-icon_huangguan2:before { 66 | content: "\e62e"; 67 | } 68 | 69 | .icon-xiangxia:before { 70 | content: "\e600"; 71 | } 72 | 73 | .icon-huangguan:before { 74 | content: "\e62d"; 75 | } 76 | 77 | .icon-geciweidianji:before { 78 | content: "\e727"; 79 | } 80 | 81 | .icon-cibiaoquanyi:before { 82 | content: "\e671"; 83 | } 84 | 85 | .icon-chaxun:before { 86 | content: "\e606"; 87 | } 88 | 89 | .icon-bofang:before { 90 | content: "\e607"; 91 | } 92 | 93 | .icon-danquxunhuan:before { 94 | content: "\e608"; 95 | } 96 | 97 | .icon-changpian:before { 98 | content: "\e609"; 99 | } 100 | 101 | .icon-erji:before { 102 | content: "\e60a"; 103 | } 104 | 105 | .icon-huatong:before { 106 | content: "\e60b"; 107 | } 108 | 109 | .icon-jianshao:before { 110 | content: "\e60c"; 111 | } 112 | 113 | .icon-jingyin:before { 114 | content: "\e60d"; 115 | } 116 | 117 | .icon-liebiaoxunhuan:before { 118 | content: "\e60e"; 119 | } 120 | 121 | .icon-shanchu:before { 122 | content: "\e60f"; 123 | } 124 | 125 | .icon-shengyin:before { 126 | content: "\e610"; 127 | } 128 | 129 | .icon-kuaitui:before { 130 | content: "\e611"; 131 | } 132 | 133 | .icon-suiji:before { 134 | content: "\e612"; 135 | } 136 | 137 | .icon-xiai:before { 138 | content: "\e613"; 139 | } 140 | 141 | .icon-zanting:before { 142 | content: "\e614"; 143 | } 144 | 145 | .icon-zhongzhi:before { 146 | content: "\e615"; 147 | } 148 | 149 | .icon-jinzhi:before { 150 | content: "\e616"; 151 | } 152 | 153 | .icon-liebiao:before { 154 | content: "\e617"; 155 | } 156 | 157 | .icon-shezhi:before { 158 | content: "\e618"; 159 | } 160 | 161 | .icon-shuaxin:before { 162 | content: "\e619"; 163 | } 164 | 165 | .icon-zengjia:before { 166 | content: "\e61a"; 167 | } 168 | 169 | .icon-kuaijin:before { 170 | content: "\e61b"; 171 | } 172 | 173 | .icon-yinle:before { 174 | content: "\e61c"; 175 | } 176 | 177 | .icon-pinghengqi:before { 178 | content: "\e61d"; 179 | } 180 | 181 | .icon-shangyitiao:before { 182 | content: "\e61e"; 183 | } 184 | 185 | .icon-xiayitiao:before { 186 | content: "\e61f"; 187 | } 188 | 189 | .icon-qiyong:before { 190 | content: "\e63a"; 191 | } 192 | 193 | .icon-nan:before { 194 | content: "\e682"; 195 | } 196 | 197 | .icon-nv:before { 198 | content: "\e683"; 199 | } 200 | 201 | .icon-pinglun:before { 202 | content: "\e684"; 203 | } 204 | 205 | .icon-shangchuan:before { 206 | content: "\e685"; 207 | } 208 | 209 | .icon-shuaxin1:before { 210 | content: "\e686"; 211 | } 212 | 213 | .icon-sousuo:before { 214 | content: "\e687"; 215 | } 216 | 217 | .icon-xiaoxizhongxin:before { 218 | content: "\e688"; 219 | } 220 | 221 | .icon-wodeguanzhu:before { 222 | content: "\e689"; 223 | } 224 | 225 | .icon-youjiantou:before { 226 | content: "\e68a"; 227 | } 228 | 229 | .icon-zuojiantou:before { 230 | content: "\e68b"; 231 | } 232 | 233 | .icon-zengjia1:before { 234 | content: "\e68c"; 235 | } 236 | 237 | .icon-fuzhi-01:before { 238 | content: "\e620"; 239 | } 240 | 241 | .icon-dianzan-shixin:before { 242 | content: "\e621"; 243 | } 244 | 245 | .icon-dianzan-xiankuang:before { 246 | content: "\e622"; 247 | } 248 | 249 | .icon-sousuo1:before { 250 | content: "\e623"; 251 | } 252 | 253 | .icon-pinglun1:before { 254 | content: "\e624"; 255 | } 256 | 257 | .icon-zanting1:before { 258 | content: "\e625"; 259 | } 260 | 261 | .icon-bofang1:before { 262 | content: "\e626"; 263 | } 264 | 265 | .icon-jingyin1:before { 266 | content: "\e627"; 267 | } 268 | 269 | .icon-shangyiji:before { 270 | content: "\e628"; 271 | } 272 | 273 | .icon-xiayiji:before { 274 | content: "\e629"; 275 | } 276 | 277 | .icon-yinliang:before { 278 | content: "\e62a"; 279 | } 280 | 281 | .icon-User:before { 282 | content: "\e62b"; 283 | } 284 | 285 | .icon-Headset:before { 286 | content: "\e62c"; 287 | } -------------------------------------------------------------------------------- /src/components/song-sheet/SongSheetCard.vue: -------------------------------------------------------------------------------- 1 | 27 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/song-sheet/BoutiqueSongSheet.vue: -------------------------------------------------------------------------------- 1 | 26 | 98 | -------------------------------------------------------------------------------- /src/components/rank/RankShow.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 76 | 77 | -------------------------------------------------------------------------------- /src/components/song-sheet/BoutiqueSongSheetCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/home-page/Nav.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 76 | 77 | 211 | -------------------------------------------------------------------------------- /src/views/mv/MvSort.vue: -------------------------------------------------------------------------------- 1 | 40 | 151 | -------------------------------------------------------------------------------- /src/components/song-sheet/SongList.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 70 | 71 | -------------------------------------------------------------------------------- /src/views/singer/Singer.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 172 | 173 | -------------------------------------------------------------------------------- /src/views/singer/SingerDetails.vue: -------------------------------------------------------------------------------- 1 | 43 | 195 | -------------------------------------------------------------------------------- /src/assets/font/fonticon/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3613962", 3 | "name": "no name", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "1488891", 10 | "name": "查询", 11 | "font_class": "chaxun", 12 | "unicode": "e606", 13 | "unicode_decimal": 58886 14 | }, 15 | { 16 | "icon_id": "1488892", 17 | "name": "播放", 18 | "font_class": "bofang", 19 | "unicode": "e607", 20 | "unicode_decimal": 58887 21 | }, 22 | { 23 | "icon_id": "1488893", 24 | "name": "单曲循环", 25 | "font_class": "danquxunhuan", 26 | "unicode": "e608", 27 | "unicode_decimal": 58888 28 | }, 29 | { 30 | "icon_id": "1488894", 31 | "name": "唱片", 32 | "font_class": "changpian", 33 | "unicode": "e609", 34 | "unicode_decimal": 58889 35 | }, 36 | { 37 | "icon_id": "1488895", 38 | "name": "耳机", 39 | "font_class": "erji", 40 | "unicode": "e60a", 41 | "unicode_decimal": 58890 42 | }, 43 | { 44 | "icon_id": "1488896", 45 | "name": "话筒", 46 | "font_class": "huatong", 47 | "unicode": "e60b", 48 | "unicode_decimal": 58891 49 | }, 50 | { 51 | "icon_id": "1488897", 52 | "name": "减少", 53 | "font_class": "jianshao", 54 | "unicode": "e60c", 55 | "unicode_decimal": 58892 56 | }, 57 | { 58 | "icon_id": "1488898", 59 | "name": "静音", 60 | "font_class": "jingyin", 61 | "unicode": "e60d", 62 | "unicode_decimal": 58893 63 | }, 64 | { 65 | "icon_id": "1488899", 66 | "name": "列表循环", 67 | "font_class": "liebiaoxunhuan", 68 | "unicode": "e60e", 69 | "unicode_decimal": 58894 70 | }, 71 | { 72 | "icon_id": "1488900", 73 | "name": "删除", 74 | "font_class": "shanchu", 75 | "unicode": "e60f", 76 | "unicode_decimal": 58895 77 | }, 78 | { 79 | "icon_id": "1488901", 80 | "name": "声音", 81 | "font_class": "shengyin", 82 | "unicode": "e610", 83 | "unicode_decimal": 58896 84 | }, 85 | { 86 | "icon_id": "1488902", 87 | "name": "快退", 88 | "font_class": "kuaitui", 89 | "unicode": "e611", 90 | "unicode_decimal": 58897 91 | }, 92 | { 93 | "icon_id": "1488903", 94 | "name": "随机", 95 | "font_class": "suiji", 96 | "unicode": "e612", 97 | "unicode_decimal": 58898 98 | }, 99 | { 100 | "icon_id": "1488904", 101 | "name": "喜爱", 102 | "font_class": "xiai", 103 | "unicode": "e613", 104 | "unicode_decimal": 58899 105 | }, 106 | { 107 | "icon_id": "1488905", 108 | "name": "暂停", 109 | "font_class": "zanting", 110 | "unicode": "e614", 111 | "unicode_decimal": 58900 112 | }, 113 | { 114 | "icon_id": "1488906", 115 | "name": "重置", 116 | "font_class": "zhongzhi", 117 | "unicode": "e615", 118 | "unicode_decimal": 58901 119 | }, 120 | { 121 | "icon_id": "1488907", 122 | "name": "禁止", 123 | "font_class": "jinzhi", 124 | "unicode": "e616", 125 | "unicode_decimal": 58902 126 | }, 127 | { 128 | "icon_id": "1488908", 129 | "name": "列表", 130 | "font_class": "liebiao", 131 | "unicode": "e617", 132 | "unicode_decimal": 58903 133 | }, 134 | { 135 | "icon_id": "1488909", 136 | "name": "设置", 137 | "font_class": "shezhi", 138 | "unicode": "e618", 139 | "unicode_decimal": 58904 140 | }, 141 | { 142 | "icon_id": "1488911", 143 | "name": "刷新", 144 | "font_class": "shuaxin", 145 | "unicode": "e619", 146 | "unicode_decimal": 58905 147 | }, 148 | { 149 | "icon_id": "1488912", 150 | "name": "音乐", 151 | "font_class": "zengjia", 152 | "unicode": "e61a", 153 | "unicode_decimal": 58906 154 | }, 155 | { 156 | "icon_id": "1488913", 157 | "name": "快进", 158 | "font_class": "kuaijin", 159 | "unicode": "e61b", 160 | "unicode_decimal": 58907 161 | }, 162 | { 163 | "icon_id": "1488915", 164 | "name": "音乐", 165 | "font_class": "yinle", 166 | "unicode": "e61c", 167 | "unicode_decimal": 58908 168 | }, 169 | { 170 | "icon_id": "1488916", 171 | "name": "平衡器", 172 | "font_class": "pinghengqi", 173 | "unicode": "e61d", 174 | "unicode_decimal": 58909 175 | }, 176 | { 177 | "icon_id": "16733760", 178 | "name": "上一条", 179 | "font_class": "shangyitiao", 180 | "unicode": "e61e", 181 | "unicode_decimal": 58910 182 | }, 183 | { 184 | "icon_id": "16733831", 185 | "name": "下一条", 186 | "font_class": "xiayitiao", 187 | "unicode": "e61f", 188 | "unicode_decimal": 58911 189 | }, 190 | { 191 | "icon_id": "17726569", 192 | "name": "启用", 193 | "font_class": "qiyong", 194 | "unicode": "e63a", 195 | "unicode_decimal": 58938 196 | }, 197 | { 198 | "icon_id": "29865176", 199 | "name": "男", 200 | "font_class": "nan", 201 | "unicode": "e682", 202 | "unicode_decimal": 59010 203 | }, 204 | { 205 | "icon_id": "29865177", 206 | "name": "女", 207 | "font_class": "nv", 208 | "unicode": "e683", 209 | "unicode_decimal": 59011 210 | }, 211 | { 212 | "icon_id": "29865180", 213 | "name": "评论", 214 | "font_class": "pinglun", 215 | "unicode": "e684", 216 | "unicode_decimal": 59012 217 | }, 218 | { 219 | "icon_id": "29865182", 220 | "name": "上传", 221 | "font_class": "shangchuan", 222 | "unicode": "e685", 223 | "unicode_decimal": 59013 224 | }, 225 | { 226 | "icon_id": "29865193", 227 | "name": "刷新", 228 | "font_class": "shuaxin1", 229 | "unicode": "e686", 230 | "unicode_decimal": 59014 231 | }, 232 | { 233 | "icon_id": "29865195", 234 | "name": "搜索", 235 | "font_class": "sousuo", 236 | "unicode": "e687", 237 | "unicode_decimal": 59015 238 | }, 239 | { 240 | "icon_id": "29865211", 241 | "name": "消息中心", 242 | "font_class": "xiaoxizhongxin", 243 | "unicode": "e688", 244 | "unicode_decimal": 59016 245 | }, 246 | { 247 | "icon_id": "29865213", 248 | "name": "我的关注", 249 | "font_class": "wodeguanzhu", 250 | "unicode": "e689", 251 | "unicode_decimal": 59017 252 | }, 253 | { 254 | "icon_id": "29865219", 255 | "name": "右箭头", 256 | "font_class": "youjiantou", 257 | "unicode": "e68a", 258 | "unicode_decimal": 59018 259 | }, 260 | { 261 | "icon_id": "29865221", 262 | "name": "左箭头", 263 | "font_class": "zuojiantou", 264 | "unicode": "e68b", 265 | "unicode_decimal": 59019 266 | }, 267 | { 268 | "icon_id": "29865222", 269 | "name": "增加", 270 | "font_class": "zengjia1", 271 | "unicode": "e68c", 272 | "unicode_decimal": 59020 273 | }, 274 | { 275 | "icon_id": "30075740", 276 | "name": "复制", 277 | "font_class": "fuzhi-01", 278 | "unicode": "e620", 279 | "unicode_decimal": 58912 280 | }, 281 | { 282 | "icon_id": "31314850", 283 | "name": "点赞-实心", 284 | "font_class": "dianzan-shixin", 285 | "unicode": "e621", 286 | "unicode_decimal": 58913 287 | }, 288 | { 289 | "icon_id": "31314853", 290 | "name": "点赞-线框", 291 | "font_class": "dianzan-xiankuang", 292 | "unicode": "e622", 293 | "unicode_decimal": 58914 294 | }, 295 | { 296 | "icon_id": "31314855", 297 | "name": "搜索", 298 | "font_class": "sousuo1", 299 | "unicode": "e623", 300 | "unicode_decimal": 58915 301 | }, 302 | { 303 | "icon_id": "31314856", 304 | "name": "评论", 305 | "font_class": "pinglun1", 306 | "unicode": "e624", 307 | "unicode_decimal": 58916 308 | }, 309 | { 310 | "icon_id": "31314860", 311 | "name": "暂停", 312 | "font_class": "zanting1", 313 | "unicode": "e625", 314 | "unicode_decimal": 58917 315 | }, 316 | { 317 | "icon_id": "31314862", 318 | "name": "播放", 319 | "font_class": "bofang1", 320 | "unicode": "e626", 321 | "unicode_decimal": 58918 322 | }, 323 | { 324 | "icon_id": "31314864", 325 | "name": "静音", 326 | "font_class": "jingyin1", 327 | "unicode": "e627", 328 | "unicode_decimal": 58919 329 | }, 330 | { 331 | "icon_id": "31314866", 332 | "name": "上一集", 333 | "font_class": "shangyiji", 334 | "unicode": "e628", 335 | "unicode_decimal": 58920 336 | }, 337 | { 338 | "icon_id": "31314867", 339 | "name": "下一集", 340 | "font_class": "xiayiji", 341 | "unicode": "e629", 342 | "unicode_decimal": 58921 343 | }, 344 | { 345 | "icon_id": "31314868", 346 | "name": "音量", 347 | "font_class": "yinliang", 348 | "unicode": "e62a", 349 | "unicode_decimal": 58922 350 | }, 351 | { 352 | "icon_id": "31345316", 353 | "name": "User", 354 | "font_class": "User", 355 | "unicode": "e62b", 356 | "unicode_decimal": 58923 357 | }, 358 | { 359 | "icon_id": "31345320", 360 | "name": "Headset", 361 | "font_class": "Headset", 362 | "unicode": "e62c", 363 | "unicode_decimal": 58924 364 | } 365 | ] 366 | } 367 | -------------------------------------------------------------------------------- /src/assets/font/fonticon/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /src/components/search/SearchMusic.vue: -------------------------------------------------------------------------------- 1 | 46 | 152 | --------------------------------------------------------------------------------