├── src
├── index.css
├── index.css.d.ts
├── components
│ ├── Cover
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ ├── placeholder.svg
│ │ └── view.tsx
│ ├── Matrix
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── Track
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── TrackList
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── Carousel
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ └── SectionTitle
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
├── layouts
│ ├── BottomBar
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── HeaderBar
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── ExploreHeaderBar
│ │ ├── index.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ └── GithubFork
│ │ └── index.tsx
├── pages
│ ├── Account
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── Explore
│ │ ├── Banner
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── ListCover
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── RecommendList
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── style.scss.d.ts
│ │ ├── List.tsx
│ │ ├── style.scss
│ │ ├── index.tsx
│ │ ├── Custom.tsx
│ │ └── Slider.tsx
│ ├── Friends
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── Mine
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── index.tsx
│ ├── Playing
│ │ ├── index.tsx
│ │ ├── ControlBar
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── HeaderBar
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── RotatingCover
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ ├── Playlist
│ │ ├── index.tsx
│ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ ├── style.scss.d.ts
│ │ │ ├── style.scss
│ │ │ └── view.tsx
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── view.tsx
│ └── Video
│ │ ├── style.scss.d.ts
│ │ ├── style.scss
│ │ └── index.tsx
├── assets
│ └── cover-default.jpg
├── utils
│ ├── models
│ │ ├── index.tsx
│ │ └── componentFetchModel.tsx
│ ├── ee.tsx
│ └── calcFunctions.tsx
├── router
│ ├── routerTrans.scss.d.ts
│ ├── slideContext.tsx
│ ├── routerTrans.scss
│ └── index.tsx
├── App.tsx
├── App.css.d.ts
├── index.tsx
├── constant
│ ├── api.tsx
│ └── style.scss
├── registerServiceWorker.ts
└── store.tsx
├── tsconfig.prod.json
├── .eslintignore
├── docs
├── qr.png
├── context.png
└── preview.png
├── images.d.ts
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── tsconfig.test.json
├── .vscode
└── settings.json
├── config
├── jest
│ ├── typescriptTransform.js
│ ├── fileTransform.js
│ └── cssTransform.js
├── polyfills.js
├── paths.js
├── env.js
├── webpackDevServer.config.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── .prettierrc.js
├── .gitignore
├── scripts
├── test.js
├── start.js
└── build.js
├── tsconfig.json
├── .postcssrc.js
├── .eslintrc.js
├── LICENSE
├── README.md
├── tslint.json
└── package.json
/src/index.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.css.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/src/components/Cover/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/components/Matrix/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/components/Track/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/layouts/BottomBar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/layouts/HeaderBar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/src/components/Matrix/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const row: string;
2 |
--------------------------------------------------------------------------------
/src/components/TrackList/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/pages/Account/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 |
--------------------------------------------------------------------------------
/src/pages/Explore/Banner/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/pages/Friends/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 |
--------------------------------------------------------------------------------
/src/pages/Mine/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 |
--------------------------------------------------------------------------------
/src/pages/Playing/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/pages/Playlist/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/pages/Video/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 |
--------------------------------------------------------------------------------
/src/components/Carousel/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/components/Carousel/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const loading: string;
2 |
--------------------------------------------------------------------------------
/src/components/SectionTitle/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/layouts/ExploreHeaderBar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/pages/Explore/ListCover/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/pages/Explore/RecommendList/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
--------------------------------------------------------------------------------
/src/pages/Playlist/Header/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/components/SectionTitle/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const title: string;
2 |
--------------------------------------------------------------------------------
/src/pages/Playing/ControlBar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/pages/Playing/HeaderBar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/src/pages/Playing/RotatingCover/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './view'
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /config/*
2 | /src/registerServiceWorker.js
3 | /node_modules/*
4 |
--------------------------------------------------------------------------------
/src/pages/Playing/RotatingCover/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const coverImg: string;
2 |
--------------------------------------------------------------------------------
/docs/qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fi3ework/react-cloud-music/HEAD/docs/qr.png
--------------------------------------------------------------------------------
/src/pages/Explore/RecommendList/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const recommendList: string;
2 |
--------------------------------------------------------------------------------
/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 |
--------------------------------------------------------------------------------
/src/pages/Playing/ControlBar/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const controlButtonsWrapper: string;
2 |
--------------------------------------------------------------------------------
/docs/context.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fi3ework/react-cloud-music/HEAD/docs/context.png
--------------------------------------------------------------------------------
/docs/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fi3ework/react-cloud-music/HEAD/docs/preview.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fi3ework/react-cloud-music/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/pages/Explore/RecommendList/style.scss:
--------------------------------------------------------------------------------
1 | .recommendList{
2 | margin: 10px 10px 50px 10px;
3 | }
--------------------------------------------------------------------------------
/src/pages/Explore/Banner/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const slideItem: string;
2 | export const slideImg: string;
3 |
--------------------------------------------------------------------------------
/src/assets/cover-default.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fi3ework/react-cloud-music/HEAD/src/assets/cover-default.jpg
--------------------------------------------------------------------------------
/src/utils/models/index.tsx:
--------------------------------------------------------------------------------
1 | import ComponentFetchModel from './componentFetchModel'
2 |
3 | export { ComponentFetchModel }
--------------------------------------------------------------------------------
/src/components/SectionTitle/style.scss:
--------------------------------------------------------------------------------
1 | .title{
2 | text-align: left;
3 | color: #333;
4 | margin: 20px 0 20px 10px;
5 | }
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/components/Matrix/style.scss:
--------------------------------------------------------------------------------
1 | .row{
2 | display: flex;
3 | justify-content: space-between;
4 | margin-bottom: 10px;
5 | }
--------------------------------------------------------------------------------
/src/layouts/HeaderBar/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const headerBar: string;
2 | export const playingLink: string;
3 | export const search: string;
4 |
--------------------------------------------------------------------------------
/src/pages/Explore/ListCover/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const coverImg: string;
2 | export const wrapper: string;
3 | export const previews: string;
4 |
--------------------------------------------------------------------------------
/src/router/routerTrans.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const routeWrapper: string;
2 | export const rootWrapper: string;
3 | export const transitionGroup: string;
4 |
--------------------------------------------------------------------------------
/src/utils/ee.tsx:
--------------------------------------------------------------------------------
1 | import EE from 'event-emitter'
2 |
3 | const ee = new EE()
4 |
5 | // ee.on('onTouchMove', disPercentX => {})
6 |
7 | export default ee
8 |
--------------------------------------------------------------------------------
/src/pages/Playing/RotatingCover/style.scss:
--------------------------------------------------------------------------------
1 | .coverImg {
2 | width: 500px;
3 | border-radius: 50%;
4 | border: 20px solid rgba($color: #fff, $alpha: 0.5);
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/Explore/Banner/style.scss:
--------------------------------------------------------------------------------
1 |
2 | .slideItem{
3 | background-color: #ccc;
4 | }
5 |
6 | .slideImg{
7 | width: 100%;
8 | display: block;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/Playing/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 | export const content: string;
3 | export const foreground: string;
4 | export const playingBg: string;
5 |
--------------------------------------------------------------------------------
/src/pages/Playing/HeaderBar/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const headerWrapper: string;
2 | export const songName: string;
3 | export const artists: string;
4 | export const back: string;
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.implicitProjectConfig.experimentalDecorators": true,
3 | "files.exclude": {
4 | "**/.git": true
5 | },
6 | "cSpell.words": ["NETEASE"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/layouts/BottomBar/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const bottomBar: string;
2 | export const icon: string;
3 | export const netease: string;
4 | export const title: string;
5 | export const activeLink: string;
6 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Router from '@/router'
3 |
4 | class App extends React.Component {
5 | render() {
6 | return
7 | }
8 | }
9 |
10 | export default App
11 |
--------------------------------------------------------------------------------
/src/pages/Playlist/Header/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const headerWrapper: string;
2 | export const foreground: string;
3 | export const back: string;
4 | export const playingLink: string;
5 | export const bgImg: string;
6 |
--------------------------------------------------------------------------------
/config/jest/typescriptTransform.js:
--------------------------------------------------------------------------------
1 | // Copyright 2004-present Facebook. All Rights Reserved.
2 |
3 | 'use strict';
4 |
5 | const tsJestPreprocessor = require('ts-jest/preprocessor');
6 |
7 | module.exports = tsJestPreprocessor;
8 |
--------------------------------------------------------------------------------
/src/App.css.d.ts:
--------------------------------------------------------------------------------
1 | export const App: string;
2 | export const app: string;
3 | export const appLogo: string;
4 | export const appLogoSpin: string;
5 | export const appHeader: string;
6 | export const appTitle: string;
7 | export const appIntro: string;
8 |
--------------------------------------------------------------------------------
/src/components/Cover/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const cover: string;
2 | export const coverImg: string;
3 | export const playCountWrapper: string;
4 | export const playCountIcon: string;
5 | export const listName: string;
6 | export const linkWrapper: string;
7 |
--------------------------------------------------------------------------------
/src/pages/Playing/ControlBar/style.scss:
--------------------------------------------------------------------------------
1 | .controlButtonsWrapper {
2 | margin: 0 auto 90px auto;
3 | i {
4 | font-weight: lighter;
5 | color: rgba($color: #fff, $alpha: 0.5);
6 | margin: 0 60px;
7 | font-size: 100px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Track/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const trackWrapper: string;
2 | export const index: string;
3 | export const info: string;
4 | export const songName: string;
5 | export const album: string;
6 | export const bar: string;
7 | export const loading: string;
8 |
--------------------------------------------------------------------------------
/src/components/SectionTitle/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 |
4 | const SectionTitle: React.SFC = ({ children }) => {
5 | return
{children}
6 | }
7 |
8 | export default SectionTitle
9 |
--------------------------------------------------------------------------------
/src/components/TrackList/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const trackWrapper: string;
2 | export const bar: string;
3 | export const playAllIcon: string;
4 | export const index: string;
5 | export const info: string;
6 | export const album: string;
7 | export const loading: string;
8 |
--------------------------------------------------------------------------------
/src/layouts/ExploreHeaderBar/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const headerBar: string;
2 | export const wrapper: string;
3 | export const slideNav: string;
4 | export const slider: string;
5 | export const index0: string;
6 | export const index1: string;
7 | export const back: string;
8 |
--------------------------------------------------------------------------------
/src/pages/Explore/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const exploreWrapper: string;
2 | export const isTransitioning: string;
3 | export const custom: string;
4 | export const lists: string;
5 | export const innerWrapper: string;
6 | export const banners: string;
7 | export const redBg: string;
8 |
--------------------------------------------------------------------------------
/src/pages/Account/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | text-align: center;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | background-color: mintcream;
10 | overflow: hidden;
11 | color: #333;
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Video/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | text-align: center;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | background-color: paleturquoise;
10 | overflow: hidden;
11 | color: #333;
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Friends/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | text-align: center;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | background-color: lightgoldenrodyellow;
10 | overflow: hidden;
11 | color: #333;
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Mine/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | text-align: center;
5 | height: 100vh;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | background-color: salmon;
10 | overflow: hidden;
11 | color: #333;
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/calcFunctions.tsx:
--------------------------------------------------------------------------------
1 | const calcPlayCount = playCount => {
2 | if (playCount > 100000000) {
3 | return `${(playCount / 100000000).toFixed(1)}亿`
4 | }
5 |
6 | if (playCount > 10000) {
7 | return `${Math.floor(playCount / 10000)}万`
8 | }
9 |
10 | return String(playCount)
11 | }
12 |
13 | export { calcPlayCount }
14 |
--------------------------------------------------------------------------------
/src/pages/Playlist/style.scss.d.ts:
--------------------------------------------------------------------------------
1 | export const wrapper: string;
2 | export const foreground: string;
3 | export const back: string;
4 | export const content: string;
5 | export const infoWrapper: string;
6 | export const bgImg: string;
7 | export const coverWrapper: string;
8 | export const playCount: string;
9 | export const listName: string;
10 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 2,
4 | // useTabs: false,
5 | semi: false,
6 | singleQuote: true
7 | // trailingComma: 'none'
8 | // bracketSpacing: true,
9 | // jsxBracketSameLine: false,
10 | // arrowParens: 'avoid',
11 | // rangeStart: 0,
12 | // rangeEnd: Infinity,
13 | // proseWrap: "preserve"
14 | }
15 |
--------------------------------------------------------------------------------
/src/router/slideContext.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const slidePos = {
4 | pageIndex: 0,
5 | pos: 0,
6 | changePos(newPos) {
7 | slidePos.pos = newPos
8 | },
9 | setPageIndex(index) {
10 | slidePos.pageIndex = index
11 | }
12 | }
13 |
14 | export const SlideContext = React.createContext(
15 | slidePos // default value
16 | )
17 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | // This is a custom Jest transformer turning file imports into filenames.
6 | // http://facebook.github.io/jest/docs/en/webpack.html
7 |
8 | module.exports = {
9 | process(src, filename) {
10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`;
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/layouts/GithubFork/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default () => {
4 | return (
5 |
6 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/Mine/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import Fork from '@/layouts/GithubFork'
4 |
5 | interface IProps {
6 | style: React.CSSProperties
7 | }
8 |
9 | class App extends React.Component {
10 | render() {
11 | return (
12 |
13 |
👶🏻 mine page
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | export default App
21 |
--------------------------------------------------------------------------------
/src/pages/Video/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import Fork from '@/layouts/GithubFork'
4 |
5 | interface IProps {
6 | style: React.CSSProperties
7 | }
8 |
9 | class App extends React.Component {
10 | render() {
11 | return (
12 |
13 |
🎞 video page
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | export default App
21 |
--------------------------------------------------------------------------------
/src/pages/Playing/RotatingCover/view.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as style from './style.scss'
3 | import { IPlayingSong, IPlayState } from '../../../store'
4 |
5 | type IProps = {
6 | playingSong: IPlayingSong
7 | }
8 |
9 | export default class RotatingCover extends React.Component {
10 | render() {
11 | return (
12 |
13 |

14 |
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/Explore/ListCover/style.scss:
--------------------------------------------------------------------------------
1 | .coverImg {
2 | width: 200px;
3 | height: 200px;
4 | border-radius: 10px;
5 | margin-right: 30px;
6 | }
7 |
8 | .wrapper {
9 | display: flex;
10 | padding: 10px 30px;
11 | border-bottom: 1px solid #eee;
12 | box-sizing: border-box;
13 | }
14 |
15 | .previews {
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | align-items: flex-start;
20 | padding: 40px 0;
21 | font-size: 30px;
22 | line-height: 40px;
23 | color: #555;
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Explore/List.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import ListCover from './ListCover'
4 |
5 | const listIndexes: number[] = Array(23)
6 | .fill(0)
7 | .map((item, index) => index)
8 |
9 | export default class List extends React.Component {
10 | render() {
11 | return (
12 |
13 | {listIndexes.map(itemIndex => {
14 | return
15 | })}
16 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/router/routerTrans.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .switch-wrapper {
3 | position: relative;
4 | height: 100%;
5 | width: 100%;
6 | }
7 | .switch-wrapper>div {
8 | position: absolute;
9 | height: 100%;
10 | width: 100%;
11 | }
12 | }
13 |
14 | .routeWrapper {
15 | height: 100%;
16 | width: 100%;
17 | overflow-y: scroll;
18 | overflow-x: hidden;
19 | }
20 |
21 | .rootWrapper {
22 | background-color: #aaa;
23 | width: 100%;
24 | height: 100%;
25 | overflow: hidden;
26 | }
27 |
28 | .transitionGroup {
29 | position: relative;
30 | }
--------------------------------------------------------------------------------
/src/pages/Account/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import Fork from '@/layouts/GithubFork'
4 |
5 | interface IProps {
6 | style: React.CSSProperties
7 | }
8 |
9 | class App extends React.Component {
10 | render() {
11 | return (
12 |
18 | )
19 | }
20 | }
21 |
22 | export default App
23 |
--------------------------------------------------------------------------------
/src/pages/Playing/HeaderBar/style.scss:
--------------------------------------------------------------------------------
1 | .headerWrapper {
2 | position: relative;
3 | width: 100%;
4 | color: #fff;
5 | padding: 20px 0 10px 0;
6 | border-bottom: 1px solid rgba($color: #fff, $alpha: 0.5);
7 | }
8 |
9 | .songName {
10 | margin: 0 auto 5px auto;
11 | text-align: center;
12 | }
13 |
14 | .artists {
15 | font-size: 20px;
16 | text-align: center;
17 | }
18 |
19 | .back {
20 | position: absolute;
21 | top: 50%;
22 | transform: translateY(-50%);
23 | left: 10px;
24 | i {
25 | font-size: 50px;
26 | font-weight: bold;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Carousel/style.scss:
--------------------------------------------------------------------------------
1 | :global {
2 | .swipe-wrapper {
3 | position: relative;
4 | }
5 |
6 | .swipe-item {
7 | text-align: center;
8 | width: 100%;
9 | .swipe-image {
10 | width: 100%; // height: 2.2rem;
11 | }
12 | }
13 |
14 | .swipe-dots {
15 | position: absolute;
16 | width: 100%;
17 | bottom: 0.07rem;
18 | text-align: center;
19 | .dot {
20 | display: inline-block;
21 | width: 15px;
22 | height: 3px;
23 | margin-right: 10px;
24 | background: #d6d7d8;
25 | &.active {
26 | background: #b4282d;
27 | }
28 | }
29 | }
30 | }
31 |
32 | .loading {
33 | background-color: #ccc;
34 | width: 100%;
35 | height: 300px;
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/Playing/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | position: relative;
5 | height: 750px;
6 | height: 1334px;
7 | }
8 |
9 | .content {
10 | height: 2222px;
11 | }
12 |
13 | .foreground {
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: space-between;
17 | background-color: rgba($color: #000, $alpha: 0.2);
18 | align-items: center;
19 | position: absolute;
20 | z-index: 2;
21 | top: 0;
22 | left: 0;
23 | width: 100%;
24 | height: 100%;
25 | }
26 |
27 | .playingBg {
28 | position: absolute;
29 | left: 0;
30 | top: 0;
31 | background-size: cover;
32 | background-position: center;
33 | filter: blur(30px);
34 | width: 100%;
35 | height: 100%;
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/Friends/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import Fork from '@/layouts/GithubFork'
4 |
5 | interface IProps {
6 | style: React.CSSProperties
7 | }
8 |
9 | class App extends React.Component {
10 | // count: number
11 |
12 | // state = {
13 | // count: 0
14 | // }
15 |
16 | // componentWillUnmount() {
17 | // console.log('=== Friends will unmount =====')
18 | // }
19 |
20 | // componentDidMount() {
21 | // setInterval(() => {
22 | // this.setState({
23 | // count: this.state.count + 1
24 | // })
25 | // }, 200)
26 | // }
27 |
28 | render() {
29 | return (
30 |
31 |
🍻 friends page
32 |
33 |
34 | )
35 | }
36 | }
37 |
38 | export default App
39 |
--------------------------------------------------------------------------------
/src/pages/Playing/HeaderBar/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEventHandler } from 'react'
2 | import style from './style.scss'
3 | import { withRouter } from 'react-router-dom'
4 | type IProps = {
5 | artists: string
6 | name: string
7 | history: any
8 | }
9 |
10 | class RotatingCover extends React.Component {
11 | goBack: MouseEventHandler = e => {
12 | this.props.history.goBack()
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
{this.props.name}
22 |
{this.props.artists}
23 |
24 | )
25 | }
26 | }
27 |
28 | export default withRouter(RotatingCover)
29 |
--------------------------------------------------------------------------------
/src/components/Cover/style.scss:
--------------------------------------------------------------------------------
1 | .cover {
2 | width: 100%;
3 | height: 100%;
4 | position: relative;
5 | overflow: hidden;
6 | }
7 |
8 | .coverImg {
9 | display: block;
10 | width: 100%;
11 | border-radius: 5px;
12 | background-color: #ddd;
13 | }
14 |
15 | .playCountWrapper {
16 | position: absolute;
17 | font-size: 20px;
18 | color: #fff;
19 | text-shadow: 2px 1px 1px rgba(0, 0, 0, 0.2);
20 | margin: 0;
21 | top: 10px;
22 | right: 10px;
23 | }
24 |
25 | .playCountIcon {
26 | font-size: 18px !important;
27 | margin-right: 5px;
28 | }
29 |
30 | .listName {
31 | font-size: 24px;
32 | color: #333;
33 | margin: 10px auto;
34 | }
35 |
36 | .linkWrapper {
37 | display: block;
38 | width: 100%;
39 | }
40 |
41 | :global {
42 | .SVGInline {
43 | display: block;
44 | border-radius: 5px;
45 | background-color: #eee;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import App from './App'
4 | import registerServiceWorker from './registerServiceWorker'
5 | import { Provider } from 'react-redux'
6 | import { createStore, applyMiddleware } from 'redux'
7 | import { reducers, defaultState } from './store'
8 | import promiseMiddleware from 'redux-promise'
9 | import thunkMiddleware from 'redux-thunk'
10 |
11 | const middlewares = [thunkMiddleware, promiseMiddleware]
12 | if (process.env.NODE_ENV === `development`) {
13 | const { logger } = require(`redux-logger`)
14 | middlewares.push(logger)
15 | }
16 |
17 | const store = createStore(reducers, defaultState as object, applyMiddleware(...middlewares))
18 |
19 | ReactDOM.render(
20 |
21 |
22 | ,
23 | document.getElementById('root') as HTMLElement
24 | )
25 | registerServiceWorker()
26 |
--------------------------------------------------------------------------------
/src/layouts/HeaderBar/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .headerBar {
4 | position: absolute;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | top: 0;
9 | left: 0;
10 | right: 0;
11 | z-index: 99;
12 | width: 100%;
13 | height: $headerBarHeight - 5px;
14 | background-color: $ncmRed;
15 | overflow: hidden;
16 | }
17 |
18 | .playingLink {
19 | position: absolute;
20 | right: 20px;
21 | i {
22 | font-size: 50px;
23 | }
24 | }
25 |
26 | .search {
27 | &::placeholder {
28 | color: rgba($color: #fff, $alpha: 0.5);
29 | }
30 | display: block;
31 | font-size: 30px;
32 | line-height: 50px;
33 | height: 45px;
34 | width: 70%;
35 | margin: 0 auto;
36 | padding-left: 30px;
37 | color: #eee;
38 | background-color: rgba($color: #eee, $alpha: 0.3);
39 | border: 1px solid $ncmRed;
40 | border-radius: 100px;
41 | outline: none;
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 | const jest = require('jest');
19 | let argv = process.argv.slice(2);
20 |
21 | // Watch unless on CI, in coverage mode, or explicitly running all tests
22 | if (
23 | !process.env.CI &&
24 | argv.indexOf('--coverage') === -1 &&
25 | argv.indexOf('--watchAll') === -1
26 | ) {
27 | argv.push('--watch');
28 | }
29 |
30 |
31 | jest.run(argv);
32 |
--------------------------------------------------------------------------------
/config/polyfills.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (typeof Promise === 'undefined') {
4 | // Rejection tracking prevents a common issue where React gets into an
5 | // inconsistent state due to an error, but it gets swallowed by a Promise,
6 | // and the user has no idea what causes React's erratic future behavior.
7 | require('promise/lib/rejection-tracking').enable();
8 | window.Promise = require('promise/lib/es6-extensions.js');
9 | }
10 |
11 | // fetch() polyfill for making API calls.
12 | require('whatwg-fetch');
13 |
14 | // Object.assign() is commonly used with React.
15 | // It will use the native implementation if it's present and isn't buggy.
16 | Object.assign = require('object-assign');
17 |
18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet.
19 | // We don't polyfill it in the browser--this is user's responsibility.
20 | if (process.env.NODE_ENV === 'test') {
21 | require('raf').polyfill(global);
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Track/style.scss:
--------------------------------------------------------------------------------
1 | .trackWrapper {
2 | display: flex;
3 | background-color: #fff;
4 | color: #333;
5 | padding: 0 0 20px 0;
6 | }
7 |
8 | .index {
9 | display: flex;
10 | justify-content: center;
11 | flex-shrink: 0;
12 | align-items: center;
13 | width: 100px;
14 | text-align: center;
15 | color: #777;
16 | }
17 |
18 | .info {
19 | flex-grow: 1;
20 | color: #333;
21 | padding: 0 40px 10px 0;
22 | border-bottom: 1px solid #ddd;
23 | text-align: left;
24 | }
25 |
26 | .songName {
27 | margin-bottom: 10px;
28 | }
29 |
30 | .album {
31 | font-size: 20px;
32 | color: #888;
33 | }
34 |
35 | .bar {
36 | padding: 30px 100px;
37 | text-align: left;
38 | background-color: #fff;
39 | border-top-left-radius: 30px;
40 | border-top-right-radius: 30px;
41 | }
42 |
43 | .loading {
44 | border-top-left-radius: 30px;
45 | border-top-right-radius: 30px;
46 | height: 1000px;
47 | background-color: #fff;
48 | }
49 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": [
8 | "src/*"
9 | ]
10 | },
11 | "outDir": "build/dist",
12 | "module": "esnext",
13 | "target": "es5",
14 | "lib": [
15 | "es6",
16 | "dom"
17 | ],
18 | "sourceMap": true,
19 | "allowJs": true,
20 | "jsx": "react",
21 | "moduleResolution": "node",
22 | "rootDir": "src",
23 | "forceConsistentCasingInFileNames": true,
24 | "noImplicitReturns": true,
25 | "noImplicitThis": true,
26 | "noImplicitAny": false,
27 | "strictNullChecks": true,
28 | "suppressImplicitAnyIndexErrors": true,
29 | "noUnusedLocals": false
30 | },
31 | "exclude": [
32 | "node_modules",
33 | "build",
34 | "scripts",
35 | "acceptance-tests",
36 | "webpack",
37 | "jest",
38 | "src/setupTests.ts"
39 | ]
40 | }
--------------------------------------------------------------------------------
/src/layouts/BottomBar/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .bottomBar {
4 | position: absolute;
5 | left: 0;
6 | right: 0;
7 | height: 44px;
8 | bottom: 0;
9 | display: flex;
10 | bottom: 0;
11 | box-sizing: border-box;
12 | justify-content: space-between;
13 | align-items: center;
14 | width: 100%;
15 | height: $bottomBarHeight;
16 | margin: 0;
17 | padding: 10px;
18 | list-style-type: none;
19 | background-color: #ececec;
20 | border-top: 1px solid #eee;
21 | bottom: 0;
22 |
23 | li {
24 | width: 20%;
25 | text-align: center;
26 | }
27 |
28 | a {
29 | color: #555;
30 | display: flex;
31 | flex-direction: column;
32 | }
33 |
34 | .icon {
35 | font-size: 35px;
36 | font-weight: bold;
37 | margin: 0 0 10px 0;
38 | }
39 |
40 | .netease {
41 | font-weight: lighter;
42 | }
43 | }
44 |
45 | .title {
46 | font-size: 13px;
47 | }
48 |
49 | .activeLink {
50 | color: $ncmRed;
51 | }
52 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "plugins": {
3 | "postcss-import": {},
4 | "postcss-url": {},
5 | "postcss-aspect-ratio-mini": {},
6 | "postcss-write-svg": {
7 | utf8: false
8 | },
9 | "postcss-cssnext": {},
10 | "postcss-px-to-viewport": {
11 | viewportWidth: 750,
12 | viewportHeight: 1334, // (Number) The height of the viewport.
13 | unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
14 | viewportUnit: 'vw', // (String) Expected units.
15 | selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px.
16 | minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
17 | mediaQuery: false // (Boolean) Allow px to be converted in media queries.
18 | },
19 | "postcss-viewport-units": {},
20 | "cssnano": {
21 | preset: "advanced",
22 | autoprefixer: false,
23 | "postcss-zindex": false
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/pages/Playing/ControlBar/view.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as style from './style.scss'
3 |
4 | type IProps = {
5 | isPlaying: boolean
6 | switchPrevSong: React.MouseEventHandler
7 | switchNextSong: React.MouseEventHandler
8 | switchPlayState: React.MouseEventHandler
9 | }
10 |
11 | const RotatingCover: React.SFC = props => {
12 | return (
13 |
14 |
15 |
16 |
17 | {props.isPlaying ? (
18 |
19 |
20 |
21 | ) : (
22 |
23 |
24 |
25 | )}
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default RotatingCover
34 |
--------------------------------------------------------------------------------
/src/components/Cover/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Playlist/Header/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .headerWrapper {
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | z-index: 2;
9 | height: $headerBarHeight;
10 | background-color: #666;
11 | color: rgba(255, 255, 255, 0.9);
12 | line-height: $headerBarHeight;
13 | background-size: cover;
14 | overflow: hidden;
15 | }
16 |
17 | .foreground {
18 | position: absolute;
19 | display: flex;
20 | top: 0;
21 | left: 0;
22 | width: 100%;
23 | height: 100%;
24 | z-index: 2;
25 | justify-content: space-between;
26 | }
27 |
28 | .back {
29 | font-weight: bolder;
30 | font-size: 40px;
31 | padding-left: 20px;
32 | }
33 |
34 | .playingLink {
35 | position: absolute;
36 | right: 20px;
37 | i {
38 | font-size: 50px;
39 | color: rgba(255, 255, 255, 0.9);
40 | }
41 | }
42 |
43 | .bgImg {
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | width: 100%;
48 | height: calc(100% + 50px);
49 | z-index: 1;
50 | background-size: cover;
51 | filter: blur(15px);
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/TrackList/style.scss:
--------------------------------------------------------------------------------
1 | .trackWrapper {
2 | display: flex;
3 | background-color: #fff;
4 | color: #333;
5 | padding: 0 0 20px 0;
6 | }
7 |
8 | // bar
9 | .bar {
10 | display: flex;
11 | padding: 30px 0;
12 | text-align: left;
13 | background-color: #fff;
14 | border-top-left-radius: 30px;
15 | border-top-right-radius: 30px;
16 | color: #333;
17 | i {
18 | margin: 0;
19 | width: 100px;
20 | text-align: center;
21 | }
22 | }
23 |
24 | .playAllIcon {
25 | margin-right: 10px;
26 | }
27 |
28 | // list
29 | .index {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | width: 100px;
34 | text-align: center;
35 | color: #777;
36 | }
37 |
38 | .info {
39 | flex-grow: 1;
40 | color: #333;
41 | padding-bottom: 10px;
42 | border-bottom: 1px solid #ddd;
43 | text-align: left;
44 | }
45 |
46 | .album {
47 | font-size: 20px;
48 | color: #888;
49 | }
50 |
51 | .loading {
52 | border-top-left-radius: 30px;
53 | border-top-right-radius: 30px;
54 | height: 1000px;
55 | background-color: #fff;
56 | }
57 |
--------------------------------------------------------------------------------
/src/layouts/ExploreHeaderBar/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .headerBar {
4 | height: 170px;
5 | }
6 |
7 | .wrapper {
8 | position: relative;
9 | background-color: $ncmRed;
10 | margin: 10px 0 0 0;
11 | }
12 |
13 | .slideNav {
14 | display: flex;
15 | position: relative;
16 | padding: 0 0 10px 0;
17 | list-style-type: none;
18 | justify-content: center;
19 | a {
20 | width: 250px;
21 | padding: 10px 0;
22 | text-align: center;
23 | }
24 | }
25 |
26 | .slider {
27 | transition: all ease 0.4s;
28 | position: absolute;
29 | bottom: 0px;
30 | width: 100px;
31 | left: 0;
32 | height: 6px;
33 | background-color: #fff;
34 | border-radius: 3px;
35 | }
36 |
37 | .index0 {
38 | transform: translate(190px, 0);
39 | }
40 |
41 | .index1 {
42 | transform: translate(480px, 0);
43 | }
44 |
45 | .back {
46 | display: block;
47 | height: $headerBarHeight;
48 | color: rgba(255, 255, 255, 0.7);
49 | line-height: $headerBarHeight;
50 | font-weight: bolder;
51 | padding-left: 30px;
52 | i {
53 | font-size: 40px;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Track/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { Link } from 'react-router-dom'
4 |
5 | type IArtist = {
6 | name: string
7 | }
8 |
9 | type IAlbum = {
10 | name: string
11 | }
12 |
13 | type ITrackProps = {
14 | name: string
15 | artists: IArtist[]
16 | album: IAlbum
17 | index: number
18 | id: string
19 | play: any
20 | }
21 |
22 | export default class Track extends React.Component {
23 | handleClick: React.MouseEventHandler = e => {
24 | this.props.play()
25 | }
26 |
27 | render() {
28 | const { name, artists, album, index } = this.props
29 | return (
30 |
31 | {index}
32 |
33 |
{name}
34 |
35 | {artists[0].name} - {album.name}
36 |
37 |
38 |
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['eslint-config-alloy/typescript-react', 'prettier', 'prettier/react'],
3 | plugins: ['prettier', 'typescript'],
4 | globals: {
5 | // 这里填入你的项目需要的全局变量
6 | // 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
7 | //
8 | // React: false,
9 | // ReactDOM: false
10 | },
11 | parser: 'typescript-eslint-parser',
12 | rules: {
13 | // 这里填入你的项目需要的个性化配置,比如:
14 | //
15 | // // @fixable 一个缩进必须用两个空格替代
16 | semi: ['error', 'never'],
17 | // 'no-console': 'off',
18 | 'no-unused-vars': [
19 | 'warn',
20 | {
21 | vars: 'all',
22 | args: 'none',
23 | caughtErrors: 'none'
24 | }
25 | ],
26 | 'max-nested-callbacks': 'off',
27 | 'react/no-children-prop': 'off',
28 | 'typescript/member-ordering': 'off',
29 | 'typescript/member-delimiter-style': 'off',
30 | 'react/jsx-indent-props': 'off',
31 | 'react/no-did-update-set-state': 'off',
32 | indent: [
33 | 'off',
34 | 2,
35 | {
36 | SwitchCase: 1,
37 | flatTernaryExpressions: true
38 | }
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Wee
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 |
--------------------------------------------------------------------------------
/src/pages/Explore/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 | .exploreWrapper {
3 | margin: 0;
4 | width: 200%;
5 | text-align: center;
6 | background-color: $ncmRed;
7 | height: 100%;
8 | overflow-x: scroll;
9 | overflow-y: hidden;
10 | display: flex;
11 | }
12 |
13 | .isTransitioning {
14 | transition: transform 0.2s ease-out;
15 | }
16 |
17 | .custom {
18 | position: relative;
19 | background-color: #fff;
20 | padding: 0 0 1px 0;
21 | }
22 |
23 | .lists {
24 | padding-top: 30px;
25 | background-color: #fff;
26 | }
27 |
28 | .innerWrapper {
29 | flex-shrink: 0;
30 | box-sizing: border-box;
31 | border: 1px solid transparent;
32 | border-width: 170px 0 $bottomBarHeight 0;
33 | width: 50%;
34 | height: 100%;
35 | overflow-x: auto;
36 | overflow-y: auto;
37 | }
38 |
39 | .banners {
40 | border-radius: 10px;
41 | margin: 10px auto 10px auto;
42 | -webkit-mask-image: -webkit-radial-gradient(white, black); // TODO
43 | width: 98%;
44 | overflow: hidden;
45 | }
46 |
47 | .redBg {
48 | position: absolute;
49 | background-color: $ncmRed;
50 | width: 100%;
51 | height: 120px;
52 | }
53 |
--------------------------------------------------------------------------------
/src/layouts/HeaderBar/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 | import style from '@/layouts/HeaderBar/style.scss'
3 | import { Link } from 'react-router-dom'
4 | import cs from 'classnames'
5 |
6 | type IProps = {
7 | style?: any
8 | render?: (props) => ReactNode
9 | component?: React.ComponentClass
10 | pos?: number
11 | pageIndex?: number
12 | setPageIndex?: any
13 | }
14 |
15 | const HeaderBar: React.SFC = props => {
16 | let children
17 | const { component, render } = props
18 | if (component) {
19 | children = React.createElement(component, props)
20 | } else if (render) {
21 | children = render(props)
22 | }
23 |
24 | return (
25 |
39 | )
40 | }
41 |
42 | export default HeaderBar
43 |
--------------------------------------------------------------------------------
/src/components/Matrix/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, Children } from 'react'
2 | import style from './style.scss'
3 |
4 | type IProps = {
5 | cols?: number
6 | width: number
7 | children: ReactNode[]
8 | }
9 |
10 | const defaultProps: Partial = {
11 | cols: 3
12 | }
13 |
14 | const Matrix: React.SFC = ({ children, cols, width }) => {
15 | if (!children) {
16 | return null
17 | }
18 |
19 | const layoutWidth = width ? `${width}%` : `${100 / (cols as number)}%`
20 |
21 | const rows: ReactNode[] = []
22 | for (let i = 0; i < children.length; i += cols as number) {
23 | const currRow = children.slice(i, i + (cols as number))
24 | rows.push(
25 | currRow.map((item, index) => {
26 | return (
27 |
28 | {item}
29 |
30 | )
31 | })
32 | )
33 | }
34 | // wrap a col
35 | const colElements = rows.map((row, index) => {
36 | return (
37 |
38 | {row}
39 |
40 | )
41 | })
42 |
43 | return {colElements}
44 | }
45 |
46 | Matrix.defaultProps = defaultProps
47 |
48 | export default Matrix
49 |
--------------------------------------------------------------------------------
/src/pages/Playlist/Header/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { withRouter } from 'react-router'
4 | import { Link } from 'react-router-dom'
5 | import cs from 'classnames'
6 |
7 | type IProps = {
8 | history: any
9 | bgImgUrl: string
10 | }
11 |
12 | class Header extends React.Component {
13 | goBack: React.MouseEventHandler = e => {
14 | this.props.history.goBack()
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | export default withRouter(Header)
43 |
--------------------------------------------------------------------------------
/src/pages/Playlist/style.scss:
--------------------------------------------------------------------------------
1 | @import '@/constant/style.scss';
2 |
3 | .wrapper {
4 | position: relative;
5 | border: 1px solid transparent;
6 | border-width: 0 0 $bottomBarHeight 0;
7 | height: 100%;
8 | box-sizing: border-box;
9 | overflow-y: auto;
10 | background-color: #666;
11 | clear: both;
12 | }
13 |
14 | .foreground {
15 | position: absolute;
16 | margin-top: $headerBarHeight;
17 | width: 100%;
18 | left: 0;
19 | z-index: 2;
20 | }
21 |
22 | .back {
23 | display: block;
24 | color: rgba(255, 255, 255, 0.7);
25 | height: $headerBarHeight;
26 | line-height: $headerBarHeight;
27 | font-weight: bolder;
28 | padding-left: 30px;
29 | }
30 |
31 | .content {
32 | height: 2222px;
33 | }
34 |
35 | .infoWrapper {
36 | display: flex;
37 | align-items: center;
38 | padding: 0 30px;
39 | margin-bottom: 40px;
40 | background: transparent;
41 | }
42 |
43 | .bgImg {
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | width: 100%;
48 | height: 500px;
49 | z-index: 1;
50 | background-size: cover;
51 | filter: blur(15px);
52 | }
53 |
54 | .coverWrapper {
55 | flex-shrink: 0;
56 | position: relative;
57 | width: 300px;
58 | height: 300px;
59 | border-radius: 5px;
60 | overflow: hidden;
61 |
62 | img {
63 | height: 100%;
64 | width: 100%;
65 | }
66 | }
67 |
68 | .playCount {
69 | position: absolute;
70 | top: 10px;
71 | right: 10px;
72 | color: #fff;
73 | margin: 0;
74 | }
75 |
76 | .listName {
77 | margin-left: 20px;
78 | color: #fff;
79 | font-size: 30px;
80 | }
81 |
--------------------------------------------------------------------------------
/src/utils/models/componentFetchModel.tsx:
--------------------------------------------------------------------------------
1 | import { configure, observable, action, runInAction, computed, autorun } from 'mobx'
2 | import { IRecommendListPayload } from '../../pages/Explore/RecommendList/view'
3 | import { IBannerPayload } from '../../pages/Explore/Banner/view'
4 |
5 | configure({ enforceActions: true })
6 |
7 | type IOption = {
8 | URL: string
9 | }
10 |
11 | class Store {
12 | @observable URL: string = ''
13 | @observable payload: IRecommendListPayload | IBannerPayload | null = null
14 | @observable state: string = 'pending' // "pending" / "done" / "error"
15 |
16 | constructor(option: IOption) {
17 | this.URL = option.URL
18 | }
19 |
20 | fetchURL = () => {
21 | return fetch(this.URL, {})
22 | }
23 |
24 | @action
25 | fetchData() {
26 | this.state = 'pending'
27 | this.fetchURL().then(
28 | response => {
29 | if (!(response.status === 200 || response.status === 304)) {
30 | throw new Error('Fail to get response with status:' + response.status)
31 | }
32 | response
33 | .json()
34 | .then(payload => {
35 | runInAction(() => {
36 | this.state = 'done'
37 | this.payload = payload
38 | })
39 | })
40 | .catch(error => {
41 | throw new Error('Invalid json response: ' + error)
42 | })
43 | },
44 | error => {
45 | runInAction(() => {
46 | this.state = 'error'
47 | })
48 | }
49 | )
50 | }
51 | }
52 |
53 | export default Store
54 |
--------------------------------------------------------------------------------
/src/pages/Explore/RecommendList/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { observer } from 'mobx-react'
4 | import Matrix from '@/components/Matrix'
5 | import Cover from '@/components/Cover'
6 | import SectionTitle from '@/components/SectionTitle'
7 | import Store from '@/utils/models/componentFetchModel'
8 |
9 | type IProps = {
10 | store: Store
11 | normalizer: (result: object) => any[]
12 | title: string
13 | }
14 |
15 | export type IRecommendListPayload = {
16 | code: number
17 | result: object
18 | }
19 |
20 | const RecommendList: React.SFC = ({ store, normalizer, title }) => {
21 | const payload = store.payload as IRecommendListPayload
22 | const lists = payload
23 | ? normalizer(payload.result)
24 | : Array(6)
25 | .fill({ key: '', coverImg: null, link: '' })
26 | .map((item, index) => ({ ...item, key: index }))
27 |
28 | return (
29 |
30 | {title}
31 |
32 | {lists.slice(0, 6).map((list, index) => {
33 | return (
34 |
42 | {list.id}
43 |
44 | )
45 | })}
46 |
47 |
48 | )
49 | }
50 |
51 | export default observer(RecommendList)
52 |
--------------------------------------------------------------------------------
/src/constant/api.tsx:
--------------------------------------------------------------------------------
1 | import { compile } from 'path-to-regexp'
2 |
3 | type IPath = {
4 | path: string
5 | }
6 |
7 | type IApi =
8 | | string
9 | | {
10 | path: string
11 | }
12 |
13 | type Iapis = {
14 | banner: IApi
15 | recommendList: IApi
16 | recommendSong: IApi
17 | songDetail: IApi
18 | playlist: IApi
19 | songUrl: IApi
20 | songUrlBackUp: IApi
21 | list: IApi
22 | }
23 |
24 | const PROXY_HOST = process.env.NODE_ENV === 'production' ? 'http://118.24.21.99:4001' : '/api'
25 |
26 | const NETEASE_API: Iapis = {
27 | banner: '/banner', // 轮播图
28 | recommendList: '/personalized', // 推荐歌单
29 | recommendSong: '/personalized/newsong', // 推荐歌曲
30 | // 歌单详情
31 | playlist: {
32 | path: '/playlist/detail?id=:id'
33 | },
34 | // 歌曲URL
35 | songUrl: {
36 | path: '/music/url?id=:ids'
37 | },
38 | // 歌曲详情
39 | songDetail: {
40 | path: '/song/detail?ids=:ids'
41 | },
42 | // 歌曲 URL 备胎
43 | songUrlBackUp: {
44 | path: 'http://music.163.com/song/media/outer/url?id=:id.mp3'
45 | },
46 | // 排行榜
47 | list: {
48 | path: '/top/list?idx=:idx'
49 | }
50 | }
51 |
52 | // 给 URL 添加 hostPath
53 | const addHost = (URL, hostPath) => {
54 | return hostPath + URL
55 | }
56 |
57 | export default NETEASE_API
58 |
59 | // 根据 API 和 params 来 compose URL
60 | export const getURL = (API: IApi, params?) => {
61 | // simple API
62 | if (!params) {
63 | return addHost(API, PROXY_HOST)
64 | }
65 | // complex API
66 | const toPath = compile(`${(API as IPath).path}`)
67 | const urlWithoutHost = toPath(params)
68 | return addHost(urlWithoutHost, PROXY_HOST)
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Carousel/view.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactSwipe from 'react-swipe'
3 | import style from './style.scss'
4 |
5 | type IProps = {}
6 |
7 | type IState = {
8 | index: number
9 | }
10 |
11 | export default class Carousel extends React.Component {
12 | swipe: ReactSwipe | null = null
13 |
14 | state = {
15 | index: 0
16 | }
17 |
18 | swipeOpt: SwipeOptions = {
19 | auto: 5000,
20 | continuous: false,
21 | callback: index => {
22 | this.setState({
23 | index
24 | })
25 | }
26 | }
27 |
28 | dotClass = index => {
29 | return this.state.index === index ? 'dot active' : 'dot'
30 | }
31 |
32 | handleClickDot = index => {
33 | if (this.swipe !== null) {
34 | this.swipe.slide(index, 1000)
35 | }
36 | }
37 |
38 | render() {
39 | const children = this.props.children ? this.props.children :
40 | return (
41 |
42 |
{
46 | this.swipe = ref
47 | }}
48 | >
49 | {children}
50 |
51 |
52 | {Array(React.Children.count(this.props.children))
53 | .fill('ph')
54 | .map((val, index) => (
55 | this.handleClickDot(index)} key={index} className={this.dotClass(index)} />
56 | ))}
57 |
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/layouts/BottomBar/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { Link, withRouter } from 'react-router-dom'
4 | import cs from 'classnames'
5 |
6 | class BottomBar extends React.Component {
7 | render() {
8 | const linkData = [
9 | {
10 | router: 'explore',
11 | name: '发现',
12 | icon: '\ue67c', // TODO: 不能直接写
13 | styleName: 'netease'
14 | },
15 | {
16 | router: 'video',
17 | name: '视频',
18 | icon: '\ue61c'
19 | },
20 | {
21 | router: 'mine',
22 | name: '我的',
23 | icon: '\ue680'
24 | },
25 | {
26 | router: 'friends',
27 | name: '朋友',
28 | icon: '\ue60b'
29 | },
30 | {
31 | router: 'account',
32 | name: '账号',
33 | icon: '\ue63b'
34 | }
35 | ]
36 |
37 | return (
38 |
39 | {linkData.map((item, index) => {
40 | let computedClassName = { 'iconfont-ncm': true, [style.icon]: true }
41 | if (typeof item.styleName === 'string') {
42 | computedClassName = { ...computedClassName, [style[item.styleName]]: true }
43 | }
44 |
45 | return (
46 | -
47 |
48 | {item.icon}
49 |
{item.name}
50 |
51 |
52 | )
53 | })}
54 |
55 | )
56 | }
57 | }
58 |
59 | export default withRouter(BottomBar)
60 |
--------------------------------------------------------------------------------
/src/pages/Explore/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from '@/pages/Explore/style.scss'
3 | import Custom from '@/pages/Explore/Custom'
4 | import List from '@/pages/Explore/List'
5 | import Slider from './Slider'
6 | import { SlideContext } from '@/router/slideContext'
7 | import { withRouter } from 'react-router'
8 |
9 | interface IProps {
10 | location: { pathname: string }
11 | history: any
12 | style: React.CSSProperties
13 | }
14 |
15 | class Explore extends React.Component {
16 | state = { hasRankLoaded: false }
17 |
18 | static getDerivedStateFromProps(nextProps, prevState) {
19 | if (nextProps.location.pathname.split('/').indexOf('rank') >= 0 && !prevState.hasRankLoaded) {
20 | return { hasRankLoaded: true }
21 | }
22 | return null
23 | }
24 |
25 | setRankLoaded = isLoaded => {
26 | this.setState({
27 | hasRankLoaded: isLoaded
28 | })
29 | }
30 |
31 | render() {
32 | return (
33 |
34 | {({ changePos, setPageIndex, pageIndex }) => (
35 |
43 |
44 |
45 |
46 | {this.state.hasRankLoaded ?
: null}
47 |
48 | )}
49 |
50 | )
51 | }
52 | }
53 |
54 | export default Explore
55 |
--------------------------------------------------------------------------------
/src/constant/style.scss:
--------------------------------------------------------------------------------
1 | $ncmRed: #e24e48; // netease cloud music red
2 | $headerBarHeight: 100px;
3 | $bottomBarHeight: 100px;
4 |
5 | @font-face {
6 | font-family: 'iconfont-cloud-music'; /* project id 669459 */
7 | src: url('//at.alicdn.com/t/font_669459_rskbi91kov.eot');
8 | src: url('//at.alicdn.com/t/font_669459_rskbi91kov.eot?#iefix') format('embedded-opentype'),
9 | url('//at.alicdn.com/t/font_669459_rskbi91kov.woff') format('woff'),
10 | url('//at.alicdn.com/t/font_669459_rskbi91kov.ttf') format('truetype'),
11 | url('//at.alicdn.com/t/font_669459_rskbi91kov.svg#iconfont-cloud-music') format('svg');
12 | }
13 |
14 | $base-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB',
15 | STHeiti, 'Microsoft YaHei', 'Microsoft JhengHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Source Han Sans CN',
16 | 'Noto Sans SC', 'Source Han Sans TC', 'Noto Sans CJK TC', 'WenQuanYi Micro Hei', SimSun, sans-serif;
17 |
18 | :global {
19 | html,
20 | body,
21 | #root {
22 | -webkit-overflow-scrolling: touch;
23 | position: relative;
24 | height: 100%;
25 | overflow: hidden;
26 | font-family: $base-font-family;
27 | outline: 0;
28 | -webkit-text-size-adjust: none;
29 | -webkit-tap-highlight-color: transparent;
30 | }
31 | a {
32 | color: #fff;
33 | text-decoration: none;
34 | }
35 | img {
36 | content: normal !important;
37 | }
38 |
39 | .iconfont-ncm {
40 | font-family: 'iconfont-cloud-music' !important;
41 | font-size: 1rem;
42 | font-style: normal;
43 | -webkit-font-smoothing: antialiased;
44 | -moz-osx-font-smoothing: grayscale;
45 | }
46 | }
47 |
48 | @function height-by-width($width) {
49 | @return (750 / 1334 * $width) * 1px;
50 | }
51 |
--------------------------------------------------------------------------------
/src/pages/Explore/ListCover/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { observer } from 'mobx-react'
4 | import { ComponentFetchModel } from '@/utils/models'
5 | import NETEASE_API, { getURL } from '@/constant/api'
6 | import get from 'lodash/get'
7 | import { Link } from 'react-router-dom'
8 |
9 | type IProps = {
10 | store?: any
11 | listIndex: number
12 | }
13 |
14 | type IState = {
15 | store: any
16 | }
17 |
18 | @observer
19 | export default class Custom extends React.Component {
20 | store: any = new ComponentFetchModel({ URL: getURL(NETEASE_API.list, { idx: this.props.listIndex }) })
21 |
22 | componentDidMount() {
23 | this.store.fetchData()
24 | }
25 |
26 | render() {
27 | const coverImgUrl = get(this.store, 'payload.playlist.coverImgUrl')
28 | const previewItems = get(this.store, 'payload.playlist.tracks')
29 | const name = get(this.store, 'payload.playlist.name')
30 | const playCount = get(this.store, 'payload.playlist.playCount')
31 | const path = `/playlist/${get(this.store, 'payload.playlist.id')}`
32 | return (
33 |
44 |
45 |
46 | {previewItems &&
47 | previewItems.splice(0, 3).map((track, index) => (
48 |
49 | {index + 1}. {track.name}
50 |
51 | ))}
52 |
53 |
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/pages/Playlist/view.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { calcPlayCount } from '@/utils/calcFunctions'
4 | import { ComponentFetchModel } from '@/utils/models'
5 | import { observer } from 'mobx-react'
6 | import get from 'lodash/get'
7 | import TrackList from '@/components/TrackList'
8 | import NETEASE_API, { getURL } from '@/constant/api'
9 | import Header from './Header'
10 |
11 | type IProps = {
12 | style?: React.CSSProperties
13 | location?: any
14 | match?: any
15 | }
16 |
17 | @observer
18 | class Playlist extends React.Component {
19 | listStore = new ComponentFetchModel({
20 | URL: getURL(NETEASE_API.playlist, { id: this.props.match.params.id })
21 | })
22 |
23 | componentDidMount() {
24 | this.listStore.fetchData()
25 | }
26 |
27 | render() {
28 | const locationState = this.props.location.state || {}
29 | const { playCount, picUrl, name } = locationState
30 | const coverImg = get(this.listStore, 'payload.playlist.coverImgUrl')
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |

38 |
{calcPlayCount(playCount)}
39 |
40 |
{name}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 | }
51 |
52 | export default Playlist
53 |
--------------------------------------------------------------------------------
/src/pages/Explore/Custom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as style from './style.scss'
3 | import { ComponentFetchModel } from '@/utils/models'
4 | import RecommendList from './RecommendList'
5 | import Banner from './Banner'
6 | import NETEASE_API, { getURL } from '../../constant/api'
7 |
8 | const bannerStore: ComponentFetchModel = new ComponentFetchModel({ URL: getURL(NETEASE_API.banner) })
9 | const listStore: ComponentFetchModel = new ComponentFetchModel({ URL: getURL(NETEASE_API.recommendList) })
10 | const songStore: ComponentFetchModel = new ComponentFetchModel({ URL: getURL(NETEASE_API.recommendSong) })
11 |
12 | const listNormalizer = result =>
13 | result.map(item => ({
14 | id: item.id,
15 | picUrl: item.picUrl,
16 | playCount: item.playCount,
17 | name: item.name,
18 | path: `/playlist/${item.id}`
19 | }))
20 |
21 | const songNormalizer = result =>
22 | result.map(item => ({
23 | id: item.song.id,
24 | picUrl: item.song.album.picUrl,
25 | playCount: null,
26 | name: item.name,
27 | path: `/playlist/${item.id}`
28 | }))
29 |
30 | export default class Custom extends React.Component {
31 | banner: HTMLDivElement | null
32 | componentDidMount() {
33 | if (this.banner) {
34 | this.banner.addEventListener('touchmove', e => {
35 | e.stopPropagation()
36 | })
37 | }
38 |
39 | bannerStore.fetchData()
40 | listStore.fetchData()
41 | songStore.fetchData()
42 | }
43 |
44 | render() {
45 | return (
46 |
47 |
48 |
{
51 | this.banner = ref
52 | }}
53 | >
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 🎶 基于 React 实现的仿 iOS 客户端网易云音乐。
2 |
3 | 在线地址:**[戳我](http://118.24.21.99:5001/)**(PC 浏览器需切换到移动端模式)
4 |
5 | 移动端体验:
6 |
7 | 
8 |
9 | ## 预览
10 |
11 | 
12 |
13 | ## 技术栈
14 |
15 | - React 16.3
16 | - TypeScript
17 | - Mobx + Redux
18 | - react-redux
19 | - react-router-v4
20 | - Scss
21 |
22 | ## 实现细节
23 |
24 | 目前只实现了上面四个页面,但是总体的结构已经形成了,其他页面的添加只是时间上的问题 ~~(其实是懒)~~,暂时没有实现,下面是目前已实现的功能的细节:
25 |
26 | ### 局部状态管理
27 |
28 | 像首页的 banner 或者推荐歌单等,都是不会被共享的局部状态,使用 Mobx 来进行请求的发起和状态的管理。
29 |
30 | ### 全局状态
31 |
32 | 播放器的状态是一个全局状态,包括当前的播放列表,切歌,播放 / 暂停等,所以很自然的使用 redux 来进行管理,可以清楚的掌握所有改变全局状态的行为。
33 |
34 | ### TypeScript
35 |
36 | 尽管上手需要掌握一些语法,但是静态类型与自动提示都能提供很大的帮助,在这个并不大的项目中我也体验到了很大的帮助。但是要注意的是 TS 其实并不严格限制对象的类型,只要够懒,遍地 any,就会把 TS 写成 JS,所以为了充分发挥 TS 的威力,一定要有良好的 TS 代码风格。
37 |
38 | ### 手势滑动
39 |
40 | 为了模仿 iOS 端可以通过滑屏切换页面的功能,通过监听 `touchStart`,`touchMove`,`touchEnd` 来进行手势的判断并通过 `transform` 触发模拟滚动实现,在 `touchMove` 中检测监听滑动的方向及距离,在 `touchEnd` 中触发路由的切换及页面吸附到整屏的位置。
41 |
42 | ### 歌单的状态保留
43 |
44 | 有这么一个操作需要注意:用户在某歌单往下滑了几下,然后点了某歌播放然后进入了播放器,会发生路由的改变,如果此时从播放器返回,会丢包包括滚动位置在内的歌单页的所有状态丢失(因为 re-mount 了)。
45 |
46 | 我造了一个轮子来解决这个问题:[react-live-route](https://github.com/fi3ework/react-live-route),是对 react-router-v4 中 Route 组件的增强,简单的说就是将歌单页隐藏掉而不是 unmount 掉,具体的解决思路可以参考轮子里的文档。
47 |
48 | ### 跨组件传递状态
49 |
50 | 在 iOS 版的网易云中,可以滑动来切换页面,同时会触发顶部 tab 下的滑块移动。在项目中,滑动页面与滑块分属于两个兄弟组件的子组件且嵌套层次较深,如果直接通过 prop 来传递略显丑陋,有如下解决方案:
51 |
52 | 1. 通过 redux,但是 redux 最好只负责领域数据,这种 UI 的状态就不要往 store 中放了。
53 |
54 | 2. 通过 event-emitter,其实和 redux 差不多,因为 redux 也是基于 event-emitter 实现的, 但是不经过 react-redux 虽然可以实现,但是破坏了 react 整个自顶向下界面更新的原则。
55 |
56 | 3. 通过新的 context API 实现,如下图:
57 |
58 | 
59 |
60 |
61 |
62 | ## API
63 |
64 | 项目中用到的网易云音乐的 API 来自 [NeteaseCloudMusicApi](https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi)。
65 |
66 | ## TODO
67 |
68 | 目前还有一些部分没完成,包括但不限于:
69 |
70 | - [ ] code splitting
71 | - [ ] 组件中有些功能还是有耦合,需要再抽象
72 | - [ ] SSR
73 |
74 | ## 开发
75 |
76 | 克隆代码到本地之后,需要在 4000 端口运行 [NeteaseCloudMusicApi](https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi)。
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true
6 | },
7 | "linterOptions": {
8 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "src/react-live-router/**/*.js"]
9 | },
10 | "rules": {
11 | "no-var-requires": false,
12 | "max-classes-per-file": false,
13 | // "prettier": true,
14 | "arrow-parens": false,
15 | "arrow-return-shorthand": [false],
16 | "comment-format": [true, "check-space"],
17 | "import-blacklist": [true, "rxjs"],
18 | "interface-over-type-literal": false,
19 | "interface-name": false,
20 | "member-access": false,
21 | "member-ordering": [true, { "order": "fields-first" }],
22 | // "newline-before-return": false,]
23 | "no-any": false,
24 | "no-empty-interface": false,
25 | "no-import-side-effect": [true],
26 | "no-inferrable-types": [true, "ignore-params", "ignore-properties"],
27 | "no-invalid-this": [true, "check-function-in-method"],
28 | "no-null-keyword": false,
29 | "no-require-imports": false,
30 | "no-this-assignment": [true, { "allow-destructuring": true }],
31 | "no-trailing-whitespace": true,
32 | "no-unused-variable": [false, "react"],
33 | "object-literal-sort-keys": false,
34 | "object-literal-shorthand": false,
35 | "one-variable-per-declaration": [false],
36 | "only-arrow-functions": [true, "allow-declarations"],
37 | "ordered-imports": [false],
38 | "no-console": [false],
39 | "prefer-method-signature": false,
40 | "prefer-template": [true, "allow-single-concat"],
41 | "quotemark": [true, "single", "jsx-double"],
42 | "triple-equals": [true, "allow-null-check"],
43 | // "type-literal-delimiter": true,
44 | "typedef": {
45 | "severity": "off",
46 | "options": ["parameter", "property-declaration"]
47 | },
48 | "variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"],
49 | // tslint-react
50 | "jsx-no-lambda": false
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/Explore/Banner/view.tsx:
--------------------------------------------------------------------------------
1 | import Carousel from '@/components/Carousel'
2 | import * as React from 'react'
3 | import * as style from './style.scss'
4 | import { observer } from 'mobx-react'
5 | import Store from '@/utils/models/componentFetchModel'
6 | import get from 'lodash/get'
7 |
8 | type IBannerItem = {
9 | url: string
10 | picUrl: string
11 | }
12 |
13 | export type IBannerPayload = {
14 | code: number
15 | banners: IBannerItem[]
16 | }
17 |
18 | type IProps = {
19 | store: Store
20 | }
21 |
22 | type IState = {
23 | isImgsLoaded: boolean
24 | }
25 |
26 | @observer
27 | class Banner extends React.Component {
28 | state = {
29 | isInited: false,
30 | isImgsLoaded: false
31 | }
32 |
33 | componentDidMount() {
34 | this.isImgsLoadComplete(get(this.props, 'store.payload.banners'))
35 | }
36 |
37 | componentDidUpdate(prevProps, prevState) {
38 | if (!this.state.isInited) {
39 | this.isImgsLoadComplete(get(this.props, 'store.payload.banners'))
40 | }
41 | }
42 |
43 | isImgsLoadComplete = urls => {
44 | const length = get(urls, 'length')
45 | if (!length) {
46 | return
47 | }
48 |
49 | const totalImgCount = length
50 | let loadedImgCount = 0
51 | urls.forEach(img => {
52 | const testImg = new Image()
53 | testImg.src = img.picUrl
54 | testImg.onload = () => {
55 | loadedImgCount++
56 | if (loadedImgCount === totalImgCount) {
57 | this.setState({
58 | isImgsLoaded: true,
59 | isInited: true
60 | })
61 | }
62 | }
63 | })
64 | }
65 |
66 | render() {
67 | const payload = this.props.store.payload as IBannerPayload
68 | return (
69 |
70 | {payload && this.state.isImgsLoaded
71 | ? payload.banners.map(banner => {
72 | return (
73 |
74 |

75 |
76 | )
77 | })
78 | : null}
79 |
80 | )
81 | }
82 | }
83 |
84 | export default Banner
85 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
33 | React Cloud Music
34 |
35 |
36 |
37 |
40 |
41 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(path, needsSlash) {
15 | const hasSlash = path.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return path.substr(path, path.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${path}/`;
20 | } else {
21 | return path;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right