├── .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 |
--------------------------------------------------------------------------------
/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 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
![]()
43 |
44 |
45 |
{{data.name}}
46 |
{{data.alia}}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
![]()
54 |
55 |
56 |
57 |
58 |
59 |
60 |
{{data.currentTime/1000 | formatTime}}
61 |
72 |
{{data.durationTime/1000 | formatTime}}
73 |
74 |
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 |
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 |
23 |
热门歌单推荐
24 |
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 |
3 |
4 |
{{songDetailList.name}}
5 |
6 |
7 |
13 |
14 |
15 |
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 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | - PLAYLIST
9 | - Drive Loud
10 | - Forget the traffic stress.
11 | -
12 | Created by
13 | Spottily
14 | 50 songs, 2 hr 46 min
15 |
16 |
17 |
18 |
19 | - PLAY
20 | - FOLLOW
21 |
22 |
23 |
24 |
25 |
26 |
FOLLOWERS
27 |
67,543
28 |
29 |
30 |
31 |
32 |
FILTER
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 |
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 |
2 |
热门搜索
3 |
4 | 周深 好的晚安
5 | 关于孤独我想说的话
6 | 摩登兄弟 乞丐
7 | 你的酒馆对我打了烊
8 | 如果这都不算爱
9 |
10 |
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 |
2 |
3 |
6 |
7 |
张含韵
8 |
9 | 单曲:65
10 | 专辑:16
11 |
12 |
13 |
14 |
15 |
18 |
19 |
张含韵
20 |
21 | 单曲:65
22 | 专辑:16
23 |
24 |
25 |
26 |
27 |
30 |
31 |
张含韵
32 |
33 | 单曲:65
34 | 专辑:16
35 |
36 |
37 |
38 |
39 |
42 |
43 |
张含韵
44 |
45 | 单曲:65
46 | 专辑:16
47 |
48 |
49 |
50 |
51 |
54 |
55 |
张含韵
56 |
57 | 单曲:65
58 | 专辑:16
59 |
60 |
61 |
62 |
63 |
66 |
67 |
张含韵
68 |
69 | 单曲:65
70 | 专辑:16
71 |
72 |
73 |
74 |
75 |
78 |
79 |
张含韵
80 |
81 | 单曲:65
82 | 专辑:16
83 |
84 |
85 |
86 |
87 |
90 |
91 |
张含韵
92 |
93 | 单曲:65
94 | 专辑:16
95 |
96 |
97 |
98 |
99 |
102 |
103 |
张含韵
104 |
105 | 单曲:65
106 | 专辑:16
107 |
108 |
109 |
110 |
111 |
114 |
115 |
张含韵
116 |
117 | 单曲:65
118 | 专辑:16
119 |
120 |
121 |
122 |
123 |
126 |
127 |
张含韵
128 |
129 | 单曲:65
130 | 专辑:16
131 |
132 |
133 |
134 |
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 |

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 |
--------------------------------------------------------------------------------