├── .cz-config.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc.js ├── .vscode └── extensions.json ├── README.md ├── auto-imports.d.ts ├── commitlint.config.js ├── components.d.ts ├── deploy.sh ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── logo.png └── ui │ ├── 008-01.jpg │ ├── 008-02.jpg │ ├── 008-03.jpg │ ├── 008-04.jpg │ ├── 008-05.jpg │ └── 008-06.jpg ├── src ├── App.vue ├── assets │ ├── base.scss │ ├── img │ │ ├── OpticalDisk.png │ │ ├── ScanQrCode.png │ │ ├── index.ts │ │ └── logo.png │ └── theme.scss ├── components │ ├── Banner.vue │ ├── CoverPlay.vue │ ├── IconPark.vue │ ├── Loading.vue │ ├── MoreText.vue │ ├── MvList.vue │ ├── PlayedList.vue │ ├── Playing.vue │ ├── SongList.vue │ └── Title.vue ├── layout │ ├── footer │ │ ├── Footer.vue │ │ ├── PlayerAction.vue │ │ ├── PlayerController.vue │ │ ├── PlayerSlider.vue │ │ ├── PlayerSong.vue │ │ └── PlayerVolumeSlider.vue │ ├── header │ │ ├── Header.vue │ │ ├── SearchPop.vue │ │ ├── SearchSuggest.vue │ │ └── UserInfo.vue │ └── menu │ │ ├── Menu.vue │ │ ├── MenuList.vue │ │ └── useMenu.ts ├── main.ts ├── models │ ├── album.ts │ ├── artist.ts │ ├── artist_detail.ts │ ├── banner.ts │ ├── dj.ts │ ├── mv.ts │ ├── personalized.ts │ ├── playlist.ts │ ├── playlist_cat.ts │ ├── playlist_hot.ts │ ├── search.ts │ ├── song.ts │ ├── song_url.ts │ ├── top_song.ts │ ├── toplist_detail.ts │ ├── user.ts │ ├── video.ts │ └── video_detail.ts ├── router │ └── index.ts ├── stores │ ├── common.ts │ ├── index.ts │ ├── music.ts │ ├── personalized.ts │ ├── player.ts │ ├── search.ts │ ├── user.ts │ └── video.ts ├── utils │ ├── api.ts │ ├── extend.ts │ ├── http.ts │ └── number.ts ├── views │ ├── Index.vue │ ├── album │ │ ├── Desc.vue │ │ ├── Index.vue │ │ └── Info.vue │ ├── artist │ │ ├── Album.vue │ │ ├── ArtistDetail.vue │ │ ├── Desc.vue │ │ ├── Info.vue │ │ ├── Mv.vue │ │ └── Songs.vue │ ├── discover │ │ ├── DjProgram.vue │ │ ├── Index.vue │ │ ├── Mv.vue │ │ ├── Personalized.vue │ │ └── PersonalizedNewSong.vue │ ├── dj │ │ ├── Index.vue │ │ ├── Newcomer.vue │ │ ├── Pay.vue │ │ ├── Personalize.vue │ │ ├── Popular.vue │ │ └── Today.vue │ ├── music │ │ ├── Index.vue │ │ ├── MusicController.ts │ │ ├── artist │ │ │ └── Artist.vue │ │ ├── category │ │ │ ├── Category.vue │ │ │ └── PlaylistHot.vue │ │ ├── picked │ │ │ ├── Picked.vue │ │ │ └── Video.vue │ │ ├── topList │ │ │ ├── GlobalList.vue │ │ │ ├── OfficialList.vue │ │ │ └── TopList.vue │ │ └── topSong │ │ │ └── TopSong.vue │ ├── mv │ │ ├── MvDetail │ │ │ ├── Index.vue │ │ │ ├── Info.vue │ │ │ └── SimiMV.vue │ │ └── MvList │ │ │ ├── Exclusive.vue │ │ │ ├── Hot.vue │ │ │ ├── Index.vue │ │ │ └── New.vue │ ├── playlist │ │ ├── Index.vue │ │ ├── PlayListInfo.vue │ │ └── SongsList.vue │ └── video │ │ ├── Index.vue │ │ ├── VideoController.ts │ │ ├── videoDetail │ │ ├── Index.vue │ │ ├── Info.vue │ │ └── SimiVideo.vue │ │ └── videoList │ │ └── Index.vue └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feature', name: 'feature: 新增功能' }, 4 | { value: 'bug', name: 'bug: 测试反馈bug列表中的bug号' }, 5 | { value: 'fix', name: 'fix: 修复 bug' }, 6 | { value: 'ui', name: 'ui: 更新 UI' }, 7 | { value: 'docs', name: 'docs: 更新文档' }, 8 | { value: 'style', name: 'style: 样式更改' }, 9 | { value: 'perf', name: 'perf: 性能优化' }, 10 | { value: 'refactor', name: 'refactor: 重构(既不是增加feature,也不是修复bug)' }, 11 | { value: 'test', name: 'test: 增加测试' }, 12 | { value: 'chore', name: 'chore: 杂项、其他更改' }, 13 | { value: 'ci', name: 'ci: 更新 CI/CD 等自动化配置' }, 14 | { value: 'revert', name: 'revert: 回退' }, 15 | { value: 'merge', name: 'merge: 合并分支' }, 16 | { value: 'release', name: 'release: 发布' }, 17 | { value: 'deploy', name: 'deploy: 部署' }, 18 | { value: 'build', name: 'build: 打包' } 19 | ], 20 | scopes: [''], 21 | // override the messages, defaults are as follows 22 | messages: { 23 | type: '请选择提交类型:', 24 | scope: '请输入您修改的范围(可选):', 25 | subject: '请简要描述提交 message (必填):', 26 | body: '请输入详细描述(可选,待优化去除,跳过即可):', 27 | footer: '请输入要关闭的issue(待优化去除,跳过即可):', 28 | confirmCommit: '确认使用以上信息提交?(y/n/e/h)' 29 | }, 30 | allowCustomScopes: true, 31 | // skipEmptyScopes: true, 32 | skipQuestions: ['body', 'footer'], 33 | subjectLimit: 72 34 | } 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | index.html -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser', 6 | ecmaVersion: 2020, 7 | sourceType: 'module', 8 | ecmaFeatures: { 9 | jsx: true 10 | } 11 | }, 12 | 13 | extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'], 14 | 15 | rules: { 16 | // override/add rules settings here, such as: 17 | //关闭组件强制驼峰命名规则 18 | 'vue/multi-word-component-names': 'off' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.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 | pnpm-lock.yaml 10 | 11 | node_modules 12 | # dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多 150 字符 3 | printWidth: 150, 4 | // 使用 2 个空格缩进 5 | tabWidth: 2, 6 | // 不使用 tab 缩进,而使用空格 7 | useTabs: false, 8 | // 行尾需要有分号 9 | semi: false, 10 | // 使用单引号代替双引号 11 | singleQuote: true, 12 | // 末尾使用逗号 13 | trailingComma: 'none', 14 | // 对象的 key 仅在必要时用引号 15 | quoteProps: 'as-needed', 16 | // jsx 不使用单引号,而使用双引号 17 | jsxSingleQuote: false, 18 | // 大括号内的首尾需要空格 { foo: bar } 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 箭头函数,只有一个参数的时候,也需要括号 23 | arrowParens: 'always', 24 | // 每个文件格式化的范围是文件的全部内容 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 32 | proseWrap: 'preserve', 33 | // 根据显示样式决定 html 要不要折行 34 | htmlWhitespaceSensitivity: 'css', 35 | // 换行符使用 lf 36 | endOfLine: 'auto' 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

VUE3-MUSIC

3 |
4 |

5 | vite 6 | vue3 7 | pinia 8 | tailwindcss 9 |

