├── NeteaseCloudPC
├── .env
├── .gitignore
├── README.md
├── config-overrides.js
├── jsconfig.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo.png
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── api
│ │ ├── album.js
│ │ ├── common.js
│ │ ├── disk.js
│ │ ├── dj.js
│ │ ├── friend.js
│ │ ├── home.js
│ │ ├── http.js
│ │ ├── login.js
│ │ ├── radio.js
│ │ ├── search.js
│ │ ├── singer.js
│ │ ├── toplist.js
│ │ ├── user.js
│ │ └── video.js
│ ├── assets
│ │ └── style
│ │ │ └── style.scss
│ ├── components
│ │ ├── Album
│ │ │ ├── AlbumContent.jsx
│ │ │ └── AlbumTemp.jsx
│ │ ├── Aside
│ │ │ └── UserInfo.jsx
│ │ ├── AsyncComponent.jsx
│ │ ├── BackTop.jsx
│ │ ├── Banner
│ │ │ ├── Banner.jsx
│ │ │ └── index.scss
│ │ ├── BlockTitle.jsx
│ │ ├── Cache.jsx
│ │ ├── Comment
│ │ │ └── Comment.jsx
│ │ ├── Common
│ │ │ ├── BtnTools.jsx
│ │ │ └── NavTitle.jsx
│ │ ├── Covers
│ │ │ └── CoverItem.jsx
│ │ ├── Footer
│ │ │ ├── Footer.jsx
│ │ │ └── index.scss
│ │ ├── Header
│ │ │ ├── Header.jsx
│ │ │ └── nav.js
│ │ ├── Layout.jsx
│ │ ├── Login
│ │ │ └── LoginForm.jsx
│ │ ├── Main.jsx
│ │ ├── PlayBar
│ │ │ ├── LrcPanel.jsx
│ │ │ ├── PlayBar.jsx
│ │ │ └── index.scss
│ │ └── VideoPlayer
│ │ │ ├── VideoPlayer.jsx
│ │ │ └── index.scss
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── reportWebVitals.js
│ ├── router
│ │ ├── generateRoute.js
│ │ ├── index.js
│ │ ├── modules
│ │ │ ├── friend.js
│ │ │ ├── home.js
│ │ │ ├── my.js
│ │ │ ├── search.js
│ │ │ └── singer.js
│ │ └── routerTable.js
│ ├── setupTests.js
│ ├── store
│ │ ├── action-type.js
│ │ ├── action.js
│ │ ├── global-data.js
│ │ ├── index.js
│ │ └── user.js
│ ├── utils
│ │ ├── eventBus.js
│ │ ├── hooks.js
│ │ ├── localStore.js
│ │ ├── methods
│ │ │ └── login.js
│ │ ├── pureFunctions.js
│ │ ├── sessionStore.js
│ │ └── utils.js
│ └── views
│ │ ├── Album
│ │ ├── Album.jsx
│ │ └── PlayList.jsx
│ │ ├── Disk
│ │ ├── Disk.jsx
│ │ └── components
│ │ │ ├── DiskItem.jsx
│ │ │ └── DiskWrap.jsx
│ │ ├── Friend
│ │ ├── Friend.jsx
│ │ └── components
│ │ │ └── Event.jsx
│ │ ├── Home
│ │ ├── AsideComponents.jsx
│ │ ├── Home.jsx
│ │ ├── PlayList
│ │ │ ├── CateSelector.jsx
│ │ │ └── PlayList.jsx
│ │ ├── Rank.jsx
│ │ ├── Swiper.jsx
│ │ ├── config.js
│ │ ├── index.scss
│ │ └── store
│ │ │ ├── action.js
│ │ │ └── index.js
│ │ ├── My
│ │ ├── Dj.jsx
│ │ ├── Login.jsx
│ │ ├── MV.jsx
│ │ ├── MainPage.jsx
│ │ ├── Music.jsx
│ │ ├── MyArtist.jsx
│ │ ├── PlayList.jsx
│ │ ├── components
│ │ │ └── menu
│ │ │ │ ├── Menu.jsx
│ │ │ │ └── mock.js
│ │ └── store
│ │ │ └── index.js
│ │ ├── Personal
│ │ ├── Personal.jsx
│ │ └── index.scss
│ │ ├── Radio
│ │ ├── Radio.jsx
│ │ ├── RadioCate.jsx
│ │ ├── RadioDetails.jsx
│ │ ├── components
│ │ │ ├── Category.jsx
│ │ │ ├── Introduce.jsx
│ │ │ ├── RadioTable.jsx
│ │ │ ├── Recommend.jsx
│ │ │ └── TypeRecommend.jsx
│ │ └── store
│ │ │ ├── index.js
│ │ │ └── initialState.js
│ │ ├── Search
│ │ ├── Search.jsx
│ │ └── components.js
│ │ ├── Singer
│ │ ├── Album.jsx
│ │ ├── CateAside.jsx
│ │ ├── Introduce.jsx
│ │ ├── MV.jsx
│ │ ├── Singer.jsx
│ │ ├── SingerCates.jsx
│ │ ├── SingerItem.jsx
│ │ ├── SingerList.jsx
│ │ ├── SingerMoreCates.jsx
│ │ └── Song.jsx
│ │ ├── Song
│ │ ├── Song.jsx
│ │ └── components.js
│ │ ├── Toplist
│ │ ├── AsideNav.jsx
│ │ ├── Details.jsx
│ │ └── TopList.jsx
│ │ └── Video
│ │ ├── Components.jsx
│ │ └── Video.jsx
└── yarn.lock
└── README.md
/NeteaseCloudPC/.env:
--------------------------------------------------------------------------------
1 | FAST_REFRESH=false
2 | GENERATE_SOURCEMAP=false
3 | PORT=8082
--------------------------------------------------------------------------------
/NeteaseCloudPC/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /neteasecloudmusic
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/config-overrides.js:
--------------------------------------------------------------------------------
1 | const {
2 | override,
3 | addWebpackAlias,
4 | addDecoratorsLegacy,
5 | overrideDevServer,
6 | fixBabelImports,
7 | } = require('customize-cra');
8 |
9 | const path = require('path');
10 | const paths = require('react-scripts/config/paths')
11 | const CompressionWebpackPlugin = require('compression-webpack-plugin');
12 | const rewireUglifyjs = require('react-app-rewire-uglifyjs');
13 |
14 | /**
15 | * 代理配置
16 | */
17 | const addProxy = () => config => {
18 | return {
19 | ...config,
20 | proxy: {
21 | '/api': {
22 | // 环境设定后期会讲, 这里可以参考.env中的配置
23 | target: 'http://localhost:5000/',
24 | changeOrigin: true,
25 | pathRewrite: {
26 | '^/api': '',
27 | },
28 | },
29 | },
30 | };
31 | };
32 |
33 | //配置路径以及压缩
34 | const buildFile = () => config => {
35 | if (config.mode === 'production') {
36 | console.log('evn is production, change build path...');
37 |
38 | //打包路径需要设置为相同的名字,不然会报错
39 | paths.appBuild = path.join(path.dirname(paths.appBuild), 'neteasecloudmusic')
40 | config.output.path = path.join(path.dirname(config.output.path), 'neteasecloudmusic')
41 | // 添加js打包gzip配置
42 | config.plugins.push(
43 | new CompressionWebpackPlugin({
44 | test: /\.js$|\.scss$|\.css$/,
45 | threshold: 1024
46 | })
47 | )
48 | }
49 | return config;
50 | }
51 |
52 | //生产环境去除console.* functions
53 | const dropConsole = () => {
54 | return config => {
55 | if (config.optimization.minimizer) {
56 | config.optimization.minimizer.forEach(minimizer => {
57 | if (minimizer.constructor.name === 'TerserPlugin') {
58 | minimizer.options.terserOptions.compress.drop_console = true
59 | }
60 | })
61 | }
62 | return config
63 | }
64 | }
65 |
66 | /* module.exports = override(
67 | // enable legacy decorators babel plugin
68 | addDecoratorsLegacy(),
69 | addWebpackAlias({
70 | '@': path.resolve(__dirname, 'src')
71 | })
72 | ) */
73 |
74 | module.exports = {
75 | webpack: override(
76 | // 使用修饰器
77 | addDecoratorsLegacy(),
78 | fixBabelImports("import", {
79 | libraryName: "antd",
80 | libraryDirectory: "es",
81 | style: true // change importing css to less
82 | }),
83 | // 路径别名
84 | addWebpackAlias({
85 | '@': path.resolve(__dirname, 'src'),
86 | '@components': path.resolve(__dirname, 'src/components'),
87 | '@views': path.resolve(__dirname, 'src/views'),
88 | '@assets': path.resolve(__dirname, 'src/assets'),
89 | '@store': path.resolve(__dirname, 'src/store'),
90 | '@utils': path.resolve(__dirname, 'src/utils'),
91 | '@api': path.resolve(__dirname, 'src/api'),
92 | }),
93 | buildFile(),
94 | dropConsole(),
95 | rewireUglifyjs
96 | ),
97 | // 开发环境服务器代理, 一般情况下不需要我们自己配
98 | devServer: overrideDevServer(addProxy()),
99 | };
--------------------------------------------------------------------------------
/NeteaseCloudPC/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "baseUrl": "./",
5 | "paths": {
6 | "@/*": ["src/*"],
7 | "@components/*": ["src/components/*"],
8 | "@views/*": ["src/views/*"],
9 | "@assets/*": ["src/assets/*"],
10 | "@store/*": ["src/store/*"],
11 | "@utils/*": ["src/utils/*"],
12 | "@api/*": ["src/api/*"]
13 | }
14 | },
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ttt",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "@vant/area-data": "^1.1.1",
10 | "antd": "^4.12.2",
11 | "axios": "^0.21.1",
12 | "crypto": "^1.0.1",
13 | "customize-cra": "^1.0.0",
14 | "fullscreen-react": "^1.0.4",
15 | "http-proxy-middleware": "^1.0.6",
16 | "node-sass": "4.14.0",
17 | "react": "^17.0.1",
18 | "react-app-rewired": "^2.1.8",
19 | "react-dom": "^17.0.1",
20 | "react-lazy-load-image-component": "^1.5.1",
21 | "react-lazyload": "^3.2.0",
22 | "react-redux": "^7.2.2",
23 | "react-router-dom": "^5.2.0",
24 | "react-scripts": "4.0.2",
25 | "react-transition-group": "^4.4.2",
26 | "redux": "^4.0.5",
27 | "redux-thunk": "^2.3.0",
28 | "web-vitals": "^1.0.1"
29 | },
30 | "scripts": {
31 | "start": "react-app-rewired start",
32 | "build": "react-app-rewired build",
33 | "test": "react-app-rewired test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": [
38 | "react-app",
39 | "react-app/jest"
40 | ]
41 | },
42 | "browserslist": {
43 | "production": [
44 | ">0.2%",
45 | "not dead",
46 | "not op_mini all"
47 | ],
48 | "development": [
49 | "last 1 chrome version",
50 | "last 1 firefox version",
51 | "last 1 safari version"
52 | ]
53 | },
54 | "devDependencies": {
55 | "@babel/plugin-proposal-decorators": "^7.12.13",
56 | "babel-plugin-import": "^1.13.3",
57 | "compression-webpack-plugin": "5.0.1",
58 | "mini-css-extract-plugin": "^2.2.0",
59 | "prop-types": "^15.7.2",
60 | "react-app-rewire-uglifyjs": "^0.1.1"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/react17-project/92fa757612ce81a226bdc0cc3dd4c3f2556c4706/NeteaseCloudPC/public/favicon.ico
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
18 |
19 |
28 | Jacky 仿网易 仅供学习
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/react17-project/92fa757612ce81a226bdc0cc3dd4c3f2556c4706/NeteaseCloudPC/public/logo.png
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/react17-project/92fa757612ce81a226bdc0cc3dd4c3f2556c4706/NeteaseCloudPC/public/logo192.png
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/react17-project/92fa757612ce81a226bdc0cc3dd4c3f2556c4706/NeteaseCloudPC/public/logo512.png
--------------------------------------------------------------------------------
/NeteaseCloudPC/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 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/App.js:
--------------------------------------------------------------------------------
1 | import Router from './router';
2 | import Layout from '@components/Layout';
3 | import BackTop from '@/components/BackTop';
4 | import localStore from '@/utils/localStore';
5 | import sessionStore from './utils/sessionStore';
6 | import loginFn from '@/utils/methods/login';
7 | import { routerRef } from './router/generateRoute';
8 | import LrcPanel from '@/components/PlayBar/LrcPanel';
9 | import { enterTips } from '@/utils/pureFunctions';
10 |
11 | import { useEffect, useState } from 'react';
12 | import { useSelector, useDispatch } from 'react-redux';
13 |
14 | export let historyAlpha;
15 |
16 | function App() {
17 | const token = useSelector(state => state.user.token);
18 | const localUser = localStore.get('user');
19 | const dispatch = useDispatch();
20 |
21 | useEffect(() => {
22 | enterTips();
23 | if (token == null) { //未登录
24 | if (localUser?.remember) { //自动登录
25 | const {phone, password} = localUser;
26 | loginFn.login({phone, password});
27 | }
28 | }
29 | historyAlpha = routerRef.current;
30 | routerRef.current.history.listen((route) => {
31 | console.log(route);
32 | if (route.pathname.startsWith('/video')) {
33 | dispatch({type: 'setShowPlaybar', show: false});
34 | document.querySelector('.j-flag.pas')?.click();
35 | } else {
36 | dispatch({type: 'setShowPlaybar', show: true});
37 | }
38 | })
39 | }, [])
40 | return (
41 |
42 | {Router}
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/album.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getAlbumInfo(id) { //获取专辑内容
5 | try {
6 | const res = await http.get(`/album`, {
7 | id,
8 | });
9 | return res;
10 | } catch (err) {
11 | return err;
12 | }
13 | },
14 | async getAlbumCmt(id, limit = 20, offset = 0) { //获取专辑评论
15 | try {
16 | const res = await http.get(`/comment/album`, {
17 | id,
18 | limit,
19 | offset
20 | });
21 | return res;
22 | } catch (err) {
23 | return err;
24 | }
25 | },
26 | async getPlayListInfo(id) { //获取歌单内容
27 | try {
28 | const res = await http.get(`/playlist/detail`, {
29 | id,
30 | });
31 | return {
32 | album: res.playlist,
33 | songs: res.playlist.tracks
34 | };
35 | } catch (err) {
36 | return err;
37 | }
38 | },
39 | async getPlayListCmt(id, limit = 20, offset = 0) { //获取歌单评论
40 | try {
41 | const res = await http.get(`/comment/playlist`, {
42 | id,
43 | limit,
44 | offset
45 | });
46 | return res;
47 | } catch (err) {
48 | return err;
49 | }
50 | },
51 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/common.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 | import { message } from 'antd';
3 |
4 | export default {
5 | async getSongUrl(id) { //获取音乐url
6 | try {
7 | const res = await http.get(`/song/url`, {
8 | id,
9 | random: Date.now()
10 | });
11 | return res.data[0].url;
12 | } catch (err) {
13 | return err;
14 | }
15 | },
16 | async getLyric(id, key = false) { //获取歌词
17 | try {
18 | message.loading({
19 | content: '获取歌词中...',
20 | key: 'loadlrc',
21 | duration: 0 ,
22 | style: {
23 | marginTop: '40vh',
24 | },
25 | });
26 | const res = await http.get(`/lyric`, {
27 | id
28 | })
29 | if (key) {
30 | return res;
31 | }
32 | return res.lrc.lyric
33 | } catch (err) {
34 | return err;
35 | } finally {
36 | message.destroy('loadlrc');
37 | }
38 | },
39 | async getSongDetails(ids) { //获取歌曲详情
40 | try {
41 | const res = await http.get(`/song/detail`, {
42 | ids
43 | })
44 | return res.songs[0]
45 | } catch (err) {
46 | return err;
47 | }
48 | },
49 | async getCmtOfSong({id, limit = 20, offset = 0} = {}) { //获取歌曲评论
50 | try {
51 | const res = await http.get(`/comment/music`, {
52 | id,
53 | limit,
54 | offset
55 | })
56 | return res;
57 | } catch (err) {
58 | return err;
59 | }
60 | },
61 | async getSameList(id) { //获取相似歌单
62 | try {
63 | const res = await http.get(`/simi/playlist`, {
64 | id,
65 | })
66 | return res.playlists;
67 | } catch (err) {
68 | return err;
69 | }
70 | },
71 | async getSameSong(id) { //获取相似歌曲
72 | try {
73 | const res = await http.get(`/simi/song`, {
74 | id,
75 | })
76 | return res.songs;
77 | } catch (err) {
78 | return err;
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/disk.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getNewDisk({
5 | limit = 10,
6 | offset = 0,
7 | area = 'ALL',
8 | type = 'hot',
9 | year = new Date().getFullYear()
10 | } = {}) { //新碟上架
11 | try {
12 | const res = await http.get(`/top/album`, {
13 | limit,
14 | offset,
15 | area,
16 | type,
17 | year
18 | });
19 | return res.weekData.slice(0, limit);
20 | } catch (err) {
21 | return err;
22 | }
23 | },
24 | async getNewDiskByCate({
25 | limit = 35,
26 | offset = 0,
27 | area = 'ALL',
28 | } = {}) { //新碟上架
29 | try {
30 | const res = await http.get(`/album/new`, {
31 | limit,
32 | offset,
33 | area,
34 | });
35 | return res;
36 | } catch (err) {
37 | return err;
38 | }
39 | },
40 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/dj.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getHotDjs(limit = 20) { //最热主播
5 | try {
6 | let res = await http.get(`/dj/toplist/popular`, {
7 | limit,
8 | });
9 | return res.data;
10 | } catch (err) {
11 | return err;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/friend.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getFriendEvent(pagesize = 20, lasttime = -1) { //朋友动态
5 | try {
6 | let res = await http.get(`/event`, {
7 | pagesize,
8 | lasttime
9 | });
10 | return res;
11 | } catch (err) {
12 | return err;
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/home.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export const homeApis = {
4 | async getBanners() { //首页广告
5 | try {
6 | const res = await http.get(`/banner`);
7 | return res.banners;
8 | } catch (err) {
9 | return err;
10 | }
11 | },
12 |
13 | async getRecommend(params) { //热门推荐
14 | const {limit=10, order="hot"} = params
15 | try {
16 | const res = await http.get(`/top/playlist`, {
17 | limit,
18 | order
19 | })
20 | return res.playlists;
21 | } catch (err) {
22 | return err;
23 | }
24 | },
25 |
26 | async getNewDisk(params) { //新碟上架
27 | const { //新碟上架
28 | limit = 50,
29 | offset = 0,
30 | area = 'ALL', //ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本
31 | } = params;
32 | try {
33 | const res = await http.get(`/album/new`, {
34 | limit,
35 | offset,
36 | area,
37 | })
38 | return res.albums;
39 | } catch (err) {
40 | return err;
41 | }
42 | },
43 |
44 | async getTopList() { //所有榜单
45 | try {
46 | const res = await http.get(`/toplist`)
47 | return res.list;
48 | } catch (err) {
49 | return err;
50 | }
51 | },
52 |
53 | async getRank(id) { //榜单
54 | try {
55 | const res = await http.get(`/playlist/detail`, {id})
56 | return res.playlist;
57 | } catch (err) {
58 | return err;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/http.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import store from '@/store';
4 | import { historyAlpha } from '@/App';
5 | import localStore from '@/utils/localStore';
6 | import loginFn from '@/utils/methods/login';
7 |
8 | const isDev = process.env.NODE_ENV === 'development';
9 |
10 | class Http {
11 | instance = null;
12 | constructor() {
13 | this.init();
14 | }
15 | init() {
16 | // 创建 axios 实例
17 | this.instance = axios.create({
18 | baseURL: isDev ? '/api' : 'http://zhoup.top:7003/',
19 | timeout: 10000, // 请求超时时间
20 | })
21 | this.instance.defaults.withCredentials = true;
22 | // 拦截器配置---------------------------------------------------------------------------------------------------------------
23 | this.instance.interceptors.request.use(function (config) {
24 | // 在发送请求之前做些什么
25 | return config
26 | }, function (error) {
27 | return Promise.reject(error)
28 | })
29 | this.instance.interceptors.response.use(function (res) {
30 | const { data: {code} } = res;
31 | // do something
32 | if (code === 200) {
33 | return res.data;
34 | } else {
35 | return Promise.reject(res.data);
36 | }
37 | }, function (error) {
38 | console.log(error.response);
39 | const { data: {code, msg} } = error.response
40 | switch (code) {
41 | case 301: //未登录或登录token失效
42 | notLoginHandler();
43 | return Promise.reject(msg);
44 | case 400:
45 | return Promise.reject(false)
46 | default:
47 | return '系统异常'
48 | }
49 | })
50 | }
51 | get(url, params = {}) {
52 | return this.instance.get(url, {
53 | params
54 | })
55 | }
56 | post(url, data = {}) {
57 | return this.instance.post(url, {
58 | data
59 | })
60 | }
61 | }
62 |
63 | function notLoginHandler() { //未登录或登录失效
64 | const userInfo = store.getState().user;
65 | const localUser = localStore.get('user');
66 | const {phone, password, remember} = localUser;
67 | //未登录
68 | if (userInfo?.token == undefined) { //没有token记录,则退回首页
69 | historyAlpha.history.replace('/')
70 | } else { //token存在,但已过期
71 | loginFn.logout(historyAlpha.history.replace('/'));
72 | //记住密码情况
73 | if (remember) {
74 | loginFn.login({phone, password})
75 | }
76 | }
77 | }
78 |
79 | export default new Http();
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/login.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 | import { message } from 'antd';
3 |
4 | export default {
5 | async login(phone, password) { //登录
6 | try {
7 | const res = await http.get('/login/cellphone', {
8 | phone,
9 | password
10 | })
11 | if (res.code !== 200) { //未登录成功
12 | message.error(res.message);
13 | return false;
14 | }
15 | message.success(`登录成功!欢迎${res.profile.nickname}来到Jacky网易music`);
16 | return res;
17 | } catch (err) {
18 | console.log(err)
19 | const { msg = '账号错误或不存在' } = err;
20 | message.error(msg);
21 | return {
22 | error: true
23 | };
24 | }
25 | },
26 | async logout() { //登出
27 | try {
28 | const res = await http.get('/logout');
29 | if (res.code === 200) {
30 | message.success('已安全退出');
31 | return true;
32 | } else {
33 | message.warning('退出失败,请重试!');
34 | return false;
35 | }
36 | } catch (e) {
37 | message.warning('退出失败,请重试!');
38 | return false;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/radio.js:
--------------------------------------------------------------------------------
1 | import http from "./http";
2 |
3 | export default {
4 | async getCateLists() { //电台分类列表
5 | try {
6 | const res = await http.get('/dj/catelist')
7 | return res.categories ?? [];
8 | } catch (err) {
9 | console.log(err);
10 | return [];
11 | }
12 | },
13 | async getRecommend(limit=10) { //电台推荐
14 | try {
15 | const res = await http.get('/dj/recommend', {limit})
16 | return res.djRadios ?? [];
17 | } catch (err) {
18 | console.log(err);
19 | return [];
20 | }
21 | },
22 | async getToplists(limit=10) { //电台节目榜
23 | try {
24 | const res = await http.get('/dj/program/toplist', {limit})
25 | return res.toplist ?? [];
26 | } catch (err) {
27 | console.log(err);
28 | return [];
29 | }
30 | },
31 | async getRecommendByType(type) { //电台分类推荐
32 | try {
33 | const res = await http.get('/dj/recommend/type', {type})
34 | return res.djRadios ?? [];
35 | } catch (err) {
36 | console.log(err);
37 | return [];
38 | }
39 | },
40 | async getHotByType({limit = 30, offset = 0, cateId} = {}) { //电台-类别热门电台
41 | try {
42 | const res = await http.get('/dj/radio/hot', {limit, offset, cateId})
43 | return res;
44 | } catch (err) {
45 | console.log(err);
46 | return {};
47 | }
48 | },
49 | async getDetails(rid) { //电台详情
50 | try {
51 | const res = await http.get('/dj/detail', {rid})
52 | return res.data;
53 | } catch (err) {
54 | console.log(err);
55 | return {};
56 | }
57 | },
58 | async getProgram(rid, {limit=30, offset=0, asc=false} = {}) { //电台节目详情
59 | try {
60 | const res = await http.get('/dj/program', {
61 | rid,
62 | limit,
63 | offset,
64 | asc
65 | })
66 | return res;
67 | } catch (err) {
68 | console.log(err);
69 | return {};
70 | }
71 | },
72 |
73 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/search.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export const searchApi = {
4 | async searchByKw(keywords, type, limit=30, offset=0) {
5 | try {
6 | const res = await http.get(`/cloudsearch`, {
7 | type,
8 | limit,
9 | offset,
10 | keywords
11 | });
12 | return res.result;
13 | } catch (err) {
14 | return err;
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/singer.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getHotSingers(limit=50, offset=0) { //热门歌手
5 | try {
6 | let res = await http.get(`/top/artists`, {
7 | limit,
8 | offset
9 | });
10 | return res.artists;
11 | } catch (err) {
12 | return err;
13 | }
14 | },
15 | async getSongs(id, limit=21, offset=0, order='hot') { //歌手歌曲
16 | try {
17 | let res = await http.get(`/artist/songs`, {
18 | limit,
19 | offset,
20 | id,
21 | order
22 | });
23 | return res;
24 | } catch (err) {
25 | return err;
26 | }
27 | },
28 | async getDetails(id) { //歌手详情
29 | try {
30 | let res = await http.get(`/artist/detail`, {
31 | id,
32 | });
33 | return res.data;
34 | } catch (err) {
35 | return err;
36 | }
37 | },
38 | async getIntr(id) { //歌手介绍
39 | try {
40 | let res = await http.get(`/artist/desc`, {
41 | id,
42 | });
43 | return res;
44 | } catch (err) {
45 | return err;
46 | }
47 | },
48 | async getAlbum(id, limit = 12, offset = 0) { //歌手专辑
49 | try {
50 | let res = await http.get(`/artist/album`, {
51 | id,
52 | limit,
53 | offset,
54 | });
55 | return res.hotAlbums;
56 | } catch (err) {
57 | return err;
58 | }
59 | },
60 | async getMv(id, limit = 12, offset = 0) { //歌手mv
61 | try {
62 | let res = await http.get(`/artist/mv`, {
63 | id,
64 | limit,
65 | offset,
66 | });
67 | return res.mvs;
68 | } catch (err) {
69 | return err;
70 | }
71 | },
72 | async getSimi(id) { //相似歌手
73 | try {
74 | let res = await http.get(`/simi/artist`, {
75 | id,
76 | });
77 | return res.artists;
78 | } catch (err) {
79 | return err;
80 | }
81 | },
82 | async getCateList({
83 | type = -1,
84 | area = -1,
85 | initial = '',
86 | limit = 50,
87 | offset = 0
88 | }) { //歌手分类列表
89 | try {
90 | let res = await http.get(`/artist/list`, {
91 | type,
92 | area,
93 | initial,
94 | limit,
95 | offset
96 | });
97 | return res.artists;
98 | } catch (err) {
99 | return err;
100 | }
101 | },
102 | async topSinger(type = 1) { //歌手排行榜
103 | try {
104 | let res = await http.get(`/toplist/artist`, {
105 | type,
106 | });
107 | return res.list.artists;
108 | } catch (err) {
109 | return err;
110 | }
111 | },
112 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/toplist.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getTopLists() {
5 | try {
6 | let res = await http.get(`/toplist`);
7 | return res.list;
8 | } catch (err) {
9 | return err;
10 | }
11 | },
12 | async getTopDetails(id) {
13 | try {
14 | let res = await http.get(`/playlist/detail`, {id});
15 | return res.playlist;
16 | } catch (err) {
17 | return err;
18 | }
19 | },
20 | async getCmts(id, limit=20, offset=0) {
21 | try {
22 | let res = await http.get(`/comment/playlist`, {id, limit, offset});
23 | return res;
24 | } catch (err) {
25 | return err;
26 | }
27 | },
28 | async getCatlist() {
29 | try {
30 | let res = await http.get(`/playlist/catlist`);
31 | return res;
32 | } catch (err) {
33 | return err;
34 | }
35 | },
36 | async getCateLists(params = {}) {
37 | const {limit=35, order="hot", offset=0, cat="全部"} = params
38 | try {
39 | const res = await http.get(`/top/playlist`, {
40 | limit,
41 | order,
42 | offset,
43 | cat
44 | })
45 | return res;
46 | } catch (err) {
47 | return err;
48 | }
49 | },
50 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/user.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getRecord(uid, type) {
5 | try {
6 | const res = await http.get('/user/record', {
7 | uid,
8 | type
9 | })
10 | return res.allData || res.weekData;
11 | } catch (e) {
12 | return e;
13 | }
14 | },
15 | async getLevel() {
16 | try {
17 | const res = await http.get('/user/level')
18 | return res.data;
19 | } catch (e) {
20 | return e;
21 | }
22 | },
23 | async getPlayList(uid) {
24 | try {
25 | const res = await http.get('/user/playlist', {
26 | uid
27 | })
28 | return res.playlist;
29 | } catch (e) {
30 | return e;
31 | }
32 | },
33 | async getSubscribe(uid) {
34 | try {
35 | const res = await http.get('/user/follows', {uid})
36 | return res.playlist;
37 | } catch (e) {
38 | return e;
39 | }
40 | },
41 | async getSubscribeSinger(uid) {
42 | try {
43 | const res = await http.get('/artist/sublist', {uid})
44 | return res.data;
45 | } catch (e) {
46 | return e;
47 | }
48 | },
49 | async getSubscribeMv() {
50 | try {
51 | const res = await http.get('/mv/sublist');
52 | return res.data;
53 | } catch (e) {
54 | return e;
55 | }
56 | },
57 | async getSubscribeDj() {
58 | try {
59 | const res = await http.get('/dj/sublist');
60 | return res.djRadios;
61 | } catch (e) {
62 | return e;
63 | }
64 | },
65 | async signIn(type = 1) {
66 | try {
67 | const res = await http.get('/yunbei/sign', {type});
68 | return res;
69 | } catch (e) {
70 | return e;
71 | }
72 | },
73 | async signInInfo() {
74 | try {
75 | const res = await http.get('/yunbei/today');
76 | return res;
77 | } catch (e) {
78 | return e;
79 | }
80 | },
81 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/api/video.js:
--------------------------------------------------------------------------------
1 | import http from '@/api/http';
2 |
3 | export default {
4 | async getVideoInfo(id) { //获取视频信息
5 | try {
6 | let res = await http.get(`/video/detail`, {
7 | id,
8 | });
9 | return res.data;
10 | } catch (err) {
11 | return err;
12 | }
13 | },
14 | async getVideoUrl(id) { //获取视频地址
15 | try {
16 | const res = await http.get(`/video/url`, {
17 | id,
18 | });
19 | return res.urls;
20 | } catch (err) {
21 | return err;
22 | }
23 | },
24 | async getMVInfo(id) { //mv信息
25 | try {
26 | const res = await http.get(`/mv/detail`, {
27 | mvid: id
28 | })
29 | return res.data;
30 | } catch (err) {
31 | return err;
32 | }
33 | },
34 | async getMVUrl(id, r=1080) { //获取MV地址
35 | try {
36 | const res = await http.get(`/mv/url`, {
37 | id,
38 | r
39 | });
40 | return [{url: res.data.url, r: res.data.r}];
41 | } catch (err) {
42 | return err;
43 | }
44 | },
45 | async getCmts(id, limit = 20, offset = 0) { //mv评论
46 | try {
47 | const res = await http.get(`/comment/mv`, {
48 | id,
49 | limit,
50 | offset,
51 | });
52 | return res;
53 | } catch (err) {
54 | return err;
55 | }
56 | },
57 | async getVdoCmts(id, limit = 20, offset = 0) { //普通视频评论
58 | try {
59 | const res = await http.get(`/comment/video`, {
60 | id,
61 | limit,
62 | offset,
63 | });
64 | return res;
65 | } catch (err) {
66 | return err;
67 | }
68 | },
69 | async getRelatedVdo(id) { //相关视频
70 | try {
71 | const res = await http.get(`/related/allvideo`, {
72 | id,
73 | });
74 | return res.data;
75 | } catch (err) {
76 | return err;
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Album/AlbumContent.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | let cmtsDatas = {};
5 |
6 | const withAlbumWrap = (Wrap, fns, flag) => {
7 |
8 | return function() {
9 | const {id} = useParams();
10 |
11 | const [albumInfo, changeInfo] = useState({}); //专辑/歌单介绍信息
12 | const [songs, changeSongs] = useState([]); //专辑/歌单歌曲内容
13 | const [cmtsData, changeCmtsData] = useState({ //评论内容
14 | total: 0,
15 | hotCmts: [],
16 | cmts: []
17 | });
18 |
19 | const getAlbumInfo = useCallback(() => { //获取信息
20 | fns.getInfo(id).then(res => {
21 | changeInfo(res.album);
22 | changeSongs(res.songs);
23 | })
24 | }, [])
25 |
26 | const getAlbumCmts = useCallback((id, limit = 20, offset = 0) => { //获取评论
27 | fns.getCmts(id, limit, offset).then(res => {
28 | let dataSet = offset > 0 ? {
29 | ...cmtsDatas,
30 | cmts: res.comments
31 | } : {
32 | total: res.total,
33 | hotCmts: res.hotComments,
34 | cmts: res.comments
35 | }
36 | cmtsDatas = dataSet;
37 | changeCmtsData(dataSet);
38 | })
39 | }, [])
40 |
41 | const changeCmts = async (page, pageSize) => {
42 | await getAlbumCmts(id, 20, (page - 1) * pageSize);
43 | }
44 |
45 | const init = useCallback(() => { //初始化信息
46 | getAlbumInfo(id);
47 | getAlbumCmts(id);
48 | })
49 |
50 | useEffect(() => {
51 | document.body.scrollTop = 0;
52 | init();
53 | }, [])
54 |
55 | return
62 | }
63 | }
64 |
65 | export default withAlbumWrap;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Aside/UserInfo.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 |
4 | export default () => {
5 | const user = useSelector(state => state.user.profile);
6 |
7 | return (
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
16 | { user.nickname }
17 |
18 |
19 |
20 | 没有你,阴的天,极讨厌
21 |
22 |
23 |
24 |
25 | -
26 |
27 | { user.eventCount }
28 | 动态
29 |
30 |
31 | -
32 |
33 | { user.follows }
34 | 关注
35 |
36 |
37 | -
38 |
39 | { user.followeds }
40 | 粉丝
41 |
42 |
43 |
44 |
45 | )
46 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/AsyncComponent.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import Main from './Main'
3 |
4 | /*
5 | 异步加载组件,类似于vue里的 () => import('xxxxx')
6 | */
7 | const AsyncComponent = (importFn) => {
8 | return class extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | Com: null
13 | }
14 | }
15 | componentDidMount() {
16 | this.setComponents();
17 | }
18 | setComponents = () => {
19 | importFn().then(Com => {
20 | this.setState({
21 | Com: Com.default ? Com.default : null
22 | })
23 | })
24 | }
25 | render() {
26 | let Com = this.state.Com;
27 | return (
28 | Com ? (
29 |
30 | {this.props.children}
31 |
32 | ) :
33 | )
34 | }
35 | }
36 | }
37 |
38 | export default AsyncComponent;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/BackTop.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default () => {
4 | function goTop() {
5 | document.body.scrollTo({top: 0, behavior: "smooth"});
6 | }
7 | const [show, changeShow] = useState(false);
8 | useEffect(() => {
9 | document.body.addEventListener('scroll', () => {
10 | if (document.body.scrollTop > 0){
11 | changeShow(true);
12 | } else {
13 | changeShow(false);
14 | }
15 | })
16 | }, [])
17 | return (
18 |
19 | )
20 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Banner/Banner.jsx:
--------------------------------------------------------------------------------
1 | import { Carousel } from 'antd';
2 | import { useState, memo, useCallback, useEffect } from 'react';
3 | import { useSelector, useStore } from 'react-redux';
4 |
5 | import './index.scss';
6 | import { homeApis } from '@/api/home';
7 | import homeAction from '@/views/Home/store/action';
8 |
9 | const Banner = (props) => {
10 | const [bannerIdx, setIdx] = useState(0);
11 | function onChange(from, to) {
12 | setIdx(to);
13 | }
14 | const store = useStore();
15 | const bannersOfStore = useSelector(state => state.homeData.banners)
16 |
17 | const [bannersData, setBannersData] = useState([]);
18 | const _loadBanners = useCallback(async () => { //轮播图
19 | const res = await homeApis.getBanners();
20 | setBannersData(res);
21 | store.dispatch(homeAction.setBanners(res));
22 | }, [])
23 |
24 | function getStoreBanner() { //获取store里面的banner
25 | if (bannersOfStore.length) {
26 | setBannersData(bannersOfStore);
27 | return true;
28 | } else {
29 | return false;
30 | }
31 | }
32 |
33 | useEffect(() => {
34 | /*
35 | 1. 先去store里面拿banner数据
36 | 2. 若发现store里的banner为空则发请求获取
37 | 3. 然后存入store
38 | */
39 | if (getStoreBanner()) return;
40 | _loadBanners();
41 | }, [])
42 |
43 | return (
44 |
45 |
46 |
50 | {
51 | bannersData.map(item => (
52 |
53 |

54 |
55 | ))
56 | }
57 |
58 |
59 |
60 | )
61 | }
62 |
63 | export default memo(Banner);
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Banner/index.scss:
--------------------------------------------------------------------------------
1 | .banners {
2 | .banner-wrap{
3 | position: relative;
4 | width: 982px;
5 | // height: 285px;
6 | margin: 0 auto;
7 | }
8 | .banner-item{
9 | width: 100%;
10 | }
11 |
12 | background-image: url(http://p1.music.126.net/20vOlcFLxpnyXysMlKLFbw==/109951165699299172.jpg?imageView&blur=40x20);
13 | background-size: 6000px;
14 | background-position: center center;
15 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/BlockTitle.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const BlockTitle = (props) => {
5 | const { title, subs = [], path='' } = props;
6 | return (
7 |
8 |
{ title.txt }
9 |
10 | {
11 | subs.map((item, i) => {
12 | return (
13 |
14 |
15 | { item.txt }
16 |
17 | {i < subs.length-1 ? | : null}
18 |
19 | )
20 | })
21 | }
22 |
23 |
24 | 更多
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default BlockTitle;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Cache.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | /*
5 | 缓存页面,类似vue的keepalive,但仅仅是存储原来的state,如果数据极大慎用!
6 | */
7 |
8 | const mapStateToProps = (state) => {
9 | return {
10 | keep: state.globalData.keep
11 | }
12 | }
13 |
14 | const mapDispatchToProps = (dispatch) => {
15 | return {
16 | open: (key) => dispatch({type: 'open', keep: key})
17 | }
18 | }
19 |
20 | const KeepAlive = (Com) => {
21 | let cache = {};
22 | class Alive extends Component {
23 | componentWillUnmount() {
24 | const state = this.com.state;
25 | cache = this.props.keep.includes(this.props.name) ? {...state} : {};
26 | }
27 | componentDidMount() {
28 | console.log(this.props)
29 | console.log(this.com);
30 | const state = this.com.state;
31 | this.props.keep.includes(this.props.name) && this.com.setState({...state, ...cache});
32 | }
33 | componentWillReceiveProps(nextProps) {
34 | console.log(nextProps);
35 | }
36 | render() {
37 | let dom = this.com = com} {...this.props} />;
38 | return (
39 | dom
40 | )
41 | }
42 | }
43 | return connect(mapStateToProps, mapDispatchToProps)(Alive)
44 | }
45 |
46 | export default KeepAlive;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Comment/Comment.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { LazyLoadImage } from 'react-lazy-load-image-component';
3 | import { Pagination, message } from 'antd';
4 | import { useState, useCallback } from 'react';
5 |
6 | function timeFormat(timeStamp) {
7 | const time = new Date(timeStamp);
8 | return time.getFullYear() + '年'
9 | + (time.getMonth() + 1) + '月'
10 | + time.getDate() + '日';
11 | }
12 |
13 | const Comment = (props) => {
14 | const { user, content, time, likedCount, beReplied } = props;
15 |
16 | return (
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | { user.nickname }
33 | :{ content }
34 |
35 |
36 | {
37 | beReplied.length ?
38 |
39 | { beReplied[0].user.nickname }
40 | :{ beReplied[0].content }
41 |
: null
42 | }
43 |
44 |
{ timeFormat(time) }
45 |
46 |
47 | ({ likedCount })
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export const CommentWrap = (props) => {
56 | const { total, hotCmts, cmts, onChange } = props;
57 | const [curPage, changePage] = useState(1);
58 |
59 | const pageChange = useCallback(async (page, pageSize) => {
60 | changePage(page);
61 | message.loading({
62 | content: '获取评论中...',
63 | key: 'loadCmt',
64 | duration: 0 ,
65 | style: {
66 | marginTop: '40vh',
67 | },
68 | });
69 | await onChange(page, pageSize);
70 | message.destroy('loadCmt')
71 | }, [])
72 | return (
73 |
74 |
75 |
76 |
77 |
78 | 评论
79 |
80 |
81 | 共{ total }条评论
82 |
83 |
84 |
85 | {
86 | hotCmts.length > 0 ?
87 | <>
88 |
89 | 精彩评论
90 |
91 | {
92 | hotCmts.map(cmt => {
93 | return (
94 |
95 | )
96 | })
97 | }
98 | >
99 | : null
100 | }
101 |
102 |
103 | {
104 | cmts.length > 0 ?
105 | <>
106 |
107 | 最新评论
108 |
109 | {
110 | cmts.map(cmt => {
111 | return (
112 |
113 | )
114 | })
115 | }
116 | >
117 | : null
118 | }
119 |
120 | {
121 | cmts.length > 0 ?
122 |
: null
132 | }
133 |
134 |
135 |
136 | )
137 | }
138 |
139 | export default Comment;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Common/BtnTools.jsx:
--------------------------------------------------------------------------------
1 | const BtnTools = (props) => {
2 |
3 | const {
4 | shareCount = '',
5 | commentCount = 0,
6 | playHandler = () => {},
7 | addHandler = () => {}
8 | } = props;
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | 播放
17 |
18 |
19 |
20 |
21 | 收藏
22 |
23 |
24 | 分享 { shareCount }
25 |
26 |
27 | 下载
28 |
29 |
30 | 评论 { commentCount }
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default BtnTools;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Common/NavTitle.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 |
5 | export default (props) => {
6 | const { title = '标题', subs = [] } = props;
7 |
8 | return (
9 |
10 |
{ title }
11 |
12 | {
13 | subs.map((item, i) => {
14 | return (
15 |
16 |
17 | { item.txt }
18 |
19 | {i < subs.length-1 ? | : null}
20 |
21 | )
22 | })
23 | }
24 |
25 |
26 | )
27 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Covers/CoverItem.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | const CoverItem = (props) => {
4 | return (
5 |
6 |
7 |

8 |
9 |
10 | props.playFn(props.id)} className="icon-play f-fr">
11 |
12 | {props.playCount}
13 |
14 |
15 |
16 | {props.name}
17 |
18 | {props.children}
19 |
20 | )
21 | }
22 |
23 | export default CoverItem;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import './index.scss';
2 | import { Fragment } from 'react';
3 |
4 | const links = ['服务条款', '隐私政策', '儿童隐私政策', '版权投诉指引', '意见反馈'];
5 | const copys = [
6 | {
7 | href: 'https://web-amped.music.163.com/',
8 | name: 'amped'
9 | },
10 | {
11 | href: 'https://music.163.com/st/userbasic#/auth',
12 | name: 'auth'
13 | },
14 | {
15 | href: 'https://music.163.com/musician/artist',
16 | name: 'musician'
17 | },
18 | {
19 | href: 'https://music.163.com/web/reward',
20 | name: 'reward'
21 | },
22 | {
23 | href: 'https://music.163.com/uservideo#/plan',
24 | name: 'cash'
25 | }
26 | ]
27 |
28 | const CopyItem = (props) => {
29 | const {name, href} = props;
30 | return (
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const Footer = () => {
39 | return (
40 |
41 |
42 |
43 |
80 |
81 | {
82 | copys.map(copy => {
83 | return (
84 |
85 | )
86 | })
87 | }
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | export default Footer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Footer/index.scss:
--------------------------------------------------------------------------------
1 | .g-ft {
2 | position: relative;
3 | height: 172px;
4 | overflow: hidden;
5 | border-top: 1px solid #d3d3d3;
6 | background: #f2f2f2;
7 | .copy {
8 | float: left;
9 | width: 520px;
10 | padding-top: 15px;
11 | line-height: 24px;
12 | .line {
13 | margin: 0 8px 0 10px;
14 | color: #c2c2c2;
15 | }
16 | .police-logo {
17 | width: 14px;
18 | height: 14px;
19 | background: url(https://s2.music.126.net/style/web2/img/3rd/police.png?5b7d7a95c38c2c96979fe26536c2e31f) no-repeat;
20 | background-size: cover;
21 | display: inline-block;
22 | margin-right: 2px;
23 | vertical-align: -2px;
24 | }
25 | .police-text {
26 | font-size: 12px;
27 | }
28 | }
29 | }
30 |
31 | .m-ft {
32 | width: 980px;
33 | margin: 0 auto;
34 | .enter {
35 | width: 420px;
36 | margin-top: 33px;
37 | .unit {
38 | float: left;
39 | width: 60px;
40 | height: 70px;
41 | margin-left: 30px;
42 | text-align: center;
43 | color: #666;
44 | &:first-child {
45 | margin-left: 0;
46 | }
47 | }
48 | .tt {
49 | display: inline-block;
50 | margin: 5px 5px 0;
51 | width: 52px;
52 | height: 14px;
53 | }
54 | .tt-amped {
55 | background-position: 0 -108px !important;
56 | margin-left: -6px;
57 | width: 72px;
58 | }
59 | .tt-auth {
60 | background-position: -1px -91px !important;
61 | }
62 | .tt-musician {
63 | background-position: 0 0 !important;
64 | }
65 | .tt-reward {
66 | background-position: 0 -54px !important;
67 | }
68 | .tt-cash {
69 | background-position: -1px -72px !important;
70 | }
71 | }
72 | .logonew {
73 | display: block;
74 | float: none;
75 | width: 50px;
76 | height: 45px;
77 | margin: 0 auto;
78 | &+ .tt {
79 | background: url(https://s2.music.126.net/style/web2/img/foot_enter_tt.png?7c878947e6917400f79b0559edccb43e) no-repeat;
80 | background-size: 180px 139px;
81 | }
82 | }
83 | .logo.logonew {
84 | background: url(https://s2.music.126.net/style/web2/img/foot_enter_new.png?c9f97f7ec7246d8055a94bc6dc229483) no-repeat;
85 | background-size: 110px 552px;
86 | }
87 | .logonew.logo-amped {
88 | background-position: -63px -456.5px;
89 | }
90 | .logonew.logo-auth {
91 | background-position: -63px -101px;
92 | }
93 | .logonew.logo-musician {
94 | background-position: 0 0;
95 | }
96 | .logonew.logo-reward {
97 | background-position: -60px -50px;
98 | }
99 | .logonew.logo-cash {
100 | background-position: 0 -101px;
101 | }
102 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Header/nav.js:
--------------------------------------------------------------------------------
1 | export const navs = [
2 | {
3 | title: '发现音乐',
4 | path: '/home'
5 | },
6 | {
7 | title: '我的音乐',
8 | path: '/my'
9 | },
10 | {
11 | title: '朋友',
12 | path: '/friend'
13 | },
14 | {
15 | title: '商城',
16 | path: '/Mall'
17 | },
18 | {
19 | title: '音乐人',
20 | path: '/MusicPeople'
21 | },
22 | {
23 | title: '下载客户端',
24 | path: '/Download'
25 | },
26 | ];
27 |
28 | export const subNav = [
29 | {
30 | path: '',
31 | name: '推荐'
32 | },
33 | {
34 | path: '/toplist',
35 | name: '排行榜'
36 | },
37 | {
38 | path: '/playlist',
39 | name: '歌单'
40 | },
41 | {
42 | path: '/djradio',
43 | name: '主播电台'
44 | },
45 | {
46 | path: '/singer',
47 | name: '歌手'
48 | },
49 | {
50 | path: '/disk',
51 | name: '新碟上架'
52 | },
53 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import Header from '@/components/Header/Header';
2 | import { HashRouter as Router, useHistory, } from 'react-router-dom';
3 | import Footer from '@/components/Footer/Footer';
4 | import PlayBar from '@/components/PlayBar/PlayBar';
5 |
6 | const Layout = (props) => {
7 | return (
8 | <>
9 |
10 |
11 |
12 | {props.children}
13 |
14 |
15 |
16 |
17 | >
18 | )
19 | }
20 |
21 | export default Layout;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Login/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Form, Input, Checkbox, Modal, message } from 'antd';
2 | import { useState, forwardRef, useImperativeHandle } from 'react';
3 |
4 | import loginApi from '@/api/login';
5 | import store from '@/store';
6 | import { routerRef } from '@/router/generateRoute';
7 | import localStore from '@/utils/localStore';
8 | import { aesEncrypt } from '@/utils/pureFunctions';
9 | import { historyAlpha } from '@/App'
10 |
11 |
12 | const isDev = process.env.NODE_ENV === 'development';
13 |
14 | const layout = {
15 | labelCol: {
16 | span: 6,
17 | },
18 | wrapperCol: {
19 | span: 16,
20 | },
21 | };
22 | const tailLayout = {
23 | wrapperCol: {
24 | offset: 6,
25 | span: 16,
26 | },
27 | };
28 |
29 | const LoginForm = ({onFinish, onFinishFailed}) => {
30 | return (
31 |
50 |
51 |
52 |
53 |
63 |
64 |
65 |
66 |
67 | 自动登录
68 |
69 |
70 |
71 |
74 |
75 |
76 | )
77 | }
78 |
79 | export const LoginModalCom = forwardRef((props, ref) => {
80 | console.log(historyAlpha);
81 |
82 | const { title = '手机号登录' } = props;
83 | const [isModalVisible, showModal] = useState(false);
84 | const onFinishFailed = (err) => {
85 | console.log(err);
86 | }
87 | const onFinish = (values) => {
88 | login({
89 | phone:values.phone,
90 | password:values.password,
91 | remember: values.remember
92 | })
93 | }
94 | useImperativeHandle(ref, () => {
95 | return {
96 | show: showModal,
97 | setJump
98 | }
99 | })
100 | const [needJump, setJump] = useState(true)
101 | const login = ({phone, password, remember}) => {
102 | let encryptPhone = phone, encrypPassword = password;
103 | if (!isDev) { //生产环境
104 | encryptPhone = aesEncrypt(phone);
105 | encrypPassword = aesEncrypt(password);
106 | }
107 | loginApi.login(encryptPhone, encrypPassword).then(res => {
108 | if (res.error) {
109 | return;
110 | }
111 | // 登录成功后写入store里面
112 | store.dispatch({type: 'setUserInfo', userInfo: res});
113 | if (remember) { //自动登录
114 | localStore.set('user', {
115 | phone,
116 | password,
117 | remember
118 | })
119 | }
120 | showModal(false);
121 | // 根据条件判断是否进行跳转
122 | needJump && historyAlpha.history.replace('/my/music');
123 | })
124 | }
125 | return (
126 | showModal(false)}
130 | footer={null}
131 | >
132 |
133 |
134 | )
135 | })
136 |
137 | export default LoginForm;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/components/Main.jsx:
--------------------------------------------------------------------------------
1 | const Main = (props) => {
2 | return (
3 |
4 | {props.children}
5 |
6 | )
7 | }
8 |
9 | export default Main;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { Provider } from 'react-redux';
5 | import store from "./store";
6 | import 'antd/dist/antd.css';
7 | import '@assets/style/style.scss';
8 |
9 | const style = {
10 | height: 40,
11 | width: 40,
12 | lineHeight: '40px',
13 | borderRadius: 4,
14 | backgroundColor: '#1088e9',
15 | color: '#fff',
16 | textAlign: 'center',
17 | fontSize: 14,
18 | };
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 | ,
26 | document.getElementById('root')
27 | );
28 |
29 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/generateRoute.js:
--------------------------------------------------------------------------------
1 | import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
2 | import { createRef } from 'react';
3 | export const routerRef = createRef();
4 |
5 | export const generateFn = (route, basePath="") => {
6 | return (
7 |
8 |
9 | {
10 | route.map(parentRoute => {
11 | let realPath = '', redirectPath = '';
12 | if (parentRoute.path.startsWith('/')) {
13 | realPath = parentRoute.path;
14 | } else {
15 | realPath = basePath + '/' + parentRoute.path;
16 | }
17 | if (parentRoute.redirect) {
18 | if (parentRoute.redirect.startsWith('/')) {
19 | redirectPath = parentRoute.redirect;
20 | } else {
21 | redirectPath = basePath + '/' + parentRoute.redirect;
22 | }
23 | }
24 | if (parentRoute.children) {
25 | return (
26 |
30 |
31 | {
32 | generateFn(parentRoute.children, parentRoute.path)
33 | }
34 |
35 |
36 | )
37 | } else {
38 | if (parentRoute.redirect) {
39 | return (
40 |
41 |
42 |
43 | )
44 | } else {
45 | return (
46 |
52 | )
53 | }
54 | }
55 | })
56 | }
57 |
58 |
59 | )
60 | }
61 |
62 |
63 | export default generateFn;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | 路由配置
3 | */
4 |
5 | import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
6 | import AsyncComponent from '@/components/AsyncComponent';
7 | import generateFn from './generateRoute';
8 | import routerTable from './routerTable';
9 |
10 | const Home = AsyncComponent(() => import('@/views/Home/Home'));
11 | const Personal = AsyncComponent(() => import('@/views/Personal/Personal'));
12 |
13 |
14 | const routes = () => (
15 |
16 |
17 |
18 |
19 |
20 | {/* }>
21 | */}
22 |
23 |
24 | {
25 |
26 | }
27 |
28 |
29 |
30 |
31 |
32 | )
33 | const routes2 = generateFn(routerTable)
34 | // console.log(generateFn(routerTable))
35 | export default routes2;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/modules/friend.js:
--------------------------------------------------------------------------------
1 | import AsyncComponent from '@/components/AsyncComponent';
2 | const Friend = AsyncComponent(() => import('@/views/Friend/Friend'));
3 |
4 | export default [
5 | {
6 | path: '/friend',
7 | component: Friend,
8 | name: 'Friend',
9 | },
10 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/modules/home.js:
--------------------------------------------------------------------------------
1 | import AsyncComponent from '@/components/AsyncComponent';
2 | const Home = AsyncComponent(() => import('@/views/Home/Home'));
3 |
4 | const TopList = AsyncComponent(() => import('@/views/Toplist/TopList'));
5 | const TopDetails = AsyncComponent(() => import('@/views/Toplist/Details'));
6 | const SingerList = AsyncComponent(() => import('@/views/Singer/SingerList'));
7 | const SingerCates = AsyncComponent(() => import('@/views/Singer/SingerCates'));
8 | const SingerMoreCates = AsyncComponent(() => import('@/views/Singer/SingerMoreCates'));
9 | const PlayList = AsyncComponent(() => import('@/views/Home/PlayList/PlayList'));
10 | const Disk = AsyncComponent(() => import('@/views/Disk/Disk'));
11 | const Radio = AsyncComponent(() => import('@/views/Radio/Radio'));
12 | const RadioCate = AsyncComponent(() => import('@/views/Radio/RadioCate'));
13 | const RadioDetails = AsyncComponent(() => import('@/views/Radio/RadioDetails'));
14 |
15 | export default [
16 | {
17 | path: '/',
18 | redirect: '/home',
19 | name: 'Home'
20 | },
21 | {
22 | path: '/home',
23 | component: Home,
24 | name: 'Home',
25 | },
26 | {
27 | path: '/home/toplist',
28 | component: TopList,
29 | name: 'TopList',
30 | children: [
31 | {
32 | path: ':id',
33 | component: TopDetails,
34 | name: 'TopDetails',
35 | }
36 | ]
37 | },
38 | {
39 | path: '/home/singer',
40 | component: SingerList,
41 | name: 'SingerList',
42 | children: [
43 | {
44 | path: 'cate/:path',
45 | component: SingerCates,
46 | name: 'SingerCates'
47 | },
48 | {
49 | path: 'cate/:area/:type',
50 | component: SingerMoreCates,
51 | name: 'SingerMoreCates'
52 | }
53 | ]
54 | },
55 | {
56 | path: '/home/playlist',
57 | component: PlayList,
58 | name: 'PlayList'
59 | },
60 | {
61 | path: '/home/disk',
62 | component: Disk,
63 | name: 'Disk'
64 | },
65 | {
66 | path: '/home/djradio',
67 | component: Radio,
68 | name: 'Radio'
69 | },
70 | {
71 | path: '/home/djradio/cate',
72 | component: RadioCate,
73 | name: 'RadioCate'
74 | },
75 | {
76 | path: '/home/djradio/details/:id',
77 | component: RadioDetails,
78 | name: 'RadioDetails'
79 | }
80 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/modules/my.js:
--------------------------------------------------------------------------------
1 | import AsyncComponent from '@/components/AsyncComponent';
2 | const Login = AsyncComponent(() => import('@/views/My/Login'));
3 | const MainPage = AsyncComponent(() => import('@/views/My/MainPage'));
4 | const MyMusic = AsyncComponent(() => import(('@/views/My/Music')));
5 | const MyArtist = AsyncComponent(() => import('@/views/My/MyArtist'));
6 | const MyMv = AsyncComponent(() => import('@/views/My/MV'));
7 | const MyDj = AsyncComponent(() => import('@/views/My/Dj'));
8 | const MyPlayList = AsyncComponent(() => import('@/views/My/PlayList'));
9 |
10 | export default [
11 | {
12 | path: '/my',
13 | redirect: '/my/music',
14 | name: 'MyProfile'
15 | },
16 | {
17 | path: '/my/login',
18 | component: Login,
19 | name: 'Login'
20 | },
21 | {
22 | path: '/user/main',
23 | component: MainPage,
24 | name: 'MyProfile'
25 | },
26 | {
27 | path: '/my/music',
28 | component: MyMusic,
29 | name: 'MyMusic',
30 | children: [
31 | {
32 | path: 'artist',
33 | component: MyArtist,
34 | name: 'MyArtist',
35 | },
36 | {
37 | path: 'mv',
38 | component: MyMv,
39 | name: 'MyMv',
40 | },
41 | {
42 | path: 'dj',
43 | component: MyDj,
44 | name: 'MyDj',
45 | },
46 | {
47 | path: 'playlist/:id',
48 | component: MyPlayList,
49 | name: 'MyPlayList',
50 | },
51 | ]
52 | }
53 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/modules/search.js:
--------------------------------------------------------------------------------
1 | import AsyncComponent from '@/components/AsyncComponent';
2 | const Search = AsyncComponent(() => import('@/views/Search/Search'));
3 |
4 | export default [
5 | {
6 | path: '/search/:kw',
7 | component: Search,
8 | name: 'Search'
9 | },
10 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/modules/singer.js:
--------------------------------------------------------------------------------
1 | import AsyncComponent from '@/components/AsyncComponent';
2 | const Singer = AsyncComponent(() => import('@/views/Singer/Singer'));
3 | const SingerSong = AsyncComponent(() => import('@/views/Singer/Song'));
4 | const SingerIntroduce = AsyncComponent(() => import('@/views/Singer/Introduce'));
5 | const SingerAlbum = AsyncComponent(() => import('@/views/Singer/Album'));
6 | const SingerMV = AsyncComponent(() => import('@/views/Singer/MV'));
7 |
8 | export default [
9 | {
10 | path: '/singer/:id',
11 | component: Singer,
12 | name: 'Singer',
13 | children: [
14 | {
15 | path: 'song',
16 | component: SingerSong,
17 | name: 'SingerSong',
18 | },
19 | {
20 | path: 'introduce',
21 | component: SingerIntroduce,
22 | name: 'SingerIntroduce',
23 | },
24 | {
25 | path: 'album',
26 | component: SingerAlbum,
27 | name: 'SingerAlbum'
28 | },
29 | {
30 | path: 'mv',
31 | component: SingerMV,
32 | name: 'SingerMV'
33 | }
34 | ]
35 | },
36 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/router/routerTable.js:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from 'react';
2 | import AsyncComponent from '@/components/AsyncComponent';
3 | import homeRouter from './modules/home';
4 | import searchRouter from './modules/search';
5 | import singerRouter from './modules/singer';
6 | import myRouter from './modules/my';
7 | import friendRouter from './modules/friend';
8 |
9 | const Personal = AsyncComponent(() => import('@/views/Personal/Personal'));
10 | const Song = AsyncComponent(() => import('@/views/Song/Song'));
11 | const Video = AsyncComponent(() => import('@/views/Video/Video'));
12 | const Album = AsyncComponent(() => import('@/views/Album/Album'));
13 | const PlayList = AsyncComponent(() => import('@/views/Album/PlayList'));
14 |
15 |
16 |
17 | const routes = [
18 | ...homeRouter,
19 | ...searchRouter,
20 | ...singerRouter,
21 | ...myRouter,
22 | ...friendRouter,
23 | {
24 | path: '/song/:id',
25 | component: Song,
26 | name: 'Song'
27 | },
28 | {
29 | path: '/video/:id',
30 | component: Video,
31 | name: 'Video'
32 | },
33 | {
34 | path: '/album/:id',
35 | component: Album,
36 | name: 'Album'
37 | },
38 | {
39 | path: '/playlist/:id',
40 | component: PlayList,
41 | name: 'PlayList'
42 | },
43 |
44 | ]
45 |
46 | export default routes;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/store/action-type.js:
--------------------------------------------------------------------------------
1 | export const SET_CURSONG = 'setCurSong';
2 | export const SET_USERINFO = 'setUserInfo';
3 | export const SET_HISTORY = 'setHistory';
4 | export const GET_SONGINFO = 'getSongInfo';
5 | export const SET_LOCK = 'setLock';
6 | export const SET_LOADING = 'playbarLoading';
7 | export const SET_SUB_NAV = 'setSubNav';
8 | export const SET_SEARCH_TAB = 'setSearchTab';
9 | export const SET_SEARCH_PAGE = 'setSearchPage';
10 | export const SET_SHOWLRC = 'setShowLrc';
11 | export const SET_SHOW_PLAYBAR = 'setShowPlaybar';
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/store/action.js:
--------------------------------------------------------------------------------
1 | import { SET_CURSONG,
2 | SET_USERINFO,
3 | SET_HISTORY,
4 | SET_LOCK,
5 | SET_LOADING,
6 | SET_SUB_NAV,
7 | SET_SEARCH_TAB,
8 | SET_SEARCH_PAGE } from "./action-type";
9 | import commonRequest from '@/api/common';
10 | import { mediaTimeFormat, artistsFormat } from '@/utils/utils';
11 | import store from './index'
12 |
13 | async function getSongById(defaultId) { //根据id获取歌曲信息
14 | const id = defaultId;
15 | const url = await commonRequest.getSongUrl(id);
16 | const lyc = await commonRequest.getLyric(id);
17 | const details = await commonRequest.getSongDetails(id);
18 | return { id, details, lyc, url };
19 | }
20 |
21 | export const setCurSong = (song) => {
22 | return {
23 | type: SET_CURSONG,
24 | song,
25 | }
26 | }
27 |
28 | export const setHistory = (history) => {
29 | return {
30 | type: SET_HISTORY,
31 | history,
32 | }
33 | }
34 |
35 | export const setUserInfo = (userInfo) => {
36 | return {
37 | type: SET_USERINFO,
38 | userInfo
39 | }
40 | }
41 |
42 | export const setLoadingPlaybar = (loading) => {
43 | return {
44 | type: SET_LOADING,
45 | loading,
46 | }
47 | }
48 |
49 | export const getSongInfo = (id) => {
50 | return async (dispatch, getState) => {
51 | dispatch(setLoadingPlaybar(true));
52 | const res = await getSongById(id);
53 | const { lyc, url, details } = res;
54 | dispatch(setLoadingPlaybar(false));
55 | const dispatchRes = dispatch(setCurSong({
56 | url,
57 | name: details.name,
58 | singer: artistsFormat(details.ar),
59 | singerId: details.ar[0].id,
60 | lyc: lyc,
61 | id,
62 | alblum: details.al,
63 | duration: mediaTimeFormat(details.dt / 1000),
64 | mv: details.mv
65 | }))
66 | return dispatchRes;
67 | }
68 | }
69 |
70 | export const setLock = (lock) => {
71 | return {
72 | type: SET_LOCK,
73 | lock
74 | }
75 | }
76 |
77 | export const setSubNav = (show) => {
78 | return {
79 | type: SET_SUB_NAV,
80 | show
81 | }
82 | }
83 |
84 | export const setSearchTab = (tab) => {
85 | return {
86 | type: SET_SEARCH_TAB,
87 | tab
88 | }
89 | }
90 |
91 | export const setSearchPage = (page) => {
92 | return {
93 | type: SET_SEARCH_PAGE,
94 | page
95 | }
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/store/global-data.js:
--------------------------------------------------------------------------------
1 | import { SET_CURSONG,
2 | SET_HISTORY,
3 | SET_SHOWLRC,
4 | SET_LOCK,
5 | SET_LOADING,
6 | SET_SUB_NAV,
7 | SET_SEARCH_TAB,
8 | SET_SEARCH_PAGE,
9 | SET_SHOW_PLAYBAR } from "./action-type";
10 | import sessionStore from '@/utils/sessionStore';
11 |
12 | let initialData = {
13 | curSong: sessionStore.get('globalData').curSong || null, //当前歌曲
14 | keep: ['Home'],
15 | historyPlay: sessionStore.get('globalData').historyPlay || [], //历史播放记录
16 | lock: sessionStore.get('globalData').lock ?? true, //是否锁定播放栏
17 | showPlaybar: sessionStore.get('globalData').showPlaybar ?? true, //是否展示播放条
18 | loading: false, //歌曲加载状态
19 | showSubNav: true, //是否展示二级导航条
20 | searchTab: '1', //搜素类型默认为1
21 | searchPage: 1, //搜素页数默认为1
22 | showLrc: sessionStore.get('globalData').showLrc ?? false, //展示歌词面板
23 | }
24 |
25 | const globalReducer = (state = initialData, action) => {
26 | switch (action.type) {
27 | case SET_CURSONG:
28 | sessionStore.set('globalData', {
29 | ...state,
30 | curSong: action.song
31 | })
32 | return {
33 | ...state,
34 | curSong: action.song
35 | }
36 | case 'setKeep':
37 | let keeps = new Set(state.keep);
38 | keeps.add(action.name);
39 | /* sessionStore.set('globalData', {
40 | ...state,
41 | keep: [...keeps]
42 | }) */
43 | return {
44 | ...state,
45 | keep: [...keeps]
46 | }
47 | case SET_HISTORY:
48 | /*
49 | 此处踩坑😂,redux是对地址的比较
50 | 如果地址没变,即使值变了,UI也不会更新
51 | */
52 | sessionStore.set('globalData', {
53 | ...state,
54 | historyPlay: action.history
55 | })
56 | return {
57 | ...state,
58 | historyPlay: action.history
59 | }
60 | case SET_LOCK:
61 | sessionStore.set('globalData', {
62 | ...state,
63 | lock: action.lock
64 | })
65 | return {
66 | ...state,
67 | lock: action.lock
68 | }
69 | case SET_LOADING:
70 | return {
71 | ...state,
72 | loading: action.loading
73 | }
74 | case SET_SUB_NAV:
75 | return {
76 | ...state,
77 | showSubNav: action.show
78 | }
79 | case SET_SEARCH_TAB:
80 | return {
81 | ...state,
82 | searchTab: action.tab
83 | }
84 | case SET_SEARCH_PAGE:
85 | return {
86 | ...state,
87 | searchPage: action.page
88 | }
89 | case SET_SHOW_PLAYBAR:
90 | sessionStore.set('globalData', {
91 | ...state,
92 | showPlaybar: action.show
93 | })
94 | return {
95 | ...state,
96 | showPlaybar: action.show
97 | }
98 | case SET_SHOWLRC:
99 | sessionStore.set('globalData', {
100 | ...state,
101 | showLrc: action.show
102 | })
103 | return {
104 | ...state,
105 | showLrc: action.show
106 | }
107 | default:
108 | return state;
109 | }
110 | }
111 |
112 | export default globalReducer;
113 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import globalReducer from './global-data';
4 | import userReducer from '@/views/My/store';
5 | import homeReducer from '@/views/Home/store';
6 | import radioReducer from '@/views/Radio/store';
7 |
8 | const reducers = combineReducers({
9 | globalData: globalReducer,
10 | user: userReducer,
11 | homeData: homeReducer,
12 | radioData: radioReducer
13 | })
14 |
15 | const store = createStore(reducers, applyMiddleware(thunk));
16 |
17 | export default store;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/store/user.js:
--------------------------------------------------------------------------------
1 | import { SET_USERINFO } from "./action-type";
2 |
3 | const initialData = {
4 | userInfo: {
5 | name: 'Jacky',
6 | age: 25,
7 | city: 'ChengDu'
8 | }
9 | }
10 |
11 | const userReducer = (state = initialData, action) => {
12 | switch (action.type) {
13 | case SET_USERINFO:
14 | return {
15 | ...state,
16 | userInfo: action.userInfo
17 | }
18 | default:
19 | return state;
20 | }
21 | }
22 |
23 | export default userReducer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/eventBus.js:
--------------------------------------------------------------------------------
1 | class EventBus {
2 |
3 | listenObj = {}
4 |
5 | on(eventName, callback) {
6 | if (this.listenObj[eventName] === undefined) {
7 | this.listenObj[eventName] = [];
8 | }
9 | this.listenObj[eventName].push(callback);
10 | }
11 |
12 | emit(eventName, paramLists) {
13 | if (this.listenObj[eventName]) {
14 | this.listenObj[eventName].forEach(function(cb) {
15 | cb(paramLists);
16 | })
17 | }
18 | }
19 |
20 | off(eventName, callback) {
21 | if (this.listenObj[eventName]) {
22 | let idx = this.listenObj[eventName].findIndex(item => item === callback);
23 | this.listenObj[eventName].splice(idx, 1);
24 | }
25 | }
26 |
27 | once(eventName, callback) {
28 | const cb = (...arg) => {
29 | callback([...arg]);
30 | this.off(eventName, callback);
31 | }
32 | this.on(eventName, cb);
33 | }
34 | }
35 |
36 |
37 | export default new EventBus();
38 |
39 |
40 | /*
41 | eBus.on('xxx', (parmas) => {
42 |
43 | })
44 |
45 | eBus.emit('xxx', [1,2,3])
46 | */
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/hooks.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export const usePage = () => { //分页相关
4 | const [pageInfo, setPageInfo] = useState({
5 | total: 0,
6 | curPage: 1,
7 | });
8 |
9 | const [total, setTotal] = useState(0);
10 | const [curPage, setCurPage] = useState(1);
11 |
12 | return {total, setTotal, curPage, setCurPage};
13 | }
14 |
15 | export const useLoading = () => {
16 | const [loading, setLoading] = useState(true);
17 |
18 | const toggleLoad = (flag = true, timeout) => {
19 | if (typeof timeout === 'number') {
20 | setTimeout(setLoading(flag), timeout);
21 | } else {
22 | setLoading(flag);
23 | }
24 | }
25 |
26 | return { loading, toggleLoad }
27 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/localStore.js:
--------------------------------------------------------------------------------
1 | const localStore = {
2 | get(key) {
3 | try{
4 | let res = localStorage.getItem(key);
5 | if(res == null) {
6 | return {};
7 | }else {
8 | res = JSON.parse(res);
9 | return res;
10 | }
11 | }catch(e) {
12 | return {};
13 | }
14 | },
15 | set(key, val) {
16 | localStorage.setItem(key, JSON.stringify(val));
17 | },
18 | remove(key) {
19 | localStorage.removeItem(key);
20 | },
21 | clear() {
22 | localStorage.clear();
23 | }
24 | }
25 |
26 | export default localStore;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/methods/login.js:
--------------------------------------------------------------------------------
1 | import { render, unmountComponentAtNode } from 'react-dom';
2 |
3 | import { LoginModalCom } from '@/components/Login/LoginForm';
4 | import { createRef } from 'react';
5 | import store from '@/store';
6 | import loginApi from '@/api/login';
7 | import localStore from '@/utils/localStore';
8 | import { aesEncrypt } from '@/utils/pureFunctions';
9 |
10 | const isDev = process.env.NODE_ENV === 'development';
11 |
12 | function createModal() {
13 | if (instance) return;
14 | instance = document.createElement('div');
15 | render(, instance);
16 | document.body.appendChild(instance);
17 | }
18 |
19 | let instance,
20 | loginRef = createRef(); //实例
21 |
22 | export default {
23 | openLogin(jump = true) {
24 | // 1.查看是否已经创建了login实例
25 | createModal();
26 | // 2.调用实例show方法
27 | loginRef.current.show(true);
28 | loginRef.current.setJump(jump);
29 | },
30 | killLogin() {
31 | instance && unmountComponentAtNode(instance);
32 | loginRef = null;
33 | },
34 | logout(jump) {
35 | loginApi.logout().then(res => {
36 | if (!res) return;
37 | store.dispatch({type: 'setUserInfo', userInfo: {}});
38 | localStore.clear();
39 | jump && jump();
40 | })
41 | },
42 | login({phone, password}) {
43 | if (!isDev) {
44 | phone = aesEncrypt(phone);
45 | password = aesEncrypt(password);
46 | }
47 | return loginApi.login(phone, password).then(res => {
48 | if (res.error) return;
49 | // 登录成功后写入store里面
50 | store.dispatch({type: 'setUserInfo', userInfo: res});
51 | })
52 | }
53 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/pureFunctions.js:
--------------------------------------------------------------------------------
1 | import { areaList } from '@vant/area-data';
2 | import * as crypto from 'crypto';
3 | import { message, Modal } from 'antd';
4 | import sessionStore from './sessionStore';
5 |
6 | var key = '@(5h)-$3_if(*%#';
7 |
8 | export const areaFormat = (cityCode, type = 1) => { //根据城市编号获取地区
9 | /*
10 | cityCode 代表城市代码
11 | type 代表转换形式
12 | 1. 省 - 市
13 | 2. 省
14 | 3. 市
15 | */
16 | if (cityCode == null) {
17 | return '来自火星';
18 | }
19 | let provinceCode = ~~(cityCode / 1000) * 1000;
20 | // 510100 成都市
21 | // 510000 四川省
22 | let province = areaList.province_list[provinceCode],
23 | city = areaList.city_list[cityCode];
24 | switch (type) {
25 | case 1:
26 | return `${province} - ${city}`;
27 | case 2:
28 | return `${province}`;
29 | case 3:
30 | return `${city}`;
31 | }
32 | }
33 |
34 | export function aesEncrypt(data, key='g6@d5*&f8fe$s4ff8e') { //加密
35 | const cipher = crypto.createCipher('aes192', key);
36 | var crypted = cipher.update(data, 'utf8', 'hex');
37 | crypted += cipher.final('hex');
38 | return crypted;
39 | }
40 |
41 | export function downLoadImg(url, name) { //下载图片
42 | const aTag = document.createElement('a');
43 | aTag.setAttribute('href', url);
44 | aTag.setAttribute('download', name);
45 | document.body.append(aTag);
46 | aTag.click();
47 | document.body.removeChild(aTag);
48 | }
49 |
50 | // 下载服务器的MP3文件
51 | export const downloadMp3 = (filePath, name, showTips = true) => {
52 | const key = filePath;
53 | showTips && message.loading({ content: `《${name}》正在下载中...`, key })
54 | fetch(filePath).then(res => res.blob()).then(blob => {
55 | const a = document.createElement('a');
56 | document.body.appendChild(a)
57 | a.style.display = 'none';
58 | // 使用获取到的blob对象创建的url
59 | const url = window.URL.createObjectURL(blob);
60 | a.href = url;
61 | // 指定下载的文件名
62 | a.download = name;
63 | a.click();
64 | document.body.removeChild(a)
65 | // 移除blob对象的url
66 | window.URL.revokeObjectURL(url);
67 | showTips && message.success({ content: `《${name}》下载完成`, key })
68 | });
69 | }
70 |
71 | export function parseJson(jsonObj, path) {
72 | let target = JSON.parse(jsonObj),
73 | pathArr = path.split('.');
74 |
75 | while (pathArr.length) {
76 | let prop = pathArr.shift();
77 | target = target?.[prop];
78 | }
79 |
80 | return target;
81 | }
82 |
83 | export function enterTips() {
84 | const tag = sessionStore.get('enter');
85 | if (JSON.stringify(tag) === '{}') {
86 | Modal.info({
87 | width: 700,
88 | title: 'FBI Warning 郑重提示',
89 | content: (
90 |
103 | ),
104 | onOk() {
105 | sessionStore.set('enter', true);
106 | },
107 | });
108 | }
109 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/utils/sessionStore.js:
--------------------------------------------------------------------------------
1 | const sessionStore = {
2 | get(key) {
3 | try{
4 | let res = sessionStorage.getItem(key);
5 | if(res == null) {
6 | return {};
7 | }else {
8 | res = JSON.parse(res);
9 | return res;
10 | }
11 | }catch(e) {
12 | return {};
13 | }
14 | },
15 | set(key, val) {
16 | sessionStorage.setItem(key, JSON.stringify(val));
17 | },
18 | remove(key) {
19 | sessionStorage.removeItem(key);
20 | },
21 | clear() {
22 | sessionStorage.clear();
23 | }
24 | }
25 |
26 | export default sessionStore;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Album/Album.jsx:
--------------------------------------------------------------------------------
1 | import AlbumTemp from '@/components/Album/AlbumTemp';
2 | import withAlbumWrap from '@/components/Album/AlbumContent';
3 | import albumApi from '@/api/album';
4 |
5 | const getInfo = async (id) => {
6 | const res = await albumApi.getAlbumInfo(id);
7 | return res;
8 | }
9 |
10 | const getCmts = async (id, limit = 20, offset = 0) => {
11 | const res = await albumApi.getAlbumCmt(id, limit, offset);
12 | return res;
13 | }
14 |
15 | const fns = {
16 | getInfo,
17 | getCmts
18 | }
19 |
20 | const Page = withAlbumWrap(AlbumTemp, fns, 'album');
21 |
22 | export default Page;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Album/PlayList.jsx:
--------------------------------------------------------------------------------
1 | import AlbumTemp from '@/components/Album/AlbumTemp';
2 | import withAlbumWrap from '@/components/Album/AlbumContent';
3 | import albumApi from '@/api/album';
4 |
5 | const getInfo = async (id) => {
6 | const res = await albumApi.getPlayListInfo(id);
7 | return res;
8 | }
9 |
10 | const getCmts = async (id, limit = 20, offset = 0) => {
11 | const res = await albumApi.getPlayListCmt(id, limit, offset);
12 | return res;
13 | }
14 |
15 | const fns = {
16 | getInfo,
17 | getCmts
18 | }
19 |
20 | const Page = withAlbumWrap(AlbumTemp, fns, 'playlist');
21 |
22 | export default Page;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Disk/Disk.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { Skeleton, Pagination } from 'antd';
4 |
5 | import Main from '@/components/Main';
6 | import NavTitle from '@/components/Common/NavTitle';
7 | import DiskWrap from './components/DiskWrap';
8 | import DiskItem from './components/DiskItem';
9 | import diskApi from '@/api/disk';
10 |
11 | const subs = [
12 | {
13 | txt: '全部',
14 | path: '/home/disk/'
15 | },
16 | {
17 | txt: '华语',
18 | path: '/home/disk/?area=ZH'
19 | },
20 | {
21 | txt: '欧美',
22 | path: '/home/disk/?area=EA'
23 | },
24 | {
25 | txt: '韩国',
26 | path: '/home/disk/?area=KR'
27 | },
28 | {
29 | txt: '日本',
30 | path: '/home/disk/?area=JP'
31 | }
32 | ]
33 |
34 | export default () => {
35 |
36 | // 热门新碟
37 | const [hotDisk, setHotDisk] = useState([]);
38 | const getNewDisk = useCallback(async () => {
39 | const res = await diskApi.getNewDisk();
40 | res && setHotDisk(res);
41 | }, [])
42 |
43 | // 全部新碟分类
44 | const [cateDisk, setCateDisk] = useState({
45 | albums: [],
46 | total: 0
47 | })
48 | const [curPage, setCurPage] = useState(1);
49 | const [area, setArea] = useState('ALL');
50 | const getNewDiskByCate = useCallback(async ({
51 | limit = 35,
52 | area = 'ALL',
53 | offset = 0
54 | } = {}) => {
55 | setCateDisk(prev => ({
56 | total: prev.total,
57 | albums: []
58 | }))
59 | const res = await diskApi.getNewDiskByCate({limit, area, offset});
60 | const { albums = [], total = 0 } = res;
61 | setCateDisk({ albums, total });
62 | }, [])
63 |
64 | const history = useHistory();
65 | const search = history.location.search;
66 | function pageChange(page, pageSize) { //分页改变
67 | setCurPage(page);
68 | history.replace({pathname: '/home/disk', search: `page=${page}&area=${area}`})
69 | }
70 |
71 | function watchPageChange(search) {
72 | const searchParams = new URLSearchParams(search);
73 | console.log(searchParams.get('page'));
74 | const nowPage = searchParams.get('page') ?? 1;
75 | const nowArea = searchParams.get('area') ?? 'ALL';
76 | setCurPage(+nowPage);
77 | setArea(nowArea);
78 | getNewDiskByCate({ offset: (nowPage - 1) * 35, area: nowArea});
79 | }
80 |
81 | useEffect(() => {
82 | getNewDisk();
83 | document.body.scrollTo(0, 0);
84 | }, [])
85 |
86 | useEffect(() => {
87 | watchPageChange(search);
88 | }, [search])
89 |
90 | return (
91 |
92 |
93 |
94 |
95 | {
96 | hotDisk.map(item => {
97 | return
98 | })
99 | }
100 |
101 |
102 |
103 |
104 |
105 | {
106 | cateDisk.albums.map(item => {
107 | return
108 | })
109 | }
110 |
111 |
121 |
122 |
123 | )
124 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Disk/components/DiskItem.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { LazyLoadImage } from 'react-lazy-load-image-component';
3 |
4 | import { artistsFormat, playAlbum } from "@/utils/utils";
5 |
6 | export default (props) => {
7 |
8 | const {
9 | picUrl,
10 | name,
11 | artists,
12 | id
13 | } = props;
14 |
15 | return (
16 |
17 |
18 |
24 |
25 |
26 | playAlbum(id)}>
27 |
28 |
29 | {name}
30 |
31 |
32 |
33 | { artistsFormat(artists) }
34 |
35 |
36 |
37 | )
38 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Disk/components/DiskWrap.jsx:
--------------------------------------------------------------------------------
1 | export default ({children}) => {
2 | return (
3 |
6 | )
7 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Friend/Friend.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/Main';
2 | import UserInfo from '@/components/Aside/UserInfo';
3 | import friendApi from '@/api/friend';
4 | import { EventWrap, EventItem } from './components/Event';
5 |
6 | import { useCallback, useEffect, useState } from 'react';
7 | import { useHistory } from 'react-router-dom';
8 | import { useSelector } from 'react-redux';
9 | import { message } from 'antd';
10 |
11 | const Friend = () => {
12 |
13 | const [eventObj, setEvent] = useState({});
14 | const getEvents = useCallback(async () => {
15 | const res = await friendApi.getFriendEvent();
16 | setEvent({
17 | event: res.event,
18 | lasttime: res.lasttime,
19 | more: res.more
20 | })
21 | }, [])
22 |
23 | const userInfo = useSelector(state => state.user);
24 | const history = useHistory();
25 | useEffect(() => {
26 | if (Reflect.ownKeys(userInfo).length === 0) {
27 | message.warning('请先登录后查看朋友动态!')
28 | history.replace('/my/login');
29 | return;
30 | }
31 | getEvents();
32 | }, [])
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 | 动态
41 |
42 |
43 | {
44 | eventObj?.event?.map(item => {
45 | return
46 | })
47 | }
48 |
49 |
50 |
51 |
52 |
53 |
54 | {
55 | userInfo.token ?
56 | :
57 | null
58 | }
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export default Friend;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Friend/components/Event.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import { timeToYMD, artistsFormat } from '@/utils/utils';
4 | import { parseJson } from '@/utils/pureFunctions';
5 |
6 | export const EventWrap = (props) => {
7 | return (
8 |
9 | { props.children }
10 |
11 | )
12 | }
13 |
14 | export const EventItem = ({info} = {}) => {
15 | const jsonObj = JSON.parse(info.json)
16 |
17 | function shareType() {
18 | if (jsonObj.song) {
19 | return '分享单曲';
20 | } else if (jsonObj.program) {
21 | return '分享节目';
22 | } else {
23 | return '';
24 | }
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |

32 |
33 |
34 |
35 |
36 | { info?.user?.nickname }
37 | { shareType() }
38 |
39 |
40 | {timeToYMD(info?.showTime)}
41 |
42 |
43 | { parseJson(info?.json, 'msg') }
44 |
45 | {
46 | parseJson(info?.json, 'song') ?? parseJson(info?.json, 'program') ?
47 |
48 |
49 |

50 |
51 |
52 |
53 |
54 |
55 | { parseJson(info?.json, 'song.name') ?? parseJson(info?.json, 'program.name')}
56 |
57 |
58 |
59 | {
60 | parseJson(info?.json, 'program.radio.category') ?
61 | { parseJson(info?.json, 'program.radio.category') }
62 | : null
63 | }
64 | { artistsFormat(parseJson(info?.json, 'song.artists') ?? []) || parseJson(info?.json, 'program.radio.name') }
65 |
66 |
67 |
: null
68 | }
69 |
70 | {
71 | info?.pics?.map(pic => {
72 | return (
73 | -
74 |
75 |
76 | )
77 | })
78 | }
79 |
80 |
81 |
82 | )
83 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/PlayList/CateSelector.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Popover, Divider } from 'antd';
2 | import { DownOutlined, GlobalOutlined, ControlOutlined, CoffeeOutlined, SmileOutlined, CustomerServiceOutlined } from '@ant-design/icons';
3 |
4 | import Main from '@/components/Main';
5 | import topListApi from '@/api/toplist';
6 | import { useEffect, useState } from 'react';
7 |
8 | const icons = [
9 | ,
10 | ,
11 | ,
12 | ,
13 |
14 | ]
15 |
16 | function formatCateLists(categories, subs) { //歌单分类整理
17 | const cateLists = new Array(categories.length);
18 | subs.map(sub => {
19 | let idx = sub.category;
20 | if (!cateLists[idx]) {
21 | cateLists[idx] = {
22 | title: '',
23 | sub: []
24 | }
25 | }
26 | if (!cateLists[idx].title) {
27 | cateLists[idx].title = categories[idx];
28 | }
29 | if (!cateLists[idx].icon) {
30 | cateLists[idx].icon = icons[idx];
31 | }
32 | cateLists[idx].sub.push(sub.name);
33 | })
34 |
35 | return cateLists;
36 | }
37 |
38 | function createCatePannel(cateLists, clickHanddler) {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 | {
47 | cateLists.map(cate => {
48 | return (
49 |
50 |
51 | { cate.icon }
52 | { cate.title }
53 |
54 |
55 | {
56 | cate.sub.map(item => {
57 | return clickHanddler(item)} key={item} className="cate-item">{ item }
58 | })
59 | }
60 |
61 |
62 | )
63 | })
64 | }
65 |
66 |
67 | )
68 | }
69 |
70 | export default ({fn}) => {
71 |
72 | const [showCate, setShowCate] = useState(false);
73 | const [content, setCon] = useState(null);
74 | const getCateLists = async () => { //获取所有歌单种类
75 | const res = await topListApi.getCatlist();
76 | const cateLists = formatCateLists(Object.values(res.categories), res.sub)
77 | setCon(createCatePannel(cateLists, catClick))
78 | }
79 |
80 | function catClick(cat) {
81 | setShowCate(false);
82 | console.log(cat);
83 | fn({cat})
84 | }
85 |
86 | useEffect(() => {
87 | getCateLists();
88 | }, [])
89 |
90 | return (
91 | setShowCate(visible)}
93 | visible={showCate}
94 | placement="bottomLeft"
95 | content={content}
96 | trigger="click"
97 | >
98 | } className="choose-kind">选择分类
99 |
100 | )
101 | }
102 |
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/PlayList/PlayList.jsx:
--------------------------------------------------------------------------------
1 | import { Pagination, Skeleton } from 'antd';
2 | import { useHistory } from 'react-router-dom';
3 |
4 | import Main from '@/components/Main';
5 | import topListApi from '@/api/toplist';
6 | import { useCallback, useEffect, useMemo, useState } from 'react';
7 | import CateSelector from './CateSelector';
8 | import AsyncComponent from '@/components/AsyncComponent';
9 | import { playList } from '@/utils/utils';
10 |
11 | const CoverItem = AsyncComponent(() => import('@/components/Covers/CoverItem'));
12 |
13 | function matchParams(str, queryNameArr) {
14 | str = decodeURIComponent(str)
15 | let queryArr = [], reg;
16 | queryNameArr.map(name => {
17 | reg = new RegExp(`${name}\=(.+)(?=\&)`);
18 | let matchRes = str.match(reg);
19 | matchRes && queryArr.push(matchRes[1]);
20 | })
21 |
22 | reg = /(?<=\=)(.+?)\S/g;
23 | let matchRes = str.match(reg);
24 | matchRes && queryArr.push(matchRes[1]);
25 |
26 | return queryArr;
27 | }
28 |
29 | export default () => {
30 |
31 | const history = useHistory();
32 |
33 | const [playLists, setLists] = useState([]);
34 | const [listsInfo, setListsInfo] = useState({});
35 | const [loading, setLoading] = useState(true);
36 | const getCatelists = useCallback(async (params = {}) => {
37 | setLoading(true);
38 | const res = await topListApi.getCateLists(params);
39 | setLists(res.playlists);
40 | setListsInfo({
41 | cat: res.cat,
42 | total: res.total
43 | })
44 | setLoading(false);
45 | }, [])
46 |
47 | // 页码变化
48 | const [curPage, setCurPage] = useState(1);
49 | function pageChange(page, pageSize) {
50 | setCurPage(page);
51 | getCatelists({
52 | cat: listsInfo.cat,
53 | offset: (page - 1) * pageSize,
54 | })
55 | history.replace(`/home/playlist?page=${page}&cat=${listsInfo.cat}`)
56 | }
57 |
58 | useEffect(async () => {
59 | const [page = 1, cat = '全部'] = matchParams(history.location.search, ['page', 'cat'])
60 | await getCatelists({cat, offset: (page - 1) * 35});
61 | setTimeout(() => setCurPage(Number(page)))
62 | }, [])
63 |
64 | //分类变化自动初始化为第一页
65 | const cat = useMemo(() => listsInfo.cat)
66 | useEffect(() => {
67 | setCurPage(1);
68 | history.replace(`/home/playlist?page=1&cat=${cat}`)
69 | }, [cat])
70 |
71 | return (
72 |
73 |
74 |
75 |
76 | { listsInfo?.cat }
77 |
78 |
79 |
80 |
81 | 热门
82 |
83 |
84 |
91 |
92 | {
93 | playLists.map((item, i) => {
94 | return (
95 |
100 | )
101 | })
102 | }
103 |
104 |
113 |
114 |
115 | )
116 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/Swiper.jsx:
--------------------------------------------------------------------------------
1 | import { createRef, memo, useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useEffect, useCallback } from 'react';
4 | import { useStore, useSelector } from 'react-redux';
5 |
6 | import { Carousel } from 'antd';
7 | import './index.scss';
8 | import homeAction from './store/action';
9 | import { homeApis } from '@/api/home';
10 |
11 | const NewDiskSwiper = (props) => {
12 | const { newDisk } = props;
13 | const swp = createRef(null);
14 | function changeSwiper(flag) {
15 | flag ? swp.current.next() : swp.current.prev()
16 | }
17 |
18 | const [newDiskData, setNewDiskData] = useState([]);
19 | const store = useStore();
20 | const _getNewDisk = useCallback(async () => {
21 | let res = await homeApis.getNewDisk({limit: 10});
22 | res = [[...res.slice(0,5)], [...res.slice(5,)]];
23 | setNewDiskData(res);
24 | store.dispatch(homeAction.setNewDisk(res));
25 | }, [])
26 |
27 | const newDiskDataOfStore = useSelector(state => state.homeData.newDisk)
28 | function getNewDiskOfStore() {
29 | console.log(newDiskDataOfStore);
30 | if (newDiskDataOfStore.length) {
31 | setNewDiskData(newDiskDataOfStore);
32 | return true;
33 | } else {
34 | return false;
35 | }
36 | }
37 |
38 | useEffect(() => {
39 | if (getNewDiskOfStore()) return;
40 | _getNewDisk();
41 | }, [])
42 |
43 | return (
44 |
45 |
changeSwiper(false)}
47 | className="click-flag pre s-bg s-bg-7 f-tdn"
48 | >
49 |
50 | {newDiskData.map((item, i) => {
51 | return (
52 |
75 | );
76 | })}
77 |
78 |
changeSwiper(true)}
80 | className="click-flag nxt s-bg s-bg-8 f-tdn"
81 | >
82 |
83 | );
84 | };
85 |
86 |
87 | export default memo(NewDiskSwiper);
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | hotNav: {
3 | title: {
4 | txt: '热门推荐',
5 | path: '/home/playlist'
6 | },
7 | subs: [
8 | {
9 | path: '/home/playlist?page=1&cat=华语',
10 | txt: '华语'
11 | },
12 | {
13 | path: '/home/playlist?page=1&cat=流行',
14 | txt: '流行'
15 | },
16 | {
17 | path: '/home/playlist?page=1&cat=摇滚',
18 | txt: '摇滚'
19 | },
20 | {
21 | path: '/home/playlist?page=1&cat=民谣',
22 | txt: '民谣'
23 | },
24 | {
25 | path: '/home/playlist?page=1&cat=电子',
26 | txt: '电子'
27 | }
28 | ]
29 | }
30 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/index.scss:
--------------------------------------------------------------------------------
1 | .new-swiper {
2 | >span{
3 | cursor: pointer;
4 | }
5 | margin: 20px 0 37px;
6 | }
7 |
8 | .n-bilst {
9 | height: 472px;
10 | margin-top: 20px;
11 | padding-left: 1px;
12 | .blk {
13 | float: left;
14 | width: 228px;
15 | }
16 | .top {
17 | height: 100px;
18 | padding: 20px 0 0 19px;
19 | box-sizing: content-box;
20 | .msk {
21 | cursor: pointer;
22 | }
23 | }
24 | .cver {
25 | float: left;
26 | display: inline;
27 | }
28 | .tit {
29 | float: left;
30 | width: 116px;
31 | margin: 6px 0 0 10px;
32 | h3 {
33 | width: 100%;
34 | }
35 | >div {
36 | cursor: pointer;
37 | }
38 | }
39 | .btn {
40 | margin-top: 10px;
41 | span {
42 | display: block;
43 | float: left;
44 | width: 22px;
45 | height: 22px;
46 | margin-right: 10px;
47 | text-indent: -9999px;
48 | cursor: pointer;
49 | }
50 | }
51 | ol {
52 | height: 319px;
53 | margin-left: 50px;
54 | line-height: 32px;
55 | li {
56 | height: 32px;
57 | * {
58 | vertical-align: middle;
59 | }
60 | &:hover .oper{
61 | display: block;
62 | }
63 | }
64 | .no {
65 | float: left;
66 | position: relative;
67 | width: 35px;
68 | height: 32px;
69 | margin-left: -35px;
70 | text-align: center;
71 | color: #666;
72 | font-size: 16px;
73 | }
74 | .no-top {
75 | color: #c10d0c;
76 | }
77 | .nm {
78 | float: left;
79 | width: 96px;
80 | height: 32px;
81 | cursor: pointer;
82 | }
83 | .oper {
84 | display: none;
85 | float: right;
86 | width: 82px;
87 | margin-top: 7px;
88 | em {
89 | float: left;
90 | width: 17px;
91 | height: 17px;
92 | margin-right: 10px;
93 | cursor: pointer;
94 | }
95 | .u-icn-81 {
96 | margin: 2px 6px 0 0;
97 | background-position: 0 -700px;
98 | &:hover {
99 | background-position: -22px -700px;
100 | }
101 | }
102 | }
103 | }
104 | .more {
105 | clear: both;
106 | height: 32px;
107 | margin-right: 32px;
108 | text-align: right;
109 | line-height: 32px;
110 | }
111 | .blk-1 {
112 | width: 228px;
113 | }
114 | }
115 |
116 | .n-bilst {
117 | background: url(https://s2.music.126.net/style/web2/img/index/index_bill.png?eb8079bc90081df0aac2e62fb669fda1) no-repeat;
118 | }
119 |
120 | .u-cover-4 {
121 | width: 80px;
122 | height: 80px;
123 | .msk {
124 | background-position: -145px -57px;
125 | }
126 | }
127 |
128 | .u-icn-81 {
129 | width: 13px;
130 | height: 13px;
131 | background-position: 0 -700px;
132 | }
133 | .u-icn, .u-icn2, .u-icn3 {
134 | display: inline-block;
135 | overflow: hidden;
136 | vertical-align: middle;
137 | }
138 |
139 | .u-icn, .u-title-1 .out .icon {
140 | background: url(https://s2.music.126.net/style/web2/img/icon.png?bd9557321f2cc59d67f4fc72e00b1ffa) no-repeat 0 9999px;
141 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/store/action.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setBanners(banners) {
3 | return {
4 | type: 'setBanners',
5 | banners,
6 | }
7 | },
8 | setRecommends(recommends) {
9 | return {
10 | type: 'setRecommends',
11 | recommends
12 | }
13 | },
14 | setNewDisk(newDisk) {
15 | return {
16 | type: 'setNewDisk',
17 | newDisk
18 | }
19 | },
20 | setRanks(ranks) {
21 | return {
22 | type: 'setRanks',
23 | ranks
24 | }
25 | },
26 | setHotSingers(hotSingers) {
27 | return {
28 | type: 'setHotSingers',
29 | hotSingers
30 | }
31 | },
32 | setHotDjs(hotDjs) {
33 | return {
34 | type: 'setHotDjs',
35 | hotDjs
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Home/store/index.js:
--------------------------------------------------------------------------------
1 | const initailState = {
2 | banners: [],
3 | recommends: [],
4 | newDisk: [],
5 | ranks: [],
6 | hotSingers: [],
7 | hotDjs: []
8 | }
9 |
10 | const homeReducer = (state = initailState, action) => {
11 | switch (action.type) {
12 | case 'setBanners':
13 | return {
14 | ...state,
15 | banners: action.banners
16 | }
17 | case 'setRecommends':
18 | return {
19 | ...state,
20 | recommends: action.recommends
21 | }
22 | case 'setNewDisk':
23 | return {
24 | ...state,
25 | newDisk: action.newDisk
26 | }
27 | case 'setRanks':
28 | return {
29 | ...state,
30 | ranks: action.ranks
31 | }
32 | case 'setHotSingers':
33 | return {
34 | ...state,
35 | hotSingers: action.hotSingers
36 | }
37 | case 'setHotDjs':
38 | return {
39 | ...state,
40 | hotDjs: action.hotDjs
41 | }
42 | default:
43 | return state;
44 | }
45 | }
46 |
47 | export default homeReducer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/Dj.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useCallback, useEffect, useState } from 'react';
3 | import { Skeleton } from 'antd';
4 |
5 | import userApi from '@api/user';
6 |
7 | const DjItem = (props) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | { props.name }
17 |
18 |
19 |
20 | by
21 | { props.dj.nickname }
22 |
23 |
24 |
25 | { props.programCount }期
26 |
27 | )
28 | }
29 |
30 | const MyDj = () => {
31 |
32 | const [djList, setList] = useState([]);
33 | const [loading, setLoading] = useState(true);
34 | const getDjList = useCallback(async () => {
35 | setLoading(true);
36 | const res = await userApi.getSubscribeDj();
37 | setList(res);
38 | setTimeout(() => setLoading(false), 500);
39 | }, [])
40 |
41 | useEffect(() => {
42 | getDjList();
43 | }, [])
44 |
45 | return (
46 |
47 |
48 |
49 | 我订阅的电台
50 |
51 |
52 |
53 |
54 | {
55 | djList.map(item => {
56 | return (
57 |
58 | )
59 | })
60 | }
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default MyDj;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/Login.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/Main';
2 | import loginFun from '@/utils/methods/login';
3 |
4 | import { useEffect } from 'react';
5 | import { useSelector } from 'react-redux';
6 | import { useHistory } from 'react-router';
7 |
8 | const Login = () => {
9 |
10 | const token = useSelector(state => state.user.token);
11 | const history = useHistory();
12 |
13 | useEffect(() => {
14 | token && history.replace('/my/music');
15 | }, [])
16 |
17 | return (
18 |
19 |
20 |
21 |
登录网易云音乐
22 | loginFun.openLogin(true)}>
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default Login;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/MV.jsx:
--------------------------------------------------------------------------------
1 | import userApi from '@/api/user';
2 | import { useCallback, useEffect, useState } from 'react';
3 | import { Skeleton } from 'antd';
4 |
5 | import { VideoItem } from '@/views/Search/components';
6 |
7 | const MV = () => {
8 |
9 | //我的收藏的mv
10 | const [mv, setMv] = useState([]);
11 | const [loading, setLoading] = useState(true);
12 | const getSubscribeMv = useCallback(async () => {
13 | setLoading(true);
14 | const res = await userApi.getSubscribeMv();
15 | setMv(res);
16 | setTimeout(() => {setLoading(false)}, 500)
17 | } ,[])
18 |
19 | useEffect(() => {
20 | getSubscribeMv();
21 | }, [])
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | 我的视频
29 |
30 |
31 |
32 | {
33 | mv.map(item => {
34 | return
35 | })
36 | }
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export default MV;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/Music.jsx:
--------------------------------------------------------------------------------
1 | import { useLocation, useHistory } from 'react-router-dom';
2 | import { useEffect, useRef } from 'react';
3 | import { useDispatch, useSelector } from 'react-redux';
4 |
5 | import Main from '@/components/Main';
6 | import { MyMenu } from './components/menu/Menu';
7 | import { setSubNav } from '@store/action';
8 |
9 | const MyMusic = (props) => {
10 |
11 | const location = useLocation();
12 | const history = useHistory();
13 | const dispatch = useDispatch();
14 | const userInfo = useSelector(state => state.user);
15 |
16 | useEffect(() => {
17 | if (Reflect.ownKeys(userInfo).length === 0) {
18 | history.replace('/my/login');
19 | return;
20 | }
21 | if (location.pathname === "/my/music") {
22 | history.replace('/my/music/artist')
23 | }
24 | dispatch(setSubNav(false));
25 | const matches = location.pathname.match(/(?<=\/).+?(?=\b)/g);
26 | matches && menuRef.current.setK(matches[matches.length - 1])
27 | })
28 |
29 | const menuRef = useRef();
30 |
31 | return (
32 |
33 |
34 |
35 | { props.children }
36 |
37 |
38 | )
39 | }
40 |
41 | export default MyMusic;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/MyArtist.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useSelector } from 'react-redux';
3 | import { Skeleton } from 'antd';
4 | import { useCallback, useEffect, useState } from 'react';
5 |
6 | import userApi from '@/api/user';
7 |
8 | const SingerItem = ({
9 | img1v1Url = '',
10 | name = '',
11 | mvSize = 0,
12 | albumSize = 0,
13 | id = ''
14 | }) => {
15 | return (
16 |
17 |
18 |
19 |

20 |
21 |
22 |
23 |
24 | { name }
25 |
26 |
{ albumSize }个专辑 { mvSize }个MV
27 |
28 |
29 | )
30 | }
31 |
32 | const MyArtist = () => {
33 |
34 | const user = useSelector(state => state.user.profile);
35 | const [loading, setLoading] = useState(true);
36 |
37 | //获取订阅歌手
38 | const [artists, setArtists] = useState([]);
39 | const getSubscribeSinger = useCallback(async () => {
40 | setLoading(true);
41 | const res = await userApi.getSubscribeSinger(user.userId);
42 | setArtists(res);
43 | setTimeout(() => setLoading(false), 500);
44 | }, [])
45 |
46 | useEffect(() => {
47 | getSubscribeSinger();
48 | }, [])
49 |
50 | return (
51 |
52 |
53 |
54 |
55 | 我的歌手({ artists.length })
56 |
57 |
58 |
59 | {
60 | artists.map(item => {
61 | return
62 | })
63 | }
64 |
65 |
66 |
67 | )
68 | }
69 |
70 | export default MyArtist;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/PlayList.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { Skeleton } from 'antd';
4 |
5 | import BtnTools from '@/components/Common/BtnTools'
6 | import albumApi from '@/api/album';
7 | import { timeToYMD, playList, replaceHistory } from '@/utils/utils';
8 | import { SongItem } from '@/views/Search/components';
9 |
10 | const PlayList = () => {
11 |
12 | const [playListInfo, setPlayInfo] = useState({ //歌单内容
13 | album: null,
14 | songs: []
15 | })
16 | const [loading, setLoading] = useState(true);
17 | const getPlayListInfo = async (id) => {
18 | setLoading(true);
19 | document.body.scrollTo(0,0);
20 | const res = await albumApi.getPlayListInfo(id);
21 | setPlayInfo({
22 | album: res.album,
23 | songs: res.songs
24 | })
25 | setTimeout(() => setLoading(false), 300)
26 | }
27 |
28 | const {id} = useParams();
29 | useEffect(() => {
30 | getPlayListInfo(id);
31 | }, [id])
32 |
33 | return (
34 |
40 |
41 |

42 |
43 |
44 |
45 |
46 | { playListInfo.album?.name }
47 |
48 |
49 |
50 |

51 |
{ playListInfo.album?.creator?.nickname }
52 |
53 |
54 |
playList(id)}
56 | addHandler={() => replaceHistory(playListInfo.songs)}
57 | />
58 |
59 |
60 |
61 | 歌曲列表
62 |
63 |
{ playListInfo.album?.trackCount }首歌
64 |
65 | 播放:{ playListInfo.album?.playCount } 次
66 |
67 |
68 |
69 |
70 | {
71 | playListInfo.songs.map((song, i) => {
72 | return (
73 |
74 | )
75 | })
76 | }
77 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default PlayList;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/components/menu/Menu.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, Switch } from 'antd';
2 | import { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import { useHistory } from 'react-router-dom';
5 |
6 | import { menuData } from './mock';
7 | import userApi from '@/api/user';
8 | import { getTypeOfPlaylist } from '../../MainPage';
9 |
10 | const { SubMenu } = Menu;
11 |
12 | const ListItem = (props) => {
13 | return (
14 |
15 |
16 |

17 |
18 |
19 |
{ props.name }
20 |
{ props.total }首
21 |
22 |
23 | )
24 | }
25 |
26 | export const MyMenu = forwardRef((props, ref) => {
27 | const history = useHistory();
28 | const handleClick = (e) => {
29 | console.log(e);
30 | setK(e.key);
31 | let path;
32 | if (e.key.match(/^\d+$/)) {
33 | path = `/my/music/playlist/${e.key}`;
34 | } else {
35 | path = `/my/music/${e.key}`;
36 | }
37 | history.push(path);
38 | }
39 |
40 | const menuConvert = (menuItem) => {
41 | /*
42 | 1. 是否有后代
43 | 2. 有后代则递归
44 | */
45 | if (!menuItem?.children?.length) {
46 | if (menuItem.info) {
47 | return
48 | } else {
49 | return { menuItem.title }
50 | }
51 | } else {
52 | return (
53 |
54 | {
55 | menuItem.children.map(item => {
56 | return menuConvert(item);
57 | })
58 | }
59 |
60 | )
61 | }
62 | }
63 |
64 | const user = useSelector(state => state.user.profile);
65 | const [realMenuData, setMenuData] = useState(menuData);
66 | const getPlayList = () => { //获取用户歌单
67 | if (!user) {
68 | return;
69 | }
70 | userApi.getPlayList(user.userId).then(res => {
71 | // 设置到我的歌单二级菜单内
72 | const {ownList, collectList} = getTypeOfPlaylist(res, user.userId)
73 | menuData[3].children = [];
74 | menuData[4].children = [];
75 | ownList.map((item, i) => {
76 | menuData[3].children.push({
77 | key: `${item.id}`,
78 | title: item.name,
79 | info: {
80 | id: item.id,
81 | img: item.coverImgUrl,
82 | total: item.trackCount,
83 | name: item.name
84 | }
85 | })
86 | })
87 | collectList.map(item => {
88 | menuData[4].children.push({
89 | key: `${item.id}`,
90 | title: item.name,
91 | info: {
92 | id: item.id,
93 | img: item.coverImgUrl,
94 | total: item.trackCount,
95 | name: item.name
96 | }
97 | })
98 | })
99 | let newMenu = Object.assign([], menuData);
100 | setMenuData(newMenu);
101 | })
102 | }
103 |
104 | useEffect(() => {
105 | getPlayList();
106 | }, [])
107 |
108 | const [curKey, setK] = useState('artist');
109 |
110 | useImperativeHandle(ref, () => ({
111 | setK
112 | }))
113 |
114 | return (
115 |
128 | )
129 | })
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/components/menu/mock.js:
--------------------------------------------------------------------------------
1 | import { WechatOutlined, VideoCameraFilled, CustomerServiceFilled, FileFilled, HeartFilled, UserOutlined } from '@ant-design/icons';
2 |
3 | export const menuData = [
4 | {
5 | title: '我的歌手',
6 | key: 'artist',
7 | icon:
8 | },
9 | {
10 | title: '我的视频',
11 | key: 'mv',
12 | icon:
13 | },
14 | {
15 | title: '我的电台',
16 | key: 'dj',
17 | icon:
18 | },
19 | {
20 | title: '创建的歌单',
21 | key: 'mCreate',
22 | icon: ,
23 | children: [
24 | ]
25 | },
26 | {
27 | title: '收藏的歌单',
28 | key: 'mCollect',
29 | icon: ,
30 | children: [
31 | ]
32 | }
33 | ]
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/My/store/index.js:
--------------------------------------------------------------------------------
1 | import sessionStore from '@/utils/sessionStore';
2 | import localStore from '@/utils/localStore';
3 |
4 | const initialState = sessionStore.get('userInfo') || {};
5 |
6 | /*
7 | login之后,看是否需要自动登录
8 | 需要,则在localStore里存入phone,pwd
9 | */
10 |
11 | const userReducer = (state = initialState, action) => {
12 | switch (action.type) {
13 | case 'setUserInfo':
14 | sessionStore.set('userInfo', action.userInfo);
15 | return {
16 | ...action.userInfo
17 | }
18 | default:
19 | return state;
20 | }
21 | }
22 |
23 | export default userReducer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Personal/Personal.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class Personal extends Component {
4 | render() {
5 | return (
6 |
7 | this is Personal page
8 |
9 | )
10 | }
11 | }
12 |
13 | export default Personal;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Personal/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chensidi/react17-project/92fa757612ce81a226bdc0cc3dd4c3f2556c4706/NeteaseCloudPC/src/views/Personal/index.scss
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/Radio.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@components/Main';
2 | import Category from './components/Category';
3 | import Recommend from './components/Recommend';
4 | import { withDjRecommend, RecommendTemp } from './components/TypeRecommend';
5 | import radioApi from "@api/radio";
6 |
7 | const types = [
8 | {
9 | title: '音乐推荐',
10 | id: 2
11 | },
12 | {
13 | title: '生活',
14 | id: 6
15 | },
16 | {
17 | title: '情感',
18 | id: 3
19 | },
20 | {
21 | title: '创作翻唱',
22 | id: 2001
23 | },
24 | {
25 | title: '知识',
26 | id: 11
27 | }
28 | ]
29 |
30 |
31 | const WrapTypeRecommend = withDjRecommend(RecommendTemp, radioApi.getRecommendByType, {
32 | limit: 4,
33 | })
34 |
35 | export default () => {
36 | return (
37 |
38 |
39 |
40 | {
41 | types.map((item) => {
42 | return
43 | })
44 | }
45 |
46 | )
47 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/RadioCate.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/Main';
2 | import Category from './components/Category';
3 | import radioApi from '@/api/radio';
4 | import { withDjRecommend, RecommendTemp } from './components/TypeRecommend';
5 |
6 | import { useLocation, useHistory } from 'react-router-dom';
7 | import { useCallback, useEffect, useRef, useState } from 'react';
8 | import { Pagination } from 'antd';
9 | import { usePage } from '@/utils/hooks';
10 |
11 |
12 | function getSearchParam(str, k) {
13 | const s = new URLSearchParams(str);
14 | return s.get(k);
15 | }
16 |
17 | const WrapTypeRecommend = withDjRecommend(RecommendTemp, radioApi.getHotByType, {
18 | limit: 20,
19 | title: '电台排行榜',
20 | })
21 |
22 | const RadioCate = () => {
23 |
24 | const { search } = useLocation();
25 | const id = getSearchParam(search, 'id');
26 | const page = getSearchParam(search, 'page');
27 | const [sid, setId] = useState(id);
28 |
29 | const { total, setTotal, curPage, setCurPage } = usePage();
30 | const totalRef = useRef(setTotal);
31 |
32 | const history = useHistory();
33 | const change = (cur) => {
34 | setCurPage(cur);
35 | const search = new URLSearchParams('');
36 | search.set('id', id);
37 | search.set('page', cur);
38 | history.replace('/home/djradio/cate?' + search.toString())
39 | }
40 |
41 | useEffect(() => {
42 | setId(id);
43 | setCurPage(1);
44 | }, [id])
45 |
46 | useEffect(() => {
47 | page && setCurPage(+page);
48 | }, [page])
49 |
50 |
51 | return (
52 |
53 |
54 |
64 |
65 |
75 |
76 | )
77 | }
78 |
79 | export default RadioCate;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/RadioDetails.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/Main';
2 | import Introduce from './components/Introduce';
3 | import RadioTable from './components/RadioTable';
4 | import radioApi from '@/api/radio';
5 | import { useEffect, useState, createContext } from 'react';
6 | import { useParams } from 'react-router';
7 |
8 | export const ApiCtx = createContext();
9 |
10 | export default function () {
11 |
12 | const [intr, setIntr] = useState({dj: {}});
13 | const getDetails = async () => { //电台介绍
14 | const res = await radioApi.getDetails(id);
15 | setIntr(res);
16 | }
17 |
18 | const [programList, setProgram] = useState([]);
19 | const [total, setTotal] = useState([]);
20 | const getProgram = async ({asc} = {}) => { //获取节目列表
21 | const res = await radioApi.getProgram(id, {asc});
22 | setProgram(res.programs)
23 | setTotal(res.count)
24 | }
25 |
26 | const { id } = useParams();
27 |
28 | useEffect(() => {
29 | document.body.scrollTo(0, 0)
30 | getDetails();
31 | getProgram();
32 | }, [])
33 |
34 | return (
35 |
36 |
46 |
47 | )
48 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/components/Category.jsx:
--------------------------------------------------------------------------------
1 | import radioApi from '@api/radio';
2 |
3 | import { useEffect, useState } from 'react';
4 | import { Link, useLocation } from 'react-router-dom';
5 | import { useStore, useSelector } from 'react-redux';
6 |
7 |
8 | const CateItem = ({id, pic84x84IdUrl, name, on}) => {
9 | return (
10 |
14 |
15 | { name }
16 |
17 | )
18 | }
19 |
20 | //Category
21 | export default () => {
22 |
23 | const [cateList, setCateLists] = useState([]);
24 | const store = useStore();
25 | const storeCates = useSelector(state => state.radioData.cates)
26 | const getCateList = async () => {
27 | console.log(storeCates)
28 | if (storeCates.length) {
29 | setCateLists(storeCates);
30 | } else {
31 | const res = await radioApi.getCateLists();
32 | setCateLists(res);
33 | store.dispatch({type: 'setCates', cates: res});
34 | }
35 | }
36 |
37 | const { search } = useLocation();
38 | const id = new URLSearchParams(search).get('id');
39 |
40 | useEffect(() => {
41 | getCateList();
42 | }, []);
43 |
44 | return (
45 |
46 | {
47 | cateList.map((cate) => {
48 | const on = cate.id == id;
49 | return (
50 |
51 | )
52 | })
53 | }
54 |
55 | )
56 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/components/Introduce.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { Button } from 'antd';
3 |
4 | export default function(props) {
5 | console.log(props)
6 |
7 | const {
8 | picUrl,
9 | name,
10 | dj: {
11 | avatarUrl,
12 | nickname
13 | },
14 | category,
15 | desc
16 | } = props
17 |
18 | return (
19 |
20 |
21 |

22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | { name }
31 |
32 |
33 |
34 |
35 |
36 |

37 |
38 |
39 |
40 | { nickname }
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {category}
51 |
52 | { desc }
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/components/RadioTable.jsx:
--------------------------------------------------------------------------------
1 | import { UpSquareFilled, DownSquareFilled } from '@ant-design/icons'
2 | import { Link } from 'react-router-dom';
3 | import { useContext, useState } from 'react';
4 |
5 | import { mediaTimeFormat, timeToYMD, playTimesFormat } from '@utils/utils';
6 | import { ApiCtx } from '../RadioDetails';
7 |
8 | function ProgramItem(props) {
9 | const { serialNum, name, listenerCount, likedCount, createTime, duration, idx } = props;
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | { serialNum }
17 |
18 | |
19 |
20 |
21 | { name }
22 |
23 |
24 |
25 |
26 |
27 |
28 | |
29 |
30 |
31 | 播放{ playTimesFormat(listenerCount) }
32 |
33 | |
34 |
35 |
36 | 赞 { likedCount }
37 |
38 | |
39 |
40 |
41 | { timeToYMD(createTime) }
42 |
43 | |
44 |
45 |
46 | { mediaTimeFormat(duration / 1000) }
47 |
48 | |
49 |
50 | )
51 | }
52 |
53 | export default function({list = [], total = 0}) {
54 |
55 | const { getProgram } = useContext(ApiCtx);
56 |
57 | const [sort, setSort] = useState(false); //默认降序
58 |
59 | const sortQuery = (flag) => {
60 | if (sort === flag) {
61 | return;
62 | }
63 | getProgram({asc: flag});
64 | setSort(flag);
65 | }
66 |
67 | return (
68 |
69 |
70 |
71 |
72 | 节目列表
73 |
74 |
75 |
76 | 共{ total }期
77 |
78 |
79 | sortQuery(false)}
82 | title="降序"
83 | />
84 | sortQuery(true)}
87 | title="升序"
88 | />
89 |
90 |
91 |
92 |
93 | {
94 | list.length > 0 && list.map((item, i) => )
95 | }
96 |
97 |
98 |
99 | )
100 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/components/Recommend.jsx:
--------------------------------------------------------------------------------
1 | import NavTitle from '@/components/Common/NavTitle';
2 | import radioApi from '@api/radio';
3 |
4 | import { useEffect, useState } from 'react';
5 | import { Progress, Icon } from 'antd';
6 | import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'
7 |
8 | const RecommendItem = ({
9 | picUrl,
10 | name,
11 | dj: {nickname},
12 | category,
13 | idx,
14 | rcmdtext
15 | }) => {
16 | return (
17 |
18 |
19 |
20 |
{ name }
21 |
{ rcmdtext ?? nickname }
22 |
23 | { category }
24 |
25 | )
26 | }
27 |
28 | function getPercent(val, total) {
29 | return val / total * 100;
30 | }
31 |
32 | const TopTag = ({rank, lastRank}) => {
33 | if (lastRank === -1) { //new
34 | return NEW
35 | } else if (rank >= lastRank) { //down
36 | return <>
37 |
38 | { rank - lastRank }
39 | >
40 | } else { //up
41 | return <>
42 |
43 | { lastRank - rank }
44 | >
45 | }
46 | }
47 |
48 | const TopItem = ({
49 | program: {
50 | coverUrl,
51 | name,
52 | dj: {
53 | brand
54 | }
55 | },
56 | rank,
57 | lastRank,
58 | totalScore,
59 | score,
60 | idx
61 | }) => {
62 | return (
63 |
64 |
65 | {rank.toString().padStart(2, 0)}
66 |
67 |
68 |
69 |
70 |
{ name }
71 |
{ brand }
72 |
73 |
82 |
83 | )
84 | }
85 |
86 | export default () => {
87 |
88 | //推荐节目
89 | const [recommend, setRecommend] = useState([])
90 | const getRecommend = async () => {
91 | const res = await radioApi.getRecommend();
92 | setRecommend(res);
93 | }
94 |
95 | //节目榜
96 | const [topList, setToplist] = useState([]);
97 | const [totalScore, setTotal] = useState(0);
98 | const getToplists = async () => {
99 | const res = await radioApi.getToplists();
100 | setToplist(res);
101 | setTotal(res[0].score);
102 | }
103 |
104 | useEffect(() => {
105 | getRecommend();
106 | getToplists();
107 | }, [])
108 |
109 | return (
110 |
111 |
112 |
113 |
114 | {
115 | recommend.map((item, i) => {
116 | return
117 | })
118 | }
119 |
120 |
121 |
122 |
123 |
124 | {
125 | topList.map((item, i) => {
126 | return
127 | })
128 | }
129 |
130 |
131 |
132 | )
133 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/components/TypeRecommend.jsx:
--------------------------------------------------------------------------------
1 | import NavTitle from "@/components/Common/NavTitle";
2 | import { useLoading } from '@/utils/hooks';
3 |
4 | import { Link } from "react-router-dom";
5 | import { useState, useEffect, forwardRef } from "react";
6 | import { LazyLoadImage } from 'react-lazy-load-image-component';
7 | import { Skeleton } from 'antd';
8 |
9 | const RdoItem = ({
10 | picUrl,
11 | name,
12 | rcmdtext,
13 | id
14 | }) => {
15 | return (
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 | { name }
29 |
30 |
{ rcmdtext }
31 |
32 |
33 | )
34 | }
35 |
36 | export const RecommendTemp = ({title, list}) => {
37 | return (
38 |
39 |
40 |
41 | {
42 | list.map(item => )
43 | }
44 |
45 |
46 | )
47 | }
48 |
49 | export function withDjRecommend(Wrap, fn, options) {
50 | return forwardRef(({title, params}, ref) => {
51 |
52 | const [list, setList] = useState([]);
53 | const { loading, toggleLoad } = useLoading();
54 | const getRecommend = async () => {
55 | toggleLoad(true);
56 | const res = await fn(params);
57 | if (res.djRadios) {
58 | setList(res.djRadios.slice(0, options.limit));
59 | ref.current(res.count);
60 | } else {
61 | setList(res.slice(0, options.limit));
62 | }
63 | toggleLoad(false, 200);
64 | }
65 |
66 | useEffect(() => {
67 | getRecommend();
68 | }, [params.id])
69 |
70 | return
71 |
72 |
73 | })
74 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/store/index.js:
--------------------------------------------------------------------------------
1 | import initialState from './initialState';
2 | import sessionStore from '@/utils/sessionStore';
3 |
4 | const radioReducer = (state = initialState, action) => {
5 | switch (action.type) {
6 | case 'setCates':
7 | sessionStore.set('radioCates', action.cates)
8 | return {
9 | ...state,
10 | cates: action.cates
11 | }
12 | default:
13 | return {...state}
14 | }
15 | }
16 |
17 | export default radioReducer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Radio/store/initialState.js:
--------------------------------------------------------------------------------
1 | import sessionStore from "@/utils/sessionStore";
2 |
3 | const initialState = {
4 | cates: sessionStore.get('radioCates') ?? []
5 | }
6 |
7 | export default initialState;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/Album.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback, useState, useContext } from 'react';
2 | import { useParams, Link, useLocation, useHistory } from 'react-router-dom';
3 | import { LazyLoadImage } from 'react-lazy-load-image-component'
4 | import { Pagination, message } from 'antd';
5 |
6 | import singerApi from '@/api/singer';
7 | import { timeToYMD, playAlbum } from '@/utils/utils';
8 | import { Intrs } from './Singer';
9 |
10 | const AlbumItem = (props) => {
11 | const {
12 | picUrl,
13 | name,
14 | publishTime,
15 | id
16 | } = props;
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | playAlbum(id)}
28 | >
29 |
30 |
31 |
32 |
33 | { name }
34 |
35 |
36 |
37 | { timeToYMD(publishTime) }
38 |
39 |
40 | )
41 | }
42 |
43 | const Album = () => {
44 |
45 | const { albumSize } = useContext(Intrs);
46 | const location = useLocation();
47 | const history = useHistory();
48 |
49 | let { id } = useParams();
50 | const [lists, setLists] = useState([]);
51 | const [curPage, setCurPage] = useState(1);
52 |
53 | const getAlbum = useCallback(async (limit, offset) => {
54 | message.loading({
55 | content: '专辑获取中...',
56 | duration: 0,
57 | key: 'singerAlbum',
58 | style: {
59 | marginTop: '30vh',
60 | }
61 | });
62 | const res = await singerApi.getAlbum(id, limit, offset);
63 | setLists(res);
64 | message.destroy('singerAlbum')
65 | }, [])
66 |
67 | const pageChange = useCallback((page, pageSize) => {
68 | setCurPage(page);
69 | getAlbum(pageSize, (page - 1) * pageSize)
70 | history.replace(history.location.pathname + `?page=${page}`)
71 | }, [])
72 |
73 | const computedPage = useCallback(() => {
74 | return new Promise((resolve, reject) => {
75 | setTimeout(() => {
76 | const page = location.search.match(/\=(.+)$/)[1];
77 | setCurPage(Number(page));
78 | resolve(Number(page));
79 | })
80 | })
81 | })
82 |
83 | useEffect(async () => {
84 | const page = await computedPage();
85 | getAlbum(12, (page - 1) * 12);
86 | }, [])
87 |
88 | return (
89 | <>
90 |
91 | {
92 | lists.map(list => {
93 | return
94 | })
95 | }
96 |
97 |
106 | >
107 | )
108 | }
109 |
110 | export default Album;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/CateAside.jsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { Link, useLocation } from 'react-router-dom';
3 |
4 | export const cateList = [
5 | {name: '推荐', val: '-1'},
6 | {name: '华语', val: 7},
7 | {name: '欧美', val: 96},
8 | {name: '日本', val: 8},
9 | {name: '韩国', val: 16},
10 | {name: '其他', val: 0}
11 | ];
12 |
13 | export const typeList = ['男歌手', '女歌手', '组合/其他'];
14 |
15 | const CateGroup = (props) => {
16 | const { title = '', list = [], idx } = props;
17 | const { pathname } = useLocation()
18 |
19 | return (
20 | 0 ? 'blk': ''}`}>
21 |
{ title }
22 |
23 | {
24 | list.map(cate => {
25 | return (
26 | -
27 | { cate.cate }
28 |
29 | )
30 | })
31 | }
32 |
33 |
34 | )
35 | }
36 |
37 | const CateAside = () => {
38 |
39 | // const [asideCates, setCates] = useState([]);
40 |
41 | const asideConvert = () => {
42 | let newCate = [];
43 | cateList.map((item, i) => {
44 | let cateObj;
45 | if (i === 0) {
46 | cateObj = {
47 | title: item.name,
48 | list: [
49 | {
50 | cate: '推荐歌手',
51 | path: '/home/singer/cate/recommend'
52 | },
53 | {
54 | cate: '热门歌手',
55 | path: '/home/singer/cate/hot'
56 | }
57 | ]
58 | }
59 | } else {
60 | cateObj = {
61 | title: item.name,
62 | list: []
63 | };
64 | ['男歌手', '女歌手', '组合/其他'].map((cate, i) => {
65 | cateObj.list.push({
66 | cate: `${item.name}${cate}`,
67 | path: `/home/singer/cate/${item.val}/${i+1}`
68 | })
69 | })
70 | }
71 |
72 | newCate.push(cateObj);
73 | })
74 |
75 | return newCate;
76 | }
77 |
78 | const asideCates = useMemo(() => asideConvert(), [])
79 |
80 | // useEffect(() => {
81 | // const res = asideConvert();
82 | // setCates(res);
83 | // }, [])
84 | return (
85 |
86 |
87 | {
88 | asideCates.map((item, i) => {
89 | return (
90 |
91 | )
92 | })
93 | }
94 |
95 |
96 | )
97 | }
98 |
99 | export default CateAside;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/Introduce.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, Fragment } from 'react'
2 | import { Intrs } from './Singer';
3 |
4 | const Introduce = () => {
5 | const {
6 | briefDesc = '',
7 | introduction = [],
8 | name = ''
9 | } = useContext(Intrs);
10 | return (
11 |
12 |
13 |
14 | {name}简介
15 |
16 |
17 | { briefDesc }
18 |
19 | {
20 | introduction.map(item => {
21 | return (
22 |
23 | { item.ti }
24 | { item.txt }
25 |
26 | )
27 | })
28 | }
29 |
30 | )
31 | }
32 |
33 | export default Introduce;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/MV.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useParams, useLocation, useHistory } from 'react-router-dom';
2 | import { useCallback, useContext, useEffect, useState } from 'react';
3 | import { Pagination, message } from 'antd';
4 | import { LazyLoadImage } from 'react-lazy-load-image-component'
5 |
6 | import singerApi from '@/api/singer';
7 | import { Intrs } from './Singer';
8 |
9 | const MVItem = (props) => {
10 | const {
11 | id,
12 | imgurl,
13 | name
14 | } = props;
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | { name }
25 |
26 |
27 | )
28 | }
29 |
30 | const SingerMV = () => {
31 | const location = useLocation();
32 | const history = useHistory();
33 | const { mvSize } = useContext(Intrs);
34 | const { id } = useParams();
35 | const [lists, setLists] = useState([]);
36 | const [curPage, setCurPage] = useState(1);
37 |
38 | const getMVs = useCallback(async (limit, offset) => {
39 | const res = await singerApi.getMv(id, limit, offset);
40 | setLists(res);
41 | }, [])
42 |
43 | const pageChange = useCallback((page, pageSize) => {
44 | setCurPage(page);
45 | getMVs(pageSize, (page - 1) * pageSize);
46 | history.replace(history.location.pathname + `?page=${page}`)
47 | }, [])
48 |
49 | const computedPage = useCallback(() => {
50 | return new Promise((resolve, reject) => {
51 | setTimeout(() => {
52 | const page = location.search.match(/\=(.+)$/)[1];
53 | setCurPage(Number(page));
54 | resolve(Number(page));
55 | })
56 | })
57 | })
58 |
59 | useEffect(async () => {
60 | const page = await computedPage();
61 | getMVs(12, 12 * (page - 1));
62 | }, [])
63 |
64 | return (
65 | <>
66 |
67 | {
68 | lists.map(item => {
69 | return
70 | })
71 | }
72 |
73 |
82 | >
83 | )
84 | }
85 |
86 | export default SingerMV;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/Singer.jsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/Main';
2 | import { Tabs } from 'antd';
3 | import { useCallback, useEffect, useState, createContext, memo, useRef } from 'react';
4 | import { useHistory, useParams, Link, useLocation } from 'react-router-dom';
5 |
6 | import singerApi from '@/api/singer';
7 |
8 | const { TabPane } = Tabs;
9 | const subRouter = [
10 | 'song',
11 | 'album',
12 | 'mv',
13 | 'introduce'
14 | ]
15 |
16 | export const Intrs = createContext({});
17 |
18 | const SimiItem = (props) => {
19 | const { img1v1Url, name, id } = props;
20 | return (
21 |
22 |
23 |
24 |

25 |
26 |
27 |
28 | { name }
29 |
30 |
31 | )
32 | }
33 |
34 | const Singer = memo((props) => {
35 |
36 | const history = useHistory();
37 | const location = useLocation();
38 | let { id } = useParams();
39 | const [info, setInfo] = useState({});
40 | const [intr, setIntr] = useState({});
41 | const [simi, setSimi] = useState([]);
42 | const infos = useRef();
43 | const [active, setActive] = useState('song');
44 |
45 | const tabChange = useCallback((key) => { //子页面跳转
46 | const path = key;
47 | history.replace(`/singer/${id}/${path}?page=1`);
48 | setActive(key);
49 | }, [id])
50 |
51 | const getSingerInfo = useCallback(async () => {
52 | const { artist={} } = await singerApi.getDetails(id);
53 | setInfo(artist);
54 | infos.current = artist;
55 | }, [id])
56 |
57 | const getSingerIntr = useCallback(async () => {
58 | const res = await singerApi.getIntr(id);
59 | setIntr({
60 | ...res,
61 | ...infos.current
62 | });
63 | }, [id])
64 |
65 | const getSimi = useCallback(async () => {
66 | const res = await singerApi.getSimi(id);
67 | setSimi(res.slice(0, 6));
68 | }, [id])
69 |
70 | const init = useCallback(() => {
71 | getSingerInfo(id);
72 | getSingerIntr(id);
73 | getSimi(id);
74 | if (location.pathname === `/singer/${id}`) {
75 | history.replace(`/singer/${id}/song?page=1`)
76 | }
77 | }, [id])
78 |
79 | useEffect(async () => { //默认初次跳转到歌手的歌曲子页
80 | init();
81 | const subPath = location.pathname.match(/\/([^\/]{1,})$/)[1];
82 | setActive(subRouter.includes(subPath) ? subPath : 'song');
83 | }, [id])
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | { info.name }
94 |
95 |
96 | {info.name}
97 |
98 |
99 |

100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | { props.children }
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | 相似歌手
123 |
124 |
125 | {
126 | simi.map(item => {
127 | return
128 | })
129 | }
130 |
131 |
132 |
133 |
134 | )
135 | })
136 |
137 | export default Singer;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/SingerCates.jsx:
--------------------------------------------------------------------------------
1 | import { useParams, Link } from 'react-router-dom';
2 | import { useCallback, useEffect, useState } from 'react';
3 |
4 | import singerApi from '@/api/singer';
5 | import SingerItem from './SingerItem';
6 |
7 |
8 | const SingerCates = () => {
9 |
10 | const { path } = useParams();
11 | const [singerList, setList] = useState([]);
12 | const [title, setTitle] = useState('');
13 |
14 | const getHots = useCallback(async () => { //热门歌手
15 | const res = await singerApi.getHotSingers(20);
16 | setList(res)
17 | })
18 |
19 | const topSinger = useCallback(async () => { //推荐歌手
20 | const res = await singerApi.topSinger();
21 | setList(res.slice(0, 20))
22 | })
23 |
24 | useEffect(async () => {
25 | console.log(path);
26 | if (path === 'recommend') {
27 | topSinger();
28 | setTitle('推荐歌手')
29 | } else {
30 | getHots();
31 | setTitle('热门歌手')
32 | }
33 | }, [path])
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 | { title }
42 |
43 |
44 |
45 |
46 | {
47 | singerList.map(item => {
48 | return
49 | })
50 | }
51 |
52 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default SingerCates;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/SingerItem.jsx:
--------------------------------------------------------------------------------
1 | import { LazyLoadImage } from 'react-lazy-load-image-component'
2 | import { Link } from 'react-router-dom'
3 |
4 | const SingerItem = (props) => {
5 | const { img1v1Url, name, id, picUrl } = props;
6 | return (
7 |
8 |
9 |
15 |
16 | {/*
17 |
*/}
18 |
19 |
20 |
21 | { name }
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default SingerItem;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/SingerList.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useHistory } from 'react-router-dom';
2 | import { useEffect } from 'react';
3 |
4 | import Main from '@/components/Main';
5 | import CateAside from './CateAside';
6 |
7 | const SingerList = (props) => {
8 |
9 | const history = useHistory();
10 |
11 | useEffect(() => {
12 | if (history.location.pathname.endsWith('/singer')) {
13 | history.replace('/home/singer/cate/recommend')
14 | }
15 | }, [])
16 |
17 | return (
18 |
19 |
20 | {
21 | props.children
22 | }
23 |
24 | )
25 | }
26 |
27 | export default SingerList;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/SingerMoreCates.jsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from 'antd';
2 | import { useCallback, useEffect, useState } from 'react';
3 | import { useParams } from 'react-router';
4 |
5 | import singerApi from '@/api/singer';
6 | import SingerItem from './SingerItem';
7 | import { cateList, typeList } from './CateAside';
8 |
9 | const { TabPane } = Tabs;
10 |
11 | function createCateList() {
12 | const list = [];
13 | for(let i = 65; i <= 90; i ++) {
14 | const item = {
15 | code: String.fromCharCode(i),
16 | name: String.fromCharCode(i)
17 | };
18 | list.push(item);
19 | }
20 | list.unshift({
21 | code: '',
22 | name: '全部'
23 | });
24 | return list;
25 | }
26 |
27 | const SingerMoreCates = () => {
28 |
29 | const { area, type } = useParams();
30 |
31 | const [cates, setCates] = useState(createCateList());
32 | const [initial, setInitial] = useState('');
33 | const [singerList, setSingers] = useState([]);
34 | const [title, setTitle] = useState('');
35 |
36 | const initialChange = useCallback((key) => {
37 | setInitial(key);
38 | }, [])
39 |
40 | const getSinger = useCallback(async () => {
41 | const res = await singerApi.getCateList({
42 | type,
43 | area,
44 | initial
45 | });
46 | setSingers(res.slice(0, 10));
47 | }, [area, type, initial])
48 |
49 | const getTitle = useCallback(() => {
50 | for (let i = 0; i < cateList.length; i ++) {
51 | if (cateList[i].val == area) {
52 | let types = typeList[type - 1];
53 | let title = `${cateList[i].name}${types}`;
54 | setTitle(title);
55 | }
56 | }
57 | }, [area, type])
58 |
59 | useEffect(() => {
60 | getSinger();
61 | getTitle();
62 | }, [area, type, initial])
63 |
64 | return (
65 |
66 |
67 |
68 |
69 |
70 | { title }
71 |
72 |
73 |
74 |
75 | {
76 | cates.map(cate => {
77 | return (
78 |
79 |
80 | )
81 | })
82 | }
83 |
84 |
85 |
86 |
87 | {
88 | singerList.map(item => {
89 | return
90 | })
91 | }
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
100 | export default SingerMoreCates;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Singer/Song.jsx:
--------------------------------------------------------------------------------
1 | import { useParams, useHistory } from 'react-router-dom'
2 | import { useCallback, useEffect, useState } from 'react'
3 | import { Pagination, message } from 'antd';
4 |
5 | import { SongItem } from '@/views/Search/components';
6 | import singerApi from '@/api/singer';
7 |
8 | const Song = () => {
9 |
10 | const { id } = useParams();
11 | const [lists, setLists] = useState([]);
12 | const [total, setTotal] = useState(0);
13 | const [curPage, setCurPage] = useState(1);
14 | const history = useHistory();
15 |
16 | const getSongs = useCallback(async (limit, offset) => { //获取歌曲
17 | message.loading({
18 | content: '歌曲获取中...',
19 | duration: 0,
20 | key: 'singerSong',
21 | style: {
22 | marginTop: '30vh',
23 | }
24 | });
25 | const {songs=[], total=0} = await singerApi.getSongs(id, limit, offset);
26 | setLists(songs);
27 | setTotal(total);
28 | setTimeout(() => message.destroy('singerSong'), 500);
29 | }, [id])
30 |
31 | const pageChange = useCallback((page, pageSize) => { //分页
32 | setCurPage(page);
33 | getSongs(pageSize, (page - 1) * pageSize);
34 | history.replace(history.location.pathname + `?page=${page}`)
35 | }, [id])
36 |
37 | const computedPage = useCallback(() => {
38 | return new Promise((resolve, reject) => {
39 | setTimeout(() => {
40 | const page = history.location.search.match(/\=(.+)$/)[1];
41 | setCurPage(Number(page));
42 | resolve(Number(page));
43 | })
44 | })
45 | })
46 |
47 | useEffect(async () => {
48 | const page = await computedPage();
49 | getSongs(21, 20 * (page - 1));
50 | }, [id])
51 |
52 | return (
53 |
54 |
55 | {
56 | lists.map(item => {
57 | return
58 | })
59 | }
60 |
61 |
70 |
71 | )
72 | }
73 |
74 | export default Song;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Song/components.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { artistsFormat, playItem, addToPlay } from '@/utils/utils';
3 |
4 | export const SameLists = (props) => {
5 | const {lists} = props;
6 | return (
7 | <>
8 |
9 |
10 | 包含这首歌的歌单
11 |
12 |
13 |
14 | {
15 | lists.map(list => {
16 | return (
17 | -
18 |
19 |
20 |

21 |
22 |
23 |
24 |
25 |
26 | { list.name }
27 |
28 |
29 |
30 |
31 | by
32 |
33 |
34 | { list.creator.nickname }
35 |
36 |
37 |
38 |
39 | )
40 | })
41 | }
42 |
43 | >
44 | )
45 | }
46 |
47 | export const SameSongs = (props) => {
48 | const {songs} = props;
49 | return (
50 | <>
51 |
52 | 相似歌曲
53 |
54 |
78 | >
79 | )
80 | }
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Toplist/AsideNav.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useLocation } from 'react-router-dom';
2 | import { LazyLoadImage } from 'react-lazy-load-image-component';
3 | import { useContext, createContext } from 'react';
4 |
5 | const ActiveId = createContext();
6 | const ListItem = (props) => {
7 |
8 | const activeId = Number(useContext(ActiveId));
9 |
10 | const { coverImgUrl = '', name = '', updateFrequency = '', id = 0 } = props;
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | { name }
24 |
25 |
26 |
27 | { updateFrequency }
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | const ListGroup = (props) => {
35 | const { title = '', list = [] } = props;
36 | return (
37 | <>
38 | { title }
39 |
40 | {
41 | list.map(item => {
42 | return
43 | })
44 | }
45 |
46 | >
47 | )
48 | }
49 |
50 | const AsideNav = (props) => {
51 |
52 | const { specialLists = {}, globalLists = {} } = props;
53 | const { pathname } = useLocation();
54 | const tid = pathname.match(/toplist\/(.+)$/) && pathname.match(/toplist\/(.+)$/)[1];
55 |
56 | return (
57 |
65 | )
66 | }
67 |
68 | export default AsideNav;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Toplist/Details.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useReducer, useState } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import { Skeleton } from 'antd';
4 |
5 | import BtnTools from '@/components/Common/BtnTools';
6 | import toplistApi from '@/api/toplist';
7 | import { SongItem } from '@/views/Search/components';
8 | import { playList } from '@utils/utils';
9 | import { CommentWrap } from '@/components/Comment/Comment';
10 |
11 | const cmtsInit = {
12 | total: 0,
13 | hotCmts: [],
14 | cmts: []
15 | }
16 |
17 | const cmtsReducer = (state, action) => {
18 | switch (action.type) {
19 | case 'setTotal':
20 | return {
21 | ...state,
22 | total: action.total
23 | }
24 | case 'setHot':
25 | return {
26 | ...state,
27 | hotCmts: action.hot
28 | }
29 | case 'setCmts':
30 | return {
31 | ...state,
32 | cmts: action.cmts
33 | }
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | const TopDetails = () => {
40 |
41 | const location = useLocation();
42 | const id = location.pathname.match(/toplist\/(.+)$/)[1];
43 | const [details, setDetails] = useState({});
44 | const [loading, setLoading] = useState(false);
45 |
46 | const [cmtsObj, dispatch] = useReducer(cmtsReducer, cmtsInit);
47 |
48 | const getDetails = useCallback(async () => {
49 | setLoading(true);
50 | const res = await toplistApi.getTopDetails(id);
51 | setDetails(res);
52 | setLoading(false);
53 | document.querySelector('.m-back').click();
54 | }, [id])
55 |
56 | const playAlbumFn = useCallback(() => { //播放全部歌曲
57 | playList(id, details?.tracks);
58 | }, [details?.tracks, id])
59 |
60 | const getCmts = useCallback(async (limit, offset) => { //获取评论
61 | const res = await toplistApi.getCmts(id, limit, offset);
62 | dispatch({type: 'setTotal', total: res.total});
63 | res.hotComments && dispatch({type: 'setHot', hot: res.hotComments});
64 | dispatch({type: 'setCmts', cmts: res.comments});
65 | }, [id])
66 |
67 | const pageChange = useCallback((page, pageSize) => {
68 | getCmts(pageSize, (page -1) * pageSize)
69 | }, [])
70 |
71 | useEffect(async () => {
72 | await getDetails();
73 | await getCmts();
74 | }, [id])
75 |
76 | return (
77 |
78 |
87 |
88 |
89 |
90 |

91 |
92 |
93 |
94 |
95 |
{ details.name }
96 |
97 |
98 |
99 |
100 | { details.description }
101 |
102 |
107 |
108 |
109 |
110 |
111 |
112 | {
113 | details?.tracks?.map(item => {
114 | return
115 | })
116 | }
117 |
118 |
119 |
120 |
121 |
122 |
123 | )
124 | }
125 |
126 | export default TopDetails;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Toplist/TopList.jsx:
--------------------------------------------------------------------------------
1 | import { useHistory, useLocation, useParams } from 'react-router-dom';
2 | import { useCallback, useEffect, useState } from 'react';
3 |
4 | import Main from '@/components/Main';
5 | import toplistApi from '@/api/toplist';
6 | import AsideNav from './AsideNav';
7 |
8 | const TopList = (props) => {
9 |
10 | const history = useHistory();
11 | const location = useLocation();
12 | const [asideData, setAside] = useState({});
13 |
14 | const getTopLists = useCallback(async () => { //获取榜单
15 | const lists = await toplistApi.getTopLists();
16 | const { specialLists, globalLists } = listConvert(lists);
17 | location.pathname.endsWith('/toplist') && history.replace(`/home/toplist/${specialLists.list[0].id}`);
18 | setAside({specialLists, globalLists});
19 | }, [])
20 |
21 | const listConvert = useCallback((lists) => { //转换榜单
22 | const [specialLists, globalLists] = [
23 | {
24 | title: '云音乐特色榜',
25 | list: lists.slice(0, 4),
26 | },
27 | {
28 | title: '全球媒体榜',
29 | list: lists.slice(4)
30 | }
31 | ];
32 | return {specialLists, globalLists};
33 | }, [])
34 |
35 | useEffect(() => {
36 | getTopLists();
37 | }, [])
38 |
39 | useEffect(() => { //监听重定向
40 | if (location.pathname.endsWith('toplist')) {
41 | if (asideData.specialLists) {
42 | history.replace(`/home/toplist/${asideData.specialLists.list[0].id}`);
43 | }
44 | }
45 | }, [location.pathname])
46 |
47 | return (
48 |
49 |
50 | { props.children }
51 |
52 | )
53 | }
54 |
55 | export default TopList;
--------------------------------------------------------------------------------
/NeteaseCloudPC/src/views/Video/Components.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import { playTimesFormat, mediaTimeFormat } from '@/utils/utils';
4 |
5 | const Item = (props) => {
6 | const {
7 | vid,
8 | coverUrl,
9 | playTime,
10 | title,
11 | durationms,
12 | creator
13 | } = props;
14 | return (
15 |
16 |
17 |

18 |
19 |
20 |
21 | { playTimesFormat(playTime) }
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 | { title }
32 |
33 |
34 |
{ mediaTimeFormat(durationms / 1000) }
35 |
36 | by { creator[0].userName }
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export const RelatedVdo = (props) => {
44 | const {relatedVdo} = props;
45 | return (
46 | <>
47 |
48 | 相关推荐
49 |
50 |
51 | {
52 | relatedVdo.map(vdo => {
53 | return (
54 |
55 | )
56 | })
57 | }
58 |
59 | >
60 | )
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react17-project
2 | 新版react重构网易云PC版
3 |
4 | 预览地址 [Jacky仿网易PC版](http://zhoup.top:7000/)
5 | > 访问该地址需要登录,用户名为`jacky`, 密码为`123456`
6 |
7 | > 本项目同时在GitHub和gitee上更新
8 | + github地址为
9 | + gitee地址为
10 |
11 | ## 说明
12 | 本人是音乐兼代码兼足球的忠实粉丝,平时不是写代码就去KTV,所以总是喜欢写一些与音乐或者足球的web应用,此前曾写过很多版本的music app, 比如vue2.x和react重构过移动端的仿网易云,也自己写后台node转发第三方的足球api,当然在此申明,**只是为了研究学习技术,绝不作为商业盈利!若有不法分子使用该程序谋利,后果自负**
13 |
14 | 这个网易云的web应用我从2019年初就开始陆续动工,先后有几个版本的,但是都不太满意,所以从去年9月18日,vue3.0正式发布了后,决定完整地再重构一次,终于历时几个月写了一个令我比较满意的移动端web应用,地址在[仿网易云APP vue3版](https://github.com/chensidi/vue3-project/tree/master/vue3NeteaseCloud), 预览地址为[Jacky 移动music](http://zhoup.top:7002/),用户名密码同样,而这个PC端版本的则是采用最新的react重构,功能几乎与官网一样,这个项目是启动与2021新春前夕,并且在一直更新。
15 |
16 | 本项目依赖于`react17`和`antd`,将项目拉下来后,要先下载好后台,也就是网易云的nodeJS后台,它会提供一个与网易云官方一模一样的数据,然后把端口改为`5000`,然后下载本项目的依赖,启动即可。
17 |
18 | 本人是学友哥的忠实粉丝,绰号达州小歌神😏😂🤣
19 | 
20 | .png '预览')
21 | .png '预览')
22 | .png '预览')
23 |
24 | > 补充说明,本项目还有其他基于音乐后台的项目都依赖与这个下面这个nodejs后台,在此非常感谢这位大神数据支援。
25 |
26 | 网易云的后台地址为
27 |
28 | ## 功能清单
29 |
30 | + 首页展示,基本与官方保持一致,分为**顶部导航**, **轮播**,**热门推荐**,**新碟上架**, **榜单**等主要模块,这些模块某些暂未配上跳转链接。
31 | + 搜素功能,这是目前最丰富的一个功能,从导航栏输入搜素内容后,回车进入搜素也没面,可以得到**单曲**,**歌手**,**专辑**,**视频**,**歌词**,**歌单**,**主播**,**用户**一共8个类型,这是用antd的tabs展示的,内容与官方基本一致,但做了分页优化,官方目前只能搜出一页数据。目前单曲,专辑,视频三个板块已经配好链接可以点击进入,其他的待后面慢慢配置。
32 | + 播放功能,作为一个music为主题的应用,播放是最基本也是最重要的,本人作文学友粉丝,设置了用户进入后,默认配置了一首 **咖啡** 作为保底曲目,然后通过搜素功能或者其他模块,点击一首歌曲的icon就能play了,也支持添加到播放列表,然后操作就跟官方一样的,可以到专辑里面播放整张专辑的歌曲。
33 | + MV/视频功能,采用原生的video,实现与官方基本一致MV播放器,可以切换分辨率,音量,进度条等,但需要说明的是,由于接口并未返回视频的所有码率,所以视频没有切换分辨率功能,MV支持。
34 | + 歌手模块一共分为四大板块,包括歌曲,专辑,mv,歌手简介,并支持分页功能,同时会推荐相似歌手。
35 | + 还有更多功能在持续更新中...,欢迎issue。
--------------------------------------------------------------------------------