├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package.json ├── server.ts ├── src ├── app │ ├── animations.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.less │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── common │ │ ├── big-card │ │ │ ├── big-card.component.html │ │ │ ├── big-card.component.less │ │ │ ├── big-card.component.spec.ts │ │ │ └── big-card.component.ts │ │ ├── drawer-list │ │ │ ├── drawer-list.component.html │ │ │ ├── drawer-list.component.less │ │ │ ├── drawer-list.component.spec.ts │ │ │ └── drawer-list.component.ts │ │ ├── headline │ │ │ ├── headline.component.html │ │ │ ├── headline.component.less │ │ │ ├── headline.component.spec.ts │ │ │ └── headline.component.ts │ │ ├── list-content │ │ │ ├── list-content.component.html │ │ │ ├── list-content.component.less │ │ │ ├── list-content.component.spec.ts │ │ │ └── list-content.component.ts │ │ ├── scroll │ │ │ ├── scroll.component.html │ │ │ ├── scroll.component.less │ │ │ ├── scroll.component.spec.ts │ │ │ └── scroll.component.ts │ │ ├── slider │ │ │ ├── slider.component.html │ │ │ ├── slider.component.less │ │ │ ├── slider.component.spec.ts │ │ │ └── slider.component.ts │ │ └── small-card │ │ │ ├── small-card.component.html │ │ │ ├── small-card.component.less │ │ │ ├── small-card.component.spec.ts │ │ │ └── small-card.component.ts │ ├── control │ │ ├── control.component.html │ │ ├── control.component.less │ │ ├── control.component.spec.ts │ │ └── control.component.ts │ ├── details │ │ ├── details.component.html │ │ ├── details.component.less │ │ ├── details.component.spec.ts │ │ └── details.component.ts │ ├── helpers │ │ ├── common.ts │ │ └── constants.less │ ├── hot │ │ ├── hot-routing.module.ts │ │ ├── hot.component.html │ │ ├── hot.component.less │ │ ├── hot.component.spec.ts │ │ ├── hot.component.ts │ │ ├── hot.module.ts │ │ └── song-list-detail │ │ │ ├── song-list-detail.component.html │ │ │ ├── song-list-detail.component.less │ │ │ ├── song-list-detail.component.spec.ts │ │ │ └── song-list-detail.component.ts │ ├── list │ │ ├── list-routing.module.ts │ │ ├── list.component.html │ │ ├── list.component.less │ │ ├── list.component.spec.ts │ │ ├── list.component.ts │ │ └── list.module.ts │ ├── my-counter │ │ ├── my-counter.component.html │ │ ├── my-counter.component.less │ │ ├── my-counter.component.spec.ts │ │ └── my-counter.component.ts │ ├── navbar │ │ ├── navbar.component.html │ │ ├── navbar.component.less │ │ ├── navbar.component.spec.ts │ │ └── navbar.component.ts │ ├── portal │ │ ├── banner │ │ │ ├── banner.component.html │ │ │ ├── banner.component.less │ │ │ ├── banner.component.spec.ts │ │ │ └── banner.component.ts │ │ ├── portal.component.html │ │ ├── portal.component.less │ │ ├── portal.component.spec.ts │ │ └── portal.component.ts │ ├── profile │ │ ├── profile.component.html │ │ ├── profile.component.less │ │ ├── profile.component.spec.ts │ │ └── profile.component.ts │ ├── search │ │ ├── hot-search │ │ │ ├── hot-search.component.html │ │ │ ├── hot-search.component.less │ │ │ ├── hot-search.component.spec.ts │ │ │ └── hot-search.component.ts │ │ ├── search-input │ │ │ ├── search-input.component.html │ │ │ ├── search-input.component.less │ │ │ ├── search-input.component.spec.ts │ │ │ └── search-input.component.ts │ │ ├── search-list │ │ │ ├── search-list.component.html │ │ │ ├── search-list.component.less │ │ │ ├── search-list.component.spec.ts │ │ │ └── search-list.component.ts │ │ ├── search.component.html │ │ ├── search.component.less │ │ ├── search.component.spec.ts │ │ └── search.component.ts │ ├── share.module.ts │ └── smile │ │ ├── smile-routing.module.ts │ │ ├── smile.component.html │ │ ├── smile.component.less │ │ ├── smile.component.spec.ts │ │ ├── smile.component.ts │ │ └── smile.module.ts ├── assets │ ├── .gitkeep │ ├── MP_verify_xJSgoVrk3swqlXsD.txt │ ├── imgs │ │ ├── audio │ │ │ ├── icon_add@2x.png │ │ │ ├── icon_add@2x.svg │ │ │ ├── icon_back@2x.png │ │ │ ├── icon_back@2x.svg │ │ │ ├── icon_forward@2x.png │ │ │ ├── icon_forward@2x.svg │ │ │ ├── icon_heart@2x.png │ │ │ ├── icon_heart@2x.svg │ │ │ ├── icon_loop@2x.png │ │ │ ├── icon_loop@2x.svg │ │ │ ├── icon_oval.png │ │ │ ├── icon_play@2x.png │ │ │ ├── icon_play@2x.svg │ │ │ ├── icon_play_circle@2x.png │ │ │ ├── icon_play_circle@2x.svg │ │ │ ├── icon_shuffle@2x.png │ │ │ ├── icon_shuffle@2x.svg │ │ │ ├── image_schermata@2x.png │ │ │ ├── image_song_cover@2x.png │ │ │ └── img@2x.png │ │ ├── icon │ │ │ ├── icon_go_back.svg │ │ │ ├── icon_list.svg │ │ │ ├── icon_loop.svg │ │ │ ├── icon_next.svg │ │ │ ├── icon_pause.svg │ │ │ ├── icon_play.svg │ │ │ ├── icon_pre.svg │ │ │ ├── icon_qq_music.svg │ │ │ └── icon_single_loop.svg │ │ ├── navbar │ │ │ ├── icon_flash@2x.png │ │ │ ├── icon_flash@2x.svg │ │ │ ├── icon_flash_active@2x.png │ │ │ ├── icon_flash_active@2x.svg │ │ │ ├── icon_music@2x.png │ │ │ ├── icon_music@2x.svg │ │ │ ├── icon_music_active@2x.png │ │ │ ├── icon_music_active@2x.svg │ │ │ ├── icon_profile@2x.png │ │ │ ├── icon_profile@2x.svg │ │ │ ├── icon_profile_active@2x.png │ │ │ ├── icon_profile_active@2x.svg │ │ │ ├── icon_search@2x.png │ │ │ ├── icon_search@2x.svg │ │ │ ├── icon_search_active@2x.png │ │ │ └── icon_search_active@2x.svg │ │ └── smile_icon.png │ ├── logo │ │ ├── icon_add.svg │ │ ├── icon_equal.svg │ │ ├── image_angular.svg │ │ ├── image_ngrx.svg │ │ ├── page01-min.jpg │ │ └── page02-min.jpg │ └── robots.txt ├── browserslist ├── common.js ├── directive │ └── hammertime.directive.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── interceptor │ └── httpconfig.interceptor.ts ├── interface.ts ├── karma.conf.js ├── main.server.ts ├── main.ts ├── pipes │ ├── format-time.pipe.spec.ts │ └── format-time.pipe.ts ├── polyfills.ts ├── proxy.conf.json ├── services │ ├── control.service.ts │ ├── hot.service.ts │ ├── index.ts │ ├── list.service.ts │ └── search.service.ts ├── store │ ├── actions │ │ ├── control.action.ts │ │ ├── counter.action.ts │ │ ├── hot.action.ts │ │ ├── index.ts │ │ ├── list.action.ts │ │ └── search.actions.ts │ ├── effects │ │ ├── control.effects.ts │ │ ├── hot.effects.ts │ │ ├── index.ts │ │ ├── list.effects.ts │ │ └── search.effects.ts │ ├── index.ts │ └── reducers │ │ ├── control.reducer.ts │ │ ├── counter.reducer.ts │ │ ├── hot.reducer.ts │ │ ├── index.ts │ │ ├── list.reducer.ts │ │ └── search.reducer.ts ├── styles.less ├── test.ts ├── tsconfig.app.json ├── tsconfig.server.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json ├── tslint.json └── webpack.server.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | /.vscode 48 | yarn.lock 49 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 angular-music-player 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

QQ音乐[Angular7.x + NGRX]

2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | ## 预览图 12 |

13 | 14 | 15 |