10 | 11 | ## 仿网易云音乐 12 | 13 | 基于 VITE + VUE3 + TS + PINIA + TAILWINDCSS 开发的音乐播放器,界面模仿网易云音乐客户端。 14 | 15 | 参考 [SmallRuralDog/vue3-music](https://github.com/SmallRuralDog/vue3-music) 进行开发、改进和完善。 16 | 17 | 在线体验:[https://m.zugelu.com/](https://m.zugelu.com/) 18 | 19 | 如果觉得项目不错,欢迎 Star 支持,感谢! 20 | 21 | ## 本地安装 22 | 23 | ``` 24 | git clone https://github.com/luzhe0359/vue3-music.git 25 | cd vue3-music 26 | pnpm install 27 | pnpm run dev 28 | ``` 29 | 30 | ## 网易云音乐 API 31 | 32 | 需运行 API 服务,才能正常访问 33 | 34 | [开发文档](https://binaryify.github.io/NeteaseCloudMusicApi) 35 | 36 | ## 功能模块 37 | 38 | - [ ] 手机号登录 39 | - [x] 二维码登录 40 | - [x] 主题切换 41 | 42 | **推荐** 43 | 44 | - [x] 专属歌单 45 | - [x] 推荐新音乐 46 | - [x] 推荐 MV 47 | 48 | **音乐馆** 49 | 50 | - [x] 精选 51 | - [x] 歌单 52 | - [x] 排行榜 53 | - [x] 歌手 54 | - [x] 新歌速递 55 | 56 | **视频** 57 | 58 | - [x] 视频列表 59 | - [x] mv 列表 60 | 61 | **电台** 62 | 63 | - [x] 推荐电台 64 | - [x] 最热主播榜 65 | - [x] 主播新人榜 66 | - [x] 付费精品 67 | 68 | ## 四、UI 界面 69 | 70 | ![discover.jpg](public/ui/008-01.jpg) 71 | 72 | ![music.jpg](public/ui/008-02.jpg) 73 | 74 | ![video.jpg](public/ui/008-03.jpg) 75 | 76 | ![dj.jpg](public/ui/008-04.jpg) 77 | 78 | ![_music.jpg](public/ui/008-05.jpg) 79 | 80 | ![_dj.jpg](public/ui/008-06.jpg) 81 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global {} 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional', 'cz'], 3 | rules: { 4 | // 选项 5 | 'type-enum': [ 6 | 2, 7 | 'always', 8 | [ 9 | 'feature', // 新增功能 10 | 'bug', // 此项特别针对bug号,用于向测试反馈bug列表的bug修改情况 11 | 'fix', // 修补 bug 12 | 'ui', // 更新 UI 13 | 'docs', // 更新文档 14 | 'style', // 样式更改 15 | 'perf', // 性能优化 16 | 'refactor', // 重构 17 | 'test', // 增加测试 18 | 'chore', // 杂项、其他更改 19 | 'ci', // 更新 CI/CD 等自动化配置 20 | 'revert', // 代码回滚 21 | 'merge', // 合并分支 22 | 'release', // 发布 23 | 'deploy', // 部署 24 | 'build' // 打包 25 | ] 26 | ], 27 | // 格式 小写 28 | 'type-case': [2, 'always', 'lower-case'], 29 | // 不能为空 30 | 'type-empty': [2, 'never'], 31 | // 范围选项 32 | 'scope-enum': [0, 'always'], 33 | // 范围格式 34 | 'scope-case': [0], 35 | // 范围是否为空 36 | 'scope-empty': [0], 37 | // 主要 message 不能为空 38 | 'subject-empty': [2, 'never'], 39 | // 以什么为结束标志,禁用 40 | 'subject-full-stop': [0, 'never'], 41 | // 格式,禁用 42 | 'subject-case': [0, 'never'], 43 | // 以空行开头 44 | 'body-leading-blank': [1, 'always'], 45 | 'header-max-length': [0, 'always', 72] 46 | } 47 | } 48 | /** 49 | * 代码提交规范 - 统一风格提交 (需要用到的包及作用) 50 | * commitizen cz-conventional-changelog 51 | * - 系统会提示您在提交时填写所有必需的提交字段 52 | * cz-customizable 53 | * - 汉化以上插件 54 | * @commitlint/config-conventional @commitlint/cli 55 | * - 限制提交格式 56 | */ 57 | -------------------------------------------------------------------------------- /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/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | Banner: typeof import('./src/components/Banner.vue')['default'] 11 | CoverPlay: typeof import('./src/components/CoverPlay.vue')['default'] 12 | ElAffix: typeof import('element-plus/es')['ElAffix'] 13 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 14 | ElBadge: typeof import('element-plus/es')['ElBadge'] 15 | ElButton: typeof import('element-plus/es')['ElButton'] 16 | ElDialog: typeof import('element-plus/es')['ElDialog'] 17 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 18 | ElImage: typeof import('element-plus/es')['ElImage'] 19 | ElInput: typeof import('element-plus/es')['ElInput'] 20 | ElPopover: typeof import('element-plus/es')['ElPopover'] 21 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 22 | ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] 23 | ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] 24 | ElSlider: typeof import('element-plus/es')['ElSlider'] 25 | ElSpace: typeof import('element-plus/es')['ElSpace'] 26 | ElTable: typeof import('element-plus/es')['ElTable'] 27 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 28 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 29 | ElTabs: typeof import('element-plus/es')['ElTabs'] 30 | IconPark: typeof import('./src/components/IconPark.vue')['default'] 31 | Loading: typeof import('./src/components/Loading.vue')['default'] 32 | MoreText: typeof import('./src/components/MoreText.vue')['default'] 33 | MvList: typeof import('./src/components/MvList.vue')['default'] 34 | PlayedList: typeof import('./src/components/PlayedList.vue')['default'] 35 | Playing: typeof import('./src/components/Playing.vue')['default'] 36 | RouterLink: typeof import('vue-router')['RouterLink'] 37 | RouterView: typeof import('vue-router')['RouterView'] 38 | SongList: typeof import('./src/components/SongList.vue')['default'] 39 | Title: typeof import('./src/components/Title.vue')['default'] 40 | } 41 | export interface ComponentCustomProperties { 42 | vInfiniteScroll: typeof import('element-plus/es')['ElInfiniteScroll'] 43 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # 发生任何错误时终止 2 | set -e 3 | 4 | # 构建 5 | npm run build 6 | 7 | # 进入输出产物文件夹 8 | cd dist 9 | 10 | # 解决github-page刷新 404 11 | cp index.html 404.html 12 | 13 | # 如果你要部署到自定义域名 14 | echo 'm.zugelu.com' > CNAME 15 | 16 | git init 17 | git add -A 18 | git commit -m 'deploy' 19 | 20 | # 如果你要部署在 https://.github.io 21 | # git push -f git@github.com:/.github.io.git master 22 | 23 | # 如果你要部署在 https://.github.io/ 24 | git push -f git@github.com:luzhe0359/vue3-music.git master:gh-pages 25 | 26 | cd - -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue3 - music 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-music", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview", 9 | "eslint:comment": "使用 ESLint 检查并自动修复 src 目录下所有扩展名为 .js 和 .vue 的文件", 10 | "eslint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", 11 | "prettier:comment": "自动格式化当前目录下的所有文件", 12 | "prettier": "prettier . --write", 13 | "commit:comment": "引导设置规范化的提交信息", 14 | "commit": "git-cz" 15 | }, 16 | "dependencies": { 17 | "@icon-park/vue-next": "^1.4.2", 18 | "@types/lodash-es": "^4.17.6", 19 | "@vueuse/components": "^9.1.1", 20 | "@vueuse/core": "^9.1.1", 21 | "axios": "^0.27.2", 22 | "dayjs": "^1.11.5", 23 | "element-plus": "2.2.26", 24 | "lodash": "^4.17.21", 25 | "pinia": "^2.0.21", 26 | "swiper": "^8.3.2", 27 | "vue": "^3.2.38", 28 | "vue-router": "^4.1.5" 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "^17.1.2", 32 | "@commitlint/config-conventional": "^17.1.0", 33 | "@types/lodash": "^4.14.184", 34 | "@types/node": "^18.7.14", 35 | "@typescript-eslint/eslint-plugin": "^5.36.1", 36 | "@typescript-eslint/parser": "^5.36.1", 37 | "@vitejs/plugin-vue": "^3.0.3", 38 | "autoprefixer": "^10.4.8", 39 | "commitizen": "^4.2.5", 40 | "commitlint-config-cz": "^0.13.3", 41 | "cz-conventional-changelog": "^3.3.0", 42 | "cz-customizable": "^6.9.2", 43 | "eslint": "^8.23.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-plugin-prettier": "^4.2.1", 46 | "eslint-plugin-vue": "^9.4.0", 47 | "husky": "^8.0.1", 48 | "lint-staged": "^13.0.3", 49 | "postcss": "^8.4.16", 50 | "prettier": "^2.7.1", 51 | "sass": "^1.54.8", 52 | "tailwindcss": "^3.1.8", 53 | "typescript": "^4.8.2", 54 | "unplugin-auto-import": "^0.11.2", 55 | "unplugin-vue-components": "^0.22.4", 56 | "vite": "^3.0.9", 57 | "vue-tsc": "^0.39.5" 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-customizable" 62 | } 63 | }, 64 | "lint-staged": { 65 | "*.{vue,js,ts,jsx,tsx}": [ 66 | "pnpm run eslint", 67 | "pnpm run prettier" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/logo.png -------------------------------------------------------------------------------- /public/ui/008-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-01.jpg -------------------------------------------------------------------------------- /public/ui/008-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-02.jpg -------------------------------------------------------------------------------- /public/ui/008-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-03.jpg -------------------------------------------------------------------------------- /public/ui/008-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-04.jpg -------------------------------------------------------------------------------- /public/ui/008-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-05.jpg -------------------------------------------------------------------------------- /public/ui/008-06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/public/ui/008-06.jpg -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/base.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --el-color-primary: #34d399 !important; 7 | --el-color-primary-light-1: #065f46 !important; 8 | --el-color-primary-light-2: #047857 !important; 9 | --el-color-primary-light-3: #059669 !important; 10 | --el-color-primary-light-4: #10b981 !important; 11 | --el-color-primary-light-5: #34d399 !important; 12 | --el-color-primary-light-6: #6ee7b7 !important; 13 | --el-color-primary-light-7: #a7f3d0 !important; 14 | --el-color-primary-light-8: #d1fae5 !important; 15 | --el-color-primary-light-9: #ecfdf5 !important; 16 | --el-color-primary-dark-2: #047857 !important; 17 | --el-text-color-primary: text-slate-700 !important; 18 | } 19 | 20 | :root { 21 | &.dark { 22 | --el-color-primary: #34d399 !important; 23 | --el-color-primary-light-9: transparent !important; // hover按钮背景色 24 | --el-border-color-base: #57534e !important; 25 | --el-text-color-regular: #e2e8f0 !important; 26 | --el-text-color-primary: text-slate-200 !important; 27 | --el-bg-color: #141414 !important; 28 | --el-color-white: #fff !important; 29 | --el-border-color-lighter: #474648 !important; 30 | --el-border-color-light: #474648 !important; 31 | --el-button-active-bg-color: red; 32 | 33 | --el-popover-bg-color: #292524 !important; 34 | --el-fill-color-blank: #141414 !important; 35 | --el-fill-color-light: #222222 !important; 36 | --el-bg-color-overlay: #141414 !important; 37 | --el-mask-color: #222222 !important; 38 | } 39 | } 40 | 41 | @media (prefers-color-scheme: dark) { 42 | :root { 43 | --el-color-primary: #059669 !important; 44 | 45 | --el-color-primary-light-9: #57534e !important; 46 | 47 | --el-border-color-base: #57534e !important; 48 | --el-text-color-regular: #e2e8f0 !important; 49 | --el-text-color-primary: text-slate-200 !important; 50 | --el-bg-color: #474648 !important; 51 | --el-color-white: #141414 !important; 52 | --el-border-color-lighter: #474648 !important; 53 | --el-border-color-light: #474648 !important; 54 | 55 | --el-popover-bg-color: #292524 !important; 56 | } 57 | } 58 | 59 | html, 60 | body { 61 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 62 | @apply text-slate-700 bg-white transition-all; 63 | @apply dark:bg-[#141414] dark:text-slate-100; 64 | } 65 | 66 | .hover-text { 67 | @apply hover:text-emerald-400 transition-colors cursor-pointer; 68 | } 69 | 70 | .badge { 71 | .el-badge__content.is-fixed { 72 | @apply scale-75 bg-white text-slate-500 text-xs bg-opacity-50 border-0 left-2 -top-2; 73 | } 74 | } 75 | 76 | .el-popover.el-popper { 77 | min-width: auto; 78 | } 79 | 80 | .el-tabs__nav-wrap::after { 81 | height: 0 !important; 82 | } 83 | 84 | .el-tabs__header { 85 | margin: 0 !important; 86 | } 87 | 88 | /* el-divider样式无效bug */ 89 | .el-divider--vertical { 90 | display: inline-block; 91 | width: 1px; 92 | height: 1em; 93 | margin: 0 8px; 94 | vertical-align: middle; 95 | position: relative; 96 | border-left: 1px var(--el-border-color) var(--el-border-style); 97 | @apply dark:border-stone-800; 98 | } 99 | 100 | .el-table { 101 | // 播放中+背景 102 | .table-playing { 103 | @apply bg-emerald-50 dark:bg-[#222]; 104 | } 105 | // 边框 106 | td, 107 | th { 108 | border-bottom: none !important; 109 | } 110 | .el-table__inner-wrapper::before { 111 | height: 0; 112 | } 113 | // 过渡 114 | tr { 115 | @apply transition-colors; 116 | } 117 | } 118 | 119 | .el-tabs { 120 | .el-tabs__header { 121 | @apply sticky top-0 z-10 bg-white dark:bg-[#141414] transition-all; 122 | } 123 | } 124 | 125 | .el-skeleton { 126 | font-size: 0; // 骨架屏图片间隙 127 | } 128 | -------------------------------------------------------------------------------- /src/assets/img/OpticalDisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/src/assets/img/OpticalDisk.png -------------------------------------------------------------------------------- /src/assets/img/ScanQrCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/src/assets/img/ScanQrCode.png -------------------------------------------------------------------------------- /src/assets/img/index.ts: -------------------------------------------------------------------------------- 1 | import Logo from './logo.png' 2 | import OpticalDisk from './OpticalDisk.png' 3 | import ScanQrCode from './ScanQrCode.png' 4 | 5 | export { Logo, OpticalDisk, ScanQrCode } 6 | -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhe0359/vue3-music/c8006f214554917ca48dd1352c35b9d1f2143b82/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/theme.scss: -------------------------------------------------------------------------------- 1 | // .bg-main { 2 | // @apply bg-gray-50 transition-all; 3 | // @apply dark:bg-[#141414]; 4 | // } 5 | 6 | // .hover-bg-main { 7 | // @apply hover:bg-gray-200; 8 | // @apply dark:hover:bg-[#262727]; 9 | // } 10 | 11 | .bg-card { 12 | @apply bg-gray-100 transition-all; 13 | @apply dark:bg-[#222]; 14 | @apply dark:hover:bg-neutral-800; 15 | } 16 | 17 | .hover-bg-card { 18 | @apply hover:bg-gray-200 transition-all duration-300; 19 | @apply dark:hover:bg-[#262727]; 20 | } 21 | 22 | .text-main { 23 | @apply text-slate-700; 24 | @apply dark:text-slate-200; 25 | } 26 | 27 | .text-active { 28 | @apply text-emerald-400; 29 | } 30 | 31 | .text-title { 32 | @apply text-slate-400; 33 | @apply dark:text-[#999]; 34 | } 35 | 36 | .text-desc { 37 | @apply text-slate-500; 38 | @apply dark:text-neutral-400; 39 | } 40 | 41 | .text-tag { 42 | @apply text-slate-400; 43 | @apply dark:text-stone-500; 44 | } 45 | 46 | .button { 47 | @apply flex bg-emerald-600 rounded-full justify-center items-center h-8 text-sm transition-all; 48 | @apply text-white; 49 | @apply hover:bg-emerald-700; 50 | } 51 | 52 | .button-outline { 53 | @apply flex rounded-full justify-center items-center h-8 text-sm transition-all border; 54 | @apply hover:border-emerald-500 hover:text-emerald-500; 55 | } 56 | 57 | .button-text { 58 | @apply flex bg-[#e6e6e6] rounded justify-center items-center h-8 text-sm transition-all cursor-pointer; 59 | @apply dark:bg-[#333] dark:text-white; 60 | @apply hover:text-emerald-500; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Banner.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 47 | 48 | 64 | -------------------------------------------------------------------------------- /src/components/CoverPlay.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | 35 | 66 | -------------------------------------------------------------------------------- /src/components/IconPark.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 81 | -------------------------------------------------------------------------------- /src/components/MoreText.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/components/MvList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/PlayedList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/Playing.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/SongList.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 51 | -------------------------------------------------------------------------------- /src/components/Title.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/layout/footer/Footer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /src/layout/footer/PlayerAction.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/layout/footer/PlayerController.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/layout/footer/PlayerSlider.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 51 | -------------------------------------------------------------------------------- /src/layout/footer/PlayerSong.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 | 44 | -------------------------------------------------------------------------------- /src/layout/footer/PlayerVolumeSlider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/layout/header/Header.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 55 | 56 | 69 | -------------------------------------------------------------------------------- /src/layout/header/SearchPop.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/layout/header/SearchSuggest.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/layout/header/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 101 | 102 | 144 | -------------------------------------------------------------------------------- /src/layout/menu/Menu.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /src/layout/menu/MenuList.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /src/layout/menu/useMenu.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import { Planet, Music, VideoOne, Fm, Like, Computer, DownloadThree, PlayTwo } from '@icon-park/vue-next' 3 | import { useRoute, useRouter } from 'vue-router' 4 | 5 | interface IMenu { 6 | name: string 7 | key: string 8 | icon: any 9 | theme: 'outline' | 'filled' | 'two-tone' | 'multi-color' 10 | } 11 | 12 | interface IMenus { 13 | name: string 14 | menus: IMenu[] 15 | } 16 | 17 | export function userMenu() { 18 | const menus: IMenus[] = [ 19 | { 20 | name: '在线音乐', 21 | menus: [ 22 | { 23 | name: '推荐', 24 | key: 'discover', 25 | icon: Planet, 26 | theme: 'outline' 27 | }, 28 | { 29 | name: '音乐馆', 30 | key: 'music', 31 | icon: Music, 32 | theme: 'outline' 33 | }, 34 | { 35 | name: '视频', 36 | key: 'video', 37 | icon: VideoOne, 38 | theme: 'outline' 39 | }, 40 | { 41 | name: '电台', 42 | key: 'dj', 43 | icon: Fm, 44 | theme: 'outline' 45 | } 46 | ] 47 | }, 48 | { 49 | name: '我的音乐', 50 | menus: [ 51 | { 52 | name: '我喜欢', 53 | key: 'love', 54 | icon: Like, 55 | theme: 'outline' 56 | }, 57 | { 58 | name: '本地歌曲', 59 | key: 'local', 60 | icon: Computer, 61 | theme: 'outline' 62 | }, 63 | { 64 | name: '下载歌曲', 65 | key: 'download', 66 | icon: DownloadThree, 67 | theme: 'outline' 68 | }, 69 | { 70 | name: '最近播放', 71 | key: 'recently', 72 | icon: PlayTwo, 73 | theme: 'outline' 74 | } 75 | ] 76 | } 77 | ] 78 | 79 | const route = useRoute() 80 | 81 | const currentKey = ref(route.meta.menu) 82 | 83 | const router = useRouter() 84 | 85 | watch( 86 | () => route.meta.menu, 87 | (menu) => { 88 | currentKey.value = menu 89 | } 90 | ) 91 | 92 | const click = async (menu: IMenu) => { 93 | await router.push({ name: menu.key, replace: true }) 94 | } 95 | 96 | return { 97 | menus, 98 | click, 99 | currentKey 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import store from './stores' 4 | import router from './router' 5 | 6 | import '@/utils/extend' 7 | import '@/assets/base.scss' 8 | import '@/assets/theme.scss' 9 | import 'element-plus/theme-chalk/dark/css-vars.css' 10 | 11 | createApp(App).use(router).use(store).mount('#app') 12 | -------------------------------------------------------------------------------- /src/models/album.ts: -------------------------------------------------------------------------------- 1 | import type { Artist } from '@/models/artist' 2 | 3 | export interface Album { 4 | songs: any[] 5 | paid: boolean 6 | onSale: boolean 7 | mark: number 8 | blurPicUrl: string 9 | companyId: number 10 | alias: string[] 11 | artists: Artist[] 12 | copyrightId: number 13 | picId: number 14 | artist: Artist 15 | publishTime: number 16 | company: string 17 | briefDesc: string 18 | picUrl: string 19 | commentThreadId: string 20 | pic: number 21 | tags: string 22 | description: string 23 | status: number 24 | subType: string 25 | name: string 26 | id: number 27 | type: string 28 | size: number 29 | picId_str: string 30 | } 31 | -------------------------------------------------------------------------------- /src/models/artist.ts: -------------------------------------------------------------------------------- 1 | export interface Artist { 2 | albumSize: number 3 | alias: string[] 4 | briefDesc: string 5 | fansCount: number 6 | followed: boolean 7 | id: number 8 | img1v1Id: number 9 | img1v1Id_str: string 10 | img1v1Url: string 11 | musicSize: number 12 | name: string 13 | picId: number 14 | picId_str: string 15 | picUrl: string 16 | topicPerson: number 17 | trans: string 18 | } 19 | 20 | export interface Mv { 21 | id: number 22 | name: string 23 | status: number 24 | artistName: string 25 | artist: Artist 26 | imgurl16v9: string 27 | imgurl: string 28 | duration: number 29 | playCount: number 30 | publishTime: string 31 | subed: boolean 32 | } 33 | 34 | export interface Mvs { 35 | artistName: string 36 | duration: number 37 | id: number 38 | imgurl: string 39 | imgurl16v9: string 40 | name: string 41 | playCount: number 42 | publishTime: string 43 | status: number 44 | subed: boolean 45 | } 46 | -------------------------------------------------------------------------------- /src/models/artist_detail.ts: -------------------------------------------------------------------------------- 1 | export interface ArtistDetail { 2 | videoCount: number 3 | identify: ArtistDetailIdentify 4 | artist: ArtistDetailArtist 5 | blacklist: boolean 6 | preferShow: number 7 | showPriMsg: boolean 8 | secondaryExpertIdentiy: ArtistDetailSecondaryExpertIdentiy[] 9 | } 10 | export interface ArtistDetailIdentify { 11 | imageUrl?: any 12 | imageDesc: string 13 | actionUrl: string 14 | } 15 | export interface ArtistDetailArtistRank { 16 | rank: number 17 | type: number 18 | } 19 | export interface ArtistDetailArtist { 20 | id: number 21 | cover: string 22 | name: string 23 | transNames: string[] 24 | identities: string[] 25 | identifyTag?: any 26 | briefDesc: string 27 | rank: ArtistDetailArtistRank 28 | albumSize: number 29 | musicSize: number 30 | mvSize: number 31 | } 32 | export interface ArtistDetailSecondaryExpertIdentiy { 33 | expertIdentiyId: number 34 | expertIdentiyName: string 35 | expertIdentiyCount: number 36 | } 37 | 38 | export interface ArtistDesc { 39 | introduction: ArtistDescIntroduction[] 40 | briefDesc: string 41 | count: number 42 | topicData: ArtistDescTopicData[] 43 | } 44 | export interface ArtistDescIntroduction { 45 | ti: string 46 | txt: string 47 | } 48 | export interface ArtistDescTopicDataTopicContent { 49 | type: number 50 | id: number 51 | content: string 52 | } 53 | export interface ArtistDescTopicDataTopic { 54 | id: number 55 | addTime: number 56 | mainTitle: string 57 | title: string 58 | content: ArtistDescTopicDataTopicContent[] 59 | userId: number 60 | cover: number 61 | headPic: number 62 | shareContent: string 63 | wxTitle: string 64 | showComment: boolean 65 | status: number 66 | seriesId: number 67 | pubTime: number 68 | readCount: number 69 | tags: string[] 70 | pubImmidiatly: boolean 71 | auditor: string 72 | auditTime: number 73 | auditStatus: number 74 | startText: string 75 | delReason: string 76 | showRelated: boolean 77 | fromBackend: boolean 78 | rectanglePic: number 79 | updateTime: number 80 | reward: boolean 81 | summary: string 82 | memo?: any 83 | adInfo: string 84 | categoryId: number 85 | hotScore: number 86 | recomdTitle: string 87 | recomdContent: string 88 | number: number 89 | } 90 | export interface ArtistDescTopicDataCreatorExperts { 91 | 1: string 92 | } 93 | export interface ArtistDescTopicDataCreator { 94 | userId: number 95 | userType: number 96 | nickname: string 97 | avatarImgId: number 98 | avatarUrl: string 99 | backgroundImgId: number 100 | backgroundUrl: string 101 | signature: string 102 | createTime: number 103 | userName: string 104 | accountType: number 105 | shortUserName: string 106 | birthday: number 107 | authority: number 108 | gender: number 109 | accountStatus: number 110 | province: number 111 | city: number 112 | authStatus: number 113 | description?: any 114 | detailDescription?: any 115 | defaultAvatar: boolean 116 | expertTags?: any 117 | experts: ArtistDescTopicDataCreatorExperts 118 | djStatus: number 119 | locationStatus: number 120 | vipType: number 121 | followed: boolean 122 | mutual: boolean 123 | authenticated: boolean 124 | lastLoginTime: number 125 | lastLoginIP: string 126 | remarkName?: any 127 | viptypeVersion: number 128 | authenticationTypes: number 129 | avatarDetail?: any 130 | anchor: boolean 131 | } 132 | export interface ArtistDescTopicData { 133 | topic: ArtistDescTopicDataTopic 134 | creator: ArtistDescTopicDataCreator 135 | shareCount: number 136 | commentCount: number 137 | likedCount: number 138 | liked: boolean 139 | rewardCount: number 140 | rewardMoney: number 141 | relatedResource?: any 142 | rectanglePicUrl: string 143 | coverUrl: string 144 | categoryId: number 145 | categoryName: string 146 | reward: boolean 147 | shareContent: string 148 | wxTitle: string 149 | addTime: number 150 | seriesId: number 151 | showComment: boolean 152 | showRelated: boolean 153 | memo?: any 154 | summary: string 155 | recmdTitle: string 156 | recmdContent: string 157 | commentThreadId: string 158 | mainTitle: string 159 | tags: string[] 160 | readCount: number 161 | url: string 162 | title: string 163 | id: number 164 | number: number 165 | } 166 | -------------------------------------------------------------------------------- /src/models/banner.ts: -------------------------------------------------------------------------------- 1 | export interface Banner { 2 | pic: string 3 | targetId: number 4 | targetType: number 5 | typeTitle: string 6 | bannerId: number 7 | imageUrl: string 8 | } 9 | -------------------------------------------------------------------------------- /src/models/dj.ts: -------------------------------------------------------------------------------- 1 | export interface DJBanner { 2 | targetId: number 3 | targetType: number 4 | pic: string 5 | url: string 6 | typeTitle: string 7 | exclusive: boolean 8 | } 9 | 10 | export interface DjPersonalize { 11 | id: number 12 | dj: DjPersonalizeDj 13 | name: string 14 | picUrl: string 15 | desc: string 16 | subCount: number 17 | programCount: number 18 | createTime: number 19 | categoryId: number 20 | category: string 21 | secondCategoryId: number 22 | secondCategory: string 23 | radioFeeType: number 24 | feeScope: number 25 | buyed: boolean 26 | videos: any 27 | finished: boolean 28 | underShelf: boolean 29 | purchaseCount: number 30 | price: number 31 | originalPrice: number 32 | discountPrice: any 33 | lastProgramCreateTime: number 34 | lastProgramName: string 35 | lastProgramId: number 36 | picId: number 37 | rcmdText: string 38 | hightQuality: boolean 39 | whiteList: boolean 40 | liveInfo: any 41 | playCount: number 42 | icon: any 43 | privacy: boolean 44 | intervenePicUrl: string 45 | intervenePicId: number 46 | dynamic: boolean 47 | shortName: any 48 | taskId: 0 49 | manualTagsDTO: any 50 | scoreInfoDTO: any 51 | descPicList: any 52 | subed: boolean 53 | composeVideo: boolean 54 | rcmdtext: string 55 | lastUpdateProgramName: string 56 | alg: string 57 | } 58 | 59 | export interface DjPersonalizeDj { 60 | defaultAvatar: boolean 61 | province: number 62 | authStatus: number 63 | followed: boolean 64 | avatarUrl: string 65 | accountStatus: number 66 | gender: number 67 | city: number 68 | birthday: number 69 | userId: number 70 | userType: number 71 | nickname: string 72 | signature: string 73 | description: string 74 | detailDescription: string 75 | avatarImgId: number 76 | backgroundImgId: number 77 | backgroundUrl: string 78 | authority: number 79 | mutual: boolean 80 | expertTags: any 81 | experts: any 82 | djStatus: number 83 | vipType: number 84 | remarkName: any 85 | authenticationTypes: number 86 | avatarDetail: any 87 | backgroundImgIdStr: string 88 | avatarImgIdStr: string 89 | anchor: boolean 90 | avatarImgId_str: string 91 | } 92 | 93 | export interface DjTodayPerfered { 94 | id: number 95 | name: string 96 | rcmdText: string 97 | radioFeeType: number 98 | feeScope: number 99 | picUrl: string 100 | programCount: number 101 | subCount: number 102 | subed: boolean 103 | playCount: number 104 | alg: string 105 | originalPrice: any 106 | discountPrice: any 107 | lastProgramName: string 108 | traceId: any 109 | icon: any 110 | } 111 | 112 | export interface DjTodayPerfered { 113 | id: number 114 | name: string 115 | rcmdText: string 116 | radioFeeType: number 117 | feeScope: number 118 | picUrl: string 119 | programCount: number 120 | subCount: number 121 | subed: boolean 122 | playCount: number 123 | alg: string 124 | originalPrice: any 125 | discountPrice: any 126 | lastProgramName: string 127 | traceId: any 128 | icon: any 129 | } 130 | 131 | export interface DjNewcomer { 132 | id: number 133 | rank: number 134 | lastRank: number 135 | score: number 136 | nickName: string 137 | avatarUrl: string 138 | userType: number 139 | userFollowedCount: number 140 | mainAuthDesc: string 141 | liveStatus: number 142 | liveType: number 143 | liveId: number 144 | avatarDetail: DjNewcomerAvatarDetail 145 | roomNo: number 146 | } 147 | 148 | export interface DjNewcomerAvatarDetail { 149 | userType: number 150 | identityLevel: number 151 | identityIconUrl: string 152 | } 153 | 154 | export interface DjPay { 155 | id: number 156 | rank: number 157 | lastRank: number 158 | score: number 159 | name: string 160 | picUrl: string 161 | creatorName: string 162 | } 163 | -------------------------------------------------------------------------------- /src/models/mv.ts: -------------------------------------------------------------------------------- 1 | export interface MvUrl { 2 | id: number 3 | url: string 4 | r: number 5 | size: number 6 | md5: string 7 | code: number 8 | expi: number 9 | fee: number 10 | mvFee: number 11 | st: number 12 | promotionVo?: any 13 | msg: string 14 | } 15 | 16 | export interface MvDetail { 17 | id: number 18 | name: string 19 | artistId: number 20 | artistName: string 21 | briefDesc: string 22 | desc: string 23 | cover: string 24 | coverId_str: string 25 | coverId: number 26 | playCount: number 27 | subCount: number 28 | shareCount: number 29 | commentCount: number 30 | duration: number 31 | nType: number 32 | publishTime: string 33 | price: null 34 | brs: any[] 35 | artists: MvDetailArtists[] 36 | alias: any[] 37 | commentThreadId: string 38 | videoGroup: any[] 39 | } 40 | 41 | export interface MvDetailArtists { 42 | id: number 43 | name: string 44 | followed: boolean 45 | img1v1Url: string 46 | } 47 | 48 | export interface Mv { 49 | id: number 50 | cover: string 51 | name: string 52 | playCount: number 53 | briefDesc?: any 54 | desc?: any 55 | artistName: string 56 | artistId: number 57 | duration: number 58 | mark: number 59 | subed: boolean 60 | artists: MvArtists[] 61 | } 62 | 63 | export interface MvArtists { 64 | id: number 65 | name: string 66 | alias?: [] 67 | transNames?: null 68 | } 69 | 70 | export interface SimiMv { 71 | id: number 72 | cover: string 73 | name: string 74 | playCount: number 75 | briefDesc: string 76 | desc: null 77 | artistName: string 78 | artistId: number 79 | duration: number 80 | mark: number 81 | artists: MvArtists[] 82 | alg: string 83 | } 84 | -------------------------------------------------------------------------------- /src/models/personalized.ts: -------------------------------------------------------------------------------- 1 | export interface Personalized { 2 | id: number 3 | type: number 4 | name: string 5 | copywriter: string 6 | picUrl: string 7 | canDislike: boolean 8 | trackNumberUpdateTime: number 9 | playCount: number 10 | trackCount: number 11 | highQuality: boolean 12 | alg: string 13 | } 14 | 15 | export interface PersonalizedNewSong { 16 | id: number 17 | type: number 18 | name: string 19 | picUrl: string 20 | canDislike: boolean 21 | song: PNSSong 22 | alg: string 23 | } 24 | 25 | export interface PNSSongArtists { 26 | name: string 27 | id: number 28 | picId: number 29 | img1v1Id: number 30 | briefDesc: string 31 | picUrl: string 32 | img1v1Url: string 33 | albumSize: number 34 | alias: any[] 35 | trans: string 36 | musicSize: number 37 | topicPerson: number 38 | } 39 | 40 | export interface PNSSongAlbumArtist { 41 | name: string 42 | id: number 43 | picId: number 44 | img1v1Id: number 45 | briefDesc: string 46 | picUrl: string 47 | img1v1Url: string 48 | albumSize: number 49 | alias: any[] 50 | trans: string 51 | musicSize: number 52 | topicPerson: number 53 | } 54 | 55 | export interface PNSSongAlbumArtists { 56 | name: string 57 | id: number 58 | picId: number 59 | img1v1Id: number 60 | briefDesc: string 61 | picUrl: string 62 | img1v1Url: string 63 | albumSize: number 64 | alias: any[] 65 | trans: string 66 | musicSize: number 67 | topicPerson: number 68 | } 69 | 70 | export interface PNSSongAlbum { 71 | name: string 72 | id: number 73 | type: string 74 | size: number 75 | picId: number 76 | blurPicUrl: string 77 | companyId: number 78 | pic: number 79 | picUrl: string 80 | publishTime: number 81 | description: string 82 | tags: string 83 | company: string 84 | briefDesc: string 85 | artist: PNSSongAlbumArtist 86 | songs: any[] 87 | alias: any[] 88 | status: number 89 | copyrightId: number 90 | commentThreadId: string 91 | artists: PNSSongAlbumArtists[] 92 | subType: string 93 | onSale: boolean 94 | mark: number 95 | picId_str: string 96 | } 97 | 98 | export interface PNSSongBMusic { 99 | id: number 100 | size: number 101 | extension: string 102 | sr: number 103 | dfsId: number 104 | bitrate: number 105 | playTime: number 106 | volumeDelta: number 107 | } 108 | 109 | export interface PNSSongHMusic { 110 | id: number 111 | size: number 112 | extension: string 113 | sr: number 114 | dfsId: number 115 | bitrate: number 116 | playTime: number 117 | volumeDelta: number 118 | } 119 | 120 | export interface PNSSongMMusic { 121 | id: number 122 | size: number 123 | extension: string 124 | sr: number 125 | dfsId: number 126 | bitrate: number 127 | playTime: number 128 | volumeDelta: number 129 | } 130 | 131 | export interface PNSSongLMusic { 132 | id: number 133 | size: number 134 | extension: string 135 | sr: number 136 | dfsId: number 137 | bitrate: number 138 | playTime: number 139 | volumeDelta: number 140 | } 141 | 142 | export interface PNSSongPrivilegeFreeTrialPrivilege { 143 | resConsumable: boolean 144 | userConsumable: boolean 145 | } 146 | 147 | export interface PNSSongPrivilegeChargeInfoList { 148 | rate: number 149 | chargeType: number 150 | } 151 | 152 | export interface PNSSongPrivilege { 153 | id: number 154 | fee: number 155 | payed: number 156 | st: number 157 | pl: number 158 | dl: number 159 | sp: number 160 | cp: number 161 | subp: number 162 | cs: boolean 163 | maxbr: number 164 | fl: number 165 | toast: boolean 166 | flag: number 167 | preSell: boolean 168 | playMaxbr: number 169 | downloadMaxbr: number 170 | freeTrialPrivilege: PNSSongPrivilegeFreeTrialPrivilege 171 | chargeInfoList: PNSSongPrivilegeChargeInfoList[] 172 | } 173 | 174 | export interface PNSSong { 175 | name: string 176 | id: number 177 | position: number 178 | alias: any[] 179 | status: number 180 | fee: number 181 | copyrightId: number 182 | disc: string 183 | no: number 184 | artists: PNSSongArtists[] 185 | album: PNSSongAlbum 186 | starred: boolean 187 | popularity: number 188 | score: number 189 | starredNum: number 190 | duration: number 191 | playedNum: number 192 | dayPlays: number 193 | hearTime: number 194 | ringtone: string 195 | copyFrom: string 196 | commentThreadId: string 197 | ftype: number 198 | rtUrls: any[] 199 | copyright: number 200 | mark: number 201 | originCoverType: number 202 | single: number 203 | mvid: number 204 | bMusic: PNSSongBMusic 205 | rtype: number 206 | hMusic: PNSSongHMusic 207 | mMusic: PNSSongMMusic 208 | lMusic: PNSSongLMusic 209 | exclusive: boolean 210 | privilege: PNSSongPrivilege 211 | } 212 | 213 | export interface PersonalizedMv { 214 | id: number 215 | type: number 216 | name: string 217 | copywriter: string 218 | picUrl: string 219 | canDislike: boolean 220 | trackNumberUpdateTime?: any 221 | duration: number 222 | playCount: number 223 | subed: boolean 224 | artists: { 225 | id: number 226 | name: string 227 | }[] 228 | artistName: string 229 | artistId: number 230 | alg: string 231 | } 232 | export interface DjProgram { 233 | id: number 234 | type: number 235 | name: string 236 | copywriter: string 237 | picUrl: string 238 | canDislike: boolean 239 | trackNumberUpdateTime?: any 240 | } 241 | -------------------------------------------------------------------------------- /src/models/playlist.ts: -------------------------------------------------------------------------------- 1 | export interface PlayListDetail { 2 | id: number 3 | name: string 4 | coverImgId: number 5 | coverImgUrl: string 6 | coverImgId_str: string 7 | adType: number 8 | userId: number 9 | createTime: number 10 | status: number 11 | opRecommend: boolean 12 | highQuality: boolean 13 | newImported: boolean 14 | updateTime: number 15 | trackCount: number 16 | specialType: number 17 | privacy: number 18 | trackUpdateTime: number 19 | commentThreadId: string 20 | playCount: number 21 | trackNumberUpdateTime: number 22 | subscribedCount: number 23 | cloudTrackCount: number 24 | ordered: boolean 25 | description: string 26 | tags: string[] 27 | updateFrequency?: any 28 | backgroundCoverId: number 29 | backgroundCoverUrl?: any 30 | titleImage: number 31 | titleImageUrl?: any 32 | englishTitle?: any 33 | officialPlaylistType?: any 34 | subscribers: PlayListDetailSubscribers[] 35 | subscribed: boolean 36 | creator: PlayListDetailCreator 37 | tracks: PlayListDetailTracks[] 38 | videoIds?: any 39 | videos?: any 40 | trackIds: PlayListDetailTrackIds[] 41 | shareCount: number 42 | commentCount: number 43 | remixVideo?: any 44 | sharedUsers?: any 45 | historySharedUsers?: any 46 | } 47 | 48 | export interface PlayListDetailSubscribers { 49 | defaultAvatar: boolean 50 | province: number 51 | authStatus: number 52 | followed: boolean 53 | avatarUrl: string 54 | accountStatus: number 55 | gender: number 56 | city: number 57 | birthday: number 58 | userId: number 59 | userType: number 60 | nickname: string 61 | signature: string 62 | description: string 63 | detailDescription: string 64 | avatarImgId: number 65 | backgroundImgId: number 66 | backgroundUrl: string 67 | authority: number 68 | mutual: boolean 69 | expertTags?: any 70 | experts?: any 71 | djStatus: number 72 | vipType: number 73 | remarkName?: any 74 | authenticationTypes: number 75 | avatarDetail?: any 76 | avatarImgIdStr: string 77 | backgroundImgIdStr: string 78 | anchor: boolean 79 | avatarImgId_str: string 80 | } 81 | 82 | export interface PlayListDetailCreatorAvatarDetail { 83 | userType: number 84 | identityLevel: number 85 | identityIconUrl: string 86 | } 87 | 88 | export interface PlayListDetailCreator { 89 | defaultAvatar: boolean 90 | province: number 91 | authStatus: number 92 | followed: boolean 93 | avatarUrl: string 94 | accountStatus: number 95 | gender: number 96 | city: number 97 | birthday: number 98 | userId: number 99 | userType: number 100 | nickname: string 101 | signature: string 102 | description: string 103 | detailDescription: string 104 | avatarImgId: number 105 | backgroundImgId: number 106 | backgroundUrl: string 107 | authority: number 108 | mutual: boolean 109 | expertTags?: any 110 | experts?: any 111 | djStatus: number 112 | vipType: number 113 | remarkName?: any 114 | authenticationTypes: number 115 | avatarDetail: PlayListDetailCreatorAvatarDetail 116 | avatarImgIdStr: string 117 | backgroundImgIdStr: string 118 | anchor: boolean 119 | avatarImgId_str: string 120 | } 121 | 122 | export interface PlayListDetailTracksAr { 123 | id: number 124 | name: string 125 | tns: any[] 126 | alias: any[] 127 | } 128 | 129 | export interface PlayListDetailTracksAl { 130 | id: number 131 | name: string 132 | picUrl: string 133 | tns: any[] 134 | pic_str: string 135 | pic: number 136 | } 137 | 138 | export interface PlayListDetailTracksH { 139 | br: number 140 | fid: number 141 | size: number 142 | vd: number 143 | } 144 | 145 | export interface PlayListDetailTracksM { 146 | br: number 147 | fid: number 148 | size: number 149 | vd: number 150 | } 151 | 152 | export interface PlayListDetailTracksL { 153 | br: number 154 | fid: number 155 | size: number 156 | vd: number 157 | } 158 | 159 | export interface PlayListDetailTracks { 160 | name: string 161 | id: number 162 | pst: number 163 | t: number 164 | ar: PlayListDetailTracksAr[] 165 | alia: any[] 166 | pop: number 167 | st: number 168 | rt: string 169 | fee: number 170 | v: number 171 | crbt?: any 172 | cf: string 173 | al: PlayListDetailTracksAl 174 | dt: number 175 | h: PlayListDetailTracksH 176 | m: PlayListDetailTracksM 177 | l: PlayListDetailTracksL 178 | a?: any 179 | cd: string 180 | no: number 181 | rtUrl?: any 182 | ftype: number 183 | rtUrls: any[] 184 | djId: number 185 | copyright: number 186 | s_id: number 187 | mark: number 188 | originCoverType: number 189 | originSongSimpleData?: any 190 | single: number 191 | noCopyrightRcmd?: any 192 | cp: number 193 | mv: number 194 | rtype: number 195 | rurl?: any 196 | mst: number 197 | publishTime: number 198 | } 199 | 200 | export interface PlayListDetailTrackIds { 201 | id: number 202 | v: number 203 | t: number 204 | at: number 205 | alg?: any 206 | uid: number 207 | rcmdReason: string 208 | sc?: any 209 | } 210 | 211 | export interface PlaylistHighqualityTag { 212 | id: number 213 | name: string 214 | type: number 215 | category: number 216 | hot: boolean 217 | } 218 | -------------------------------------------------------------------------------- /src/models/playlist_cat.ts: -------------------------------------------------------------------------------- 1 | export interface PlayListCat { 2 | name: string 3 | resourceCount: number 4 | imgId: number 5 | imgUrl?: any 6 | type: number 7 | category: number 8 | resourceType: number 9 | hot: boolean 10 | activity: boolean 11 | } 12 | -------------------------------------------------------------------------------- /src/models/playlist_hot.ts: -------------------------------------------------------------------------------- 1 | export interface PlayListHot { 2 | playlistTag: PlayListHotPlaylistTag 3 | activity: boolean 4 | position: number 5 | category: number 6 | hot: boolean 7 | createTime: number 8 | usedCount: number 9 | name: string 10 | id: number 11 | type: number 12 | } 13 | 14 | export interface PlayListHotPlaylistTag { 15 | id: number 16 | name: string 17 | category: number 18 | usedCount: number 19 | type: number 20 | position: number 21 | createTime: number 22 | highQuality: number 23 | highQualityPos: number 24 | officialPos: number 25 | } 26 | -------------------------------------------------------------------------------- /src/models/search.ts: -------------------------------------------------------------------------------- 1 | export interface SearchHotDetail { 2 | searchWord: string 3 | score: number 4 | content: string 5 | source: number 6 | iconType: number 7 | iconUrl?: string 8 | url: string 9 | alg: string 10 | } 11 | 12 | export interface SearchSuggest { 13 | albums: SearchSuggestAlbums[] 14 | artists: SearchSuggestArtists[] 15 | songs: SearchSuggestSongs[] 16 | playlists: SearchSuggestPlaylists[] 17 | order: string[] 18 | } 19 | export interface SearchSuggestAlbumsArtist { 20 | id: number 21 | name: string 22 | picUrl: string 23 | alias: string[] 24 | albumSize: number 25 | picId: number 26 | img1v1Url: string 27 | img1v1: number 28 | alia: string[] 29 | trans?: any 30 | } 31 | export interface SearchSuggestAlbums { 32 | id: number 33 | name: string 34 | artist: SearchSuggestAlbumsArtist 35 | publishTime: number 36 | size: number 37 | copyrightId: number 38 | status: number 39 | picId: number 40 | mark: number 41 | } 42 | export interface SearchSuggestArtists { 43 | id: number 44 | name: string 45 | picUrl: string 46 | alias: string[] 47 | albumSize: number 48 | picId: number 49 | img1v1Url: string 50 | img1v1: number 51 | transNames: string[] 52 | alia: string[] 53 | trans: string 54 | } 55 | export interface SearchSuggestSongsArtists { 56 | id: number 57 | name: string 58 | picUrl?: any 59 | alias: any[] 60 | albumSize: number 61 | picId: number 62 | img1v1Url: string 63 | img1v1: number 64 | trans?: any 65 | } 66 | export interface SearchSuggestSongsAlbumArtist { 67 | id: number 68 | name: string 69 | picUrl?: any 70 | alias: any[] 71 | albumSize: number 72 | picId: number 73 | img1v1Url: string 74 | img1v1: number 75 | trans?: any 76 | } 77 | export interface SearchSuggestSongsAlbum { 78 | id: number 79 | name: string 80 | artist: SearchSuggestSongsAlbumArtist 81 | publishTime: number 82 | size: number 83 | copyrightId: number 84 | status: number 85 | picId: number 86 | mark: number 87 | } 88 | export interface SearchSuggestSongs { 89 | id: number 90 | name: string 91 | artists: SearchSuggestSongsArtists[] 92 | album: SearchSuggestSongsAlbum 93 | duration: number 94 | copyrightId: number 95 | status: number 96 | alias: any[] 97 | rtype: number 98 | ftype: number 99 | mvid: number 100 | fee: number 101 | rUrl?: any 102 | mark: number 103 | } 104 | export interface SearchSuggestPlaylists { 105 | id: number 106 | name: string 107 | coverImgUrl: string 108 | creator?: any 109 | subscribed: boolean 110 | trackCount: number 111 | userId: number 112 | playCount: number 113 | bookCount: number 114 | specialType: number 115 | officialTags?: any 116 | action?: any 117 | actionType?: any 118 | description?: any 119 | highQuality: boolean 120 | } 121 | -------------------------------------------------------------------------------- /src/models/song.ts: -------------------------------------------------------------------------------- 1 | export interface Song { 2 | name: string 3 | id: number 4 | pst: number 5 | t: number 6 | ar: SongAr[] 7 | alia: string[] 8 | pop: number 9 | st: number 10 | rt?: any 11 | fee: number 12 | v: number 13 | crbt?: any 14 | cf: string 15 | al: SongAl 16 | dt: number 17 | h: SongH 18 | m: SongM 19 | l: SongL 20 | a?: any 21 | cd: string 22 | no: number 23 | rtUrl?: any 24 | ftype: number 25 | rtUrls: any[] 26 | djId: number 27 | copyright: number 28 | s_id: number 29 | mark: number 30 | originCoverType: number 31 | originSongSimpleData?: any 32 | tagPicList?: any 33 | resourceState: boolean 34 | version: number 35 | songJumpInfo?: any 36 | entertainmentTags?: any 37 | single: number 38 | noCopyrightRcmd?: any 39 | rtype: number 40 | rurl?: any 41 | mst: number 42 | cp: number 43 | mv: number 44 | publishTime: number 45 | } 46 | 47 | export interface SongAr { 48 | id: number 49 | name: string 50 | tns: any[] 51 | alias: any[] 52 | } 53 | 54 | export interface SongAl { 55 | id: number 56 | name: string 57 | picUrl: string 58 | tns: any[] 59 | pic_str: string 60 | pic: number 61 | } 62 | 63 | export interface SongH { 64 | br: number 65 | fid: number 66 | size: number 67 | vd: number 68 | } 69 | 70 | export interface SongM { 71 | br: number 72 | fid: number 73 | size: number 74 | vd: number 75 | } 76 | 77 | export interface SongL { 78 | br: number 79 | fid: number 80 | size: number 81 | vd: number 82 | } 83 | -------------------------------------------------------------------------------- /src/models/song_url.ts: -------------------------------------------------------------------------------- 1 | export interface SongUrl { 2 | id: number 3 | url: string 4 | br: number 5 | size: number 6 | md5: string 7 | code: number 8 | expi: number 9 | type: string 10 | gain: number 11 | fee: number 12 | payed: number 13 | flag: number 14 | canExtend: boolean 15 | freeTrialPrivilege: RootObjectFreeTrialPrivilege 16 | freeTimeTrialPrivilege: RootObjectFreeTimeTrialPrivilege 17 | urlSource: number 18 | freeTrialInfo: { 19 | start: number 20 | end: number 21 | } 22 | } 23 | 24 | export interface RootObjectFreeTrialPrivilege { 25 | resConsumable: boolean 26 | userConsumable: boolean 27 | } 28 | 29 | export interface RootObjectFreeTimeTrialPrivilege { 30 | resConsumable: boolean 31 | userConsumable: boolean 32 | type: number 33 | remainTime: number 34 | } 35 | -------------------------------------------------------------------------------- /src/models/top_song.ts: -------------------------------------------------------------------------------- 1 | export interface TopSong { 2 | starred: boolean 3 | popularity: number 4 | starredNum: number 5 | playedNum: number 6 | dayPlays: number 7 | hearTime: number 8 | mp3Url: string 9 | rtUrls?: any 10 | audition: any 11 | ringtone: string 12 | disc: string 13 | no: number 14 | status: number 15 | crbt: any 16 | bMusic: any 17 | rtUrl: any 18 | alias: any[] 19 | artists: any[] 20 | copyFrom: string 21 | mMusic: any 22 | lMusic: any 23 | hMusic: any 24 | score: number 25 | copyrightId: number 26 | album: any 27 | commentThreadId: string 28 | fee: number 29 | mvid: number 30 | ftype: number 31 | rtype: number 32 | rurl: null 33 | position: number 34 | duration: number 35 | name: string 36 | id: number 37 | exclusive: boolean 38 | privilege: any 39 | } 40 | -------------------------------------------------------------------------------- /src/models/toplist_detail.ts: -------------------------------------------------------------------------------- 1 | export interface TopListDetail { 2 | subscribers: any[] 3 | subscribed?: any 4 | creator?: any 5 | artists?: any 6 | tracks: TopListDetailTracks[] 7 | updateFrequency: string 8 | backgroundCoverId: number 9 | backgroundCoverUrl?: any 10 | titleImage: number 11 | titleImageUrl?: any 12 | englishTitle?: any 13 | opRecommend: boolean 14 | recommendInfo?: any 15 | subscribedCount: number 16 | cloudTrackCount: number 17 | userId: number 18 | highQuality: boolean 19 | createTime: number 20 | specialType: number 21 | coverImgId: number 22 | newImported: boolean 23 | anonimous: boolean 24 | updateTime: number 25 | trackCount: number 26 | coverImgUrl: string 27 | commentThreadId: string 28 | trackUpdateTime: number 29 | totalDuration: number 30 | privacy: number 31 | playCount: number 32 | trackNumberUpdateTime: number 33 | adType: number 34 | description: string 35 | ordered: boolean 36 | tags: any[] 37 | status: number 38 | name: string 39 | id: number 40 | coverImgId_str: string 41 | ToplistType: string 42 | } 43 | export interface TopListDetailTracks { 44 | first: string 45 | second: string 46 | } 47 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | userId: number 3 | userName: string 4 | } 5 | 6 | export interface UserProfile { 7 | userId: number 8 | userType: number 9 | nickname: string 10 | avatarImgId: number 11 | avatarUrl: string 12 | backgroundImgId: number 13 | backgroundUrl: string 14 | signature?: any 15 | createTime: number 16 | userName: string 17 | accountType: number 18 | shortUserName: string 19 | birthday: number 20 | authority: number 21 | gender: number 22 | accountStatus: number 23 | province: number 24 | city: number 25 | authStatus: number 26 | description?: any 27 | detailDescription?: any 28 | defaultAvatar: boolean 29 | expertTags?: any 30 | experts?: any 31 | djStatus: number 32 | locationStatus: number 33 | vipType: number 34 | followed: boolean 35 | mutual: boolean 36 | authenticated: boolean 37 | lastLoginTime: number 38 | lastLoginIP: string 39 | remarkName?: any 40 | viptypeVersion: number 41 | authenticationTypes: number 42 | avatarDetail?: any 43 | anchor: boolean 44 | } 45 | -------------------------------------------------------------------------------- /src/models/video.ts: -------------------------------------------------------------------------------- 1 | export interface Video { 2 | type: number 3 | displayed: boolean 4 | alg: string 5 | extAlg?: any 6 | data: VideoData 7 | } 8 | export interface VideoDataResolutions { 9 | resolution: number 10 | size: number 11 | } 12 | export interface VideoDataCreator { 13 | defaultAvatar: boolean 14 | province: number 15 | authStatus: number 16 | followed: boolean 17 | avatarUrl: string 18 | accountStatus: number 19 | gender: number 20 | city: number 21 | birthday: number 22 | userId: number 23 | userType: number 24 | nickname: string 25 | signature: string 26 | description: string 27 | detailDescription: string 28 | avatarImgId: number 29 | backgroundImgId: number 30 | backgroundUrl: string 31 | authority: number 32 | mutual: boolean 33 | expertTags?: any 34 | experts?: any 35 | djStatus: number 36 | vipType: number 37 | remarkName?: any 38 | avatarImgIdStr: string 39 | backgroundImgIdStr: string 40 | } 41 | export interface VideoDataUrlInfo { 42 | id: string 43 | url: string 44 | size: number 45 | validityTime: number 46 | needPay: boolean 47 | payInfo?: any 48 | r: number 49 | } 50 | export interface VideoDataVideoGroup { 51 | id: number 52 | name: string 53 | alg?: any 54 | } 55 | export interface VideoDataRelateSongAr { 56 | id: number 57 | name: string 58 | tns: any[] 59 | alias: any[] 60 | } 61 | export interface VideoDataRelateSongAl { 62 | id: number 63 | name: string 64 | picUrl: string 65 | tns: any[] 66 | pic: number 67 | } 68 | export interface VideoDataRelateSongH { 69 | br: number 70 | fid: number 71 | size: number 72 | vd: number 73 | } 74 | export interface VideoDataRelateSongM { 75 | br: number 76 | fid: number 77 | size: number 78 | vd: number 79 | } 80 | export interface VideoDataRelateSongL { 81 | br: number 82 | fid: number 83 | size: number 84 | vd: number 85 | } 86 | export interface VideoDataRelateSongPrivilege { 87 | id: number 88 | fee: number 89 | payed: number 90 | st: number 91 | pl: number 92 | dl: number 93 | sp: number 94 | cp: number 95 | subp: number 96 | cs: boolean 97 | maxbr: number 98 | fl: number 99 | toast: boolean 100 | flag: number 101 | preSell: boolean 102 | } 103 | export interface VideoDataRelateSong { 104 | name: string 105 | id: number 106 | pst: number 107 | t: number 108 | ar: VideoDataRelateSongAr[] 109 | alia: any[] 110 | pop: number 111 | st: number 112 | rt: string 113 | fee: number 114 | v: number 115 | crbt?: any 116 | cf: string 117 | al: VideoDataRelateSongAl 118 | dt: number 119 | h: VideoDataRelateSongH 120 | m: VideoDataRelateSongM 121 | l: VideoDataRelateSongL 122 | a?: any 123 | cd: string 124 | no: number 125 | rtUrl?: any 126 | ftype: number 127 | rtUrls: any[] 128 | djId: number 129 | copyright: number 130 | s_id: number 131 | rtype: number 132 | rurl?: any 133 | mst: number 134 | cp: number 135 | mv: number 136 | publishTime: number 137 | privilege: VideoDataRelateSongPrivilege 138 | } 139 | export interface VideoData { 140 | alg: string 141 | scm: string 142 | threadId: string 143 | coverUrl: string 144 | height: number 145 | width: number 146 | title: string 147 | description?: any 148 | commentCount: number 149 | shareCount: number 150 | resolutions: VideoDataResolutions[] 151 | creator: VideoDataCreator 152 | urlInfo: VideoDataUrlInfo 153 | videoGroup: VideoDataVideoGroup[] 154 | previewUrl?: any 155 | previewDurationms: number 156 | hasRelatedGameAd: boolean 157 | markTypes: number[] 158 | relateSong: VideoDataRelateSong[] 159 | relatedInfo?: any 160 | videoUserLiveInfo?: any 161 | vid: string 162 | durationms: number 163 | playTime: number 164 | praisedCount: number 165 | praised: boolean 166 | subscribed: boolean 167 | liveData?: any 168 | } 169 | 170 | export interface PersonalizedPrivateContent { 171 | id: number 172 | url: string 173 | picUrl: string 174 | sPicUrl: string 175 | type: number 176 | copywriter: string 177 | name: string 178 | time: number 179 | } 180 | 181 | export interface VideoGroup { 182 | id: number 183 | name: string 184 | url?: any 185 | relatedVideoType?: any 186 | selectTab: boolean 187 | abExtInfo?: any 188 | } 189 | -------------------------------------------------------------------------------- /src/models/video_detail.ts: -------------------------------------------------------------------------------- 1 | export interface VideoDetail { 2 | vid: string 3 | creator: VideoDetailCreator 4 | coverUrl: string 5 | title: string 6 | description: string 7 | durationms: number 8 | threadId: string 9 | playTime: number 10 | praisedCount: number 11 | commentCount: number 12 | shareCount: number 13 | subscribeCount: number 14 | publishTime: number 15 | avatarUrl: string 16 | width: number 17 | height: number 18 | resolutions: VideoDetailResolutions[] 19 | videoGroup: VideoDetailVideoGroup[] 20 | hasRelatedGameAd: boolean 21 | advertisement: boolean 22 | authType: number 23 | markTypes: number[] 24 | videoUserLiveInfo: null 25 | } 26 | 27 | export interface VideoUrl { 28 | id: string 29 | url: string 30 | size: number 31 | validityTime: number 32 | needPay: boolean 33 | payInfo: null 34 | r: number 35 | } 36 | 37 | export interface SimiVideo { 38 | alg: string 39 | type: number 40 | title: string 41 | durationms: number 42 | creator: SimiVideoCreator[] 43 | playTime: number 44 | coverUrl: string 45 | vid: string 46 | aliaName: null 47 | transName: null 48 | markTypes: [] 49 | liveRoom: null 50 | } 51 | 52 | export interface SimiVideoCreator { 53 | userId: number 54 | userName: string 55 | } 56 | 57 | export interface VideoDetailCreator { 58 | authStatus: number 59 | followed: boolean 60 | accountStatus: number 61 | userId: number 62 | userType: number 63 | nickname: string 64 | avatarUrl: string 65 | expertTags: null 66 | experts?: any 67 | avatarDetail?: any 68 | } 69 | 70 | export interface VideoDetailResolutions { 71 | size: number 72 | resolution: number 73 | } 74 | 75 | export interface VideoDetailVideoGroup { 76 | id: number 77 | name: string 78 | alg: null 79 | } 80 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | component: () => import('@/views/Index.vue'), 8 | redirect: { name: 'discover' }, 9 | children: [ 10 | { 11 | path: 'discover', 12 | name: 'discover', 13 | component: () => import('@/views/discover/Index.vue'), 14 | meta: { 15 | menu: 'discover', 16 | keepAlive: true 17 | } 18 | }, 19 | { 20 | path: 'music', 21 | name: 'music', 22 | component: () => import('@/views/music/Index.vue'), 23 | redirect: { name: 'picked' }, 24 | meta: { 25 | menu: 'music' 26 | }, 27 | children: [ 28 | { 29 | path: 'picked', 30 | name: 'picked', 31 | component: () => import('@/views/music/picked/Picked.vue'), 32 | meta: { 33 | menu: 'music', 34 | title: '精选', 35 | keepAlive: true 36 | } 37 | }, 38 | { 39 | path: 'toplist', 40 | name: 'toplist', 41 | component: () => import('@/views/music/topList/TopList.vue'), 42 | meta: { 43 | menu: 'music', 44 | title: '排行榜', 45 | keepAlive: true 46 | } 47 | }, 48 | { 49 | path: 'artist', 50 | name: 'artist', 51 | component: () => import('@/views/music/artist/Artist.vue'), 52 | meta: { 53 | menu: 'music', 54 | title: '歌手', 55 | keepAlive: true 56 | } 57 | }, 58 | { 59 | path: 'category', 60 | name: 'category', 61 | component: () => import('@/views/music/category/Category.vue'), 62 | meta: { 63 | menu: 'music', 64 | title: '歌单', 65 | keepAlive: true 66 | } 67 | }, 68 | { 69 | path: 'topsong', 70 | name: 'topsong', 71 | component: () => import('@/views/music/topSong/TopSong.vue'), 72 | meta: { 73 | menu: 'music', 74 | title: '新歌速递', 75 | keepAlive: true 76 | } 77 | } 78 | ] 79 | }, 80 | { 81 | path: 'video', 82 | name: 'video', 83 | component: () => import('@/views/video/Index.vue'), 84 | redirect: { name: 'videoList' }, 85 | meta: { 86 | menu: 'video', 87 | title: '视频', 88 | keepAlive: true 89 | }, 90 | children: [ 91 | { 92 | path: 'list', 93 | name: 'videoList', 94 | component: () => import('@/views/video/videoList/Index.vue'), 95 | meta: { 96 | menu: 'video', 97 | title: '视频', 98 | keepAlive: true 99 | } 100 | }, 101 | { 102 | path: 'mv', 103 | name: 'mvList', 104 | component: () => import('@/views/mv/mvList/Index.vue'), 105 | meta: { 106 | menu: 'video', 107 | title: 'mv', 108 | keepAlive: true 109 | } 110 | } 111 | ] 112 | }, 113 | { 114 | path: 'dj', 115 | name: 'dj', 116 | component: () => import('@/views/dj/Index.vue'), 117 | meta: { 118 | menu: 'dj', 119 | title: '电台', 120 | keepAlive: true 121 | } 122 | }, 123 | { 124 | path: 'playlist', 125 | name: 'playlist', 126 | component: () => import('@/views/playlist/Index.vue') 127 | }, 128 | { 129 | path: 'artistDetail', 130 | name: 'artistDetail', 131 | component: () => import('@/views/artist/ArtistDetail.vue') 132 | }, 133 | { 134 | path: 'album', 135 | name: 'album', 136 | component: () => import('@/views/album/Index.vue') 137 | }, 138 | { 139 | path: 'mvDetail/:id?', 140 | name: 'mvDetail', 141 | component: () => import('@/views/mv/MvDetail/index.vue') 142 | }, 143 | { 144 | path: 'videoDetail/:id?', 145 | name: 'videoDetail', 146 | component: () => import('@/views/video/videoDetail/index.vue') 147 | } 148 | ] 149 | } 150 | ] 151 | 152 | const router = createRouter({ 153 | history: createWebHistory(), 154 | routes 155 | }) 156 | export default router 157 | -------------------------------------------------------------------------------- /src/stores/common.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import { useBanner } from '@/utils/api' 4 | import type { Banner } from '@/models/banner' 5 | 6 | export const useCommonStore = defineStore('common', () => { 7 | const banners = ref([]) 8 | 9 | const getBanners = async () => { 10 | if (banners.value.length) return 11 | banners.value = await useBanner() 12 | } 13 | 14 | return { 15 | banners, 16 | getBanners 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export default store 6 | -------------------------------------------------------------------------------- /src/stores/music.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import { usePersonalized, usePersonalizedNewSong, useTopListDetail } from '@/utils/api' 4 | import type { Personalized, PersonalizedNewSong } from '@/models/personalized' 5 | import type { TopListDetail } from '@/models/toplist_detail' 6 | 7 | export const useMusicStore = defineStore('music', () => { 8 | const topListDetailData = ref([]) 9 | const getTopListDetailData = async () => { 10 | if (topListDetailData.value.length) return 11 | topListDetailData.value = await useTopListDetail() 12 | } 13 | 14 | const personalized = ref([]) 15 | const getPersonalized = async () => { 16 | if (personalized.value.length) return 17 | personalized.value = await usePersonalized() 18 | } 19 | 20 | const personalizedNewSong = ref([]) 21 | const getPersonalizedNewSong = async () => { 22 | if (personalizedNewSong.value.length) return 23 | personalizedNewSong.value = await usePersonalizedNewSong() 24 | } 25 | 26 | return { 27 | topListDetailData, 28 | getTopListDetailData, 29 | 30 | personalized, 31 | getPersonalized, 32 | 33 | personalizedNewSong, 34 | getPersonalizedNewSong 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/stores/personalized.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import { usePersonalizedDjProgram } from '@/utils/api' 4 | import type { DjProgram } from '@/models/personalized' 5 | 6 | export const usePersonalizedStore = defineStore('personalized', () => { 7 | const djProgram = ref([]) 8 | const getDjProgram = async () => { 9 | if (djProgram.value.length) return 10 | djProgram.value = await usePersonalizedDjProgram() 11 | } 12 | 13 | return { 14 | djProgram, 15 | getDjProgram 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /src/stores/player.ts: -------------------------------------------------------------------------------- 1 | import { defineStore, storeToRefs } from 'pinia' 2 | import { useDetail, useSongUrl } from '@/utils/api' 3 | import { onMounted, onUnmounted, toRefs, watch } from 'vue' 4 | import type { Song } from '@/models/song' 5 | import type { SongUrl } from '@/models/song_url' 6 | 7 | const KEYS = { 8 | volume: 'PLAYER-VOLUME' 9 | } 10 | 11 | export const usePlayerStore = defineStore({ 12 | id: 'player', 13 | state: () => ({ 14 | audio: new Audio(), 15 | loopType: 0, //循环模式 0-单曲循环 1-列表循环 2-随机播放 16 | volume: localStorage.getItem(KEYS.volume)?.toInt() || 60, //音量 17 | playList: [] as Song[], //播放列表 18 | showPlayList: false, 19 | id: 0, 20 | url: '', 21 | songUrl: {} as SongUrl, 22 | song: {} as Song, 23 | isPlaying: false, //是否播放中 24 | isPause: true, //是否暂停 25 | sliderInput: false, //是否正在拖动进度条 26 | ended: false, //是否播放结束 27 | muted: false, //是否静音 28 | currentTime: 0, //当前播放时间 29 | duration: 0 //总播放时长 30 | }), 31 | getters: { 32 | playListCount: (state) => { 33 | return state.playList.length 34 | }, 35 | thisIndex: (state) => { 36 | return state.playList.findIndex((song) => song.id === state.id) 37 | }, 38 | nextSong(state): Song { 39 | const { thisIndex, playListCount } = this 40 | if (thisIndex === playListCount - 1) { 41 | return state.playList.first() 42 | } else { 43 | const nextIndex: number = thisIndex + 1 44 | return state.playList[nextIndex] 45 | } 46 | }, 47 | prevSong(state): Song { 48 | const { thisIndex } = this 49 | if (thisIndex === 0) { 50 | return state.playList.last() 51 | } else { 52 | const prevIndex: number = thisIndex - 1 53 | return state.playList[prevIndex] 54 | } 55 | } 56 | }, 57 | actions: { 58 | init() { 59 | this.audio.volume = this.volume / 100 60 | }, 61 | //播放列表里面添加音乐 62 | pushPlayList(replace: boolean, ...list: Song[]) { 63 | console.log('列表') 64 | 65 | if (replace) { 66 | this.playList = list 67 | return 68 | } 69 | list.forEach((song) => { 70 | if (this.playList.filter((s) => s.id == song.id).length <= 0) { 71 | this.playList.push(song) 72 | } 73 | }) 74 | }, 75 | clearPlayList() { 76 | this.songUrl = {} as SongUrl 77 | this.url = '' 78 | this.id = 0 79 | this.song = {} as Song 80 | this.isPlaying = false 81 | this.isPause = false 82 | this.sliderInput = false 83 | this.ended = false 84 | this.muted = false 85 | this.currentTime = 0 86 | this.playList = [] as Song[] 87 | this.showPlayList = false 88 | this.audio.load() 89 | setTimeout(() => { 90 | this.duration = 0 91 | }, 500) 92 | }, 93 | async play(id: number) { 94 | console.log(id) 95 | 96 | if (id == this.id) return 97 | this.isPlaying = false 98 | const data = await useSongUrl(id) 99 | this.audio.src = data.url 100 | this.audio 101 | .play() 102 | .then((res) => { 103 | this.isPlaying = true 104 | this.isPause = false 105 | this.songUrl = data 106 | this.url = data.url 107 | this.id = id 108 | this.songDetail() 109 | }) 110 | .catch((res) => { 111 | console.log(res) 112 | }) 113 | }, 114 | //播放结束 115 | playEnd() { 116 | console.log('播放结束') 117 | switch (this.loopType) { 118 | case 0: 119 | this.rePlay() 120 | break 121 | case 1: 122 | this.next() 123 | break 124 | case 2: 125 | this.randomPlay() 126 | break 127 | } 128 | }, 129 | async songDetail() { 130 | this.song = await useDetail(this.id) 131 | 132 | this.pushPlayList(false, this.song) 133 | }, 134 | //重新播放 135 | rePlay() { 136 | setTimeout(() => { 137 | this.currentTime = 0 138 | this.audio.play() 139 | }, 1500) 140 | }, 141 | //下一曲 142 | next() { 143 | if (this.loopType === 2) { 144 | this.randomPlay() 145 | } else { 146 | this.play(this.nextSong.id) 147 | } 148 | }, 149 | //上一曲 150 | prev() { 151 | this.play(this.prevSong.id) 152 | }, 153 | //随机播放 154 | randomPlay() { 155 | this.play(this.playList.sample().id) 156 | }, 157 | //播放、暂停 158 | togglePlay() { 159 | if (!this.song.id) return 160 | this.isPlaying = !this.isPlaying 161 | if (!this.isPlaying) { 162 | this.audio.pause() 163 | this.isPause = true 164 | } else { 165 | this.audio.play() 166 | this.isPause = false 167 | } 168 | }, 169 | setPlay() { 170 | if (!this.song.id) return 171 | this.isPlaying = true 172 | this.audio.play() 173 | this.isPause = false 174 | }, 175 | setPause() { 176 | if (!this.song.id) return 177 | this.isPlaying = false 178 | this.audio.pause() 179 | this.isPause = true 180 | }, 181 | //切换循环类型 182 | toggleLoop() { 183 | if (this.loopType == 2) { 184 | this.loopType = 0 185 | } else { 186 | this.loopType++ 187 | } 188 | }, 189 | //静音切换 190 | toggleMuted() { 191 | this.muted = !this.muted 192 | this.audio.muted = this.muted 193 | }, 194 | //音量设置 195 | setVolume(n: number) { 196 | n = n > 100 ? 100 : n 197 | n = n < 0 ? 0 : n 198 | this.volume = n 199 | this.audio.volume = n / 100 200 | localStorage.setItem('PLAYER-VOLUME', n.toString()) 201 | }, 202 | //修改播放时间 203 | onSliderChange(val: number) { 204 | this.currentTime = val 205 | this.sliderInput = false 206 | this.audio.currentTime = val 207 | }, 208 | //播放时间拖动中 209 | onSliderInput(val: number) { 210 | this.sliderInput = true 211 | }, 212 | //定时器 213 | interval() { 214 | if (this.isPlaying && !this.sliderInput) { 215 | this.currentTime = parseInt(this.audio.currentTime.toString()) 216 | this.duration = parseInt(this.audio.duration.toString()) 217 | this.ended = this.audio.ended 218 | } 219 | } 220 | } 221 | }) 222 | 223 | export const userPlayerInit = () => { 224 | let timer: NodeJS.Timer 225 | const { init, interval, playEnd } = usePlayerStore() 226 | 227 | const { ended } = storeToRefs(usePlayerStore()) 228 | 229 | //监听播放结束 230 | watch(ended, (ended) => { 231 | if (!ended) return 232 | playEnd() 233 | }) 234 | 235 | //启动定时器 236 | onMounted(() => { 237 | init() 238 | console.log('启动定时器') 239 | timer = setInterval(interval, 1000) 240 | }) 241 | //清除定时器 242 | onUnmounted(() => { 243 | console.log('清除定时器') 244 | clearInterval(timer) 245 | }) 246 | } 247 | -------------------------------------------------------------------------------- /src/stores/search.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useSearchSuggest } from '@/utils/api' 3 | import type { SearchSuggest } from '@/models/search' 4 | 5 | export const useSearchStore = defineStore('search', { 6 | state: () => { 7 | return { 8 | showSearchView: false, 9 | searchKeyword: '', 10 | suggestData: {} as SearchSuggest 11 | } 12 | }, 13 | getters: { 14 | showHot: (state) => { 15 | return state.searchKeyword == '' 16 | } 17 | }, 18 | actions: { 19 | async suggest() { 20 | this.suggestData = await useSearchSuggest(this.searchKeyword) 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { UserProfile } from '@/models/user' 3 | import { useLoginStatus } from '@/utils/api' 4 | 5 | export const useUserStore = defineStore({ 6 | id: 'user', // id必填,且需要唯一 7 | state: () => ({ 8 | showLogin: false, 9 | profile: {} as UserProfile 10 | }), 11 | getters: { 12 | isLogin: (state) => { 13 | return state.profile?.userId > 0 14 | } 15 | }, 16 | actions: { 17 | async checkLogin() { 18 | const cookie = localStorage.getItem('USER-COOKIE') ?? '' 19 | const { code, profile } = await useLoginStatus(cookie) 20 | if (code === 200) { 21 | this.profile = profile 22 | this.showLogin = false 23 | } 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /src/stores/video.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | import type { PersonalizedPrivateContent, Video, VideoGroup } from '@/models/video' 4 | import { usePersonalizedMv, usePersonalizedPrivateContentList, useVideoGroupList, useVideoTimelineRecommend } from '@/utils/api' 5 | import type { PersonalizedMv } from '@/models/personalized' 6 | 7 | export const useVideoStore = defineStore('video', () => { 8 | const videoTimelineRecommend = ref([]) 9 | const getVideoTimelineRecommend = async () => { 10 | if (videoTimelineRecommend.value.length) return 11 | videoTimelineRecommend.value = await useVideoTimelineRecommend() 12 | } 13 | 14 | const personalizedPrivateContent = ref([]) 15 | const getPersonalizedPrivateContent = async () => { 16 | if (personalizedPrivateContent.value.length) return 17 | personalizedPrivateContent.value = await usePersonalizedPrivateContentList(4) 18 | } 19 | 20 | const personalizedMv = ref([]) 21 | const getPersonalizedMv = async () => { 22 | if (personalizedMv.value.length) return 23 | personalizedMv.value = await usePersonalizedMv() 24 | } 25 | 26 | const videoGroup = ref([]) 27 | const getVideoGroup = async () => { 28 | if (videoGroup.value.length) return 29 | videoGroup.value = await useVideoGroupList() 30 | } 31 | 32 | return { 33 | videoTimelineRecommend, 34 | getVideoTimelineRecommend, 35 | 36 | personalizedPrivateContent, 37 | getPersonalizedPrivateContent, 38 | 39 | personalizedMv, 40 | getPersonalizedMv, 41 | 42 | videoGroup, 43 | getVideoGroup 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | import type { Banner } from '@/models/banner' 4 | import type { DjProgram, Personalized, PersonalizedMv, PersonalizedNewSong } from '@/models/personalized' 5 | import type { PlayListDetail, PlaylistHighqualityTag } from '@/models/playlist' 6 | import type { PlayListCat } from '@/models/playlist_cat' 7 | import type { Song } from '@/models/song' 8 | import type { SongUrl } from '@/models/song_url' 9 | import type { TopListDetail } from '@/models/toplist_detail' 10 | import type { Artist, Mvs } from '@/models/artist' 11 | import type { ArtistDesc, ArtistDetail } from '@/models/artist_detail' 12 | import type { Album } from '@/models/album' 13 | import type { PersonalizedPrivateContent, Video, VideoGroup } from '@/models/video' 14 | import type { VideoDetail, VideoUrl, SimiVideo } from '@/models/video_detail' 15 | import type { SearchHotDetail, SearchSuggest } from '@/models/search' 16 | import type { MvUrl, Mv, MvDetail, SimiMv } from '@/models/mv' 17 | import type { PlayListHot } from '@/models/playlist_hot' 18 | import type { UserProfile } from '@/models/user' 19 | import type { TopSong } from '@/models/top_song' 20 | import type { DjPersonalize, DjTodayPerfered, DjNewcomer, DjPay } from '@/models/dj' 21 | 22 | export async function useLogin(phone: string, password: string) { 23 | return await http.get<{ 24 | code: number 25 | cookie: string 26 | token: string 27 | }>('login/cellphone', { phone: phone, password: password }) 28 | } 29 | 30 | export async function useLoginStatus(cookie: string) { 31 | const { data } = await http.get<{ 32 | data: { 33 | code: number 34 | profile: UserProfile 35 | } 36 | }>('login/status', { cookie }) 37 | 38 | return data 39 | } 40 | 41 | export async function useSongUrl(id: number) { 42 | const { data } = await http.get<{ data: SongUrl[] }>('song/url', { id: id }) 43 | return data.first() 44 | } 45 | 46 | export async function useDetail(id: number) { 47 | const { songs } = await http.get<{ songs: Song[] }>('song/detail', { 48 | ids: id 49 | }) 50 | return songs.first() 51 | } 52 | 53 | export async function useBanner() { 54 | const { banners } = await http.get<{ banners: Banner[] }>('banner', { 55 | type: 0 56 | }) 57 | console.log(3) 58 | return banners 59 | } 60 | 61 | export async function usePersonalized() { 62 | const { result } = await http.get<{ result: Personalized[] }>('personalized') 63 | return result 64 | } 65 | 66 | export async function usePersonalizedNewSong() { 67 | const { result } = await http.get<{ result: PersonalizedNewSong[] }>('personalized/newsong') 68 | return result 69 | } 70 | 71 | export async function usePlayListDetail(id: number, s = 8) { 72 | const { playlist } = await http.get<{ playlist: PlayListDetail }>('playlist/detail', { id: id, s: s }) 73 | return playlist 74 | } 75 | 76 | export async function usePlayListTrackAll(id: number) { 77 | const { songs } = await http.get<{ songs: Song[] }>('playlist/track/all', { 78 | id: id 79 | }) 80 | return songs 81 | } 82 | 83 | export async function useTopListDetail() { 84 | const { list } = await http.get<{ list: TopListDetail[] }>('toplist/detail') 85 | return list 86 | } 87 | 88 | export async function usePlayListCatList() { 89 | const { sub, categories } = await http.get<{ 90 | sub: PlayListCat[] 91 | categories: string[] 92 | }>('playlist/catlist') 93 | 94 | return { sub, categories } 95 | } 96 | 97 | export async function userArtistList(pageData: { type: number; area: number; initial: string; page: number; limit: number }) { 98 | const res = await http.get<{ artists: Artist[] }>('artist/list', { 99 | type: pageData.type, 100 | area: pageData.area, 101 | initial: pageData.initial, 102 | limit: pageData.limit, 103 | offset: (pageData.page - 1) * pageData.limit 104 | }) 105 | 106 | return res.artists 107 | } 108 | 109 | export async function useArtistDetail(id: number) { 110 | const { data } = await http.get<{ data: ArtistDetail }>('artist/detail', { 111 | id: id 112 | }) 113 | return data 114 | } 115 | 116 | export async function useArtistDesc(id: number) { 117 | return await http.get('artist/desc', { id: id }) 118 | } 119 | 120 | export async function useArtistSongs(id: number, order = 'time', limit = 10, offset = 0) { 121 | return await http.get<{ songs: Song[]; more: boolean }>('artist/songs', { 122 | id: id, 123 | order: order, 124 | limit: limit, 125 | offset: offset 126 | }) 127 | } 128 | 129 | export async function useArtistAlbum(id: number, limit = 10, offset = 0) { 130 | return await http.get<{ hotAlbums: Album[]; more: boolean }>('artist/album', { 131 | id: id, 132 | limit: limit, 133 | offset: offset 134 | }) 135 | } 136 | 137 | export async function useArtistMv(id: number, limit = 10, offset = 0) { 138 | return await http.get<{ mvs: Mvs[]; hasMore: boolean }>('artist/mv', { 139 | id: id, 140 | limit: limit, 141 | offset: offset 142 | }) 143 | } 144 | 145 | export async function useVideoTimelineRecommend(offset = 0) { 146 | const { datas } = await http.get<{ datas: Video[] }>('video/timeline/recommend', { offset: offset }) 147 | return datas 148 | } 149 | 150 | export async function usePersonalizedPrivateContentList(limit = 10, offset = 0) { 151 | const { result } = await http.get<{ result: PersonalizedPrivateContent[] }>('personalized/privatecontent/list', { 152 | limit: limit, 153 | offset: offset 154 | }) 155 | return result 156 | } 157 | 158 | export async function usePersonalizedMv() { 159 | const { result } = await http.get<{ result: PersonalizedMv[] }>('personalized/mv') 160 | return result 161 | } 162 | 163 | export async function usePersonalizedDjProgram() { 164 | const { result } = await http.get<{ result: DjProgram[] }>('personalized/djprogram') 165 | return result 166 | } 167 | 168 | export async function useVideoGroupList() { 169 | const { data } = await http.get<{ data: VideoGroup[] }>('video/group/list') 170 | return data 171 | } 172 | 173 | export async function useVideoGroup(id?: number, offset?: number) { 174 | const { datas, hasmore } = await http.get<{ datas: Video[]; hasmore: boolean }>(id ? 'video/group' : 'video/timeline/all', { 175 | id: id, 176 | offset: offset 177 | }) 178 | return { datas, hasmore } 179 | } 180 | 181 | export async function useAlbum(id: number) { 182 | const { album, songs } = await http.get<{ album: Album; songs: Song[] }>('album', { id: id }) 183 | 184 | return { album, songs } 185 | } 186 | 187 | export async function useSearchHotDetail() { 188 | const { data } = await http.get<{ data: SearchHotDetail[] }>('search/hot/detail') 189 | return data 190 | } 191 | 192 | export async function useSearchSuggest(keywords: string) { 193 | const { result } = await http.get<{ result: SearchSuggest }>('search/suggest', { keywords: keywords }) 194 | return result 195 | } 196 | 197 | export async function useMvUrl(id: number) { 198 | const { data } = await http.get<{ data: MvUrl }>('mv/url', { id: id }) 199 | return data 200 | } 201 | 202 | export async function usePlaylistHighqualityTags() { 203 | const { tags } = await http.get<{ tags: PlaylistHighqualityTag[] }>('playlist/highquality/tags') 204 | 205 | return tags 206 | } 207 | 208 | export async function usePlaylistHot() { 209 | const { tags } = await http.get<{ tags: PlayListHot[] }>('playlist/hot') 210 | 211 | return tags 212 | } 213 | 214 | export async function useTopPlaylistHighquality(params?: { limit?: number; before?: number; cat: string }) { 215 | return await http.get<{ 216 | playlists: PlayListDetail[] 217 | total: number 218 | more: boolean 219 | lasttime: number 220 | }>('top/playlist/highquality', params) 221 | } 222 | 223 | export async function useTopSong(type: number) { 224 | const { data } = await http.get<{ data: TopSong[] }>('top/song', { type }) 225 | return data 226 | } 227 | 228 | // 二维码 key 生成 229 | export async function useLoginQrKey() { 230 | const { data } = await http.get<{ 231 | data: { 232 | code: number 233 | unikey: string 234 | } 235 | }>('login/qr/key') 236 | return data.unikey 237 | } 238 | 239 | // 二维码 生成 240 | export async function useLoginQrCreate(key: string) { 241 | const { data } = await http.get<{ 242 | data: { 243 | qrimg: string 244 | qrurl: string 245 | } 246 | }>('login/qr/create', { key, qrimg: true }) 247 | return data.qrimg 248 | } 249 | 250 | // 二维码 检测扫码状态 251 | export async function usecheckStatus(key: string) { 252 | const { code, cookie } = await http.get<{ 253 | code: number 254 | cookie: string 255 | message: string 256 | }>('login/qr/check', { key }) 257 | return { code, cookie } 258 | } 259 | 260 | // 全部mv 261 | export async function useAllMv(area?: string, type?: string, order?: string, limit = 8, offset = 0) { 262 | return await http.get<{ data: Mv[]; hasMore: boolean }>('mv/all', { 263 | area, 264 | type, 265 | order, 266 | limit, 267 | offset 268 | }) 269 | } 270 | 271 | // 最新mv 272 | export async function useNewMv(area: string, limit = 10) { 273 | return await http.get<{ data: Mv[] }>('mv/first', { 274 | area, 275 | limit 276 | }) 277 | } 278 | 279 | // 网易出品mv 280 | export async function useExclusiveMv(limit = 10, offset = 0) { 281 | return await http.get<{ data: Mv[] }>('mv/exclusive/rcmd', { 282 | offset, 283 | limit 284 | }) 285 | } 286 | 287 | // mv详情 288 | export async function useMvDetail(mvid: number) { 289 | const { data } = await http.get<{ data: MvDetail }>('mv/detail', { mvid }) 290 | return data 291 | } 292 | 293 | // 相似mv 294 | export async function useSimiMv(mvid: number) { 295 | const { mvs } = await http.get<{ mvs: SimiMv[] }>('simi/mv', { mvid }) 296 | return mvs 297 | } 298 | 299 | // video详情 300 | export async function useVideoDetail(id: string) { 301 | const { data } = await http.get<{ data: VideoDetail }>('video/detail', { id }) 302 | return data 303 | } 304 | 305 | // video播放地址 306 | export async function useVideoUrl(id: string) { 307 | const { urls } = await http.get<{ urls: VideoUrl[] }>('video/url', { id }) 308 | return urls[0] 309 | } 310 | 311 | // 相似video 312 | export async function useSimiVideo(id: string) { 313 | const { data } = await http.get<{ data: SimiVideo[] }>('related/allvideo', { id }) 314 | return data 315 | } 316 | 317 | // dj个性推荐 318 | export async function useDjPersonalize() { 319 | const { data } = await http.get<{ data: DjPersonalize[] }>('dj/personalize/recommend') 320 | return data 321 | } 322 | 323 | // dj今日优选 324 | export async function useDjTodayPerfered() { 325 | const { data } = await http.get<{ data: DjTodayPerfered[] }>('dj/today/perfered') 326 | return data 327 | } 328 | 329 | // dj主播新人榜 330 | export async function useDjNewcomer() { 331 | const { data } = await http.get<{ 332 | data: { 333 | list: DjNewcomer[] 334 | } 335 | }>('dj/toplist/newcomer?limit=10') 336 | return data.list 337 | } 338 | 339 | // dj最热主播榜 340 | export async function useDjPopular() { 341 | const { data } = await http.get<{ 342 | data: { 343 | list: DjNewcomer[] 344 | } 345 | }>('dj/toplist/popular?limit=10') 346 | return data.list 347 | } 348 | 349 | // dj付费精品 350 | export async function useDjPay() { 351 | const { data } = await http.get<{ 352 | data: { 353 | list: DjPay[] 354 | } 355 | }>('dj/toplist/pay?limit=10') 356 | return data.list 357 | } 358 | -------------------------------------------------------------------------------- /src/utils/extend.ts: -------------------------------------------------------------------------------- 1 | import { first, last, sampleSize, sample, chunk, trimEnd } from 'lodash' 2 | import dayjs from 'dayjs' 3 | import { useNumberFormat } from '@/utils/number' 4 | 5 | declare global { 6 | interface Array { 7 | /** 8 | * 获取数组第一个元素 9 | */ 10 | first(this: T[]): T 11 | 12 | last(this: T[]): T 13 | sample(this: T[]): T 14 | 15 | /** 16 | * 获得 n 个随机元素 17 | * @param size 18 | */ 19 | sampleSize(this: T[], size: number): T[] 20 | 21 | /** 22 | * 将数组(array)拆分成多个 size 长度的区块 23 | * @param size 24 | */ 25 | chunk(this: T[], size: number): T[][] 26 | } 27 | 28 | interface String { 29 | /** 30 | * 转换成int类型 31 | */ 32 | toInt(this: string): number 33 | 34 | trimEnd(this: string, chars?: string): string 35 | } 36 | 37 | interface Number { 38 | toDate(this: number, format?: string): string 39 | 40 | numberFormat(this: number): string | number 41 | } 42 | } 43 | Array.prototype.first = function (this: T[]): T { 44 | return first(this) as T 45 | } 46 | Array.prototype.last = function (this: T[]): T { 47 | return last(this) as T 48 | } 49 | Array.prototype.sample = function (this: T[]): T { 50 | return sample(this) as T 51 | } 52 | Array.prototype.sampleSize = function (this: T[], size: number): T[] { 53 | return sampleSize(this, size) as T[] 54 | } 55 | Array.prototype.chunk = function (this: T[], size: number): T[][] { 56 | return chunk(this, size) as T[][] 57 | } 58 | String.prototype.toInt = function (this: string): number { 59 | return parseInt(this) 60 | } 61 | 62 | String.prototype.trimEnd = function (this: string, chars = ' '): string { 63 | return trimEnd(this, chars as string) 64 | } 65 | 66 | Number.prototype.toDate = function (this: number, format = 'YYYY-MM-DD'): string { 67 | return dayjs(this).format(format) 68 | } 69 | 70 | Number.prototype.numberFormat = function (this: number): string | number { 71 | return useNumberFormat(this) 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosRequestConfig } from 'axios' 2 | 3 | // axios.defaults.baseURL = localStorage.getItem('BASE_URL')?.toString() 4 | axios.defaults.baseURL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://netease-cloud-music-api-eight-teal.vercel.app' 5 | axios.defaults.timeout = 20 * 1000 6 | axios.defaults.maxBodyLength = 5 * 1024 * 1024 7 | axios.defaults.withCredentials = true 8 | 9 | // 请求拦截 10 | axios.interceptors.request.use( 11 | (config: AxiosRequestConfig | any) => { 12 | config.params = { 13 | ...config.params, 14 | t: Date.now() 15 | } 16 | return config 17 | }, 18 | (error) => { 19 | return Promise.reject(error) 20 | } 21 | ) 22 | 23 | // 响应拦截 24 | axios.interceptors.response.use( 25 | (response) => { 26 | return response 27 | }, 28 | (error) => { 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | interface Http { 34 | get(url: string, params?: unknown): Promise 35 | 36 | post(url: string, params?: unknown): Promise 37 | 38 | upload(url: string, params: unknown): Promise 39 | 40 | put(url: string, params: unknown): Promise 41 | 42 | delete(url: string, params: unknown): Promise 43 | 44 | download(url: string): void 45 | } 46 | 47 | const http: Http = { 48 | get(url, params) { 49 | return new Promise((resolve, reject) => { 50 | axios 51 | .get(url, { params }) 52 | .then((res) => { 53 | resolve(res.data) 54 | }) 55 | .catch((err) => { 56 | reject(err.data) 57 | }) 58 | }) 59 | }, 60 | 61 | post(url, params) { 62 | return new Promise((resolve, reject) => { 63 | axios 64 | .post(url, JSON.stringify(params)) 65 | .then((res) => { 66 | resolve(res.data) 67 | }) 68 | .catch((err) => { 69 | reject(err.data) 70 | }) 71 | }) 72 | }, 73 | 74 | put(url, params) { 75 | return new Promise((resolve, reject) => { 76 | axios 77 | .put(url, JSON.stringify(params)) 78 | .then((res) => { 79 | resolve(res.data) 80 | }) 81 | .catch((err) => { 82 | reject(err.data) 83 | }) 84 | }) 85 | }, 86 | 87 | delete(url, params) { 88 | return new Promise((resolve, reject) => { 89 | axios 90 | .delete(url, { params }) 91 | .then((res) => { 92 | resolve(res.data) 93 | }) 94 | .catch((err) => { 95 | reject(err.data) 96 | }) 97 | }) 98 | }, 99 | 100 | upload(url, file) { 101 | return new Promise((resolve, reject) => { 102 | axios 103 | .post(url, file, { 104 | headers: { 'Content-Type': 'multipart/form-data' } 105 | }) 106 | .then((res) => { 107 | resolve(res.data) 108 | }) 109 | .catch((err) => { 110 | reject(err.data) 111 | }) 112 | }) 113 | }, 114 | 115 | download(url) { 116 | const iframe = document.createElement('iframe') 117 | iframe.style.display = 'none' 118 | iframe.src = url 119 | iframe.onload = () => { 120 | document.body.removeChild(iframe) 121 | } 122 | 123 | document.body.appendChild(iframe) 124 | } 125 | } 126 | 127 | export default http 128 | -------------------------------------------------------------------------------- /src/utils/number.ts: -------------------------------------------------------------------------------- 1 | export function useNumberFormat(number: number): string | number { 2 | if (number > 100000000) { 3 | return Number((number / 100000000).toFixed(1)) + ' 亿' 4 | } 5 | 6 | if (number > 10000000) { 7 | return Number((number / 10000000).toFixed(1)) + ' 千万' 8 | } 9 | 10 | if (number > 10000) { 11 | return Number((number / 10000).toFixed(1)) + ' 万' 12 | } 13 | 14 | return number 15 | } 16 | 17 | export function useFormatDuring(during: number) { 18 | const s = Math.floor(during) % 60 19 | during = Math.floor(during / 60) 20 | const i = during % 60 21 | 22 | const ii = i < 10 ? `0${i}` : i 23 | const ss = s < 10 ? `0${s}` : s 24 | 25 | return ii + ':' + ss 26 | } 27 | -------------------------------------------------------------------------------- /src/views/Index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | -------------------------------------------------------------------------------- /src/views/album/Desc.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /src/views/album/Index.vue: -------------------------------------------------------------------------------- 1 | 31 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/album/Info.vue: -------------------------------------------------------------------------------- 1 | 16 | 52 | -------------------------------------------------------------------------------- /src/views/artist/Album.vue: -------------------------------------------------------------------------------- 1 | 49 | 61 | -------------------------------------------------------------------------------- /src/views/artist/ArtistDetail.vue: -------------------------------------------------------------------------------- 1 | 21 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/views/artist/Desc.vue: -------------------------------------------------------------------------------- 1 | 18 | 28 | -------------------------------------------------------------------------------- /src/views/artist/Info.vue: -------------------------------------------------------------------------------- 1 | 10 | 37 | -------------------------------------------------------------------------------- /src/views/artist/Mv.vue: -------------------------------------------------------------------------------- 1 | 48 | 59 | -------------------------------------------------------------------------------- /src/views/artist/Songs.vue: -------------------------------------------------------------------------------- 1 | 64 | 84 | -------------------------------------------------------------------------------- /src/views/discover/DjProgram.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 |