├── .eslintignore ├── .vscode ├── extensions.json └── settings.json ├── public └── favicon.ico ├── src ├── images │ ├── pause.png │ ├── audio-bg.png │ ├── needle.png │ ├── refresh.png │ └── audio-bg-light.png ├── style │ ├── global.less │ └── reset.less ├── components │ ├── LoadingTag │ │ ├── LoadingTag.less │ │ └── LoadingTag.jsx │ ├── AudioDeal.vue │ ├── RefreshCard.vue │ └── MusicCard.vue ├── config │ └── config.ts ├── api │ └── service.ts ├── model │ └── HomeModel.ts ├── main.ts ├── env.d.ts ├── router │ └── index.ts ├── stores │ └── index.ts ├── App.vue ├── views │ ├── hooks │ │ └── HomeHooks.ts │ └── Home.vue └── utils │ └── request.ts ├── .prettierignore ├── .husky └── pre-commit ├── .gitignore ├── jest.conf.js ├── index.html ├── .prettierrc.js ├── tsconfig.json ├── postcss.config.js ├── .eslintrc.js ├── vite.config.ts ├── tests └── units │ └── MusicCard.spec.ts ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | # eslint 忽略检查 (根据项目需要自行添加) 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/src/images/pause.png -------------------------------------------------------------------------------- /src/images/audio-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/src/images/audio-bg.png -------------------------------------------------------------------------------- /src/images/needle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/src/images/needle.png -------------------------------------------------------------------------------- /src/images/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/src/images/refresh.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compile-hero.disable-compile-files-on-did-save-code": true 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 忽略格式化 (根据项目需要自行添加) 2 | dist/ 3 | node_modules 4 | *.log 5 | run 6 | logs/ 7 | coverage/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /src/images/audio-bg-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingfront/freely/HEAD/src/images/audio-bg-light.png -------------------------------------------------------------------------------- /src/style/global.less: -------------------------------------------------------------------------------- 1 | @theme-color: #ff976a; 2 | 3 | body { 4 | color: #323233; 5 | font-size: 28px; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | package-lock.json 7 | yarn.lock 8 | yarn-error.log 9 | coverage -------------------------------------------------------------------------------- /src/components/LoadingTag/LoadingTag.less: -------------------------------------------------------------------------------- 1 | .loading-tag { 2 | width: 100%; 3 | top: 20vh; 4 | position: fixed; 5 | } 6 | .van-loading { 7 | color: @theme-color; 8 | } 9 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | // 接口api地址常量 2 | export const URL = { 3 | musicUrl: 'https://api.uomg.com/api' 4 | } 5 | // 全局常量 6 | export const CT = { 7 | timeout: 10000 8 | } 9 | -------------------------------------------------------------------------------- /src/api/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { URL } from '@/config/config' 3 | 4 | // 获取随机音乐信息 5 | export const fetchRandMusic = () => { 6 | return request.get(`${URL.musicUrl}/rand.music?sort=热歌榜&format=json`) 7 | } 8 | -------------------------------------------------------------------------------- /src/model/HomeModel.ts: -------------------------------------------------------------------------------- 1 | // music对象约束 2 | export interface MusicModel { 3 | name?: string 4 | artistsname?: string 5 | picurl?: string 6 | url?: string 7 | } 8 | 9 | // 首页hooks对象约束 10 | export interface HomeHooksModel { 11 | loading: boolean 12 | noData: boolean 13 | musicData: MusicModel 14 | } 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router/index' 4 | import { createPinia } from 'pinia' 5 | import { Button, Loading, Empty } from 'vant' 6 | 7 | const app = createApp(App) 8 | app.use(createPinia()).use(router) 9 | app.use(Button).use(Loading).use(Empty) 10 | app.mount('#app') 11 | -------------------------------------------------------------------------------- /jest.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | globals: {}, 4 | testEnvironment: 'jsdom', 5 | transform: { 6 | '^.+\\.vue$': '@vue/vue3-jest', 7 | '^.+\\js$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'] 13 | } 14 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | // 不关心外部库的类型,并且希望将所有没有类型的库导入为any 11 | declare module '*.jsx' 12 | -------------------------------------------------------------------------------- /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/Home.vue') 8 | } 9 | ] 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(), 13 | routes 14 | }) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const refreshStore = defineStore('refresh', { 4 | state: () => { 5 | return { refreshNum: 0 } 6 | }, 7 | getters: { 8 | queryRefresh(): number { 9 | return this.refreshNum 10 | } 11 | }, 12 | actions: { 13 | upRefresh(st: number) { 14 | this.refreshNum = st 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | freely~总有你爱听的 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, //一行的字符数,如果超过会进行换行,默认为80 3 | tabWidth: 2, //一个tab代表几个空格数,默认为80 4 | useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减 5 | singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号 6 | semi: false, //行位是否使用分号,默认为true 7 | trailingComma: 'none', //是否使用尾逗号,有三个可选值"" 8 | bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar } 9 | jsxSingleQuote: true, // jsx语法中使用单引号 10 | endOfLine: 'auto', 11 | 'prettier.spaceBeforeFunctionParen': true 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "jest"], 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*.ts", "tests/**/*.*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/LoadingTag/LoadingTag.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | add by xx 3 | 二次封装加载圈 JSX 4 | */ 5 | import { defineComponent } from 'vue' 6 | import './LoadingTag.less' 7 | 8 | const LoadingTag = defineComponent({ 9 | props: { 10 | color: { 11 | type: String, 12 | default: '' 13 | }, 14 | type: { 15 | type: String, 16 | default: '' 17 | }, 18 | text: { 19 | type: String, 20 | default: '' 21 | } 22 | }, 23 | setup(props) { 24 | return () => ( 25 |
26 | 27 | {props.text} 28 | 29 |
30 | ) 31 | } 32 | }) 33 | export default LoadingTag 34 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /src/views/hooks/HomeHooks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | add by xx 3 | 首页hooks封装,也就是响应式代码块 4 | */ 5 | 6 | import { reactive, toRefs } from 'vue' 7 | import { fetchRandMusic } from '@/api/service' 8 | import { HomeHooksModel } from '@/model/HomeModel' 9 | 10 | // 首页hooks模块 11 | export const HomeHooks = () => { 12 | // 响应值定义 13 | const indexRec = reactive({ 14 | loading: true, 15 | noData: false, 16 | musicData: {} 17 | }) 18 | 19 | // 查询随机音乐信息 20 | const fetchMusicInfo = async () => { 21 | const { data } = await fetchRandMusic() 22 | indexRec.loading = false 23 | indexRec.noData = data.code !== 1 24 | indexRec.musicData = data.data 25 | } 26 | return { ...toRefs(indexRec), fetchMusicInfo } 27 | } 28 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-px-to-viewport': { 4 | unitToConvert: 'px', // 需要转换的单位,默认为"px" 5 | viewportWidth: 750, // 设计稿的视口宽度 6 | exclude: [/node_modules/], // 解决vant375,设计稿750问题。忽略某些文件夹下的文件或特定文件 7 | unitPrecision: 5, // 单位转换后保留的精度 8 | propList: ['*'], // 能转化为vw的属性列表 9 | viewportUnit: 'vw', // 希望使用的视口单位 10 | fontViewportUnit: 'vw', // 字体使用的视口单位 11 | selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。 12 | minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换 13 | mediaQuery: false, // 媒体查询里的单位是否需要转换单位 14 | replace: true, // 是否直接更换属性值,而不添加备用属性 15 | landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) 16 | landscapeUnit: 'vw', // 横屏时使用的单位 17 | landscapeWidth: 1125 // 横屏时使用的视口宽度 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | //.eslintrc.js 2 | module.exports = { 3 | root: true, 4 | env: { 5 | node: true 6 | }, 7 | parser: 'vue-eslint-parser', 8 | parserOptions: { 9 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 10 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 11 | sourceType: 'module', // Allows for the use of imports 12 | ecmaFeatures: { 13 | jsx: true 14 | } 15 | }, 16 | extends: [ 17 | 'plugin:vue/vue3-recommended', 18 | 'plugin:@typescript-eslint/recommended', 19 | 'prettier', 20 | 'plugin:prettier/recommended' 21 | ], 22 | rules: { 23 | '@typescript-eslint/no-var-requires': 0, 24 | '@typescript-eslint/explicit-module-boundary-types': 'off', 25 | 'space-before-function-paren': 0, 26 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { CT } from '@/config/config' 3 | import { Toast } from 'vant' 4 | 5 | // axios.defaults.headers.common['Content-Type'] = 'application/json' 6 | // axios.defaults.headers.common['area-code'] = 'CWHT' 7 | // 创建 axios 实例 8 | const request = axios.create({ 9 | timeout: CT.timeout // 请求超时时间 10 | }) 11 | 12 | // 异常拦截处理器 13 | const errorHandler = (error: any) => { 14 | if (error.response) { 15 | const data = error.response.data 16 | Toast(data.message) 17 | } 18 | return Promise.reject(error) 19 | } 20 | 21 | // request interceptor 22 | request.interceptors.request.use((config) => { 23 | // 自定义全局header 24 | config.headers = config.headers ? config.headers : {} 25 | config.headers['Content-Type'] = 'application/json' 26 | return config 27 | }, errorHandler) 28 | 29 | // response interceptor 30 | request.interceptors.response.use((response) => { 31 | return response 32 | }, errorHandler) 33 | 34 | export default request 35 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueJsx from '@vitejs/plugin-vue-jsx' 4 | import { resolve } from 'path' 5 | import styleImport, { VantResolve } from 'vite-plugin-style-import' 6 | 7 | // 配置参考: https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | vueJsx({}), 12 | styleImport({ 13 | resolves: [VantResolve()] 14 | }) 15 | ], 16 | resolve: { 17 | alias: { 18 | '@': resolve(__dirname, 'src') 19 | } 20 | }, 21 | css: { 22 | preprocessorOptions: { 23 | less: { 24 | charset: false, 25 | additionalData: '@import "./src/style/global.less";' // 加载全局样式,使用less特性 26 | } 27 | } 28 | }, 29 | server: { 30 | port: 3000, 31 | open: true, 32 | proxy: { 33 | '/api': { 34 | target: 'http://API网关所在域名', 35 | changeOrigin: true, 36 | rewrite: (path) => path.replace(/^\/api/, '') 37 | } 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /tests/units/MusicCard.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import MusicCard from '@/components/MusicCard.vue' 3 | const data = { 4 | name: '放空', 5 | url: 'http://music.163.com/song/media/outer/url?id=1841002409', 6 | picurl: 'http://p3.music.126.net/ocVnhvD-nXHKEM3TvBUZsw==/109951165931493179.jpg', 7 | artistsname: '大籽' 8 | } 9 | // 音频播放器组件描述作用域 10 | describe('music play test', () => { 11 | // 断言 挂载组件,并传入props data 12 | it('renders data title', () => { 13 | const wrapper = shallowMount(MusicCard, { 14 | props: { data } 15 | }) 16 | // 期望 title标题渲染成功 17 | expect(wrapper.get('.title').text()).toContain('放空') 18 | }) 19 | 20 | test('renders data author', () => { 21 | const wrapper = shallowMount(MusicCard, { 22 | props: { data } 23 | }) 24 | // 期望 author标题渲染成功 25 | expect(wrapper.get('.author').text()).toEqual('大籽') 26 | }) 27 | 28 | test('render to click poster', () => { 29 | const wrapper = shallowMount(MusicCard, { 30 | props: { data } 31 | }) 32 | wrapper.get('.player').trigger('click') 33 | // 期望 点击播放后,playing为true 34 | expect((wrapper.vm as any).playing).toBe(true) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/AudioDeal.vue: -------------------------------------------------------------------------------- 1 | 5 | 42 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/RefreshCard.vue: -------------------------------------------------------------------------------- 1 | 5 | 29 | 30 | 35 | 36 | 61 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 11 | 43 | 56 | -------------------------------------------------------------------------------- /src/style/reset.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font: inherit; 87 | vertical-align: baseline; 88 | } 89 | /* HTML5 display-role reset for older browsers */ 90 | article, 91 | aside, 92 | details, 93 | figcaption, 94 | figure, 95 | footer, 96 | header, 97 | hgroup, 98 | menu, 99 | nav, 100 | section { 101 | display: block; 102 | } 103 | body { 104 | line-height: 1; 105 | } 106 | ol, 107 | ul { 108 | list-style: none; 109 | } 110 | blockquote, 111 | q { 112 | quotes: none; 113 | } 114 | blockquote:before, 115 | blockquote:after, 116 | q:before, 117 | q:after { 118 | content: ''; 119 | content: none; 120 | } 121 | table { 122 | border-collapse: collapse; 123 | border-spacing: 0; 124 | } 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freely", 3 | "version": "1.0.1", 4 | "license": "ISC", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview", 9 | "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx", 10 | "prettier": "prettier --write .", 11 | "prepare": "husky install", 12 | "test": "jest --config ./jest.conf.js --coverage" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.26.0", 16 | "pinia": "^2.0.11", 17 | "vant": "^3.4.5", 18 | "vue": "^3.2.25", 19 | "vue-router": "^4.0.12" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^27.4.1", 23 | "@types/node": "^17.0.16", 24 | "@typescript-eslint/eslint-plugin": "^5.11.0", 25 | "@typescript-eslint/parser": "^5.11.0", 26 | "@vitejs/plugin-vue": "^2.0.0", 27 | "@vitejs/plugin-vue-jsx": "^1.3.7", 28 | "@vue/test-utils": "^2.0.0-rc.17", 29 | "@vue/vue3-jest": "^27.0.0-alpha.4", 30 | "babel-jest": "^27.5.1", 31 | "eslint": "^8.8.0", 32 | "eslint-config-prettier": "^8.3.0", 33 | "eslint-plugin-prettier": "^4.0.0", 34 | "eslint-plugin-vue": "^8.4.1", 35 | "husky": "^7.0.4", 36 | "jest": "^27.5.1", 37 | "less": "^4.1.2", 38 | "lint-staged": "^12.3.7", 39 | "mrm": "^3.0.10", 40 | "postcss-px-to-viewport": "^1.1.1", 41 | "prettier": "^2.5.1", 42 | "ts-jest": "^27.1.3", 43 | "typescript": "~4.5.2", 44 | "vite": "^2.7.2", 45 | "vite-plugin-style-import": "^1.4.1", 46 | "vue-tsc": "^0.29.8" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "*.{ts,tsx,vue,js,jsx}": [ 55 | "yarn lint", 56 | "prettier --write" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

手机端简易示例《随机热门音乐》

5 | 6 |

Vue 3 + Typescript + Vite + Vant + Pinia

7 | 8 |

9 | GitHub language count 10 | GitHub top language 11 | 12 | GitHub commit activity 13 |

14 | 15 |

16 | 🔥 Demo 访问 17 |   18 | 🌈 文档 19 |

20 | 21 | --- 22 | 23 | ## 依赖 24 | 25 | - 🚀 vue3.2 + vite + typescript + pinia + axios + vant 26 | - 💪 使用 vue3.2 ` 33 | 34 | 54 | 55 | 148 | --------------------------------------------------------------------------------