16 | 17 | ## Development server【开发环境】 18 | 19 | Run `ng serve` OR `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 20 | 21 | Run `ng serve --host ip` custom ip address. eg: `ng serve --host 192.168.0.109`,Navigate to `http://192.168.0.109:4200/`. 22 | 23 | ## Code scaffolding 24 | 25 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 26 | 27 | ## Build 28 | 29 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 30 | 31 | ## Running unit tests 32 | 33 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 34 | 35 | ## Running end-to-end tests 36 | 37 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 38 | 39 | ## Product server【正式环境】 40 | 41 | Run `npm run build:ssr && npm run serve:ssr` 42 | 43 | Run `npm run start:pro` Run with PM2 `pm2 start dist/server` 44 | 45 | ## Create Components 46 | Run `ng g c new-component --module app` 47 | 48 | ## For another place to route to, create a second feature module with routing 49 | Run `ng generate module orders --routing` AND `ng generate module customers --routing` 50 | 51 | ## 生成 `Action` `Reducer` `Effect` 52 | Run `ng generate action ../store/search --group` 53 | 54 | Run `ng generate effect ../store/search --group --spec false` 55 | 56 | Run `ng generate reducer ../store/search --group --spec false` 57 | 58 | ## Directory structure【目录结构】 59 | 60 | ```bash 61 | ├── app //组件 62 | │   ├── common //公共组件 63 | │   │   ├── big-card 64 | │   │   ├── headline 65 | │   │   ├── scroll 66 | │   │   ├── slider 67 | │   │   └── small-card 68 | │   ├── controller 69 | │   ├── details 70 | │   │   ├── cover 71 | │   │   └── list-content 72 | │   ├── helpers //公共函数 73 | │   ├── hot 74 | │   ├── list 75 | │   ├── my-counter 76 | │   ├── navbar 77 | │   ├── portal 78 | │   │   └── banner 79 | │   ├── profile 80 | │   ├── search 81 | │   │   ├── hot-search 82 | │   │   ├── search-input 83 | │   │   └── search-list 84 | │   └── smile 85 | ├── assets //静态资源 86 | │   └── imgs 87 | │   ├── audio 88 | │   ├── icon 89 | ``` -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-music-player": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "less" 14 | }, 15 | "@ngrx/schematics:component": { 16 | "styleext": "less" 17 | } 18 | }, 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/browser", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "src/tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.less" 34 | ], 35 | "scripts": [ 36 | "src/common.js" 37 | ], 38 | "es5BrowserSupport": true 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": true, 51 | "extractCss": true, 52 | "namedChunks": true, 53 | "aot": true, 54 | "extractLicenses": true, 55 | "vendorChunk": true, 56 | "buildOptimizer": false, 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "2mb", 61 | "maximumError": "5mb" 62 | } 63 | ] 64 | } 65 | } 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "angular-music-player:build", 71 | "proxyConfig": "src/proxy.conf.json" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "angular-music-player:build:production" 76 | } 77 | } 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "angular-music-player:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "src/tsconfig.spec.json", 91 | "karmaConfig": "src/karma.conf.js", 92 | "styles": [ 93 | "src/styles.less" 94 | ], 95 | "scripts": [ 96 | "src/common.js" 97 | ], 98 | "assets": [ 99 | "src/favicon.ico", 100 | "src/assets" 101 | ] 102 | } 103 | }, 104 | "lint": { 105 | "builder": "@angular-devkit/build-angular:tslint", 106 | "options": { 107 | "tsConfig": [ 108 | "src/tsconfig.app.json", 109 | "src/tsconfig.spec.json" 110 | ], 111 | "exclude": [ 112 | "**/node_modules/**" 113 | ] 114 | } 115 | }, 116 | "server": { 117 | "builder": "@angular-devkit/build-angular:server", 118 | "options": { 119 | "outputPath": "dist/server", 120 | "main": "src/main.server.ts", 121 | "tsConfig": "src/tsconfig.server.json" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "optimization": { 126 | "scripts": false, 127 | "styles": true 128 | }, 129 | "sourceMap": true, 130 | "fileReplacements": [ 131 | { 132 | "replace": "src/environments/environment.ts", 133 | "with": "src/environments/environment.prod.ts" 134 | } 135 | ] 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | "angular-music-player-e2e": { 142 | "root": "e2e/", 143 | "projectType": "application", 144 | "prefix": "", 145 | "architect": { 146 | "e2e": { 147 | "builder": "@angular-devkit/build-angular:protractor", 148 | "options": { 149 | "protractorConfig": "e2e/protractor.conf.js", 150 | "devServerTarget": "angular-music-player:serve" 151 | }, 152 | "configurations": { 153 | "production": { 154 | "devServerTarget": "angular-music-player:serve:production" 155 | } 156 | } 157 | }, 158 | "lint": { 159 | "builder": "@angular-devkit/build-angular:tslint", 160 | "options": { 161 | "tsConfig": "e2e/tsconfig.e2e.json", 162 | "exclude": [ 163 | "**/node_modules/**" 164 | ] 165 | } 166 | } 167 | } 168 | } 169 | }, 170 | "defaultProject": "angular-music-player", 171 | "cli": { 172 | "defaultCollection": "@ngrx/schematics" 173 | } 174 | } -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to angular-music-player!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-music-player", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "compile:server": "webpack --config webpack.server.config.js --progress --colors", 12 | "serve:ssr": "node dist/server", 13 | "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", 14 | "build:client-and-server-bundles": "ng build --prod && ng run angular-music-player:server:production", 15 | "start:pro": "pm2 start dist/server" 16 | }, 17 | "private": true, 18 | "browserslist": [ 19 | "last 2 Chrome versions", 20 | "last 2 Firefox versions", 21 | "last 2 ie versions", 22 | "last 2 Edge versions", 23 | "last 2 Safari versions", 24 | "last 2 iOS versions", 25 | "last 2 android versions", 26 | "last 2 ie_mob versions" 27 | ], 28 | "dependencies": { 29 | "@angular/animations": "~7.2.0", 30 | "@angular/common": "~7.2.0", 31 | "@angular/compiler": "~7.2.0", 32 | "@angular/core": "~7.2.0", 33 | "@angular/forms": "~7.2.0", 34 | "@angular/http": "~7.2.0", 35 | "@angular/platform-browser": "~7.2.0", 36 | "@angular/platform-browser-dynamic": "~7.2.0", 37 | "@angular/platform-server": "~7.2.0", 38 | "@angular/router": "~7.2.0", 39 | "@ngrx/effects": "^7.4.0", 40 | "@ngrx/entity": "^7.4.0", 41 | "@ngrx/store": "^7.4.0", 42 | "@ngrx/store-devtools": "^7.4.0", 43 | "@nguniversal/express-engine": "^7.1.1", 44 | "@nguniversal/module-map-ngfactory-loader": "0.0.0", 45 | "better-scroll": "^1.15.1", 46 | "core-js": "^2.5.4", 47 | "express": "^4.15.2", 48 | "gsap": "^2.1.2", 49 | "hammerjs": "^2.0.8", 50 | "pm2": "^3.4.1", 51 | "rxjs": "~6.3.3", 52 | "tslib": "^1.9.0", 53 | "zone.js": "~0.8.26" 54 | }, 55 | "devDependencies": { 56 | "@angular-devkit/build-angular": "~0.13.0", 57 | "@angular/cli": "~7.3.6", 58 | "@angular/compiler-cli": "~7.2.0", 59 | "@angular/language-service": "~7.2.0", 60 | "@ngrx/schematics": "^7.4.0", 61 | "@types/jasmine": "~2.8.8", 62 | "@types/jasminewd2": "~2.0.3", 63 | "@types/node": "~8.9.4", 64 | "codelyzer": "~4.5.0", 65 | "jasmine-core": "~2.99.1", 66 | "jasmine-spec-reporter": "~4.2.1", 67 | "karma": "~4.0.0", 68 | "karma-chrome-launcher": "~2.2.0", 69 | "karma-coverage-istanbul-reporter": "~2.0.1", 70 | "karma-jasmine": "~1.1.2", 71 | "karma-jasmine-html-reporter": "^0.2.2", 72 | "protractor": "~5.4.0", 73 | "ts-loader": "^5.2.0", 74 | "ts-node": "~7.0.0", 75 | "tslint": "~5.11.0", 76 | "typescript": "~3.2.2", 77 | "webpack-cli": "^3.1.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import {enableProdMode} from '@angular/core'; 3 | // Express Engine 4 | import {ngExpressEngine} from '@nguniversal/express-engine'; 5 | // Import module map for lazy loading 6 | import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; 7 | 8 | import * as express from 'express'; 9 | import {join} from 'path'; 10 | 11 | // Faster server renders w/ Prod mode (dev mode never needed) 12 | enableProdMode(); 13 | 14 | // Express server 15 | const app = express(); 16 | 17 | const PORT = process.env.PORT || 4000; 18 | const DIST_FOLDER = join(process.cwd(), 'dist/browser'); 19 | 20 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 21 | const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); 22 | 23 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 24 | app.engine('html', ngExpressEngine({ 25 | bootstrap: AppServerModuleNgFactory, 26 | providers: [ 27 | provideModuleMap(LAZY_MODULE_MAP) 28 | ] 29 | })); 30 | 31 | app.set('view engine', 'html'); 32 | app.set('views', DIST_FOLDER); 33 | 34 | // Example Express Rest API endpoints 35 | // app.get('/api/**', (req, res) => { }); 36 | // Serve static files from /browser 37 | app.get('*.*', express.static(DIST_FOLDER, { 38 | maxAge: '1y' 39 | })); 40 | 41 | // All regular routes use the Universal engine 42 | app.get('*', (req, res) => { 43 | res.render('index', { req }); 44 | }); 45 | 46 | // Start up the Node server 47 | app.listen(PORT, () => { 48 | console.log(`Node Express server listening on http://localhost:${PORT}`); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/animations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | trigger, animateChild, group, 3 | transition, animate, style, query 4 | } from '@angular/animations'; 5 | 6 | const animatePath: string[] = [ 7 | 'detail <=> hot', 8 | ]; 9 | 10 | // 路由切换过渡动画 11 | export const slideInAnimation = 12 | trigger('routeAnimations', animatePath.map(path => ( 13 | transition(path, [ 14 | style({ position: 'relative' }), 15 | query(':enter, :leave', [ 16 | style({ 17 | position: 'absolute', 18 | top: 0, 19 | left: 0, 20 | width: '100%' 21 | }) 22 | ]), 23 | query(':enter', [ 24 | style({ left: '-100%' }) 25 | ]), 26 | query(':leave', animateChild()), 27 | group([ 28 | query(':leave', [ 29 | animate('300ms ease-out', style({ left: '100%' })) 30 | ]), 31 | query(':enter', [ 32 | animate('300ms ease-out', style({ left: '0%' })) 33 | ]) 34 | ]), 35 | query(':enter', animateChild()), 36 | ]) 37 | ))); -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | // import { SmileComponent } from './smile/smile.module'; 4 | // import { PortalComponent } from './portal/portal.component'; 5 | import { SearchComponent } from './search/search.component'; 6 | import { ProfileComponent } from './profile/profile.component'; 7 | 8 | 9 | const routes: Routes = [ 10 | { path: '', pathMatch: 'full', redirectTo: '/hot' }, 11 | { path: 'hot', loadChildren: './hot/hot.module#HotModule' }, 12 | { path: 'search', component: SearchComponent }, 13 | { path: 'profile', component: ProfileComponent }, 14 | { path: 'list', loadChildren: './list/list.module#ListModule' }, 15 | { path: 'smile', loadChildren: './smile/smile.module#SmileModule' }, 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule] 21 | }) 22 | export class AppRoutingModule { } 23 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 7 |
8 | 9 |
10 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/app.component.less: -------------------------------------------------------------------------------- 1 | @import './helpers/constants.less'; 2 | .container { 3 | height: 100%; 4 | background-color: @bg_color; 5 | 6 | .music_player_box { 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | .navbar { 11 | position: fixed; 12 | width: 100%; 13 | z-index: 10; 14 | height: @32px; 15 | background-color: @bg_color; 16 | top: 0; 17 | left: 0; 18 | } 19 | 20 | .content { 21 | width: 100%; 22 | flex: 1; 23 | padding-top: @32px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { reducers } from '../store'; 6 | 7 | describe('AppComponent', () => { 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ 11 | RouterTestingModule, 12 | StoreModule.forRoot(reducers) 13 | ], 14 | declarations: [ 15 | AppComponent 16 | ], 17 | }).compileComponents(); 18 | })); 19 | 20 | it('should create the app', () => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app).toBeTruthy(); 24 | }); 25 | 26 | it(`should have as title 'angular-music-player'`, () => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | const app = fixture.debugElement.componentInstance; 29 | expect(app.title).toEqual('angular-music-player'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { slideInAnimation } from './animations'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.less'] 8 | }) 9 | 10 | export class AppComponent {} 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | declare var require: any; 2 | 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 5 | import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | 8 | import { AppRoutingModule } from './app-routing.module'; 9 | 10 | import { StoreModule } from '@ngrx/store'; 11 | import { EffectsModule } from "@ngrx/effects"; 12 | import { reducers, effects } from '../store'; 13 | 14 | // http拦截器,捕获异常,加Token 15 | import { HttpConfigInterceptor } from '../interceptor/httpconfig.interceptor'; 16 | 17 | 18 | import { AppComponent } from './app.component'; 19 | import { NavbarComponent } from './navbar/navbar.component'; 20 | import { ControlComponent } from './control/control.component'; 21 | import { PortalComponent } from './portal/portal.component'; 22 | import { BannerComponent } from './portal/banner/banner.component'; 23 | import { HeadlineComponent } from './common/headline/headline.component'; 24 | import { SearchComponent } from './search/search.component'; 25 | import { ProfileComponent } from './profile/profile.component'; 26 | import { SearchInputComponent } from './search/search-input/search-input.component'; 27 | import { HotSearchComponent } from './search/hot-search/hot-search.component'; 28 | import { SearchListComponent } from './search/search-list/search-list.component'; 29 | import { DetailsComponent } from './details/details.component'; 30 | 31 | // import { HotModule } from './hot/hot.module'; 32 | // import { ListModule } from './list/list.module'; 33 | import { ShareModule } from './share.module'; 34 | 35 | import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; 36 | import { DrawerListComponent } from './common/drawer-list/drawer-list.component'; 37 | 38 | let Hammer = { DIRECTION_ALL: {} }; 39 | if (typeof window != 'undefined') { 40 | Hammer = require('hammerjs'); 41 | } 42 | 43 | export class MyHammerConfig extends HammerGestureConfig { 44 | overrides = { 45 | // override hammerjs default configuration 46 | 'swipe': { direction: Hammer.DIRECTION_ALL } 47 | } 48 | } 49 | 50 | @NgModule({ 51 | declarations: [ 52 | AppComponent, 53 | NavbarComponent, 54 | ControlComponent, 55 | PortalComponent, 56 | BannerComponent, 57 | HeadlineComponent, 58 | SearchComponent, 59 | ProfileComponent, 60 | SearchInputComponent, 61 | HotSearchComponent, 62 | SearchListComponent, 63 | DetailsComponent, 64 | DrawerListComponent, 65 | ], 66 | imports: [ 67 | BrowserModule.withServerTransition({ appId: 'serverApp' }), 68 | BrowserAnimationsModule, 69 | AppRoutingModule, 70 | HttpClientModule, 71 | StoreModule.forRoot(reducers), 72 | EffectsModule.forRoot(effects), 73 | ShareModule 74 | ], 75 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 76 | providers: [ 77 | { 78 | provide: HTTP_INTERCEPTORS, 79 | useClass: HttpConfigInterceptor, 80 | multi: true 81 | }, 82 | { 83 | provide: HAMMER_GESTURE_CONFIG, 84 | useClass: MyHammerConfig 85 | } 86 | ], 87 | bootstrap: [AppComponent] 88 | }) 89 | export class AppModule { } 90 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { AppComponent } from './app.component'; 6 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | AppModule, 11 | ServerModule, 12 | ModuleMapLoaderModule, 13 | ], 14 | bootstrap: [AppComponent], 15 | }) 16 | export class AppServerModule {} 17 | -------------------------------------------------------------------------------- /src/app/common/big-card/big-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

{{name}}

7 |

歌手- {{singer}}

8 |

{{durationTime/1000 | formatTime}}

9 |
{{rank}}
10 |
11 |
-------------------------------------------------------------------------------- /src/app/common/big-card/big-card.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .big_card_box { 4 | display: flex; 5 | background-color: @color_white; 6 | margin-top: @12px; 7 | 8 | .image_box { 9 | width: 6rem; 10 | height: 6rem; 11 | 12 | img { 13 | width: 100%; 14 | } 15 | } 16 | 17 | .title_text_box { 18 | display: flex; 19 | flex: 1; 20 | flex-direction: column; 21 | justify-content: space-between; 22 | padding: @12px; 23 | position: relative; 24 | 25 | .text_name { 26 | font-size: @12px; 27 | } 28 | .name{ 29 | font-weight: bold; 30 | } 31 | .top_number{ 32 | position: absolute; 33 | top: 50%; 34 | right: 1.4rem; 35 | margin-top: -0.7rem; 36 | color: @red; 37 | font-size: @18px; 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/common/big-card/big-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BigCardComponent } from './big-card.component'; 4 | 5 | describe('BigCardComponent', () => { 6 | let component: BigCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BigCardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BigCardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/big-card/big-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-big-card', 5 | templateUrl: './big-card.component.html', 6 | styleUrls: ['./big-card.component.less'] 7 | }) 8 | export class BigCardComponent implements OnInit { 9 | @Input() picUrl: string = ''; 10 | @Input() name: string = ''; 11 | @Input() id: number = 0; 12 | @Input() singer: string = ''; 13 | @Input() durationTime: number = 0; 14 | @Input() rank:number; 15 | 16 | constructor() { } 17 | 18 | ngOnInit() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/common/drawer-list/drawer-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 顺序播放({{data.playList.length}}) 6 |
7 |
8 | 9 |
    10 |
  • 11 | {{item.al.name}}-{{item.name}}
  • 12 |
13 |
14 |
15 | 16 |
17 |
-------------------------------------------------------------------------------- /src/app/common/drawer-list/drawer-list.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .draw_wrapper { 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | top: 0; 8 | right: 0; 9 | z-index: 14; 10 | .mask { 11 | position: absolute; 12 | z-index: -1; 13 | background-color: rgba(0, 0, 0, .3); 14 | bottom: 0; 15 | left: 0; 16 | top: 0; 17 | right: 0; 18 | opacity: 0; 19 | } 20 | 21 | .list_wrapper { 22 | position: absolute; 23 | left: 0; 24 | bottom: 0; 25 | width: 100%; 26 | background-color: @color_white; 27 | 28 | .list_header { 29 | padding: @14px; 30 | border-bottom: 1px solid #f1f1f1; 31 | } 32 | 33 | .list_box { 34 | padding: 0 @14px 0 @14px; 35 | 36 | li { 37 | padding: @12px 0; 38 | border-bottom: 1px solid #fafafa; 39 | 40 | span { 41 | font-size: @12px; 42 | color: @gray; 43 | } 44 | } 45 | } 46 | 47 | .scroll_box { 48 | height: 16rem; 49 | position: relative; 50 | overflow: hidden; 51 | } 52 | 53 | button { 54 | width: 100%; 55 | background: none; 56 | padding: @14px 0; 57 | border: none; 58 | border-top: 1px solid #f1f1f1; 59 | outline: none; 60 | font-size: @14px; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/app/common/drawer-list/drawer-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DrawerListComponent } from './drawer-list.component'; 4 | 5 | describe('DrawerListComponent', () => { 6 | let component: DrawerListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DrawerListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DrawerListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/drawer-list/drawer-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | trigger, 4 | state, 5 | style, 6 | animate, 7 | transition 8 | } from '@angular/animations'; 9 | import { ControlState } from '../../../store/reducers/control.reducer'; 10 | import { ChangeControlValue, LoadSongUrl } from '../../../store'; 11 | import { Observable } from 'rxjs'; 12 | import { select, Store } from '@ngrx/store'; 13 | 14 | @Component({ 15 | selector: 'app-drawer-list', 16 | templateUrl: './drawer-list.component.html', 17 | styleUrls: ['./drawer-list.component.less'], 18 | animations: [ 19 | trigger('childAnimation', [ 20 | // ... 21 | state('sideUp', style({ 22 | opacity: 1, 23 | transform: 'translateY(0)', 24 | })), 25 | state('sideDown', style({ 26 | opacity: 0, 27 | transform: 'translateY(100%)', 28 | })), 29 | transition('* => *', [ 30 | animate('400ms ease-in-out') 31 | ]), 32 | ]) 33 | ] 34 | }) 35 | export class DrawerListComponent implements OnInit { 36 | public controlStore$: Observable; 37 | public data: ControlState; 38 | 39 | constructor(private store: Store<{ controlStore: ControlState }>) { 40 | this.controlStore$ = store.pipe(select('controlStore')); 41 | } 42 | 43 | ngOnInit() { 44 | this.controlStore$.subscribe(data => { 45 | this.data = data; 46 | }); 47 | } 48 | 49 | public handlerPlayerList(visible: boolean): void { 50 | this.store.dispatch(new ChangeControlValue({ key: 'playListVisible', value: visible })); 51 | } 52 | 53 | public playMusic(currentId: number, current: number): void { 54 | // 把当前播放歌曲的id和索引都存到store里面 55 | this.store.dispatch(new ChangeControlValue({ key: 'current', value: current })); 56 | this.store.dispatch(new ChangeControlValue({ key: 'currentId', value: currentId })); 57 | // 获取当前歌曲的播放地址需要这首歌的id 58 | this.store.dispatch(new LoadSongUrl(currentId)); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/app/common/headline/headline.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 | 4 |

5 | -------------------------------------------------------------------------------- /src/app/common/headline/headline.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | .color { 3 | color: @main_color; 4 | } 5 | .title { 6 | font-weight: 600; 7 | margin: @14px; 8 | } -------------------------------------------------------------------------------- /src/app/common/headline/headline.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeadlineComponent } from './headline.component'; 4 | 5 | describe('HeadlineComponent', () => { 6 | let component: HeadlineComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HeadlineComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeadlineComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/headline/headline.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-headline', 5 | templateUrl: './headline.component.html', 6 | styleUrls: ['./headline.component.less'] 7 | }) 8 | export class HeadlineComponent implements OnInit { 9 | @Input() public title: string; 10 | @Input() public color: boolean; 11 | @Input() public size: number; 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/common/list-content/list-content.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
歌单 共{{data.length}}首
4 |
    5 |
  • 6 | 7 |
    8 |

    {{item.name}}

    9 |

    {{modifyArray(item.ar)}}·{{item.al.name}}

    10 |
    11 |
  • 12 |
13 |
14 |
-------------------------------------------------------------------------------- /src/app/common/list-content/list-content.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .detail_list { 4 | background-color: @color_white; 5 | padding: 0 @14px; 6 | 7 | .count_box__desc { 8 | font-size: @14px; 9 | color: @gray; 10 | padding-top: @14px; 11 | } 12 | 13 | .list_content { 14 | ul { 15 | padding-bottom: @14px; 16 | } 17 | 18 | li { 19 | display: flex; 20 | padding: @12px 0; 21 | align-items: center; 22 | line-height: 1.4rem; 23 | 24 | .info_order { 25 | width: 2rem; 26 | color: @red; 27 | font-size: @16px; 28 | } 29 | 30 | .info_box { 31 | flex: 1; 32 | h3 { 33 | font-size: @16px; 34 | font-weight: normal; 35 | } 36 | .singer { 37 | font-size: @12px; 38 | color: #777; 39 | } 40 | } 41 | 42 | p { 43 | font-size: @12px; 44 | color: @gray; 45 | display: block; 46 | width: 96%; 47 | overflow: hidden; 48 | white-space: nowrap; 49 | text-overflow: ellipsis; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/common/list-content/list-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListContentComponent } from './list-content.component'; 4 | 5 | describe('ListContentComponent', () => { 6 | let component: ListContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ListContentComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ListContentComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/list-content/list-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { Position } from '../../common/scroll/scroll.component'; 3 | 4 | @Component({ 5 | selector: 'app-list-content', 6 | templateUrl: './list-content.component.html', 7 | styleUrls: ['./list-content.component.less'] 8 | }) 9 | export class ListContentComponent implements OnInit { 10 | @Input() public data: any[] = new Array(20); 11 | @Output() public onScroll = new EventEmitter(); 12 | @Output() public onTap = new EventEmitter(); 13 | 14 | constructor() { } 15 | 16 | ngOnInit() { } 17 | 18 | public modifyArray(data: any[]): string { 19 | return data.map(item => item.name).join('/'); 20 | } 21 | 22 | public scrollFun(position: Position) { 23 | this.onScroll.emit(position); 24 | } 25 | 26 | public handlerTap(currentId: number, current: number): void { 27 | this.onTap.emit({ currentId, current }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/common/scroll/scroll.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/common/scroll/scroll.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .scroll_box { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | z-index: 1; 10 | } -------------------------------------------------------------------------------- /src/app/common/scroll/scroll.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ScrollComponent } from './scroll.component'; 4 | 5 | describe('ScorllComponent', () => { 6 | let component: ScrollComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ScrollComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ScrollComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/scroll/scroll.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ElementRef, Input, ViewChild, Output, SimpleChanges, EventEmitter } from '@angular/core'; 2 | import BScroll from 'better-scroll'; 3 | 4 | type PullDownRefresh = { 5 | txt?: string, 6 | stop?: number, 7 | stopTime?: number 8 | } 9 | 10 | export interface Position { 11 | x: number; 12 | y: number; 13 | } 14 | 15 | @Component({ 16 | selector: 'app-scroll', 17 | templateUrl: './scroll.component.html', 18 | styleUrls: ['./scroll.component.less'] 19 | }) 20 | export class ScrollComponent implements OnInit { 21 | public scroll: BScroll; 22 | public beforePullDown: boolean = true; 23 | public isRebounding: boolean = false; 24 | public isPullingDown: boolean = false; 25 | public isPullUpLoad: boolean = false 26 | public pullUpDirty: boolean = true; 27 | public pullDownStyle: string = ''; 28 | public bubbleY: number = 0; 29 | public pullDownInitTop: number = -50; 30 | 31 | @Input() public probeType: number = 1; 32 | @Input() public click: boolean = false; 33 | @Input() public listenScroll: boolean = true; 34 | @Input() public listenBeforeScroll: boolean = false; 35 | @Input() public listenScrollEnd: boolean = false; 36 | @Input() public direction: string = 'vertical'; 37 | @Input() public scrollBar: boolean = false; 38 | @Input() public pullDownRefresh: PullDownRefresh = null; 39 | @Input() public pullUpLoad: boolean = false; 40 | @Input() public startY: number = 0; 41 | @Input() public refreshDelay: number = 20; 42 | @Input() public freeScroll: boolean = false; 43 | @Input() public mouseWheel: boolean = false; 44 | @Input() public bounce: boolean = true; 45 | @Input() public zoom: boolean = false; 46 | 47 | @Input() public pullUp: boolean = false; 48 | @Input() public beforeScroll: boolean = false; 49 | @Input() public scrollY: boolean = true; 50 | @Input() public scrollX: boolean = false; 51 | 52 | @Output() public scrollFun = new EventEmitter(); 53 | @Output() public scrollEndFun = new EventEmitter(); 54 | @Output() public beforeScrollFun: Function = () => { }; 55 | @Output() public scrollStartFun: Function = () => { }; 56 | @Output() public pullingDownFun: Function = () => { }; 57 | @Output() public pullingUpFun: Function = () => { }; 58 | 59 | @ViewChild('scrollContent') scrollContent: ElementRef; 60 | 61 | 62 | constructor(private element: ElementRef) { } 63 | 64 | ngOnInit() { } 65 | ngAfterContentInit() { 66 | if (typeof window != 'undefined') { 67 | setTimeout(() => { 68 | this._initScroll(); 69 | }, 20) 70 | } 71 | } 72 | ngOnChanges(change: SimpleChanges) { 73 | console.log(change, '发生了改变'); 74 | setTimeout(() => { 75 | this.refresh(); 76 | }, this.refreshDelay) 77 | } 78 | 79 | // 初始化滚动函数 80 | private _initScroll(): void { 81 | const scrollContent = this.scrollContent.nativeElement; 82 | if (!scrollContent) { 83 | return; 84 | } 85 | 86 | const options = { 87 | probeType: this.probeType, 88 | click: this.click, 89 | scrollY: this.freeScroll || this.direction === 'vertical', 90 | scrollX: this.freeScroll || this.direction === 'horizontal', 91 | scrollbar: this.scrollBar, 92 | pullDownRefresh: this.pullDownRefresh, 93 | pullUpLoad: this.pullUpLoad, 94 | startY: this.startY, 95 | freeScroll: this.freeScroll, 96 | mouseWheel: this.mouseWheel, 97 | bounce: this.bounce, 98 | zoom: this.zoom 99 | } 100 | this.scroll = new BScroll(scrollContent, options); 101 | if (this.listenScroll) { 102 | this.scroll.on('scroll', (pos: Position) => { 103 | this.scrollFun.emit(pos); 104 | }) 105 | } 106 | 107 | if (this.listenScrollEnd) { 108 | this.scroll.on('scrollEnd', (pos) => { 109 | this.scrollEndFun.emit(pos); 110 | }) 111 | } 112 | 113 | if (this.listenBeforeScroll) { 114 | this.scroll.on('beforeScrollStart', () => { 115 | this.beforeScrollFun(); 116 | }) 117 | 118 | this.scroll.on('scrollStart', () => { 119 | this.scrollStartFun(); 120 | }) 121 | } 122 | 123 | if (this.pullDownRefresh) { 124 | this._initPullDownRefresh(); 125 | } 126 | 127 | if (this.pullUpLoad) { 128 | this._initPullUpLoad(); 129 | } 130 | } 131 | 132 | private _initPullDownRefresh(): void { 133 | this.scroll.on('pullingDown', () => { 134 | this.beforePullDown = false; 135 | this.isPullingDown = true; 136 | this.pullingDownFun('pullingDown'); 137 | }) 138 | 139 | this.scroll.on('scroll', (pos) => { 140 | if (!this.pullDownRefresh) { 141 | return 142 | } 143 | if (this.beforePullDown) { 144 | this.bubbleY = Math.max(0, pos.y + this.pullDownInitTop); 145 | this.pullDownStyle = `top:${Math.min(pos.y + this.pullDownInitTop, 10)}px`; 146 | } else { 147 | this.bubbleY = 0; 148 | } 149 | 150 | if (this.isRebounding) { 151 | this.pullDownStyle = `top:${10 - (this.pullDownRefresh.stop - pos.y)}px`; 152 | } 153 | }) 154 | } 155 | 156 | public disable(): void { 157 | this.scroll && this.scroll.disable(); 158 | } 159 | 160 | public enable(): void { 161 | this.scroll && this.scroll.enable(); 162 | } 163 | 164 | public refresh(): void { 165 | this.scroll && this.scroll.refresh(); 166 | } 167 | 168 | public scrollTo(): void { 169 | this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments); 170 | } 171 | 172 | public autoPullDownRefresh(): void { 173 | this.scroll && this.scroll.autoPullDownRefresh(); 174 | } 175 | 176 | public scrollToElement(): void { 177 | this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) 178 | } 179 | 180 | public clickItem(e, item): void { 181 | console.log(e, item); 182 | } 183 | 184 | public destroy(): void { 185 | this.scroll.destroy() 186 | } 187 | 188 | public forceUpdate(dirty): void { 189 | if (this.pullDownRefresh && this.isPullingDown) { 190 | this.isPullingDown = false 191 | this._reboundPullDown().then(() => { 192 | this._afterPullDown() 193 | }) 194 | } else if (this.pullUpLoad && this.isPullUpLoad) { 195 | this.isPullUpLoad = false 196 | this.scroll.finishPullUp() 197 | this.pullUpDirty = dirty 198 | this.refresh() 199 | } else { 200 | this.refresh() 201 | } 202 | } 203 | 204 | private _initPullUpLoad() { 205 | this.scroll.on('pullingUp', () => { 206 | this.isPullUpLoad = true; 207 | this.pullingUpFun(); 208 | }) 209 | } 210 | 211 | private _reboundPullDown(): Promise<{}> { 212 | const { stopTime = 600 } = this.pullDownRefresh 213 | return new Promise((resolve) => { 214 | setTimeout(() => { 215 | this.isRebounding = true 216 | this.scroll.finishPullDown() 217 | resolve() 218 | }, stopTime) 219 | }) 220 | } 221 | 222 | private _afterPullDown(): void { 223 | setTimeout(() => { 224 | this.pullDownStyle = `top:${this.pullDownInitTop}px` 225 | this.beforePullDown = true 226 | this.isRebounding = false 227 | this.refresh() 228 | }, this.scroll.options.bounceTime) 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /src/app/common/slider/slider.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/common/slider/slider.component.less: -------------------------------------------------------------------------------- 1 | .slide { 2 | min-height: 1px; 3 | 4 | .slide_group { 5 | position: relative; 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .dots { 11 | 12 | position: absolute; 13 | right: 0; 14 | left: 0; 15 | bottom: 12px; 16 | transform: translateZ(1px); 17 | text-align: center; 18 | font-size: 0; 19 | 20 | .dot { 21 | display: inline-block; 22 | margin: 0 4px; 23 | width: 8px; 24 | height: 8px; 25 | border-radius: 50%; 26 | background: #ccc; 27 | } 28 | 29 | .active { 30 | width: 20px; 31 | border-radius: 5px; 32 | background: #fff; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/common/slider/slider.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SliderComponent } from './slider.component'; 4 | 5 | describe('SliderComponent', () => { 6 | let component: SliderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SliderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SliderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/slider/slider.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ElementRef, Input, ViewChild, Renderer2 } from '@angular/core'; 2 | import BScroll from 'better-scroll'; 3 | 4 | @Component({ 5 | selector: 'app-slider', 6 | templateUrl: './slider.component.html', 7 | styleUrls: ['./slider.component.less'] 8 | }) 9 | export class SliderComponent implements OnInit { 10 | public slider: BScroll; 11 | public dots: Array = []; 12 | public currentPageIndex: number = 0; 13 | public timer: number; 14 | private resizeTimer: number; 15 | 16 | @Input() public sliderData: Array = []; 17 | @Input() public loop: boolean = true; 18 | @Input() public autoPlay: boolean = true; 19 | @Input() public interval: number = 4000; 20 | @Input() public showDot: boolean = true; 21 | @Input() public click: boolean = true; 22 | @Input() public threshold: number = 0.3; 23 | @Input() public speed: number = 400; 24 | @ViewChild('slide') public slide: ElementRef; 25 | @ViewChild('slideGroup') public slideGroup: ElementRef; 26 | 27 | constructor(private renderer: Renderer2) { } 28 | 29 | ngOnInit() { } 30 | ngOnChanges(sliderData: Array): void { 31 | //Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here. 32 | //Add '${implements OnChanges}' to the class. 33 | setTimeout(() => { 34 | if (this.slider) { 35 | this.update(); 36 | } 37 | }, 20); 38 | } 39 | ngAfterContentInit() { 40 | if (typeof window != 'undefined') { 41 | this.update(); 42 | window.addEventListener('resize', () => { 43 | if (!this.slider || !this.slider.enabled) { 44 | return 45 | } 46 | clearTimeout(this.resizeTimer) 47 | this.resizeTimer = setTimeout(() => { 48 | if (this.slider.isInTransition) { 49 | this._onScrollEnd() 50 | } else { 51 | if (this.autoPlay) { 52 | this._play() 53 | } 54 | } 55 | this.refresh(); 56 | }, 20); 57 | }); 58 | } 59 | } 60 | 61 | ngOnDestroy() { 62 | if (this.slider) { 63 | this.slider.disable(); 64 | clearTimeout(this.timer); 65 | } 66 | } 67 | 68 | public update(): void { 69 | if (this.slider) { 70 | this.slider.destroy() 71 | } 72 | this._init() 73 | } 74 | 75 | public refresh(): void { 76 | this._setSlideWidth(true) 77 | this.slider.refresh() 78 | } 79 | 80 | public prev(): void { 81 | this.slider.prev(); 82 | } 83 | 84 | public next(): void { 85 | this.slider.next() 86 | } 87 | 88 | 89 | private _initSlide(): void { 90 | this.slider = new BScroll(this.slideGroup.nativeElement, { 91 | scrollX: true, 92 | scrollY: false, 93 | momentum: false, 94 | snap: { 95 | loop: this.loop, 96 | threshold: this.threshold, 97 | speed: this.speed 98 | }, 99 | bounce: false, 100 | stopPropagation: true, 101 | click: this.click 102 | }); 103 | this.slider.on('scrollEnd', (): void => { 104 | this._onScrollEnd(); 105 | }) 106 | 107 | this.slider.on('touchEnd', (): void => { 108 | if (this.autoPlay) { 109 | this._play() 110 | } 111 | }) 112 | 113 | this.slider.on('beforeScrollStart', (): void => { 114 | if (this.autoPlay) { 115 | clearTimeout(this.timer) 116 | } 117 | }) 118 | } 119 | 120 | private _init(): void { 121 | clearTimeout(this.timer) 122 | this.currentPageIndex = 0 123 | this._setSlideWidth(); 124 | if (this.showDot) { 125 | this._initDots() 126 | } 127 | this._initSlide() 128 | 129 | if (this.autoPlay) { 130 | this._play() 131 | } 132 | } 133 | 134 | private _onScrollEnd(): void { 135 | let pageIndex = this.slider.getCurrentPage().pageX; 136 | this.currentPageIndex = pageIndex; 137 | if (this.autoPlay) { 138 | this._play(); 139 | }; 140 | }; 141 | 142 | private _setSlideWidth(isResize?: boolean | undefined): void { 143 | const { children } = this.slideGroup.nativeElement; 144 | const { clientWidth } = this.slide.nativeElement; 145 | const groupChildren = children[0].children; 146 | let width = 0; 147 | let slideWidth = clientWidth; 148 | for (let i = 0; i < groupChildren.length; i++) { 149 | this.renderer.setStyle(groupChildren[i], 'width', slideWidth + 'px'); 150 | width += slideWidth 151 | } 152 | if (this.loop && !isResize) { 153 | width += 2 * slideWidth 154 | } 155 | this.renderer.setStyle(this.slideGroup.nativeElement.children[0], 'width', width + 'px'); 156 | } 157 | 158 | private _initDots(): void { 159 | const { children } = this.slideGroup.nativeElement; 160 | this.dots = new Array(children[0].children.length); 161 | } 162 | 163 | private _play(): void { 164 | clearTimeout(this.timer) 165 | this.timer = setTimeout(() => { 166 | this.slider.next(); 167 | }, this.interval); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/app/common/small-card/small-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

{{name}}

7 |

{{copywriter}}

8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/common/small-card/small-card.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .small_card_box { 4 | display: flex; 5 | margin-bottom: @14px; 6 | 7 | .image_box { 8 | width: 4rem; 9 | height: 4rem; 10 | 11 | img { 12 | width: 100%; 13 | } 14 | } 15 | 16 | .title_text_box { 17 | flex: 1; 18 | padding-left: @12px; 19 | 20 | .list_title { 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | display: -webkit-box; 24 | -webkit-line-clamp: 2; 25 | height: 2.6rem; 26 | // 默认编译的时候,会过滤 27 | /* autoprefixer: ignore next */ 28 | -webkit-box-orient: vertical; 29 | } 30 | 31 | .list_text { 32 | font-size: @12px; 33 | color: @gray; 34 | width: 200px; 35 | white-space: nowrap; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/common/small-card/small-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SmallCardComponent } from './small-card.component'; 4 | 5 | describe('SmallCardComponent', () => { 6 | let component: SmallCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SmallCardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SmallCardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/common/small-card/small-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-small-card', 6 | templateUrl: './small-card.component.html', 7 | styleUrls: ['./small-card.component.less'] 8 | }) 9 | export class SmallCardComponent implements OnInit { 10 | @Input() picUrl: string = ''; 11 | @Input() name: string = ''; 12 | @Input() copywriter: string = ''; 13 | @Input() id:number = 0; 14 | 15 | constructor(private router: Router) { 16 | } 17 | 18 | ngOnInit() { 19 | } 20 | 21 | public routerLink(): void { 22 | this.router.navigate(['/hot', this.id]) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/control/control.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 |

{{data.name}}

9 |

{{data.album}}·{{data.alia}}

10 |
11 |
12 |
13 |
14 | 15 | 17 | 播放 18 | 20 | 21 | 22 | 24 | 暂停 25 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 | 49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 | {{data.currentTime/1000 | formatTime}} 61 |
62 |
63 |
64 |
65 |
67 |
68 |
69 |
70 |
71 |
72 | {{data.durationTime/1000 | formatTime}} 73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | -------------------------------------------------------------------------------- /src/app/control/control.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ControlComponent } from './control.component'; 4 | 5 | describe('ControllerComponent', () => { 6 | let component: ControlComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ControlComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ControlComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/control/control.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { Observable, interval } from 'rxjs'; 3 | import { trigger, state, animate, transition, style } from '@angular/animations'; 4 | import { Store, select } from '@ngrx/store'; 5 | import { ChangeControlValue } from '../../store'; 6 | import { ControlState } from '../../store/reducers/control.reducer'; 7 | 8 | 9 | @Component({ 10 | selector: 'app-control', 11 | templateUrl: './control.component.html', 12 | styleUrls: ['./control.component.less'], 13 | animations: [ 14 | trigger('childAnimation', [ 15 | state('sideUp', style({ 16 | opacity: 1, 17 | transform: 'translateY(0)', 18 | })), 19 | state('sideDown', style({ 20 | opacity: 0, 21 | transform: 'translateY(100%)', 22 | })), 23 | transition('* => *', [ 24 | animate('300ms ease-in-out') 25 | ]), 26 | ]) 27 | ] 28 | }) 29 | 30 | export class ControlComponent implements OnInit { 31 | @ViewChild('audioElement') private audioElement: ElementRef; 32 | @ViewChild('progressBarElement') private progressBarElement: ElementRef; 33 | 34 | private controlStore$: Observable; 35 | public data: ControlState; 36 | private interval$: any; 37 | private startX: number = 0; 38 | public currentLineWidth: number = 0; 39 | private startWidth: number = 0; 40 | // 进度条的长度,初始化为 41 | public barWidth: number = 0; 42 | private static BTN_WIDTH: number = 16; 43 | 44 | 45 | constructor(private store: Store<{ controlStore: ControlState }>) { 46 | this.controlStore$ = store.pipe(select('controlStore')) 47 | } 48 | 49 | ngOnInit() { 50 | this.barWidth = this.progressBarElement.nativeElement.clientWidth; 51 | this.controlStore$.subscribe(data => { 52 | this.currentLineWidth = (data.currentTime / data.durationTime) * this.barWidth; 53 | this.data = data; 54 | console.log(data, '-------------data'); 55 | }) 56 | } 57 | 58 | ngAfterViewInit(): void { 59 | const audio = this.audioElement.nativeElement; 60 | // 获取audio标签 61 | this.store.dispatch(new ChangeControlValue({ key: 'audio', value: audio })); 62 | // 加载完成 63 | audio.addEventListener('canplay', () => { 64 | console.log('可以播放'); 65 | // 检测到可以播放就直接开始播放 66 | this.data.audio.play(); 67 | }, false); 68 | // 是否在播放,开始定时器 69 | audio.addEventListener('play', () => { 70 | const timeNumber: Observable = interval(500); 71 | this.interval$ = timeNumber.pipe().subscribe(() => { 72 | // 获取当前播放时间 73 | this.store.dispatch(new ChangeControlValue({ key: 'currentTime', value: Math.floor(this.data.audio.currentTime * 1000) })); 74 | }); 75 | this.store.dispatch(new ChangeControlValue({ key: 'status', value: 'pause' })); 76 | }, false); 77 | // 是否暂停,暂停定时器 78 | audio.addEventListener('pause', () => { 79 | this.interval$.unsubscribe(); 80 | this.store.dispatch(new ChangeControlValue({ key: 'status', value: 'play' })); 81 | }, false); 82 | // 播放结束 83 | audio.addEventListener('ended', () => { 84 | console.log('播放结束'); 85 | }, false); 86 | } 87 | 88 | public handlerPlayerList(visible: boolean): void { 89 | this.store.dispatch(new ChangeControlValue({ key: 'playListVisible', value: visible })); 90 | } 91 | 92 | public handlerPlay(): void { 93 | const { audio, status } = this.data; 94 | const newStatus = status == 'pause' ? 'play' : 'pause'; 95 | if (status == 'play') { 96 | audio.play(); 97 | } 98 | if (status == 'pause') { 99 | audio.pause(); 100 | } 101 | this.store.dispatch(new ChangeControlValue({ key: 'status', value: newStatus })); 102 | } 103 | 104 | // 展示出播放控制器 105 | public handlerVisible(visible: boolean) { 106 | this.store.dispatch(new ChangeControlValue({ key: 'player', value: visible })); 107 | } 108 | 109 | // 按下滑块 110 | public handlerPanstart(data: any) { 111 | // 暂停定时器 112 | if (this.interval$) { 113 | this.interval$.unsubscribe(); 114 | } 115 | this.startX = data.center.x; 116 | this.startWidth = this.currentLineWidth; 117 | } 118 | 119 | // 放开滑块 120 | public handlerPanend(data?: any) { 121 | this.percentChange(true); 122 | } 123 | 124 | // 滑动进度条 125 | public handlerPanmove(data: any) { 126 | // 滑动的差值 127 | const deltaX = data.center.x - this.startX; 128 | // 进度条的差值,大于0,小于总长度 129 | /** 130 | * @param this.barWidth 进度条总长度 131 | * @param ControlComponent.BTN_WIDTH 可点击区域宽度16 132 | * @param this.startWidth 绿色进度条的长度 133 | * @param deltaX 开始拖动的位置-拖动的距离 134 | */ 135 | const offsetWidth = Math.min(this.barWidth - ControlComponent.BTN_WIDTH, Math.max(0, this.startWidth + deltaX)); 136 | this.currentLineWidth = offsetWidth; 137 | } 138 | 139 | // 点击进度条 140 | public handlerTap(data: any) { 141 | const touchLeft = this.progressBarElement.nativeElement.getBoundingClientRect().left; 142 | // const newWidth 143 | this.currentLineWidth = Math.min(this.barWidth - ControlComponent.BTN_WIDTH, Math.max(0, data.center.x - touchLeft)); 144 | this.percentChange(); 145 | } 146 | 147 | // 进度条改变 148 | private percentChange(swipe?: boolean) { 149 | const currentTime = this.data.durationTime * (this.currentLineWidth / this.barWidth); 150 | console.log(currentTime, '----------currentTime'); 151 | this.store.dispatch(new ChangeControlValue({ key: 'currentTime', value: currentTime })); 152 | this.data.audio.currentTime = Math.floor(currentTime / 1000); 153 | this.data.audio.play(); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/app/details/details.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/details/details.component.less: -------------------------------------------------------------------------------- 1 | @import '../helpers/constants.less'; 2 | .detail_box{ 3 | position: fixed; 4 | top: 0; 5 | bottom: 0; 6 | width: 100%; 7 | z-index: 10; 8 | .nav_bar { 9 | position: absolute; 10 | top: 0; 11 | right: 0; 12 | left: 0; 13 | height: 40px; 14 | background-color: @color_white; 15 | z-index: 11; 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/details/details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DetailsComponent } from './details.component'; 4 | 5 | describe('DetailsComponent', () => { 6 | let component: DetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/details/details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-list-detail', 5 | templateUrl: './details.component.html', 6 | styleUrls: ['./details.component.less'] 7 | }) 8 | export class DetailsComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/helpers/common.ts: -------------------------------------------------------------------------------- 1 | import { ClientRectData } from '../../interface'; 2 | 3 | export function hasClass(el, className): boolean { 4 | let reg: RegExp = new RegExp('(^|\\s)' + className + '(\\s|$)') 5 | return reg.test(el.className) 6 | } 7 | 8 | export function addClass(el, className): void { 9 | if (hasClass(el, className)) { 10 | return 11 | } 12 | 13 | let newClass = el.className.split(' ') 14 | newClass.push(className) 15 | el.className = newClass.join(' ') 16 | } 17 | 18 | export function getRect(el): ClientRectData { 19 | if (el instanceof HTMLElement) { 20 | let rect: ClientRect = el.getBoundingClientRect(); 21 | return { 22 | top: rect.top, 23 | left: rect.left, 24 | width: rect.width, 25 | height: rect.height 26 | }; 27 | } 28 | return { 29 | top: el.offsetTop, 30 | left: el.offsetLeft, 31 | width: el.offsetWidth, 32 | height: el.offsetHeight 33 | }; 34 | } 35 | 36 | export function formatTime(timestamp): string { 37 | timestamp = Math.floor(timestamp); 38 | let minute = (Math.floor(timestamp / 60)).toString().padStart(2, '0'); 39 | let second = (timestamp % 60).toString().padStart(2, '0'); 40 | return `${minute}:${second}`; 41 | } 42 | 43 | export type EquipmentWidth = { 44 | width: number; 45 | height: number; 46 | } 47 | // 获取设备的宽高 48 | export function equipmentWidth(): EquipmentWidth { 49 | if (typeof window !== 'undefined') { 50 | return { 51 | width: document.body.clientWidth, 52 | height: document.body.clientHeight 53 | }; 54 | } 55 | return { width: 0, height: 0 }; 56 | } -------------------------------------------------------------------------------- /src/app/helpers/constants.less: -------------------------------------------------------------------------------- 1 | @8px: .4rem; 2 | @10px: .6rem; 3 | @12px: .8rem; 4 | @14px: 1rem; 5 | @16px: 1.2rem; 6 | @18px: 1.4rem; 7 | @20px: 1.6rem; 8 | @22px: 1.8rem; 9 | @24px: 2rem; 10 | @26px: 2.2rem; 11 | @30px: 2.6rem; 12 | @32px: 2.8rem; 13 | @34px: 3.0rem; 14 | @36px: 3.2rem; 15 | @40px: 3.6rem; 16 | 17 | @main_color: #31c27c; 18 | @black_color: #1F1F1F; 19 | @gray: rgba(0,0,0,.6); 20 | @bg_color: #FAFAFA; 21 | @color_white: #ffffff; 22 | @red: #FF400B; 23 | -------------------------------------------------------------------------------- /src/app/hot/hot-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HotComponent } from './hot.component'; 4 | import { SongListDetailComponent } from './song-list-detail/song-list-detail.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: HotComponent, 10 | data: { animation: 'hot' }, 11 | children: [ 12 | { 13 | path: ':id', 14 | component: SongListDetailComponent, 15 | data: { animation: 'songsList' } 16 | } 17 | ] 18 | } 19 | ]; 20 | 21 | @NgModule({ 22 | imports: [RouterModule.forChild(routes)], 23 | exports: [RouterModule] 24 | }) 25 | export class HotRoutingModule { } 26 | -------------------------------------------------------------------------------- /src/app/hot/hot.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |

热门歌单推荐

24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /src/app/hot/hot.component.less: -------------------------------------------------------------------------------- 1 | @import '../helpers/constants.less'; 2 | 3 | .hot_box { 4 | position: relative; 5 | height: auto; 6 | overflow: hidden; 7 | padding-top: 2.6rem; 8 | 9 | // 热门歌单推荐 10 | .hot_title { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | font-size: @14px; 15 | color: @main_color; 16 | text-align: center; 17 | font-weight: bold; 18 | padding: @14px 0; 19 | width: 60%; 20 | margin: 0 auto; 21 | 22 | &::after { 23 | width: 1rem; 24 | height: 2px; 25 | background-color: @main_color; 26 | content: ''; 27 | display: block; 28 | margin-left: @10px; 29 | } 30 | 31 | &::before { 32 | width: 1rem; 33 | height: 2px; 34 | background-color: @main_color; 35 | content: ''; 36 | display: block; 37 | margin-right: @10px; 38 | } 39 | } 40 | } 41 | 42 | .content_box { 43 | padding: 0 @14px; 44 | } 45 | 46 | .slide-wrapper { 47 | position: relative; 48 | width: 100%; 49 | padding-top: 40%; 50 | margin-bottom: 10px; 51 | overflow: hidden; 52 | } 53 | 54 | .slide_wrapper { 55 | position: relative; 56 | width: auto; 57 | height: 100%; 58 | } 59 | 60 | .slide_content { 61 | position: relative; 62 | height: auto; 63 | overflow: hidden; 64 | } 65 | 66 | .slide_item { 67 | float: left; 68 | box-sizing: border-box; 69 | overflow: hidden; 70 | text-align: center; 71 | white-space: nowrap; 72 | 73 | a { 74 | display: block; 75 | width: 100%; 76 | overflow: hidden; 77 | text-decoration: none; 78 | } 79 | 80 | img { 81 | display: block; 82 | width: 100%; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/hot/hot.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HotComponent } from './hot.component'; 4 | 5 | describe('HotComponent', () => { 6 | let component: HotComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HotComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HotComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/hot/hot.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { Store, select } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { LoadHotData } from '../../store'; 5 | import { HotState } from '../../store/reducers/hot.reducer'; 6 | import { ControlState } from '../../store/reducers/control.reducer'; 7 | 8 | @Component({ 9 | selector: 'app-hot', 10 | templateUrl: './hot.component.html', 11 | styleUrls: ['./hot.component.less'] 12 | }) 13 | export class HotComponent implements OnInit { 14 | public hotStore$: Observable; 15 | public controlStore$: Observable; 16 | public hotData: HotState = { 17 | slider: [], 18 | recommendList: [] 19 | }; 20 | public miniPlayer: boolean; 21 | 22 | @ViewChild('slider') slider: ElementRef; 23 | 24 | constructor(private store: Store<{ hotStore: HotState, controlStore: ControlState }>) { 25 | this.hotStore$ = store.pipe(select('hotStore')); 26 | this.controlStore$ = store.pipe(select('controlStore')); 27 | } 28 | 29 | ngOnInit() { 30 | this.store.dispatch(new LoadHotData()); 31 | this.hotStore$.subscribe(data => { 32 | this.hotData = data; 33 | }); 34 | this.controlStore$.subscribe(data => { 35 | this.miniPlayer = data.miniPlayer; 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/hot/hot.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { HotRoutingModule } from './hot-routing.module'; 5 | import { HotComponent } from './hot.component'; 6 | import { SongListDetailComponent } from './song-list-detail/song-list-detail.component'; 7 | import { ListContentComponent } from '../common/list-content/list-content.component'; 8 | import { SmallCardComponent } from '../common/small-card/small-card.component'; 9 | import { ShareModule } from '../share.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | HotRoutingModule, 15 | ShareModule 16 | ], 17 | declarations: [ 18 | HotComponent, 19 | SongListDetailComponent, 20 | ListContentComponent, 21 | SmallCardComponent, 22 | ] 23 | }) 24 | export class HotModule { } 25 | -------------------------------------------------------------------------------- /src/app/hot/song-list-detail/song-list-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
9 |
播放全部
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /src/app/hot/song-list-detail/song-list-detail.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .hot_detail_box { 4 | position: fixed; 5 | top: 0; 6 | bottom: 0; 7 | width: 100%; 8 | z-index: 10; 9 | background-color: @color_white; 10 | transform: translateY(100%); 11 | 12 | .filter { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | background: rgba(7, 17, 27, 0.4); 19 | } 20 | 21 | .layer_fill { 22 | position: relative; 23 | height: 100%; 24 | background: @color_white; 25 | } 26 | 27 | .nav_bar { 28 | position: absolute; 29 | top: 0; 30 | right: 0; 31 | left: 0; 32 | height: 40px; 33 | z-index: 11; 34 | .goBack { 35 | background: url('/assets/imgs/icon/icon_go_back.svg') 0 0 no-repeat; 36 | background-size: @20px; 37 | width: @20px; 38 | height: @20px; 39 | position: absolute; 40 | top: 10px; 41 | left: @14px; 42 | } 43 | h3 { 44 | width: 70%; 45 | margin: 0 auto; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | white-space: nowrap; 49 | color: @bg_color; 50 | font-size: @16px; 51 | line-height: 2.8rem; 52 | } 53 | } 54 | 55 | .cover_image { 56 | padding-top: 70%; 57 | height: 0px; 58 | transform: scale(1); 59 | z-index: 0; 60 | position: relative; 61 | background-size: cover; 62 | 63 | 64 | .play { 65 | position: absolute; 66 | box-sizing: border-box; 67 | width: 136px; 68 | padding: @10px 0; 69 | margin: 0 auto; 70 | text-align: center; 71 | background-color: @main_color; 72 | color: @color_white; 73 | border-radius: 100px; 74 | font-size: @14px; 75 | left: 50%; 76 | bottom: 2rem; 77 | margin-left: -68px; 78 | z-index: 1; 79 | } 80 | } 81 | } 82 | 83 | .scroll_content { 84 | position: absolute; 85 | top: 0; 86 | bottom: 0; 87 | width: 100%; 88 | background: #ffffff; 89 | } 90 | -------------------------------------------------------------------------------- /src/app/hot/song-list-detail/song-list-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SongListDetailComponent } from './song-list-detail.component'; 4 | 5 | describe('SongListDetailComponent', () => { 6 | let component: SongListDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SongListDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SongListDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/hot/song-list-detail/song-list-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { 4 | trigger, 5 | state, 6 | style, 7 | animate, 8 | transition 9 | } from '@angular/animations'; 10 | import { Router, ActivatedRoute } from '@angular/router'; 11 | import { Store, select } from '@ngrx/store'; 12 | import { LoadSongListData, ChangeControlValue, LoadSongUrl } from '../../../store'; 13 | import { HotState, SongListDetail } from '../../../store/reducers/hot.reducer'; 14 | import { Position } from '../../common/scroll/scroll.component'; 15 | import { controlStore, ControlState } from '../../../store/reducers/control.reducer'; 16 | 17 | 18 | @Component({ 19 | selector: 'song-list-detail', 20 | templateUrl: './song-list-detail.component.html', 21 | styleUrls: ['./song-list-detail.component.less'], 22 | animations: [ 23 | trigger('flyInOut', [ 24 | state('in', style({ 25 | opacity: 1, 26 | transform: 'translateY(0)', 27 | })), 28 | state('out', style({ 29 | opacity: 0, 30 | transform: 'translateY(100%)', 31 | })), 32 | transition('* => *', [ 33 | animate('300ms ease-in-out') 34 | ]), 35 | ]) 36 | ] 37 | }) 38 | export class SongListDetailComponent implements OnInit { 39 | @ViewChild('coverImage') coverImage: ElementRef; 40 | @ViewChild('scrollEl') scrollEl: ElementRef; 41 | @ViewChild('filterEl') filterEl: ElementRef; 42 | @ViewChild('playButtonEl') playButtonEl: ElementRef; 43 | @ViewChild('layerFillEl') layerFillEl: ElementRef; 44 | 45 | public detailStore$: Observable; 46 | public songDetailList: SongListDetail = { 47 | coverImgUrl: '', 48 | name: '', 49 | listData: [] 50 | }; 51 | public isShow: boolean = true; 52 | 53 | private static fixedHeight: number = 40; 54 | private scrollTop: number = 260; 55 | private coverImageHeight: number; 56 | 57 | // 构造方法时注入了HotState,ControlState,我们现在可以在store里调用两个action 58 | constructor( 59 | public router: Router, 60 | private store: Store<{ hotStore: HotState, controlStore: ControlState }>, 61 | private activeRouter: ActivatedRoute, 62 | private renderer: Renderer2 63 | ) { 64 | this.detailStore$ = store.pipe(select('hotStore')); 65 | } 66 | 67 | ngOnInit() { 68 | // 获取路由上的id,然后发送请求 69 | const songId: number = Number(this.activeRouter.snapshot.paramMap.get('id')); 70 | this.store.dispatch(new LoadSongListData(songId)); 71 | this.detailStore$.subscribe(data => { 72 | this.songDetailList = data.songListDetail; 73 | }) 74 | } 75 | 76 | ngAfterViewInit(): void { 77 | // 获取背景图高度 78 | this.coverImageHeight = this.coverImage.nativeElement.clientHeight; 79 | // 设置top高度 80 | // 使用renderer:Renderer修改样式 81 | this.renderer.setStyle(this.scrollEl.nativeElement, 'top', `${this.coverImageHeight}px`); 82 | } 83 | 84 | 85 | public goBack(arg?: boolean): void { 86 | if (arg) { 87 | if (!this.isShow) { 88 | this.router.navigate(['/hot']); 89 | } 90 | } else { 91 | this.isShow = false; 92 | } 93 | } 94 | 95 | public handlerScroll(position: Position): void { 96 | // 当触发滚动时 97 | let minScrollY = -this.coverImageHeight + SongListDetailComponent.fixedHeight; 98 | let moveY = Math.max(minScrollY, position.y); 99 | let zIndex = 0; 100 | 101 | // 当向上推得时候填充背景 102 | this.renderer.setStyle(this.layerFillEl.nativeElement, 'transform', `translate3d(0 ,${moveY}px, 0)`); 103 | this.renderer.setStyle(this.layerFillEl.nativeElement, 'webkit-transform', `translate3d(0 ,${moveY}px, 0)`); 104 | 105 | 106 | // 下拉放大、上拉模糊 107 | let scale = 1; 108 | let blur = 0; 109 | const formula = Math.abs(position.y / this.coverImageHeight); 110 | 111 | if (position.y > 0) { 112 | zIndex = 10; 113 | scale = 1 + formula; 114 | this.renderer.setStyle(this.coverImage.nativeElement, 'transform', `scale(${scale})`); 115 | this.renderer.setStyle(this.coverImage.nativeElement, 'webkitTransform', `scale(${scale})`); 116 | } else { 117 | blur = Math.min(20 * formula, 20); 118 | this.renderer.setStyle(this.filterEl.nativeElement, 'backdrop-filter', `blur(${blur}px)`); 119 | this.renderer.setStyle(this.filterEl.nativeElement, 'webkitBackdrop-filter', `blur(${blur}px)`); 120 | } 121 | 122 | // 不推到顶,留一部分 123 | if (position.y < minScrollY) { 124 | zIndex = 10; 125 | this.renderer.setStyle(this.coverImage.nativeElement, 'padding-top', 0); 126 | this.renderer.setStyle(this.coverImage.nativeElement, 'height', `${SongListDetailComponent.fixedHeight}px`); 127 | // 隐藏 随机播放全部 按钮 128 | this.renderer.setStyle(this.playButtonEl.nativeElement, 'display', 'none'); 129 | } else { 130 | this.renderer.setStyle(this.coverImage.nativeElement, 'padding-top', '70%'); 131 | this.renderer.setStyle(this.coverImage.nativeElement, 'height', '0'); 132 | // 显示 随机播放全部 按钮 133 | this.renderer.setStyle(this.playButtonEl.nativeElement, 'display', 'block'); 134 | } 135 | this.renderer.setStyle(this.coverImage.nativeElement, 'z-index', zIndex); 136 | } 137 | 138 | // 播放歌曲 139 | public handlerPlay(data?: any): void { 140 | const { listData } = this.songDetailList; 141 | const currentId: number = data ? data.currentId : listData[0].id; 142 | // 点击的全部播放从第一首开始播放 143 | this.store.dispatch(new ChangeControlValue({ key: 'current', value: data ? data.current : 0 })); 144 | this.store.dispatch(new ChangeControlValue({ key: 'currentId', value: currentId })); 145 | // 播放列表 146 | this.store.dispatch(new ChangeControlValue({ key: 'playList', value: this.songDetailList.listData })); 147 | // mini播放器 148 | this.store.dispatch(new ChangeControlValue({ key: 'miniPlayer', value: true })); 149 | // 播放器 150 | this.store.dispatch(new ChangeControlValue({ key: 'player', value: true })); 151 | // 获取歌曲详情 152 | this.store.dispatch(new LoadSongUrl(currentId)); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/app/list/list-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ListComponent } from './list.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: ListComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class ListRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/list/list.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/list/list.component.less: -------------------------------------------------------------------------------- 1 | @import '../helpers/constants.less'; 2 | .content_box{ 3 | padding: 2.6rem @14px @14px @14px; 4 | } -------------------------------------------------------------------------------- /src/app/list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListComponent } from './list.component'; 4 | 5 | describe('ListComponent', () => { 6 | let component: ListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store, select } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { LoadTopListData, ChangeTopListValue } from '../../store'; 5 | import { TopListState } from '../../store/reducers/list.reducer'; 6 | import { ControlState } from '../../store/reducers/control.reducer'; 7 | 8 | @Component({ 9 | selector: 'app-list', 10 | templateUrl: './list.component.html', 11 | styleUrls: ['./list.component.less'] 12 | }) 13 | export class ListComponent implements OnInit { 14 | public topListStore$: Observable; 15 | public controlStore$: Observable; 16 | public topListData: TopListState = { 17 | topList: [], 18 | totalData: [] 19 | }; 20 | public miniPlayer: boolean; 21 | 22 | public modifyArray(data: any[]): string { 23 | return data.map(item => item.name).join('/'); 24 | } 25 | 26 | constructor(private store: Store<{ topListStore: TopListState, controlStore: ControlState }>) { 27 | this.topListStore$ = store.pipe(select('topListStore')); 28 | this.controlStore$ = store.pipe(select('controlStore')); 29 | } 30 | 31 | ngOnInit() { 32 | this.store.dispatch(new LoadTopListData()); 33 | this.topListStore$.subscribe(data => { 34 | this.topListData = data; 35 | }); 36 | this.controlStore$.subscribe(data => { 37 | this.miniPlayer = data.miniPlayer; 38 | }) 39 | } 40 | 41 | public handlerScroll() { 42 | const { index, total } = this.topListData; 43 | if (index < total) { 44 | this.store.dispatch(new ChangeTopListValue({ key: 'index', value: index + 1 })); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/app/list/list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ListRoutingModule } from './list-routing.module'; 5 | import { ListComponent } from './list.component'; 6 | import { BigCardComponent } from '../common/big-card/big-card.component'; 7 | import { ShareModule } from '../share.module'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | ListComponent, 12 | BigCardComponent 13 | ], 14 | imports: [ 15 | CommonModule, 16 | ListRoutingModule, 17 | ShareModule 18 | ] 19 | }) 20 | export class ListModule { } 21 | -------------------------------------------------------------------------------- /src/app/my-counter/my-counter.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Current Count: {{ count$ | async }}
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/my-counter/my-counter.component.less: -------------------------------------------------------------------------------- 1 | .count { 2 | font-size: 20px; 3 | } -------------------------------------------------------------------------------- /src/app/my-counter/my-counter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { StoreModule } from '@ngrx/store'; 4 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 5 | import { MyCounterComponent } from './my-counter.component'; 6 | 7 | describe('MyCounterComponent', () => { 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ 11 | RouterTestingModule, 12 | StoreModule 13 | ], 14 | schemas: [ 15 | CUSTOM_ELEMENTS_SCHEMA 16 | ], 17 | declarations: [ 18 | MyCounterComponent 19 | ], 20 | }).compileComponents(); 21 | })); 22 | 23 | it('should create the app', () => { 24 | const fixture = TestBed.createComponent(MyCounterComponent); 25 | console.log(fixture, '--------'); 26 | }); 27 | }); 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/my-counter/my-counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store, select } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { Increment, Decrement, Reset } from '../../store/actions'; 5 | 6 | 7 | @Component({ 8 | selector: 'app-my-counter', 9 | templateUrl: './my-counter.component.html', 10 | styleUrls: ['./my-counter.component.less'], 11 | }) 12 | export class MyCounterComponent { 13 | public count$: Observable; 14 | 15 | 16 | constructor(private store: Store<{ count: number }>) { 17 | this.count$ = store.pipe(select('count')); 18 | } 19 | 20 | increment() { 21 | this.store.dispatch(new Increment()); 22 | } 23 | 24 | decrement() { 25 | this.store.dispatch(new Decrement()); 26 | } 27 | 28 | reset() { 29 | this.store.dispatch(new Reset()); 30 | } 31 | } -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.less: -------------------------------------------------------------------------------- 1 | @import '../helpers/constants.less'; 2 | 3 | .function_button { 4 | position: relative; 5 | font-size: @14px; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | text-align: center; 10 | line-height: 2.6rem; 11 | background-color: @color_white; 12 | 13 | li { 14 | flex: 1; 15 | } 16 | 17 | a { 18 | display: block; 19 | text-decoration: none; 20 | height: @32px; 21 | color: rgba(0, 0, 0, .6); 22 | font-size: @14px; 23 | &:active { 24 | background: none; 25 | } 26 | } 27 | 28 | .active { 29 | span { 30 | color: @main_color; 31 | position: relative; 32 | &:before{ 33 | content: ''; 34 | width: 2.4rem; 35 | height: 2px; 36 | border-radius: 1.5px; 37 | background-color: @main_color; 38 | position: absolute; 39 | bottom: -.4rem; 40 | left: -.2rem; 41 | } 42 | } 43 | } 44 | } 45 | 46 | // 播放进度条 47 | .player_box { 48 | .line { 49 | width: 100%; 50 | height: 2px; 51 | background-color: @main_color; 52 | } 53 | 54 | .text_button { 55 | display: flex; 56 | justify-content: space-between; 57 | padding: @14px; 58 | 59 | .text { 60 | flex: 1; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: center; 64 | align-items: center; 65 | line-height: @14px; 66 | 67 | h2 { 68 | font-size: @12px; 69 | color: @black_color; 70 | font-weight: normal; 71 | margin: 0; 72 | } 73 | 74 | p { 75 | font-size: @12px; 76 | color: @main_color; 77 | } 78 | } 79 | 80 | .player_button { 81 | width: @22px; 82 | height: @22px; 83 | background: url('/assets/imgs/audio/icon_play_circle@2x.svg') 0 0 no-repeat; 84 | background-size: @22px; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavbarComponent } from './navbar.component'; 4 | 5 | describe('NavbarComponent', () => { 6 | let component: NavbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NavbarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NavbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavBarIcon } from '../../interface'; 3 | 4 | @Component({ 5 | selector: 'app-navbar', 6 | templateUrl: './navbar.component.html', 7 | styleUrls: ['./navbar.component.less'] 8 | }) 9 | export class NavbarComponent implements OnInit { 10 | iconArr: NavBarIcon[] = [ 11 | { 12 | text: '推荐', 13 | routerLink: '/hot', 14 | id: 10001 15 | }, 16 | { 17 | text: '排行', 18 | routerLink: '/list', 19 | id: 10002 20 | }, 21 | { 22 | text: '搜索', 23 | routerLink: '/search', 24 | id: 10003 25 | } 26 | ]; 27 | constructor() { } 28 | 29 | ngOnInit() { 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/portal/banner/banner.component.html: -------------------------------------------------------------------------------- 1 | 31 |
32 | 33 |
DOWNLOAD
34 |
35 | -------------------------------------------------------------------------------- /src/app/portal/banner/banner.component.less: -------------------------------------------------------------------------------- 1 | .banner_box { 2 | background-color: #020916; 3 | 4 | .banner { 5 | height: 278px; 6 | width: 100%; 7 | background-image: url('/assets/imgs/audio/image_banner.jpg'); 8 | background-repeat: no-repeat; 9 | background-size: cover; 10 | } 11 | 12 | .function_info { 13 | position: relative; 14 | padding-left: 300px; 15 | } 16 | 17 | .function_box { 18 | display: flex; 19 | justify-content: space-between; 20 | padding: 24px 24px 30px 0; 21 | 22 | .info_box { 23 | display: flex; 24 | position: absolute; 25 | top: -188px; 26 | left: 60px; 27 | width: 520px; 28 | 29 | .image { 30 | width: 224px; 31 | height: 224px; 32 | background: url('/assets/imgs/audio/ramdan-authentic-unsplash.png') 0 0 no-repeat; 33 | // background-size: cover; 34 | } 35 | 36 | .description { 37 | .play_list { 38 | color: #C2C2C2; 39 | font-size: 13px; 40 | } 41 | 42 | .big_title { 43 | color: #EEEEEE; 44 | font-size: 46px; 45 | font-weight: bold; 46 | text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.23); 47 | } 48 | 49 | .name { 50 | color: #D6D6D6; 51 | font-size: 18px; 52 | line-height: 22px; 53 | text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); 54 | } 55 | 56 | .time { 57 | font-size: 14px; 58 | color: #C8C8C8; 59 | line-height: 17px; 60 | } 61 | } 62 | } 63 | 64 | 65 | .function_item { 66 | display: flex; 67 | } 68 | 69 | .follower { 70 | color: #A9A9A9; 71 | font-size: 12px; 72 | 73 | .text {} 74 | 75 | .number {} 76 | } 77 | } 78 | 79 | .play, 80 | .follow { 81 | width: 116px; 82 | height: 36px; 83 | border-radius: 18px; 84 | font-size: 14px; 85 | text-align: center; 86 | line-height: 36px; 87 | } 88 | 89 | li { 90 | margin: 0 6px; 91 | } 92 | 93 | .play { 94 | background: rgba(30, 192, 122, 1); 95 | color: #020916; 96 | } 97 | 98 | .follow { 99 | border: 1px solid #979797; 100 | background: none; 101 | color: #A9A9A9; 102 | } 103 | 104 | .share { 105 | width: 36px; 106 | height: 36px; 107 | background: url('/assets/imgs/audio/icon_share@2x.png') 0 0 no-repeat; 108 | background-size: 36px; 109 | } 110 | 111 | .more { 112 | width: 36px; 113 | height: 36px; 114 | background: url('/assets/imgs/audio/icon_more@2x.png')0 0 no-repeat; 115 | background-size: 36px; 116 | } 117 | 118 | } 119 | 120 | // 筛选过滤 121 | .filter { 122 | color: #575757; 123 | display: flex; 124 | justify-content: space-between; 125 | background-color: #020916; 126 | padding: 0 24px 24px 24px; 127 | font-size: 13px; 128 | 129 | .icon_noun_search { 130 | background: url('/assets/imgs/audio/icon_noun_search@2x.png') 0 0 no-repeat; 131 | background-size: 16px; 132 | padding-left: 24px; 133 | } 134 | 135 | .icon_noun_down { 136 | background: url('/assets/imgs/audio/icon_noun_down@2x.png') right 0 no-repeat; 137 | background-size: 16px; 138 | padding-right: 24px; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/app/portal/banner/banner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BannerComponent } from './banner.component'; 4 | 5 | describe('BannerComponent', () => { 6 | let component: BannerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BannerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BannerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/portal/banner/banner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-banner', 5 | templateUrl: './banner.component.html', 6 | styleUrls: ['./banner.component.less'] 7 | }) 8 | export class BannerComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/portal/portal.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 播放列表 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/portal/portal.component.less: -------------------------------------------------------------------------------- 1 | .audio_box { 2 | 3 | 4 | } -------------------------------------------------------------------------------- /src/app/portal/portal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PortalComponent } from './portal.component'; 4 | 5 | describe('PortalComponent', () => { 6 | let component: PortalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PortalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PortalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/portal/portal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-portal', 5 | templateUrl: './portal.component.html', 6 | styleUrls: ['./portal.component.less'] 7 | }) 8 | export class PortalComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |

2 | profile works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/app/profile/profile.component.less -------------------------------------------------------------------------------- /src/app/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProfileComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProfileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-profile', 5 | templateUrl: './profile.component.html', 6 | styleUrls: ['./profile.component.less'] 7 | }) 8 | export class ProfileComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/search/hot-search/hot-search.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/app/search/hot-search/hot-search.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .hot_search_box { 4 | background-color: @color_white; 5 | padding: 0 @14px @14px @14px; 6 | margin-top: @14px; 7 | h1 { 8 | font-size: @14px; 9 | font-weight: bold; 10 | color: @gray; 11 | padding-bottom: @14px; 12 | } 13 | 14 | span { 15 | text-decoration: none; 16 | color: @gray; 17 | padding: 0 .8rem; 18 | margin: 0 @12px @10px 0; 19 | display: inline-block; 20 | font-size: @14px; 21 | height: 22px; 22 | line-height: 22px; 23 | border: 1px solid @gray; 24 | border-radius: 4px; 25 | word-break: keep-all; 26 | font-size: @14px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/search/hot-search/hot-search.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HotSearchComponent } from './hot-search.component'; 4 | 5 | describe('HotSearchComponent', () => { 6 | let component: HotSearchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HotSearchComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HotSearchComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/search/hot-search/hot-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-hot-search', 5 | templateUrl: './hot-search.component.html', 6 | styleUrls: ['./hot-search.component.less'] 7 | }) 8 | export class HotSearchComponent implements OnInit { 9 | @Input() visible: boolean = true; 10 | 11 | constructor() { } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | public handlerHotWorlds(text: string) { 17 | console.log(text); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/search/search-input/search-input.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/app/search/search-input/search-input.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .search_box { 4 | display: flex; 5 | justify-content: space-between; 6 | padding:@14px; 7 | background-color: @bg_color; 8 | 9 | .search { 10 | display: flex; 11 | background: white; 12 | border-radius: 2px; 13 | overflow: hidden; 14 | flex: 1; 15 | align-items: center; 16 | position: relative; 17 | 18 | .icon_delete { 19 | position: absolute; 20 | top: @10px; 21 | right: @14px; 22 | width: 18px; 23 | height: 18px; 24 | background: #b1b1b1; 25 | text-indent: -9999px; 26 | border-radius: 99px; 27 | display: inline; 28 | 29 | &:after { 30 | content: ""; 31 | display: block; 32 | position: absolute; 33 | left: 50%; 34 | top: 50%; 35 | border-radius: 8px; 36 | background: #fff; 37 | transform: rotate(45deg); 38 | width: 2px; 39 | height: 10px; 40 | margin-left: -1px; 41 | margin-top: -5px; 42 | } 43 | 44 | &:before { 45 | content: ""; 46 | display: block; 47 | position: absolute; 48 | left: 50%; 49 | top: 50%; 50 | border-radius: 8px; 51 | background: #fff; 52 | transform: rotate(45deg); 53 | width: 10px; 54 | height: 2px; 55 | margin-left: -5px; 56 | margin-top: -1px; 57 | } 58 | 59 | } 60 | 61 | .icon_search { 62 | background: url('/assets/imgs/navbar/icon_search@2x.svg') @16px @12px no-repeat; 63 | background-size: @14px; 64 | width: @14px; 65 | height: @16px; 66 | padding: .6rem @16px .6rem @16px; 67 | } 68 | 69 | form { 70 | flex: 1; 71 | 72 | input { 73 | display: block; 74 | border: none; 75 | outline: none; 76 | font-size: @14px; 77 | height: 100%; 78 | width: 100%; 79 | padding: 0; 80 | } 81 | } 82 | } 83 | 84 | .cancel { 85 | width: 2.6rem; 86 | padding: 0 0 0 @12px; 87 | line-height: 2.4rem; 88 | font-size: @14px; 89 | text-align: center; 90 | color: gray; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/app/search/search-input/search-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SearchInputComponent } from './search-input.component'; 4 | 5 | describe('SearchInputComponent', () => { 6 | let component: SearchInputComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SearchInputComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SearchInputComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/search/search-input/search-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-search-input', 5 | templateUrl: './search-input.component.html', 6 | styleUrls: ['./search-input.component.less'] 7 | }) 8 | export class SearchInputComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/search/search-list/search-list.component.html: -------------------------------------------------------------------------------- 1 | 135 | -------------------------------------------------------------------------------- /src/app/search/search-list/search-list.component.less: -------------------------------------------------------------------------------- 1 | @import '../../helpers/constants.less'; 2 | 3 | .search_box { 4 | .search_list_box { 5 | display: flex; 6 | align-items: center; 7 | position: relative; 8 | padding: @12px @14px; 9 | 10 | .media_avatar { 11 | width: 2.8rem; 12 | height: 2.8rem; 13 | 14 | img { 15 | width: 100%; 16 | display: block; 17 | } 18 | } 19 | 20 | .info { 21 | height: 2.8rem; 22 | padding-left: @12px; 23 | flex-direction: column; 24 | display: flex; 25 | justify-content: center; 26 | 27 | .main_title { 28 | font-size: @14px; 29 | font-weight: normal; 30 | } 31 | 32 | .sub_title { 33 | font-size: @12px; 34 | color: @gray; 35 | } 36 | 37 | &::after { 38 | content: ""; 39 | position: absolute; 40 | height: 1px; 41 | top: 0; 42 | left: 0; 43 | right: 0; 44 | background: #e5e5e5; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/search/search-list/search-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SearchListComponent } from './search-list.component'; 4 | 5 | describe('SearchListComponent', () => { 6 | let component: SearchListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SearchListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SearchListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/search/search-list/search-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-search-list', 5 | templateUrl: './search-list.component.html', 6 | styleUrls: ['./search-list.component.less'] 7 | }) 8 | export class SearchListComponent implements OnInit { 9 | @Input() singer: boolean = true; 10 | public singerImageUrl: string = 'https://y.gtimg.cn/music/photo_new/T001R68x68M000003hP1b82zqCtm.jpg?max_age=2592000'; 11 | public songImageUrl: string = '/assets/imgs/icon/icon_qq_music.svg' 12 | constructor() { } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/search/search.component.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/app/search/search.component.less: -------------------------------------------------------------------------------- 1 | @import "../helpers/constants.less"; 2 | .search_box { 3 | position: relative; 4 | height: 100%; 5 | background-color: @color_white; 6 | .search_list{ 7 | position: fixed; 8 | top: 7.0rem; 9 | bottom: 0; 10 | width: 100%; 11 | .search_content { 12 | height: auto; 13 | padding-bottom: 60px; 14 | background-color: @color_white; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/search/search.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SearchComponent } from './search.component'; 4 | 5 | describe('SearchComponent', () => { 6 | let component: SearchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SearchComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SearchComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-search', 5 | templateUrl: './search.component.html', 6 | styleUrls: ['./search.component.less'] 7 | }) 8 | export class SearchComponent implements OnInit { 9 | public listVisible:boolean = false; 10 | public hotSearchVisible:boolean = true; 11 | /* 12 | DOM节点 13 | @params search 14 | this.search.nativeElement 15 | */ 16 | // @ViewChild('search') search; 17 | 18 | constructor() { } 19 | 20 | ngOnInit() { 21 | // console.log(this.search.nativeElement); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/share.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HammertimeDirective } from '../directive/hammertime.directive'; 4 | import { ScrollComponent } from './common/scroll/scroll.component'; 5 | import { SliderComponent } from './common/slider/slider.component'; 6 | import { FormatTimePipe } from '../pipes/format-time.pipe'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | ScrollComponent, 11 | HammertimeDirective, 12 | SliderComponent, 13 | FormatTimePipe 14 | ], 15 | imports: [ 16 | CommonModule 17 | ], 18 | exports: [ 19 | ScrollComponent, 20 | HammertimeDirective, 21 | SliderComponent, 22 | FormatTimePipe 23 | ] 24 | }) 25 | export class ShareModule { } 26 | -------------------------------------------------------------------------------- /src/app/smile/smile-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { SmileComponent } from './smile.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SmileComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SmileRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/smile/smile.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Welcome to {{ title }}! 4 |

5 | Angular Logo 6 |

Here are some links to help you start:

7 | 8 |
-------------------------------------------------------------------------------- /src/app/smile/smile.component.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/app/smile/smile.component.less -------------------------------------------------------------------------------- /src/app/smile/smile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SmileComponent } from './smile.component'; 4 | 5 | describe('SmileComponent', () => { 6 | let component: SmileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SmileComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SmileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/smile/smile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-smile', 5 | templateUrl: './smile.component.html', 6 | styleUrls: ['./smile.component.less'] 7 | }) 8 | export class SmileComponent implements OnInit { 9 | title: string = 'angular-music-player'; 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/smile/smile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { SmileComponent } from './smile.component'; 5 | import { SmileRoutingModule } from './smile-routing.module'; 6 | import { MyCounterComponent } from '../my-counter/my-counter.component'; 7 | 8 | // 按模塊加載,注入MyCounterComponent到模塊中 9 | @NgModule({ 10 | declarations: [ 11 | SmileComponent, 12 | MyCounterComponent 13 | ], 14 | imports: [ 15 | CommonModule, 16 | SmileRoutingModule 17 | ] 18 | }) 19 | export class SmileModule { } 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/MP_verify_xJSgoVrk3swqlXsD.txt: -------------------------------------------------------------------------------- 1 | xJSgoVrk3swqlXsD -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_add@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_add@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_add@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | add 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_back@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_back@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | back 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_forward@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_forward@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_forward@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | forward 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_heart@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_heart@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_heart@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | heart 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_loop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_loop@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_loop@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | loop 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_oval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_oval.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_play@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_play@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_play@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_play_circle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_play_circle@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_play_circle@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play-circle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_shuffle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/icon_shuffle@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/icon_shuffle@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | shuffle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/imgs/audio/image_schermata@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/image_schermata@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/image_song_cover@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/image_song_cover@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/audio/img@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/audio/img@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_go_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_list.svg: -------------------------------------------------------------------------------- 1 | 资源 11 -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_loop.svg: -------------------------------------------------------------------------------- 1 | icon_loopicon-play -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_next.svg: -------------------------------------------------------------------------------- 1 | icon_nexticon-play -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_pause.svg: -------------------------------------------------------------------------------- 1 | icon_pauseicon-play -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_play.svg: -------------------------------------------------------------------------------- 1 | 资源 1icon-play -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_pre.svg: -------------------------------------------------------------------------------- 1 | icon_preicon-play -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_qq_music.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Clipped 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/assets/imgs/icon/icon_single_loop.svg: -------------------------------------------------------------------------------- 1 | icon_single_loopicon-play -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_flash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_flash@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_flash@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Home 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_flash_active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_flash_active@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_flash_active@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_music@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_music@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_music@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My music 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_music_active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_music_active@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_music_active@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My music 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_profile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_profile@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_profile@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Profile 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_profile_active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_profile_active@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_profile_active@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Profile 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_search@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_search@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_search@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Search 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_search_active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/navbar/icon_search_active@2x.png -------------------------------------------------------------------------------- /src/assets/imgs/navbar/icon_search_active@2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Search 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/imgs/smile_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/imgs/smile_icon.png -------------------------------------------------------------------------------- /src/assets/logo/icon_add.svg: -------------------------------------------------------------------------------- 1 | icon_add -------------------------------------------------------------------------------- /src/assets/logo/icon_equal.svg: -------------------------------------------------------------------------------- 1 | icon_equal -------------------------------------------------------------------------------- /src/assets/logo/image_angular.svg: -------------------------------------------------------------------------------- 1 | image_angular -------------------------------------------------------------------------------- /src/assets/logo/image_ngrx.svg: -------------------------------------------------------------------------------- 1 | image_ngrx -------------------------------------------------------------------------------- /src/assets/logo/page01-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/logo/page01-min.jpg -------------------------------------------------------------------------------- /src/assets/logo/page02-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/assets/logo/page02-min.jpg -------------------------------------------------------------------------------- /src/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/common.js -------------------------------------------------------------------------------- /src/directive/hammertime.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[hammertime]' 5 | }) 6 | export class HammertimeDirective { 7 | 8 | @Output() doubleTap = new EventEmitter(); 9 | @Output() tripleTap = new EventEmitter(); 10 | 11 | constructor() { } 12 | 13 | 14 | @HostListener('tap', ['$event']) 15 | onTap(e) { 16 | if (e.tapCount === 2) { 17 | this.doubleTap.emit(e) 18 | } 19 | 20 | if (e.tapCount === 3) { 21 | this.tripleTap.emit(e) 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tecode/angular-music-player/01b7e8dd5bf55c33fc2b51423af899c8723e16d6/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QQ音乐 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/interceptor/httpconfig.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { 4 | HttpInterceptor, 5 | HttpRequest, 6 | HttpResponse, 7 | HttpHandler, 8 | HttpEvent, 9 | HttpErrorResponse 10 | } from '@angular/common/http'; 11 | 12 | import { Observable, throwError } from 'rxjs'; 13 | import { map, catchError } from 'rxjs/operators'; 14 | 15 | @Injectable() 16 | export class HttpConfigInterceptor implements HttpInterceptor { 17 | // constructor(public errorDialogService: ErrorDialogService) { } 18 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 19 | let token: string | boolean = false; 20 | // 兼容服务端渲染 21 | if (typeof window !== 'undefined') { 22 | token = localStorage.getItem('token'); 23 | } 24 | 25 | if (token) { 26 | request = request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + token) }); 27 | } 28 | 29 | if (!request.headers.has('Content-Type')) { 30 | request = request.clone({ headers: request.headers.set('Content-Type', 'application/json') }); 31 | } 32 | 33 | request = request.clone({ headers: request.headers.set('Accept', 'application/json') }); 34 | 35 | return next.handle(request).pipe( 36 | map((event: HttpEvent) => { 37 | if (event instanceof HttpResponse) { 38 | // console.log('event--->>>', event); 39 | // this.errorDialogService.openDialog(event); 40 | } 41 | return event; 42 | }), 43 | catchError((error: HttpErrorResponse) => { 44 | let data = {}; 45 | data = { 46 | reason: error && error.error.reason ? error.error.reason : '', 47 | status: error.status 48 | }; 49 | // this.errorDialogService.openDialog(data); 50 | console.log('拦截器捕获的错误', data); 51 | return throwError(error); 52 | })); 53 | } 54 | } -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface NavBarIcon { 2 | routerLink: string; 3 | text: string; 4 | id: number; 5 | } 6 | 7 | export interface ClientRectData { 8 | top: number, 9 | left: number, 10 | width: number, 11 | height: number 12 | } -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/angular-music-player'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { AppServerModule } from './app/app.server.module'; 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | // 正式环境打包去掉console.log的打印 10 | if (window) { 11 | window.console.log = function () { }; 12 | } 13 | } 14 | 15 | document.addEventListener('DOMContentLoaded', () => { 16 | platformBrowserDynamic().bootstrapModule(AppModule) 17 | .catch(err => console.error(err)); 18 | }); 19 | -------------------------------------------------------------------------------- /src/pipes/format-time.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormatTimePipe } from './format-time.pipe'; 2 | 3 | describe('FormatTimePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FormatTimePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/pipes/format-time.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { formatTime } from '../app/helpers/common'; 3 | 4 | @Pipe({ 5 | name: 'formatTime' 6 | }) 7 | export class FormatTimePipe implements PipeTransform { 8 | 9 | transform(value: any, args?: any): string { 10 | return formatTime(value); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "https://music.soscoon.com/api", 4 | "secure": false, 5 | "pathRewrite": { 6 | "^/api": "" 7 | }, 8 | "changeOrigin": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/services/control.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class ControlService { 8 | 9 | constructor(private http: HttpClient) { 10 | } 11 | 12 | // 热门歌单推荐 13 | songUrl(data) { 14 | return this.http.get(`/api/song/url?id=${data.id}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/hot.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from "@angular/common/http"; 3 | 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class HotService { 10 | 11 | constructor(private http: HttpClient) { 12 | } 13 | 14 | // 热门歌单推荐 15 | popularList() { 16 | return this.http.get('/api/personalized'); 17 | } 18 | 19 | // 轮播图 20 | loopList() { 21 | return this.http.get('/api/banner'); 22 | } 23 | 24 | // 获取歌单详情 25 | songListDetail(data: any) { 26 | return this.http.get(`/api/playlist/detail?id=${data.id}`); 27 | } 28 | } -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hot.service'; 2 | export * from './list.service'; 3 | export * from './search.service'; 4 | export * from './control.service'; -------------------------------------------------------------------------------- /src/services/list.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from "@angular/common/http"; 3 | 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class TopListService { 10 | 11 | constructor(private http: HttpClient) { 12 | } 13 | 14 | // 轮播图 15 | topList() { 16 | return this.http.get('/api/top/list?idx=1'); 17 | } 18 | } -------------------------------------------------------------------------------- /src/services/search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from "@angular/common/http"; 3 | 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class SearchService { 10 | 11 | constructor(private http: HttpClient) { 12 | } 13 | 14 | // 热搜 15 | hotKeyWorlds() { 16 | return this.http.get('/api/search/hot'); 17 | } 18 | 19 | // 搜索 20 | searchResult(value: string) { 21 | return this.http.get(`/search?keywords=${value}`); 22 | } 23 | } -------------------------------------------------------------------------------- /src/store/actions/control.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum ControlActionTypes { 4 | ToggleSong = '[ Control ] ToggleSong', 5 | ToggleStatus = '[ Control ] ToggleStatus', 6 | PlayOrder = '[ Control ] PlayOrder', 7 | RestControlData = '[ Control ] RestControlData', 8 | ChangeValue = '[ Control ] ChangeValue', 9 | LoadSongUrl = '[ Control ] LoadSongUrl', 10 | LoadSongUrlSuccess = '[ Control ] LoadSongUrlSuccess', 11 | ControlError = '[ Control ] ControlError', 12 | } 13 | 14 | // 上一曲下一曲 15 | export class ToggleSong implements Action { 16 | readonly type = ControlActionTypes.ToggleSong; 17 | constructor(public data: any) { } 18 | } 19 | 20 | // 播放暂停 21 | export class ToggleStatus implements Action { 22 | readonly type = ControlActionTypes.ToggleStatus; 23 | constructor(public data: any) { } 24 | } 25 | 26 | // 顺序播放或单曲循环 27 | export class PlayOrder implements Action { 28 | readonly type = ControlActionTypes.PlayOrder; 29 | constructor(public data: any) { } 30 | } 31 | 32 | // 重置数据 33 | export class RestControlData implements Action { 34 | readonly type = ControlActionTypes.RestControlData; 35 | } 36 | 37 | // 设置数据 38 | export class ChangeControlValue implements Action { 39 | readonly type = ControlActionTypes.ChangeValue; 40 | constructor(public payload: { key: string; value: any }) { } 41 | } 42 | 43 | // 获取音乐播放地址 44 | export class LoadSongUrl implements Action { 45 | readonly type = ControlActionTypes.LoadSongUrl; 46 | constructor(public id: number) { } 47 | } 48 | 49 | // 获取音乐播放地址成功 50 | export class LoadSongUrlSuccess implements Action { 51 | readonly type = ControlActionTypes.LoadSongUrlSuccess; 52 | constructor(public payload: { key: string; value: any }) { } 53 | } 54 | 55 | // 获取出错 56 | 57 | export class ControlError implements Action { 58 | readonly type = ControlActionTypes.ControlError; 59 | constructor(public payload: { key: string; value: any }) { } 60 | } -------------------------------------------------------------------------------- /src/store/actions/counter.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum ActionTypes { 4 | Increment = '[Counter Component] Increment', 5 | Decrement = '[Counter Component] Decrement', 6 | Reset = '[Counter Component] Reset', 7 | } 8 | 9 | export class Increment implements Action { 10 | readonly type = ActionTypes.Increment; 11 | } 12 | 13 | export class Decrement implements Action { 14 | readonly type = ActionTypes.Decrement; 15 | } 16 | 17 | export class Reset implements Action { 18 | readonly type = ActionTypes.Reset; 19 | } -------------------------------------------------------------------------------- /src/store/actions/hot.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum HotActionTypes { 4 | LoadData = '[Hot API] Load Data', 5 | LoadSuccess = '[Hot API] Data Loaded Success', 6 | LoadError = '[Hot API] Load Error', 7 | LoadSongListData = '[Hot API] Load Song List', 8 | LoadSongListSuccess = '[Hot API] Song List Data Loaded Success', 9 | LoadSongListError = '[Hot API] Song List Data Loaded Error', 10 | ChangeValue = '[Hot Page] ChangeValue' 11 | } 12 | 13 | // 获取热门推荐数据 14 | export class LoadHotData implements Action { 15 | readonly type = HotActionTypes.LoadData; 16 | } 17 | 18 | export class LoadSuccess implements Action { 19 | readonly type = HotActionTypes.LoadSuccess; 20 | } 21 | 22 | export class LoadError implements Action { 23 | readonly type = HotActionTypes.LoadError; 24 | constructor(public data: any) { } 25 | } 26 | 27 | // 获取歌单详情数据 28 | export class LoadSongListData implements Action { 29 | readonly type = HotActionTypes.LoadSongListData; 30 | constructor(public id: number) { } 31 | } 32 | 33 | export class LoadSongListSuccess implements Action { 34 | readonly type = HotActionTypes.LoadSongListSuccess; 35 | } 36 | 37 | export class LoadSongListError implements Action { 38 | readonly type = HotActionTypes.LoadSongListError; 39 | constructor(public data: any) { } 40 | } 41 | 42 | export class ChangeHotValue implements Action { 43 | readonly type = HotActionTypes.ChangeValue; 44 | constructor(public payload: { key: string; value: any }) { } 45 | } -------------------------------------------------------------------------------- /src/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './counter.action'; 2 | export * from './hot.action'; 3 | export * from './list.action'; 4 | export * from './control.action'; 5 | export * from './search.actions'; 6 | 7 | -------------------------------------------------------------------------------- /src/store/actions/list.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum TopListActionTypes { 4 | LoadData = '[TopList Page] Load Data', 5 | LoadSuccess = '[TopList API] Data Loaded Success', 6 | LoadError = '[TopList Page] Load Error', 7 | ChangeValue = '[Hot Page] ChangeValue' 8 | } 9 | 10 | // 获取数据 11 | export class LoadTopListData implements Action { 12 | readonly type = TopListActionTypes.LoadData; 13 | } 14 | 15 | export class LoadTopListSuccess implements Action { 16 | readonly type = TopListActionTypes.LoadSuccess; 17 | } 18 | 19 | export class LoadTopListError implements Action { 20 | readonly type = TopListActionTypes.LoadError; 21 | constructor(public data: any) { } 22 | } 23 | 24 | export class ChangeTopListValue implements Action { 25 | readonly type = TopListActionTypes.ChangeValue; 26 | constructor(public payload: { key: string; value: any }) { } 27 | } -------------------------------------------------------------------------------- /src/store/actions/search.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum SearchActionTypes { 4 | LoadHotKeyWord = '[Search] Load LoadHotKeyWord', 5 | LoadResultList = '[Search] Load LoadResultList', 6 | ChangeValue = '[Search] ChangeValue' 7 | } 8 | 9 | export class LoadSearchHotKeyWord implements Action { 10 | readonly type = SearchActionTypes.LoadHotKeyWord; 11 | } 12 | 13 | export class LoadSearchResultList implements Action { 14 | readonly type = SearchActionTypes.LoadResultList; 15 | constructor(public payload: { key: string; value: any }) { }; 16 | } 17 | 18 | export class ChangeSearchValue implements Action { 19 | readonly type = SearchActionTypes.ChangeValue; 20 | constructor(public payload: { key: string; value: any }) { }; 21 | } 22 | 23 | 24 | 25 | // export type SearchActions = LoadHotKeyWord; 26 | -------------------------------------------------------------------------------- /src/store/effects/control.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { mergeMap, catchError, map } from 'rxjs/operators'; 4 | import { of } from 'rxjs'; 5 | import { ControlService } from '../../services'; 6 | import { ControlActionTypes, ControlError } from '../actions'; 7 | 8 | 9 | 10 | @Injectable() 11 | export class ControlEffects { 12 | @Effect() 13 | loadSongUrl$ = this.actions$ 14 | .pipe( 15 | ofType(ControlActionTypes.LoadSongUrl), 16 | mergeMap((data) => this.controlService.songUrl(data) 17 | .pipe( 18 | map(data => ({ type: ControlActionTypes.LoadSongUrlSuccess, payload: data })), 19 | catchError((err) => { 20 | //call the action if there is an error 21 | return of(new ControlError(err["message"])); 22 | }) 23 | )) 24 | ) 25 | 26 | 27 | constructor( 28 | private actions$: Actions, 29 | private controlService: ControlService 30 | ) { } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/store/effects/hot.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { map, mergeMap, catchError } from 'rxjs/operators'; 4 | import { HotActionTypes, LoadError, LoadSongListError } from '../actions'; 5 | import { of, forkJoin } from 'rxjs'; 6 | import { HotService } from '../../services'; 7 | 8 | 9 | @Injectable() 10 | export class HotEffects { 11 | 12 | @Effect() 13 | loadHotData$ = this.actions$ 14 | .pipe( 15 | ofType(HotActionTypes.LoadData), 16 | mergeMap(() => 17 | forkJoin([ 18 | this.hotService.loopList() 19 | .pipe(catchError(() => of({ 'code': -1, banners: [] }))), 20 | this.hotService.popularList() 21 | .pipe(catchError(() => of({ 'code': -1, result: [] }))), 22 | ]) 23 | .pipe( 24 | map(data => ({ type: HotActionTypes.LoadSuccess, payload: data })), 25 | catchError((err) => { 26 | //call the action if there is an error 27 | return of(new LoadError(err["message"])); 28 | }) 29 | )) 30 | ) 31 | 32 | @Effect() 33 | loadSongListData$ = this.actions$ 34 | .pipe( 35 | ofType(HotActionTypes.LoadSongListData), 36 | mergeMap((params) => this.hotService.songListDetail(params) 37 | .pipe( 38 | map((data: any) => ({ type: HotActionTypes.LoadSongListSuccess, payload: data.playlist })), 39 | catchError((err) => { 40 | //call the action if there is an error 41 | return of(new LoadSongListError(err["message"])); 42 | }) 43 | )) 44 | ) 45 | constructor( 46 | private actions$: Actions, 47 | private hotService: HotService 48 | ) { } 49 | } -------------------------------------------------------------------------------- /src/store/effects/index.ts: -------------------------------------------------------------------------------- 1 | import { HotEffects } from './hot.effects'; 2 | import { TopListEffects } from './list.effects'; 3 | import { ControlEffects } from './control.effects' 4 | 5 | 6 | export const effects: any[] = [HotEffects, TopListEffects, ControlEffects]; 7 | 8 | 9 | export * from './hot.effects'; 10 | export * from './list.effects'; 11 | export * from './control.effects'; 12 | -------------------------------------------------------------------------------- /src/store/effects/list.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { map, mergeMap, catchError } from 'rxjs/operators'; 4 | import { of } from 'rxjs'; 5 | import { TopListActionTypes, LoadTopListError } from '../actions'; 6 | import { TopListService } from '../../services'; 7 | 8 | 9 | @Injectable() 10 | export class TopListEffects { 11 | 12 | @Effect() 13 | loadListData$ = this.actions$ 14 | .pipe( 15 | ofType(TopListActionTypes.LoadData), 16 | mergeMap(() => this.topListService.topList() 17 | .pipe( 18 | map(data => ({ type: '[TopList API] Data Loaded Success', payload: data })), 19 | catchError((err) => { 20 | //call the action if there is an error 21 | return of(new LoadTopListError(err["message"])); 22 | }) 23 | )) 24 | ) 25 | 26 | constructor( 27 | private actions$: Actions, 28 | private topListService: TopListService 29 | ) { } 30 | } -------------------------------------------------------------------------------- /src/store/effects/search.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { map, mergeMap, catchError } from 'rxjs/operators'; 4 | import { of } from 'rxjs'; 5 | import { TopListActionTypes, LoadTopListError } from '../actions'; 6 | import { SearchService } from '../../services'; 7 | 8 | 9 | @Injectable() 10 | export class SearchEffects { 11 | 12 | // 获取搜索列表 13 | @Effect() 14 | searchData$ = this.actions$ 15 | .pipe( 16 | ofType(TopListActionTypes.LoadData), 17 | mergeMap((data) => this.searchListService.searchResult(data) 18 | .pipe( 19 | map(data => ({ type: '[TopList API] Data Loaded Success', payload: data })), 20 | catchError((err) => { 21 | //call the action if there is an error 22 | return of(new LoadTopListError(err["message"])); 23 | }) 24 | )) 25 | ) 26 | 27 | // 热搜推荐 28 | 29 | constructor( 30 | private actions$: Actions, 31 | private searchListService: SearchService 32 | ) { } 33 | } -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./reducers"; //export reducers dir 2 | export * from "./actions" // export actions dir 3 | export * from "./effects" // export effects dir -------------------------------------------------------------------------------- /src/store/reducers/control.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { ControlActionTypes } from '../actions'; 3 | import { timeout } from 'rxjs/operators'; 4 | import { Observable } from 'rxjs'; 5 | 6 | export interface ControlAction extends Action { 7 | payload: any 8 | } 9 | 10 | const statusHandler = (state: ControlState, key: string, value: any): void => { 11 | console.log(state.audio); 12 | }; 13 | 14 | const modifyArray = (data: any[]): string => { 15 | return data.map(item => item.name).join('/'); 16 | } 17 | 18 | /** 19 | * @param loading 是否在加载 20 | * @param status 播放还是暂停 21 | * @param playList 播放列表 22 | * @param miniPlayer 小窗播放,播放最小化 23 | * @param player 大窗播放 24 | * @param audio audi标签 25 | * @param playListVisible 播放列表可见 26 | * @param src 播放地址 27 | * @param coverUrl 封面 28 | * @param currentTime 当前播放时间 29 | * @param durationTime 总时长 30 | * @param current 当前播放的歌曲 31 | * @param currentId 当前播放的歌曲id 32 | */ 33 | 34 | export interface ControlState { 35 | loading?: boolean; 36 | status: string, 37 | playList: any[], 38 | miniPlayer: boolean, 39 | player: boolean, 40 | audio?: HTMLAudioElement, 41 | playListVisible: boolean, 42 | src: string, 43 | coverUrl: string, 44 | currentTime: number, 45 | durationTime: number, 46 | currentId?: number, 47 | current?: number, 48 | alia?: String, 49 | name?: String, 50 | album?: String 51 | } 52 | 53 | export const initialState: ControlState = { 54 | loading: false, 55 | status: 'play', 56 | playList: [], 57 | miniPlayer: false, 58 | player: false, 59 | playListVisible: false, 60 | src: '', 61 | coverUrl: '', 62 | currentTime: 0, 63 | durationTime: 252000, 64 | current: 0, 65 | alia: '', 66 | name: '', 67 | album: '' 68 | }; 69 | 70 | 71 | 72 | export function controlStore(state = initialState, action: ControlAction): ControlState { 73 | switch (action.type) { 74 | case ControlActionTypes.ToggleSong: 75 | console.log(action); 76 | return { ...state }; 77 | 78 | case ControlActionTypes.ToggleStatus: 79 | console.log(action); 80 | return { ...state }; 81 | 82 | case ControlActionTypes.RestControlData: 83 | console.log(action); 84 | return { ...state }; 85 | 86 | case ControlActionTypes.LoadSongUrlSuccess: 87 | const { data } = action.payload; 88 | const musicInfo = state.playList[state.current]; 89 | return { 90 | ...state, 91 | src: data[0].url, 92 | coverUrl: musicInfo.al.picUrl, 93 | alia: modifyArray(musicInfo.ar), 94 | name: musicInfo.al.name, 95 | album: musicInfo.name 96 | }; 97 | 98 | case ControlActionTypes.ChangeValue: 99 | console.log(action); 100 | statusHandler(state, action.payload.key, action.payload.value); 101 | return { ...state, [action.payload.key]: action.payload.value }; 102 | 103 | default: 104 | return state; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/store/reducers/counter.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { ActionTypes } from '../actions'; 3 | 4 | 5 | export const initialState = 0; 6 | 7 | export function counterReducer(state = initialState, action: Action): number { 8 | switch (action.type) { 9 | case ActionTypes.Increment: 10 | return state + 1; 11 | 12 | case ActionTypes.Decrement: 13 | return state - 1; 14 | 15 | case ActionTypes.Reset: 16 | return 0; 17 | 18 | default: 19 | return state; 20 | } 21 | } -------------------------------------------------------------------------------- /src/store/reducers/hot.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { HotActionTypes } from '../actions'; 3 | 4 | export interface HotAction extends Action { 5 | payload: any 6 | } 7 | 8 | export interface SongListDetail { 9 | coverImgUrl: string; 10 | name: string; 11 | listData: any[] 12 | } 13 | 14 | // 由于是QQ的接口不确定他会改,定义成这样保险一点 15 | export interface HotState { 16 | loading?: boolean, 17 | slider: any[], 18 | recommendList: any[], 19 | songListDetail?: SongListDetail 20 | } 21 | 22 | const initState: HotState = { 23 | slider: [], 24 | recommendList: [], 25 | songListDetail: { 26 | coverImgUrl: '', 27 | name: '', 28 | listData: [] 29 | } 30 | }; 31 | 32 | export function hotStore(state: HotState = initState, action: HotAction): HotState { 33 | switch (action.type) { 34 | case HotActionTypes.LoadSuccess: 35 | state.slider = action.payload[0].banners; 36 | state.recommendList = action.payload[1].result; 37 | return state; 38 | case HotActionTypes.LoadError: 39 | console.log(action, '--------'); 40 | return state; 41 | case HotActionTypes.LoadSongListSuccess: 42 | state.songListDetail.coverImgUrl = action.payload.coverImgUrl; 43 | state.songListDetail.name = action.payload.name; 44 | state.songListDetail.listData = action.payload.tracks; 45 | return state; 46 | case HotActionTypes.LoadSongListError: 47 | console.log('获取出错了'); 48 | return state; 49 | default: 50 | return state; 51 | } 52 | } 53 | 54 | //------------------ Access slices of the state (something like getters) 55 | 56 | // export const getWeatherLoading = (state: number) => { 57 | // return state.loading; 58 | // } 59 | 60 | // export const getWeatherLoaded = (state: number) => { 61 | // return state.loaded; 62 | // } 63 | 64 | // export const getWeatherData = (state: number) => { 65 | // return state.data; 66 | // } -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, createSelector, createFeatureSelector } from '@ngrx/store'; 2 | 3 | //import the weather reducer 4 | import { counterReducer } from './counter.reducer'; 5 | import { hotStore, HotState } from './hot.reducer'; 6 | import { topListStore, TopListState } from './list.reducer'; 7 | import { controlStore, ControlState } from './control.reducer'; 8 | 9 | //state 10 | export interface state { 11 | count: number; 12 | hotStore: HotState; 13 | topListStore: TopListState; 14 | controlStore: ControlState; 15 | } 16 | 17 | //register the reducer functions 18 | export const reducers: ActionReducerMap = { 19 | count: counterReducer, 20 | hotStore, 21 | topListStore, 22 | controlStore, 23 | } 24 | 25 | 26 | //get the full state 27 | //export const getWeatherState = (state: weatherReducer.WeatherState) => state; 28 | 29 | //select the part of the state that you need 30 | //using the createFeatureSelector and addind the name of the state slice 31 | export const selectCountState = createFeatureSelector('count'); 32 | 33 | //get the state slices as needed 34 | export const getCountStateData = createSelector(selectCountState, counterReducer); -------------------------------------------------------------------------------- /src/store/reducers/list.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { TopListActionTypes } from '../actions'; 3 | 4 | export interface TopListAction extends Action { 5 | payload: any 6 | } 7 | 8 | 9 | export interface TopListState { 10 | loading?: boolean, 11 | topList: Array, 12 | totalData: Array, 13 | index?: number, 14 | size?: number, 15 | total?: number 16 | } 17 | 18 | const initState: TopListState = { 19 | topList: [], 20 | totalData: [], 21 | index: 1, 22 | size: 10, 23 | total: 0 24 | }; 25 | 26 | export function topListStore(state: TopListState = initState, action: TopListAction): TopListState { 27 | switch (action.type) { 28 | case TopListActionTypes.LoadData: 29 | return state; 30 | case TopListActionTypes.LoadSuccess: 31 | state.totalData = action.payload.playlist.tracks; 32 | state.topList = (action.payload.playlist.tracks || []).slice(state.index - 1, state.index * state.size); 33 | state.total = Math.ceil(action.payload.playlist.tracks.length / state.size) 34 | return state; 35 | case TopListActionTypes.LoadError: 36 | return state; 37 | case TopListActionTypes.ChangeValue: 38 | state.index = action.payload.value; 39 | state.topList = (state.totalData || []).slice(0, action.payload.value * state.size); 40 | return state; 41 | default: 42 | return state; 43 | } 44 | } -------------------------------------------------------------------------------- /src/store/reducers/search.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | 4 | export interface State { 5 | 6 | } 7 | 8 | export const initialState: State = { 9 | 10 | }; 11 | 12 | export function reducer(state = initialState, action: Action): State { 13 | switch (action.type) { 14 | 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, 3 | body { 4 | font-family: PingFangSC-Light, helvetica neue, hiragino sans gb, arial, microsoft yahei ui, microsoft yahei, simsun, sans-serif; 5 | height: 100%; 6 | } 7 | 8 | html, 9 | body, 10 | p, 11 | ul, 12 | h1, 13 | h2, 14 | h3 { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | li { 20 | list-style: none; 21 | } 22 | 23 | /* 设置字体大小 */ 24 | @media screen and (min-width: 320px) { 25 | html { 26 | font-size: 14px; 27 | } 28 | } 29 | 30 | @media screen and (min-width: 360px) { 31 | html { 32 | font-size: 16px; 33 | } 34 | } 35 | 36 | @media screen and (min-width: 400px) { 37 | html { 38 | font-size: 18px; 39 | } 40 | } 41 | 42 | @media screen and (min-width: 440px) { 43 | html { 44 | font-size: 20px; 45 | } 46 | } 47 | 48 | @media screen and (min-width: 480px) { 49 | html { 50 | font-size: 22px; 51 | } 52 | } 53 | 54 | @media screen and (min-width: 640px) { 55 | html { 56 | font-size: 28px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app-server", 5 | "baseUrl": "." 6 | }, 7 | "angularCompilerOptions": { 8 | "entryModule": "app/app.server.module#AppServerModule" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "component-class-suffix": true, 73 | "directive-class-suffix": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | // Work around for https://github.com/angular/angular-cli/issues/7200 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | mode: 'none', 8 | entry: { 9 | // This is our Express server for Dynamic universal 10 | server: './server.ts' 11 | }, 12 | target: 'node', 13 | resolve: { extensions: ['.ts', '.js'] }, 14 | optimization: { 15 | minimize: false 16 | }, 17 | output: { 18 | // Puts the output at the root of the dist folder 19 | path: path.join(__dirname, 'dist'), 20 | filename: '[name].js' 21 | }, 22 | module: { 23 | rules: [ 24 | { test: /\.ts$/, loader: 'ts-loader' }, 25 | { 26 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 27 | // Removing this will cause deprecation warnings to appear. 28 | test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, 29 | parser: { system: true }, 30 | }, 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.ContextReplacementPlugin( 35 | // fixes WARNING Critical dependency: the request of a dependency is an expression 36 | /(.+)?angular(\\|\/)core(.+)?/, 37 | path.join(__dirname, 'src'), // location of your src 38 | {} // a map of your routes 39 | ), 40 | new webpack.ContextReplacementPlugin( 41 | // fixes WARNING Critical dependency: the request of a dependency is an expression 42 | /(.+)?express(\\|\/)(.+)?/, 43 | path.join(__dirname, 'src'), 44 | {} 45 | ) 46 | ] 47 | }; 48 | --------------------------------------------------------------------------------