├── 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 | Fork me on GitHub 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 |
13 | 14 | 15 | 16 |

👤 account page

17 |
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 | ![qr](./docs/qr.png) 8 | 9 | ## 预览 10 | 11 | ![preview](./docs/preview.png) 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 | ![context](./docs/context.png) 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