├── .editorconfig ├── .gitattributes ├── .gitignore ├── .yarnrc ├── LICENSE ├── README.md ├── app.config.js ├── assets ├── icon.png └── logo.png ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── webpack.config.js └── webpackDevServer.config.js ├── main.js ├── notes ├── api.md ├── issues.md └── tech.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json ├── play.html └── renderer.js ├── scripts ├── build.js ├── start.js └── test.js ├── server ├── app.js ├── package.json └── yarn.lock ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── api │ └── index.ts ├── assets │ ├── banner-001.jpg │ ├── banner-002.jpg │ ├── banner-003.jpg │ ├── banner-004.jpg │ ├── banner-005.jpg │ ├── banner-blur-bg.png │ ├── douban-logo.png │ └── loading.svg ├── components │ ├── Footer.tsx │ └── TopNav.tsx ├── config │ └── index.ts ├── css │ ├── Detail.css │ ├── Home.css │ ├── NotFound.css │ └── Search.css ├── errors │ └── NotFound.tsx ├── global.d.ts ├── index.css ├── index.tsx ├── logo.svg ├── pages │ ├── Box.tsx │ ├── Detail.tsx │ ├── Home.tsx │ └── Search.tsx ├── react-app-env.d.ts ├── router │ ├── RouterView.tsx │ └── config.ts ├── serviceWorker.ts ├── setupProxy.js ├── skeletons │ ├── Detail.tsx │ └── Home.tsx ├── store │ └── index.ts └── utils │ └── index.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 # 编码格式 6 | end_of_line = lf # 换行符 7 | indent_size = 2 # 缩进大小 8 | indent_style = space # 缩进风格 9 | insert_final_newline = true # 是否使文件以一个空白行结尾 10 | max_line_length = 80 # 单行最大字符数 11 | trim_trailing_whitespace = true # 是否将行尾空格自动删除 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS files must always use LF for tools to work 5 | *.js eol=lf 6 | 7 | *.ts eol=lf 8 | *.tsx eol=lf 9 | 10 | *.tsx linguist-language=typescript 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | *.log 3 | logs 4 | **/*.backup.* 5 | **/*.back.* 6 | 7 | node_modules 8 | bower_componets 9 | 10 | *.sublime* 11 | 12 | psd 13 | thumb 14 | sketch 15 | 16 | 17 | /build 18 | /server/build 19 | /src/api/mock.js 20 | dist 21 | tree.txt 22 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npm.taobao.org/" 2 | sass_binary_site "https://npm.taobao.org/mirrors/node-sass/" 3 | electron_mirror "https://npm.taobao.org/mirrors/electron/" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 杨帆 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## douban-movie-electron 2 | 3 | 4 | 5 |

6 | 7 |

8 |

9 | Starts 12 | Forks 15 | License 18 |

19 | 20 | > 基于 Electron, React, React-router, Typescript 一款桌面豆瓣电影应用 21 | 22 | --- 23 | 24 | 25 | 26 | ### Start 27 | 28 | ```bash 29 | # install fe pkg 30 | $ yarn 31 | # preivew 32 | $ yarn electron-dev 33 | # build 34 | $ yarn build 35 | # pack 36 | $ yarn dist 37 | 38 | ``` 39 | 40 | 41 | ### Preview 42 | 43 | ![photo-001](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-001.png?raw=true) 44 | ![photo-002](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-002.png?raw=true) 45 | ![photo-003](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-003.png?raw=true) 46 | ![photo-004](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-004.png?raw=true) 47 | ![photo-005](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-005.png?raw=true) 48 | ![photo-006](https://github.com/Yangfan2016/PicBed/blob/master/Personal/douban-movie-electron-006.png?raw=true) 49 | 50 | ### Todo & Done 51 | 52 | #### 首页 53 | 54 | - [x] banner 55 | - [x] 历史记录 56 | - [x] 检索建议 57 | - [x] 正在热映 58 | - [x] 新片榜 59 | - [x] 北美票房榜 60 | - [x] 一周口碑榜 61 | - [x] Top250 62 | - [x] footer 63 | 64 | #### 电影详情页 65 | 66 | - [x] 电影评分、基本信息 67 | - [x] 剧照 68 | - [x] 预告片(点击打开播放虚拟页) 69 | - [x] 评论 70 | - [x] 热评 71 | 72 | #### 搜索详情页 73 | 74 | - [x] 搜索信息列表 75 | 76 | 77 | ### Tech 78 | 79 | - React(react-hooks) 80 | - React-router 81 | - Typescript 82 | ---- 83 | - ant-design 84 | - react-lazy-load 85 | ---- 86 | - axios 87 | - lodash 88 | ---- 89 | - Koa 90 | 91 | 92 | ### MIT license 93 | Copyright (c) 2019 yangfan2016 <15234408101@163.com> 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy 96 | of this software and associated documentation files (the "Software"), to deal 97 | in the Software without restriction, including without limitation the rights 98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 | copies of the Software, and to permit persons to whom the Software is 100 | furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in 103 | all copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 111 | THE SOFTWARE. 112 | 113 | --- 114 | built upon love by [docor](https://github.com/turingou/docor.git) v0.3.0 115 | -------------------------------------------------------------------------------- /app.config.js: -------------------------------------------------------------------------------- 1 | const PORT = 9876; 2 | const config = { 3 | server: { 4 | port: PORT, 5 | url: `http://localhost:${PORT}`, 6 | }, 7 | }; 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/assets/icon.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/assets/logo.png -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `const React = require('react'); 14 | module.exports = { 15 | __esModule: true, 16 | default: ${assetFilename}, 17 | ReactComponent: React.forwardRef((props, ref) => ({ 18 | $$typeof: Symbol.for('react.element'), 19 | type: 'svg', 20 | ref: ref, 21 | key: null, 22 | props: Object.assign({}, props, { 23 | children: ${assetFilename} 24 | }) 25 | })), 26 | };`; 27 | } 28 | 29 | return `module.exports = ${assetFilename};`; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 64 | 65 | 66 | 67 |

68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/renderer.js: -------------------------------------------------------------------------------- 1 | const url = require("url"); 2 | const qs = require("querystring"); 3 | 4 | const { query } = url.parse(window.location.href); 5 | const { info } = qs.parse(query); 6 | const { src, title } = JSON.parse(info); 7 | let video = document.getElementById("video"); 8 | video.src = src; 9 | 10 | 11 | const titleBar = document.getElementById("js-title-bar"); 12 | titleBar.innerHTML = title; 13 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { server } = require("../app.config"); 4 | 5 | // Do this as the first thing so that any code reading it knows the right env. 6 | process.env.BABEL_ENV = 'production'; 7 | process.env.NODE_ENV = 'production'; 8 | process.env.REACT_APP_SERVER_URL = server.url; 9 | // process.env.PUBLIC_URL = appBuild; 10 | 11 | // Makes the script crash on unhandled rejections instead of silently 12 | // ignoring them. In the future, promise rejections that are not handled will 13 | // terminate the Node.js process with a non-zero exit code. 14 | process.on('unhandledRejection', err => { 15 | throw err; 16 | }); 17 | 18 | // Ensure environment variables are read. 19 | require('../config/env'); 20 | 21 | 22 | const path = require('path'); 23 | const chalk = require('react-dev-utils/chalk'); 24 | const fs = require('fs-extra'); 25 | const webpack = require('webpack'); 26 | const bfj = require('bfj'); 27 | const configFactory = require('../config/webpack.config'); 28 | const paths = require('../config/paths'); 29 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 30 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 31 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 32 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 33 | const printBuildError = require('react-dev-utils/printBuildError'); 34 | 35 | const measureFileSizesBeforeBuild = 36 | FileSizeReporter.measureFileSizesBeforeBuild; 37 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 38 | const useYarn = fs.existsSync(paths.yarnLockFile); 39 | 40 | // These sizes are pretty large. We'll warn for bundles exceeding them. 41 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 42 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 43 | 44 | const isInteractive = process.stdout.isTTY; 45 | 46 | // Warn and crash if required files are missing 47 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 48 | process.exit(1); 49 | } 50 | 51 | // Process CLI arguments 52 | const argv = process.argv.slice(2); 53 | const writeStatsJson = argv.indexOf('--stats') !== -1; 54 | 55 | // Generate configuration 56 | const config = configFactory('production'); 57 | 58 | // We require that you explicitly set browsers and do not fall back to 59 | // browserslist defaults. 60 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 61 | checkBrowsers(paths.appPath, isInteractive) 62 | .then(() => { 63 | // First, read the current file sizes in build directory. 64 | // This lets us display how much they changed later. 65 | return measureFileSizesBeforeBuild(paths.appBuild); 66 | }) 67 | .then(previousFileSizes => { 68 | // Remove all content but keep the directory so that 69 | // if you're in it, you don't end up in Trash 70 | fs.emptyDirSync(paths.appBuild); 71 | // Merge with the public folder 72 | copyPublicFolder(); 73 | // Start the webpack build 74 | return build(previousFileSizes); 75 | }) 76 | .then( 77 | ({ stats, previousFileSizes, warnings }) => { 78 | if (warnings.length) { 79 | console.log(chalk.yellow('Compiled with warnings.\n')); 80 | console.log(warnings.join('\n\n')); 81 | console.log( 82 | '\nSearch for the ' + 83 | chalk.underline(chalk.yellow('keywords')) + 84 | ' to learn more about each warning.' 85 | ); 86 | console.log( 87 | 'To ignore, add ' + 88 | chalk.cyan('// eslint-disable-next-line') + 89 | ' to the line before.\n' 90 | ); 91 | } else { 92 | console.log(chalk.green('Compiled successfully.\n')); 93 | } 94 | 95 | console.log('File sizes after gzip:\n'); 96 | printFileSizesAfterBuild( 97 | stats, 98 | previousFileSizes, 99 | paths.appBuild, 100 | WARN_AFTER_BUNDLE_GZIP_SIZE, 101 | WARN_AFTER_CHUNK_GZIP_SIZE 102 | ); 103 | console.log(); 104 | 105 | const appPackage = require(paths.appPackageJson); 106 | const publicUrl = paths.publicUrl; 107 | const publicPath = config.output.publicPath; 108 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 109 | printHostingInstructions( 110 | appPackage, 111 | publicUrl, 112 | publicPath, 113 | buildFolder, 114 | useYarn 115 | ); 116 | }, 117 | err => { 118 | console.log(chalk.red('Failed to compile.\n')); 119 | printBuildError(err); 120 | process.exit(1); 121 | } 122 | ) 123 | .catch(err => { 124 | if (err && err.message) { 125 | console.log(err.message); 126 | } 127 | process.exit(1); 128 | }); 129 | 130 | // Create the production build and print the deployment instructions. 131 | function build(previousFileSizes) { 132 | console.log('Creating an optimized production build...'); 133 | 134 | let compiler = webpack(config); 135 | return new Promise((resolve, reject) => { 136 | compiler.run((err, stats) => { 137 | let messages; 138 | if (err) { 139 | if (!err.message) { 140 | return reject(err); 141 | } 142 | messages = formatWebpackMessages({ 143 | errors: [err.message], 144 | warnings: [], 145 | }); 146 | } else { 147 | messages = formatWebpackMessages( 148 | stats.toJson({ all: false, warnings: true, errors: true }) 149 | ); 150 | } 151 | if (messages.errors.length) { 152 | // Only keep the first error. Others are often indicative 153 | // of the same problem, but confuse the reader with noise. 154 | if (messages.errors.length > 1) { 155 | messages.errors.length = 1; 156 | } 157 | return reject(new Error(messages.errors.join('\n\n'))); 158 | } 159 | if ( 160 | process.env.CI && 161 | (typeof process.env.CI !== 'string' || 162 | process.env.CI.toLowerCase() !== 'false') && 163 | messages.warnings.length 164 | ) { 165 | console.log( 166 | chalk.yellow( 167 | '\nTreating warnings as errors because process.env.CI = true.\n' + 168 | 'Most CI servers set it automatically.\n' 169 | ) 170 | ); 171 | return reject(new Error(messages.warnings.join('\n\n'))); 172 | } 173 | 174 | const resolveArgs = { 175 | stats, 176 | previousFileSizes, 177 | warnings: messages.warnings, 178 | }; 179 | if (writeStatsJson) { 180 | return bfj 181 | .write(paths.appBuild + '/bundle-stats.json', stats.toJson()) 182 | .then(() => resolve(resolveArgs)) 183 | .catch(error => reject(new Error(error))); 184 | } 185 | 186 | return resolve(resolveArgs); 187 | }); 188 | }); 189 | } 190 | 191 | function copyPublicFolder() { 192 | fs.copySync(paths.appPublic, paths.appBuild, { 193 | dereference: true, 194 | filter: file => file !== paths.appHtml, 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | process.env.REACT_APP_SERVER_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const fs = require('fs'); 20 | const chalk = require('react-dev-utils/chalk'); 21 | const webpack = require('webpack'); 22 | const WebpackDevServer = require('webpack-dev-server'); 23 | const clearConsole = require('react-dev-utils/clearConsole'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const { 26 | choosePort, 27 | createCompiler, 28 | prepareProxy, 29 | prepareUrls, 30 | } = require('react-dev-utils/WebpackDevServerUtils'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explictly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const config = configFactory('development'); 79 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 80 | const appName = require(paths.appPackageJson).name; 81 | const useTypeScript = fs.existsSync(paths.appTsConfig); 82 | const urls = prepareUrls(protocol, HOST, port); 83 | const devSocket = { 84 | warnings: warnings => 85 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 86 | errors: errors => 87 | devServer.sockWrite(devServer.sockets, 'errors', errors), 88 | }; 89 | // Create a webpack compiler that is configured with custom messages. 90 | const compiler = createCompiler({ 91 | appName, 92 | config, 93 | devSocket, 94 | urls, 95 | useYarn, 96 | useTypeScript, 97 | webpack, 98 | }); 99 | // Load proxy config 100 | const proxySetting = require(paths.appPackageJson).proxy; 101 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 102 | // Serve webpack assets generated by the compiler over a web server. 103 | const serverConfig = createDevServerConfig( 104 | proxyConfig, 105 | urls.lanUrlForConfig 106 | ); 107 | const devServer = new WebpackDevServer(compiler, serverConfig); 108 | // Launch WebpackDevServer. 109 | devServer.listen(port, HOST, err => { 110 | if (err) { 111 | return console.log(err); 112 | } 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | console.log(chalk.cyan('Starting the development server...\n')); 117 | }); 118 | 119 | ['SIGINT', 'SIGTERM'].forEach(function (sig) { 120 | process.on(sig, function () { 121 | devServer.close(); 122 | process.exit(); 123 | }); 124 | }); 125 | }) 126 | .catch(err => { 127 | if (err && err.message) { 128 | console.log(err.message); 129 | } 130 | process.exit(1); 131 | }); 132 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, explicitly adding `--no-watch`, 42 | // or explicitly running all tests 43 | if ( 44 | !process.env.CI && 45 | argv.indexOf('--coverage') === -1 && 46 | argv.indexOf('--no-watch') === -1 && 47 | argv.indexOf('--watchAll') === -1 48 | ) { 49 | // https://github.com/facebook/create-react-app/issues/5210 50 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 51 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 52 | } 53 | 54 | // Jest doesn't have this option so we'll remove it 55 | if (argv.indexOf('--no-watch') !== -1) { 56 | argv = argv.filter(arg => arg !== '--no-watch'); 57 | } 58 | 59 | 60 | jest.run(argv); 61 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require("koa"); 2 | const proxy = require("koa-server-http-proxy"); 3 | const app = new Koa(); 4 | 5 | // proxy 6 | app.use(proxy('/api', { 7 | target: 'http://api.douban.com/', 8 | changeOrigin: true, 9 | pathRewrite: { 10 | '^/api': '/v2', // 重写路径 11 | }, 12 | })); 13 | app.use(proxy('/bing', { 14 | target: 'https://www.bing.com/', 15 | changeOrigin: true, 16 | pathRewrite: { 17 | '^/bing': '/', // 重写路径 18 | }, 19 | })); 20 | 21 | module.exports=app; 22 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "douban-movie-server", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "koa": "^2.7.0", 9 | "koa-router": "^7.4.0", 10 | "koa-server-http-proxy": "^0.1.0", 11 | "koa-static": "^5.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* reset.css */ 2 | 3 | ul { 4 | list-style: none; 5 | } 6 | 7 | body, div, p, span, a, img, ul, ol, h1, h2, h3, h4, h5, h6 { 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | :root { 13 | --themeColor: #44883e; 14 | --themeHoverColor: #080; 15 | --hoverColor: #ff5c38; 16 | --highlightColor: #f00; 17 | --backColor: #2d2e36; 18 | --foreColor: #fff; 19 | --shallowColor: rgba(255, 255, 255, .2); 20 | --oneOfSix: 16.666667%; 21 | --sideWidth: 120px; 22 | --pageWidth: 1000px; 23 | --headBarHeight: 80px; 24 | } 25 | 26 | @font-face { 27 | src: url(data:application/font-sfnt;charset=utf-8;base64,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YbkIAAgAY2MjIFhiGyFZY7gEAGIgsABQWLBAYFlmsAFjYCMuIyAgPIo4IyFZLbA9LLAAFrAQI0IgsAhDIC5HI0cjYSBgsCBgZrACYiCwAFBYsEBgWWawAWMjICA8ijgtsD4sIyAuRrACJUawEENYUBtSWVggPFkusS4BFCstsD8sIyAuRrACJUawEENYUhtQWVggPFkusS4BFCstsEAsIyAuRrACJUawEENYUBtSWVggPFkjIC5GsAIlRrAQQ1hSG1BZWCA8WS6xLgEUKy2wQSywOCsjIC5GsAIlRrAQQ1hQG1JZWCA8WS6xLgEUKy2wQiywOSuKICA8sAQjQoo4IyAuRrACJUawEENYUBtSWVggPFkusS4BFCuwBEMusC4rLbBDLLAAFrAEJbAEJiAuRyNHI2GwCUMrIyA8IC4jOLEuARQrLbBELLEIBCVCsAAWsAQlsAQlIC5HI0cjYSCwBCNCsAlDKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgR7AEQ7ACYiCwAFBYsEBgWWawAWNgILABKyCKimEgsAJDYGQjsANDYWRQWLACQ2EbsANDYFmwAyWwAmIgsABQWLBAYFlmsAFjYbACJUZhOCMgPCM4GyEgIEYjR7ABKyNhOCFZsS4BFCstsEUssQA4Ky6xLgEUKy2wRiyxADkrISMgIDywBCNCIzixLgEUK7AEQy6wListsEcssAAVIEewACNCsgABARUUEy6wNCotsEgssAAVIEewACNCsgABARUUEy6wNCotsEkssQABFBOwNSotsEossDcqLbBLLLAAFkUjIC4gRoojYTixLgEUKy2wTCywCCNCsEsrLbBNLLIAAEQrLbBOLLIAAUQrLbBPLLIBAEQrLbBQLLIBAUQrLbBRLLIAAEUrLbBSLLIAAUUrLbBTLLIBAEUrLbBULLIBAUUrLbBVLLMAAABBKy2wViyzAAEAQSstsFcsswEAAEErLbBYLLMBAQBBKy2wWSyzAAABQSstsFosswABAUErLbBbLLMBAAFBKy2wXCyzAQEBQSstsF0ssgAAQystsF4ssgABQystsF8ssgEAQystsGAssgEBQystsGEssgAARistsGIssgABRistsGMssgEARistsGQssgEBRistsGUsswAAAEIrLbBmLLMAAQBCKy2wZyyzAQAAQistsGgsswEBAEIrLbBpLLMAAAFCKy2waiyzAAEBQistsGssswEAAUIrLbBsLLMBAQFCKy2wbSyxADorLrEuARQrLbBuLLEAOiuwPistsG8ssQA6K7A/Ky2wcCywABaxADorsEArLbBxLLEBOiuwPistsHIssQE6K7A/Ky2wcyywABaxATorsEArLbB0LLEAOysusS4BFCstsHUssQA7K7A+Ky2wdiyxADsrsD8rLbB3LLEAOyuwQCstsHgssQE7K7A+Ky2weSyxATsrsD8rLbB6LLEBOyuwQCstsHsssQA8Ky6xLgEUKy2wfCyxADwrsD4rLbB9LLEAPCuwPystsH4ssQA8K7BAKy2wfyyxATwrsD4rLbCALLEBPCuwPystsIEssQE8K7BAKy2wgiyxAD0rLrEuARQrLbCDLLEAPSuwPistsIQssQA9K7A/Ky2whSyxAD0rsEArLbCGLLEBPSuwPistsIcssQE9K7A/Ky2wiCyxAT0rsEArLbCJLLMJBAIDRVghGyMhWUIrsAhlsAMkUHixBQEVRVgwWS0AAABLuADIUlixAQGOWbABuQgACABjcLEAB0KyAAEAKrEAB0KzDAIBCCqxAAdCsxAAAQgqsQAIQroDQAABAAkqsQAJQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMOAgEMKrgB/4WwBI2xAgBEswVkBgBERAAAAAAAAAEAAAAA) format("truetype"); 28 | font-display: swap; 29 | font-family: numFont 30 | } 31 | 32 | html { 33 | font-size: 20px; 34 | } 35 | 36 | body { 37 | font-family: numFont, PingFangSC-Regular, Tahoma, "Microsoft Yahei", sans-serif; 38 | background-color: transparent; 39 | color: var(--foreColor); 40 | user-select: none; 41 | } 42 | 43 | .clearfix:after { 44 | content: ""; 45 | clear: both; 46 | height: 0; 47 | visibility: hidden; 48 | } 49 | 50 | .flex-box { 51 | display: flex; 52 | } 53 | 54 | .raw-title { 55 | font-size: 1.2rem; 56 | color: var(--foreColor); 57 | } 58 | 59 | #root, .App { 60 | height: 100%; 61 | } 62 | 63 | .App { 64 | display: flex; 65 | } 66 | 67 | .win-main { 68 | width: calc(100% - var(--sideWidth)); 69 | margin: 0 0 0 var(--sideWidth); 70 | padding: 0 20px 20px 20px; 71 | background-color: var(--backColor); 72 | } 73 | 74 | .win-side { 75 | position: fixed; 76 | width: var(--sideWidth); 77 | top: var(--headBarHeight); 78 | bottom: 0; 79 | } 80 | 81 | .page { 82 | width: var(--pageWidth); 83 | margin: 0 auto; 84 | } 85 | 86 | .menu-list { 87 | padding: 20px 0; 88 | } 89 | 90 | .menu-list .list-item { 91 | padding: 10px 20px; 92 | } 93 | 94 | .menu-list .list-item .title { 95 | padding: 0 0 0 5px; 96 | border-left-width: 2px; 97 | border-left-style: solid; 98 | border-left-color: transparent; 99 | color: var(--foreColor); 100 | transition: all 0.2 ease; 101 | } 102 | 103 | .menu-list .list-item .title:hover, .menu-list .list-item .title.active { 104 | color: var(--hoverColor); 105 | border-color: var(--hoverColor); 106 | } 107 | 108 | /* ant-tag */ 109 | 110 | .ant-tag.tag-orange { 111 | background-color: var(--hoverColor); 112 | border-color: var(--hoverColor); 113 | color: var(--foreColor); 114 | } 115 | 116 | .hide{ 117 | display: none; 118 | } 119 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { HashRouter as Router, Link } from 'react-router-dom'; 3 | import { LocaleProvider } from 'antd'; 4 | import zhCN from 'antd/lib/locale-provider/zh_CN'; 5 | import moment from 'moment'; 6 | import 'moment/locale/zh-cn'; 7 | import "antd/dist/antd.css"; 8 | import "./App.css"; 9 | import RouterView from './router/RouterView'; 10 | import TopNav from './components/TopNav'; 11 | 12 | moment.locale('zh-cn'); 13 | 14 | const menuList = [ 15 | ["#hash-top", "推荐"], 16 | ["#hash-hotshow", "正在热映"], 17 | ["#hash-newmovie", "新片榜"], 18 | ["#hash-weekly", "口碑榜"], 19 | ["#hash-top250", "Top 250"] 20 | ]; 21 | 22 | 23 | 24 | function App() { 25 | let refMainBox: React.MutableRefObject = useRef(); 26 | let [activeIndex, setActiveIndex] = useState(0); 27 | 28 | function routerBeforeEnterHook(path: string) { 29 | if (path !== '/home') { 30 | let el = refMainBox.current; 31 | // 滚动条复位,回到原点 32 | el && el.scrollTo({ 33 | top: 0, 34 | }); 35 | // 取消所有请求 36 | window.cancalXHRList.forEach((source: CancelTokenSource) => { 37 | source.cancel("cancel xhr"); 38 | }); 39 | } 40 | } 41 | 42 | function scrollToAchorView(selector: string) { 43 | const parent = refMainBox.current; 44 | if (parent === void 0) return; 45 | const target = parent.querySelector(selector); 46 | target && target.scrollIntoView(); 47 | } 48 | 49 | return ( 50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 |
    58 | { 59 | menuList.map((item: string[], index: number) => { 60 | return ( 61 |
  • 62 | { scrollToAchorView(item[0]); setActiveIndex(index) }}> 63 |

    {item[1]}

    64 | 65 |
  • 66 | ); 67 | }) 68 | } 69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 |
79 | ); 80 | } 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const HOST = process.env.REACT_APP_SERVER_URL; 4 | const BASE_URL = `${HOST}/api/movie`; 5 | const API_KEY = "0b2bdeda43b5688921839c8ecb20399b"; 6 | 7 | window.cancalXHRList = []; 8 | 9 | function http() { 10 | const CancelToken = axios.CancelToken; 11 | const source = CancelToken.source(); 12 | 13 | window.cancalXHRList.push(source); 14 | 15 | let instance: AxiosInstance = axios.create({ 16 | baseURL: BASE_URL, 17 | timeout: 3e4, 18 | params: { 19 | "apikey": API_KEY, 20 | }, 21 | cancelToken: source.token, 22 | }); 23 | 24 | return instance; 25 | } 26 | 27 | // 热映 28 | export function getHotShowing(params?: iRequestGetData) { 29 | return http().get("/in_theaters", { 30 | params, 31 | }); 32 | } 33 | 34 | // top250 35 | export function getTop250(params?: iRequestGetData) { 36 | return http().get("/top250", { 37 | params, 38 | }); 39 | } 40 | 41 | // 新片 42 | export function getNew() { 43 | return http().get("/new_movies"); 44 | } 45 | 46 | // 电影详情 47 | export function getDetail(id: string) { 48 | return http().get(`/subject/${id}`); 49 | } 50 | 51 | // 北美票房榜 52 | export function getGoodbox(params?: iRequestGetData) { 53 | return http().get("/us_box", { 54 | params 55 | }); 56 | } 57 | 58 | // 搜索条目 59 | export function getContentBySearch(str: string, params?: iRequestGetData) { 60 | return http().get(`/search?q=${str}`, { 61 | params 62 | }); 63 | } 64 | 65 | // 口碑榜 66 | export function getWeeklyMovie() { 67 | return http().get("/weekly"); 68 | } 69 | 70 | // 获取每日壁纸 71 | export function getWallPaper() { 72 | return axios.get("/bing/HPImageArchive.aspx", { 73 | params: { 74 | format: "js", 75 | idx: 0, 76 | n: 1, 77 | mkt: "zh-CN", 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/assets/banner-001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-001.jpg -------------------------------------------------------------------------------- /src/assets/banner-002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-002.jpg -------------------------------------------------------------------------------- /src/assets/banner-003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-003.jpg -------------------------------------------------------------------------------- /src/assets/banner-004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-004.jpg -------------------------------------------------------------------------------- /src/assets/banner-005.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-005.jpg -------------------------------------------------------------------------------- /src/assets/banner-blur-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/banner-blur-bg.png -------------------------------------------------------------------------------- /src/assets/douban-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yangfan2016/douban-movie-electron/586b2f533eaf484565cb9379c0aada9e9fc83c75/src/assets/douban-logo.png -------------------------------------------------------------------------------- /src/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 27 | 31 | 32 | 33 | 37 | 41 | 42 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Divider } from 'antd'; 3 | 4 | 5 | export default function () { 6 | return ( 7 |
8 |
9 | 免责声明:内容来源于 豆瓣电影 ,接口来源于网络,侵删,禁止商业用途使用 10 |
11 |
12 | Copyright ©2019 yangfan2016 <15234408101@163.com> 13 | 14 | MIT 15 |
16 |
17 | 关于我 18 | 19 | About me 20 | 21 | Github 22 | 23 | 掘金 24 | 25 | 博客 26 |
27 |
28 | ); 29 | }; 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/TopNav.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { withRouter, Link } from 'react-router-dom'; 3 | import { Icon } from 'antd'; 4 | import { getHotShowing, getContentBySearch } from "../api"; 5 | import * as _ from "lodash"; 6 | import { serialize } from '../utils'; 7 | import '../css/Home.css'; 8 | 9 | function TopNav(props: iTopNavProps) { 10 | let [hostShowTitle, setHostShowTitle] = useState(""); 11 | let [hotShowList, setHotShowList] = useState([]); 12 | let [suggestList, setSuggestList] = useState([]); 13 | let [searchHistory] = useState(getSearchHistory().slice(0)); 14 | let [searchStr, setSearchStr] = useState(""); 15 | let [isShowSuggestBox, setIsShowSuggestBox] = useState(false); 16 | let [isShowTipsPanel, setIsShowTipsPanel] = useState(true); 17 | 18 | function navToSearch() { 19 | searchStr = searchStr.trim(); 20 | 21 | if (searchStr === "") { 22 | searchStr = hostShowTitle; 23 | } 24 | 25 | let query: iSearchParams = { 26 | q: searchStr, 27 | }; 28 | 29 | let search = serialize(query); 30 | 31 | props.history.push({ 32 | pathname: '/search', 33 | search, 34 | }); 35 | } 36 | 37 | function getSearchHistory() { 38 | const KEY = "SEARCH_H"; 39 | let cache = JSON.parse(localStorage.getItem(KEY) || "[]"); 40 | return cache; 41 | } 42 | 43 | function addSearchHistory(item: iSearchHistory) { 44 | const MAX_LEN_CACHE_SEARCH = 5; 45 | const KEY = "SEARCH_H"; 46 | let cache = getSearchHistory().slice(0); 47 | 48 | let isExist = cache.some((c: iSearchHistory) => { 49 | return c.id === item.id; 50 | }); 51 | 52 | if (!isExist) { 53 | cache.unshift(item); 54 | if (cache.length > MAX_LEN_CACHE_SEARCH) { 55 | cache.pop(); 56 | } 57 | localStorage.setItem(KEY, JSON.stringify(cache)); 58 | } 59 | } 60 | 61 | function getContentBySearchDebounce() { 62 | return _.debounce(function (value) { 63 | getContentBySearch(value, { 64 | count: 5, 65 | }) 66 | .then(({ data }: AxiosResponse) => { 67 | let { subjects } = data; 68 | setSuggestList(subjects); 69 | }); 70 | }, 5e2); 71 | } 72 | 73 | function getSearch(ev: any) { 74 | let value = ev.target.value; 75 | let str = value.trim(); 76 | let isValid = str.length > 0; 77 | 78 | setSearchStr(value); 79 | setIsShowTipsPanel(!isValid); 80 | 81 | // close showlist 82 | isValid && getSuggestionBySearch(str); 83 | 84 | } 85 | 86 | function closeSuggest() { 87 | setIsShowSuggestBox(false); 88 | } 89 | 90 | function renderTopBar() { 91 | return ( 92 |
93 |
94 |
95 | 96 |
97 |
{props.slotTitle}
98 |
99 |
100 |
101 |
102 | 103 | 全网搜 104 |
105 | { 110 | ev.nativeEvent.stopImmediatePropagation(); 111 | setIsShowSuggestBox(true); 112 | }} 113 | onKeyDown={ev => { 114 | setIsShowSuggestBox(true); 115 | if (ev.keyCode === 13) { 116 | setIsShowSuggestBox(false); 117 | navToSearch(); 118 | } 119 | }} /> 120 |
121 |
{ ev.nativeEvent.stopImmediatePropagation() }}> 127 | { 128 | isShowTipsPanel ? 129 |
130 |
0 ? "block" : "none", 133 | } 134 | }> 135 |

历史记录

136 |
    137 | { 138 | searchHistory.map((item: iSearchHistory, index: number) => { 139 | return ( 140 |
  • 141 | 142 |
    {item.title}
    143 | 144 |
  • 145 | ); 146 | }) 147 | } 148 |
149 |
150 |
151 |

热映

152 |
    153 | { 154 | hotShowList.slice(0, 8).map((item: any, index: number) => { 155 | return ( 156 |
  • 157 | { 159 | addSearchHistory({ 160 | id: item.id, 161 | title: item.title, 162 | }); 163 | }}> 164 | {+index + 1} 165 | {item.title} 166 | 167 |
  • 168 | ); 169 | }) 170 | } 171 |
172 |
173 |
174 | : 175 |
176 |
    177 | { 178 | suggestList.map((item: any, index: number) => { 179 | return ( 180 |
  • 181 | { 183 | addSearchHistory({ 184 | id: item.id, 185 | title: item.title, 186 | }); 187 | }}> 188 |
    {item.title}
    189 |

    {item.original_title}

    190 | 191 |
  • 192 | ); 193 | }) 194 | } 195 |
196 |
197 | } 198 |
199 |
200 |
201 |
202 |
203 | ); 204 | } 205 | 206 | let getSuggestionBySearch = getContentBySearchDebounce(); 207 | 208 | // componentDidMount 209 | useEffect(() => { 210 | getHotShowing({ 211 | start: 0, 212 | count: 12, 213 | }) 214 | .then(({ data }: AxiosResponse) => { 215 | let { subjects } = data; 216 | 217 | let title = subjects.length > 0 ? subjects[0].title : ""; 218 | 219 | setHostShowTitle(title); 220 | setHotShowList(subjects); 221 | }); 222 | 223 | 224 | 225 | document.addEventListener("click", closeSuggest); 226 | 227 | return () => { 228 | // componentWillUnMount 229 | document.removeEventListener("click", closeSuggest); 230 | } 231 | }, []); 232 | 233 | 234 | if (props.noAffix) { 235 | return renderTopBar(); 236 | } 237 | 238 | return renderTopBar(); 239 | 240 | } 241 | 242 | 243 | export default withRouter(TopNav); 244 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | -------------------------------------------------------------------------------- /src/css/Detail.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | position: relative; 3 | } 4 | 5 | .profile .block { 6 | margin: 0 0 50px 0; 7 | } 8 | 9 | .profile-img { 10 | width: 200px; 11 | box-shadow: 2px 2px 3px 0px #121111; 12 | } 13 | 14 | .profile-img img { 15 | width: 100%; 16 | height: auto; 17 | } 18 | 19 | .profile-info .tags { 20 | margin: 10px 0; 21 | } 22 | 23 | .profile-info .tags .tag-text { 24 | background-color: var(--backColor); 25 | color: var(--foreColor); 26 | } 27 | 28 | .video_summary { 29 | margin: 20px 0; 30 | } 31 | 32 | .video_summary .ant-typography { 33 | color: var(--foreColor); 34 | } 35 | 36 | .profile-rate { 37 | position: absolute; 38 | top: 0; 39 | right: 0; 40 | } 41 | 42 | .profile-rate .rate { 43 | color: var(--hoverColor); 44 | } 45 | 46 | .profile-rate .rate .units { 47 | font-size: 72px; 48 | } 49 | 50 | .profile-rate .rate .decimal { 51 | font-size: 54px; 52 | } 53 | 54 | .profile-rate .box { 55 | color: #fff; 56 | font-size: 1rem; 57 | } 58 | 59 | .profile-rate .title { 60 | position: absolute; 61 | top: 0; 62 | right: 120px; 63 | width: 10em; 64 | font-size: 3rem; 65 | color: #fff; 66 | overflow: hidden; 67 | white-space: nowrap; 68 | text-overflow: ellipsis; 69 | text-align: center; 70 | } 71 | 72 | .photos-box { 73 | overflow-x: auto; 74 | overflow-y: hidden; 75 | height: 200px; 76 | } 77 | 78 | .box-gallery { 79 | width: 2000px; 80 | height: 200px; 81 | } 82 | 83 | .gallery-img { 84 | position: relative; 85 | float: left; 86 | height: 100%; 87 | border-right: 5px solid #fff; 88 | } 89 | 90 | .gallery-img:last-of-type { 91 | border-right: none; 92 | } 93 | 94 | .gallery-img img { 95 | width: auto; 96 | height: 100%; 97 | } 98 | 99 | .gallery-img .img-icon { 100 | position: absolute; 101 | top: 50%; 102 | left: 50%; 103 | transform: translate(-50%, -50%); 104 | font-size: 2rem; 105 | color: #fff; 106 | } 107 | 108 | .movie-card .img-tag, .gallery-img .img-tag { 109 | position: absolute; 110 | top: 5px; 111 | left: 5px; 112 | } 113 | 114 | .container-video { 115 | padding: 0 10%; 116 | } 117 | 118 | .video-box { 119 | position: relative; 120 | height: 400px; 121 | overflow: hidden; 122 | background-color: #27272f; 123 | } 124 | 125 | .video-box video { 126 | width: 700px; 127 | height: 100%; 128 | } 129 | 130 | .video-box video:focus { 131 | outline: none; 132 | } 133 | 134 | .box-list { 135 | position: absolute; 136 | top: 0; 137 | right: 0; 138 | bottom: 0; 139 | width: calc(100% - 750px); 140 | padding: 0 0 0 15px; 141 | overflow-x: hidden; 142 | overflow-y: auto; 143 | } 144 | 145 | .box-list .list .list-item { 146 | cursor: pointer; 147 | } 148 | 149 | .box-list .list .list-item img { 150 | width: 200px; 151 | height: auto; 152 | } 153 | 154 | .box-list .list .list-item.active .ant-list-item-meta-title, .box-list .list .list-item:hover .ant-list-item-meta-title { 155 | color: var(--themeHoverColor); 156 | } 157 | 158 | .drawer-in { 159 | top: 100%; 160 | transition: top 1s ease; 161 | } 162 | 163 | .drawer-fixed-bottom { 164 | position: fixed; 165 | top: 460px; 166 | /* The width of top drawer */ 167 | left: 0; 168 | bottom: 0; 169 | width: 100%; 170 | z-index: 1001; 171 | background-color: #FFF; 172 | padding: 10px 10%; 173 | overflow: auto; 174 | } 175 | 176 | .profile-img-loading { 177 | width: 200px; 178 | height: 280px; 179 | background-color: #f5f6f7; 180 | background-image: url("../assets/loading.svg"); 181 | background-repeat: no-repeat; 182 | background-position: center; 183 | } 184 | 185 | /* custom ant style */ 186 | 187 | .custom-ant-rate { 188 | font-size: .7rem; 189 | margin: 0 5px; 190 | } 191 | 192 | .cutsom-ant-comment .ant-comment-content-author-name>span, .cutsom-ant-comment .ant-comment-actions>li>span { 193 | color: var(--foreColor); 194 | } 195 | 196 | .cutsom-ant-comment .ant-comment-actions>li>span:hover { 197 | color: var(--hoverColor); 198 | } 199 | 200 | /* reset ant style */ 201 | 202 | .ant-drawer .ant-drawer-content { 203 | background-color: var(--backColor); 204 | } 205 | 206 | .ant-drawer .ant-list-vertical .ant-list-item-meta-title { 207 | color: #fff; 208 | } 209 | 210 | .ant-drawer .ant-drawer-close { 211 | color: #fff; 212 | } 213 | -------------------------------------------------------------------------------- /src/css/Home.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --rankFirst: #ff183e; 3 | --rankSecond: #ff5c38; 4 | --rankThird: #ffb821; 5 | --rankOther: #666; 6 | --cardImgHeight: 220px; 7 | --cardBigImgHeight: calc(2 * var(--cardImgHeight)); 8 | --headBannerWidth: 800px; 9 | --headHistroyWidth: calc(var(--pageWidth) - var(--headBannerWidth)); 10 | } 11 | 12 | ::-moz-placeholder { 13 | color: var( --foreColor); 14 | } 15 | 16 | ::-webkit-input-placeholder { 17 | color: var( --foreColor); 18 | } 19 | 20 | :-ms-input-placeholder { 21 | color: var( --foreColor); 22 | } 23 | 24 | /* overwritten ant-design */ 25 | 26 | .ant-card { 27 | background-color: transparent; 28 | } 29 | 30 | .ant-card-meta-title { 31 | color: #fff; 32 | } 33 | 34 | .ant-card-meta-description { 35 | color: rgba(226, 226, 226, 0.45); 36 | } 37 | 38 | .ant-card-bordered { 39 | border: none; 40 | } 41 | 42 | .mian-box { 43 | position: fixed; 44 | top: var(--headBarHeight); 45 | left: var(--sideWidth); 46 | right: 0; 47 | bottom: 0; 48 | overflow: auto; 49 | } 50 | 51 | .header { 52 | position: relative; 53 | } 54 | 55 | .header .header-bar { 56 | position: fixed; 57 | top: 0; 58 | left: 0; 59 | right: 0; 60 | padding: 20px 0 20px var(--sideWidth); 61 | z-index: 2; 62 | background-color: var(--backColor); 63 | /* drag app window */ 64 | -webkit-app-region: drag; 65 | } 66 | 67 | .bar-container { 68 | width: var(--pageWidth); 69 | margin: 0 auto; 70 | } 71 | 72 | .header .header-bar .logo { 73 | display: inline-block; 74 | width: 125px; 75 | height: 25px; 76 | background-image: url("../assets/douban-logo.png"); 77 | background-repeat: no-repeat; 78 | background-size: 100% 100%; 79 | } 80 | 81 | .header .header-bar .slot-title { 82 | display: inline-block; 83 | margin: 0 0 0 15px; 84 | } 85 | 86 | .header .header-bar .search { 87 | float: right; 88 | position: relative; 89 | } 90 | 91 | .header .header-bar .search-box { 92 | float: right; 93 | height: 40px; 94 | border-radius: 20px; 95 | background-color: var(--shallowColor); 96 | box-shadow: 0 4px 18px rgba(17, 18, 38, .07); 97 | border: 1px solid transparent; 98 | overflow: hidden; 99 | } 100 | 101 | .header .search-box { 102 | background-color: transparent; 103 | box-shadow: none; 104 | border-color: var(--themeColor); 105 | } 106 | 107 | .header .header-bar .search-box .search-input, .header .header-bar .search-box .search-btn { 108 | float: right; 109 | height: 100%; 110 | } 111 | 112 | .header .header-bar .search-box .search-input { 113 | padding: 0 10px; 114 | margin: 0 0 0 20px; 115 | background-color: transparent; 116 | border: none; 117 | outline: none; 118 | } 119 | 120 | .header .header-bar .search-box .search-btn { 121 | width: 100px; 122 | text-align: center; 123 | line-height: 40px; 124 | background-color: var(--themeColor); 125 | color: var(--foreColor); 126 | cursor: pointer; 127 | } 128 | 129 | .header .header-bar .search-box .search-btn:hover { 130 | background-color: var(--themeHoverColor); 131 | } 132 | 133 | .header .header-bar .search-list { 134 | position: absolute; 135 | top: 100%; 136 | left: 15px; 137 | right: 15px; 138 | background-color: #fff; 139 | border-radius: 5px; 140 | box-shadow: 0 28px 50px rgba(25, 24, 40, .35) 141 | } 142 | 143 | .header .header-bar .search-list .list-history .panel-title, .header .header-bar .search-list .list-hot .panel-title { 144 | padding: 5px 10px; 145 | color: #999; 146 | } 147 | 148 | .header .header-bar .search-list .list-item { 149 | position: relative; 150 | margin: 5px 5px; 151 | padding: 5px; 152 | border-radius: 3px; 153 | } 154 | 155 | .header .header-bar .search-list .list-item:hover { 156 | background-color: #eee; 157 | } 158 | 159 | .list-history .list-item { 160 | display: inline-block; 161 | } 162 | 163 | .header .header-bar .search-list .list-item .index { 164 | display: inline-block; 165 | background-color: var(--rankOther); 166 | color: #fff; 167 | width: 20px; 168 | line-height: 20px; 169 | margin: 0 10px 0 0; 170 | text-align: center; 171 | border-radius: 3px; 172 | } 173 | 174 | .block-weekly .weekly-box .card-container:nth-of-type(1) .dot::before, .block-weekly .weekly-box .card-container:nth-of-type(1) .dot::after, .header .header-bar .search-list .list-item:nth-of-type(1) .index { 175 | background-color: var(--rankFirst); 176 | } 177 | 178 | .block-weekly .weekly-box .card-container:nth-of-type(2) .dot::before, .block-weekly .weekly-box .card-container:nth-of-type(2) .dot::after, .header .header-bar .search-list .list-item:nth-of-type(2) .index { 179 | background-color: var(--rankSecond); 180 | } 181 | 182 | .block-weekly .weekly-box .card-container:nth-of-type(3) .dot::before, .block-weekly .weekly-box .card-container:nth-of-type(3) .dot::after, .header .header-bar .search-list .list-item:nth-of-type(3) .index { 183 | background-color: var(--rankThird); 184 | } 185 | 186 | .header .header-bar .search-list .list-item .title { 187 | color: #000; 188 | } 189 | 190 | .header .header-banner { 191 | width: 800px; 192 | background-image: url(../assets/banner-blur-bg.png); 193 | background-repeat: no-repeat; 194 | background-size: cover; 195 | } 196 | 197 | .header .header-banner .banner-item { 198 | height: 250px; 199 | } 200 | 201 | .header .header-banner .banner-item img { 202 | display: initial; 203 | width: 100%; 204 | height: 100%; 205 | } 206 | 207 | .header .header-history { 208 | width: calc(var(--headHistroyWidth) - 15px); 209 | margin: 0 0 0 15px; 210 | background-color: var(--shallowColor); 211 | } 212 | 213 | .header .header-history .list-item { 214 | padding: 8px 15px; 215 | } 216 | 217 | .header .header-history .list-item:hover { 218 | background-color: rgba(0, 0, 0, 0.5); 219 | } 220 | 221 | .header .header-history .top-title { 222 | padding: 20px 15px; 223 | color: var(--foreColor); 224 | } 225 | 226 | .header .header-history .title { 227 | color: var(--foreColor); 228 | transition: all 0.2s ease; 229 | } 230 | 231 | .header .header-history .list-item:hover .title { 232 | transform: translateX(10px); 233 | color: var(--hoverColor); 234 | } 235 | 236 | .page-home .block { 237 | margin: 30px 0; 238 | } 239 | 240 | .block-weekly .weekly-box { 241 | margin: 50px 0 0 0; 242 | border-top: 1px solid #f0f0f0; 243 | text-align: center; 244 | } 245 | 246 | .block-top250 .cards-box.cards-box--top250 { 247 | position: relative; 248 | padding: 0 0 0 calc(2* var(--oneOfSix)); 249 | margin: 0 0 30px 0; 250 | } 251 | 252 | .cards-box .card-container { 253 | width: var(--oneOfSix); 254 | display: inline-block; 255 | } 256 | 257 | .block-top250 .cards-box .card-container { 258 | width: 25%; 259 | } 260 | 261 | .block-top250 .cards-box .card-container.card-big { 262 | position: absolute; 263 | top: 0; 264 | left: 0; 265 | width: calc(2* var(--oneOfSix)); 266 | bottom: 20px; 267 | } 268 | 269 | .block-top250 .cards-box .card-container.card-big .movie-card { 270 | height: 100%; 271 | } 272 | 273 | .block-top250 .cards-box .card-container.card-big .movie-card .card-img, .block-top250 .cards-box .card-container.card-big .loading-img-box { 274 | height: var(--cardBigImgHeight); 275 | } 276 | 277 | .block-top250 .cards-box .card-container.card-big .movie-card .img-tag { 278 | top: 10px; 279 | left: 10px; 280 | font-size: 2rem; 281 | line-height: 2rem; 282 | border-radius: 8px; 283 | } 284 | 285 | .block-top250 .cards-box .card-container.card-big .movie-card .ant-card-meta-title { 286 | font-size: 1.8rem; 287 | margin: 20px 0 10px 0; 288 | } 289 | 290 | .block-top250 .cards-box .card-container.card-big .movie-card .ant-card-meta-description { 291 | font-size: 1rem; 292 | } 293 | 294 | .block-newmovie .card-container { 295 | width: 25%; 296 | } 297 | 298 | .block-weekly .weekly-box .card-container { 299 | position: relative; 300 | padding: 15px 0 10px 0; 301 | } 302 | 303 | .block-weekly .weekly-box .card-container .rate { 304 | font-size: .9rem; 305 | font-weight: 700px; 306 | color: var(--backColor); 307 | } 308 | 309 | .block-weekly .weekly-box .card-container .title { 310 | color: #999; 311 | } 312 | 313 | .block-weekly .weekly-box .card-container .dot { 314 | position: absolute; 315 | top: -6px; 316 | /* (width-1)/2+1 */ 317 | left: 50%; 318 | width: 11px; 319 | height: 11px; 320 | transform: translate(-50%, 0); 321 | } 322 | 323 | .block-weekly .weekly-box .card-container .dot::before { 324 | content: ""; 325 | position: absolute; 326 | top: 0; 327 | left: 0; 328 | width: 100%; 329 | height: 100%; 330 | border-radius: 50%; 331 | background-color: var(--rankOther); 332 | } 333 | 334 | .block-weekly .weekly-box .card-container .dot::after { 335 | content: ""; 336 | position: absolute; 337 | top: 50%; 338 | left: 50%; 339 | width: 58%; 340 | height: 58%; 341 | transform: translate(-50%, 0) rotate(45deg); 342 | transform-origin: 50% 50%; 343 | background-color: var(--rankOther); 344 | } 345 | 346 | .block-weekly .line-raw { 347 | position: relative; 348 | } 349 | 350 | .block-weekly .line-raw .spotbox { 351 | position: absolute; 352 | top: 0; 353 | right: 0; 354 | display: flex; 355 | } 356 | 357 | .block-weekly .line-raw .spotbox .spot { 358 | position: relative; 359 | margin: 0 10px; 360 | } 361 | 362 | .block-weekly .line-raw .spotbox .spot:nth-of-type(1) { 363 | top: 20px; 364 | width: 20px; 365 | height: 50px; 366 | background-image: linear-gradient(var(--rankThird), #fff); 367 | border-radius: 10px 10px 0 0; 368 | } 369 | 370 | .block-weekly .line-raw .spotbox .spot:nth-of-type(2) { 371 | top: 10px; 372 | width: 26px; 373 | height: 50px; 374 | background-image: linear-gradient(var(--rankFirst), #fff); 375 | border-radius: 13px 13px 0 0; 376 | } 377 | 378 | .block-weekly .line-raw .spotbox .spot:nth-of-type(3) { 379 | top: 30px; 380 | width: 14px; 381 | height: 50px; 382 | background-image: linear-gradient(var(--rankSecond), #fff); 383 | border-radius: 7px 7px 0 0; 384 | } 385 | 386 | .movie-card { 387 | margin: 10px 8px 10px 0; 388 | } 389 | 390 | .block-hotshow .card-container:nth-of-type(6n) .movie-card, .block-weekly .card-container:nth-of-type(6n) .movie-card, .block-top250 .card-container:nth-of-type(5) .movie-card, .block-top250 .card-container:nth-of-type(9) .movie-card { 391 | margin-right: 0; 392 | } 393 | 394 | .movie-card .ant-card-body { 395 | padding: 5px 10px; 396 | } 397 | 398 | .movie-card .ant-card-body .ant-card-meta-detail>div:not(:last-child) { 399 | margin: 0; 400 | } 401 | 402 | .loading-img-box, .movie-card img { 403 | width: 100%; 404 | height: var(--cardImgHeight); 405 | } 406 | 407 | .loading-img-box { 408 | position: relative; 409 | background-color: #f5f6f7; 410 | cursor: not-allowed; 411 | } 412 | 413 | .loading-img-box img { 414 | position: absolute; 415 | top: 50%; 416 | left: 50%; 417 | width: auto; 418 | height: auto; 419 | transform: translate(-50%, -50%); 420 | } 421 | 422 | .block-newmovie { 423 | position: relative; 424 | } 425 | 426 | .block-newmovie .cards-box { 427 | width: calc(var(--oneOfSix) * 4); 428 | } 429 | 430 | .rate-box { 431 | position: absolute; 432 | top: 0; 433 | right: 0; 434 | width: calc(var(--pageWidth) - var(--oneOfSix) * 4); 435 | } 436 | 437 | .rate-box .goodbox { 438 | background-color: #f8f8f8; 439 | } 440 | 441 | .rate-box .goodbox .goodbox-rate { 442 | position: relative; 443 | margin: 10px 20px; 444 | padding: 10px 50px; 445 | border-top: 1px solid #eee; 446 | } 447 | 448 | .rate-box .goodbox .goodbox-rate .title { 449 | font-size: .9rem; 450 | font-weight: 800; 451 | } 452 | 453 | .rate-box .goodbox .goodbox-rate .title:hover { 454 | color: var(--themeHoverColor); 455 | } 456 | 457 | .rate-box .goodbox .goodbox-rate .rank { 458 | position: absolute; 459 | top: 0; 460 | left: 0; 461 | font-size: 2rem; 462 | color: var(--rankOther); 463 | } 464 | 465 | .rate-box .goodbox .goodbox-rate:nth-of-type(-n+3) .rank { 466 | -webkit-text-fill-color: transparent; 467 | background-clip: text; 468 | background-image: linear-gradient(180deg, #ffb821 0, #ff5c38 45%, #ff1459); 469 | } 470 | 471 | .rate-box .goodbox .goodbox-rate .box { 472 | position: absolute; 473 | top: 10px; 474 | right: 0; 475 | color: #999; 476 | } 477 | 478 | .rate-box .goodbox .goodbox-rate .summary { 479 | color: #999; 480 | } 481 | 482 | .rate-box .goodbox .goodbox-rate .box-new { 483 | color: var(--highlightColor); 484 | } 485 | 486 | .footer { 487 | border-top: 2px solid var(--themeColor); 488 | padding: 40px 0; 489 | font-size: 0.6rem; 490 | text-align: center; 491 | color: #999; 492 | } 493 | 494 | .footer-block { 495 | line-height: 1.2rem; 496 | } 497 | 498 | .footer-block a { 499 | color: inherit; 500 | } 501 | 502 | .footer-block a:hover { 503 | color: var(--themeHoverColor); 504 | } 505 | -------------------------------------------------------------------------------- /src/css/NotFound.css: -------------------------------------------------------------------------------- 1 | .page-404 { 2 | position: relative; 3 | width: 100%; 4 | height: 100vh; 5 | background-image: linear-gradient(45deg, #080 50%, #fff 50%); 6 | } 7 | 8 | .page-404 .ant-empty { 9 | position: absolute; 10 | top: 50%; 11 | left: 0; 12 | transform: translate(0, -50%); 13 | color: #fff; 14 | } 15 | 16 | .header-404 .page-404-title { 17 | font-size: 2rem; 18 | text-align: center; 19 | -webkit-text-fill-color: transparent; 20 | background-clip: text; 21 | background-image: linear-gradient(180deg, #ffb821 0, #ff5c38 45%, #ff1459); 22 | } 23 | 24 | .header-404 { 25 | width: 100%; 26 | padding: 15px 0; 27 | position: fixed; 28 | top: 30%; 29 | left: 50%; 30 | transform: translate(-50%, -50%); 31 | } 32 | 33 | .header-404 .header-bar { 34 | width: 60%; 35 | margin: 0 auto; 36 | background-color: transparent; 37 | } 38 | 39 | .header-404 .bar-container .bar-top { 40 | display: inline-block; 41 | width: 100%; 42 | text-align: center; 43 | } 44 | 45 | .header-404 .header-bar .logo { 46 | height: 34px; 47 | width: 160px; 48 | } 49 | 50 | .header-404 .header-bar .search { 51 | width: 100%; 52 | } 53 | 54 | .header-404 .header-bar .search-list, .header-404 .header-bar .search-box { 55 | border-radius: 0; 56 | } 57 | 58 | .header-404 .header-bar .search-box { 59 | width: 100%; 60 | background-color: rgba(255, 255, 255, .85); 61 | box-shadow: none; 62 | border-color: var(--themeColor); 63 | } 64 | 65 | .header-404 .header-bar .search-list { 66 | left: 0; 67 | right: 0; 68 | } 69 | 70 | .header-404 .header-bar .search-box .search-input { 71 | width: calc(100% - 100px); 72 | margin: 0; 73 | } -------------------------------------------------------------------------------- /src/css/Search.css: -------------------------------------------------------------------------------- 1 | .page-search { 2 | position: relative; 3 | margin: 30px auto; 4 | } 5 | 6 | .page-search .search-result-list { 7 | width: 70%; 8 | } 9 | 10 | .page-search .search-result-list .item-img { 11 | width: 160px; 12 | height: 220px; 13 | border-radius: 5px; 14 | } 15 | 16 | .page-search .ant-list-split .ant-list-item { 17 | border: none; 18 | } 19 | 20 | .page-search .rate-box { 21 | width: 30%; 22 | } 23 | 24 | .page-search .rate-box .goodbox .goodbox-rate { 25 | margin: 5px 10px; 26 | padding: 5px 30px; 27 | } 28 | 29 | .page-search .rate-box .goodbox .goodbox-rate .title { 30 | font-size: .8rem; 31 | } 32 | 33 | .page-search .rate-box .goodbox .goodbox-rate .rank { 34 | font-size: 1rem; 35 | } 36 | 37 | .ant-empty-description { 38 | color: var(--foreColor); 39 | } 40 | -------------------------------------------------------------------------------- /src/errors/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import TopNav from '../components/TopNav'; 3 | import { getWallPaper } from "../api"; 4 | import '../css/NotFound.css'; 5 | 6 | export default function NotFound() { 7 | let BASE_URL = "http://cn.bing.com"; 8 | let [src, setSrc] = useState(""); 9 | 10 | useEffect(() => { 11 | getWallPaper() 12 | .then(({ data }: AxiosResponse) => { 13 | let { images } = data; 14 | 15 | setSrc(BASE_URL + images[0].url); 16 | }); 17 | }, []); 18 | 19 | return ( 20 | <> 21 |
24 |
25 |
26 | 404 28 | } /> 29 |
30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AxiosInstance as AxiosInstance2, 3 | CancelTokenSource as CancelTokenSource2, 4 | AxiosResponse as AxiosResponse2, 5 | } from "axios"; 6 | 7 | // 扩展全局变量 8 | declare global { 9 | interface Window { 10 | cancalXHRList: CancelTokenSource[]; 11 | } 12 | // axios 13 | interface AxiosInstance extends AxiosInstance2 { 14 | 15 | } 16 | interface CancelTokenSource extends CancelTokenSource2 { 17 | 18 | } 19 | interface AxiosResponse extends AxiosResponse2 { 20 | 21 | } 22 | // api 请求体 23 | interface iRequestGetData { 24 | start?: number; 25 | count: number; 26 | } 27 | // 历史记录 28 | interface iSearchHistory { 29 | id: string; 30 | title: string; 31 | } 32 | // 页面参数 33 | interface iSearchParams { 34 | q: string; 35 | } 36 | // TopNav 37 | interface iTopNavProps { 38 | // react-router-dom 39 | history: any; 40 | // 标题插槽 41 | slotTitle?: JSX.Element; 42 | // 是否固定到头部 43 | noAffix?: boolean; 44 | } 45 | // Detail 46 | interface iDetailProps { 47 | match: any; 48 | } 49 | // Search 50 | interface iSearchProps { 51 | location: any; 52 | } 53 | // RouterView 54 | interface iRouterViewProps { 55 | location?: any; 56 | // 进入路由之前的钩子 57 | beforeEnter?: (path: string) => void; 58 | } 59 | // 骨架屏 60 | interface ICardList { 61 | column: number, 62 | } 63 | interface IList { 64 | row: number, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.register(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/Box.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function (props: any) { 4 | return ( 5 |
Box page
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/pages/Detail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Tag, Statistic, Comment, Avatar, Tooltip, Rate, Icon, Drawer, List, Typography } from 'antd'; 3 | import moment from 'moment'; 4 | import PageSkeleton from '../skeletons/Detail'; 5 | import { getDetail } from '../api'; 6 | import '../css/Detail.css'; 7 | const { ipcRenderer } = (window as any).electron; 8 | 9 | 10 | export default function (props: iDetailProps) { 11 | let { params } = props.match; 12 | 13 | let [currentPlayData] = useState({ 14 | index: 0, 15 | isAutoPlay: true, 16 | }); 17 | let [detailData, setDetailData] = useState({}); 18 | let [isLoadingDetail, setIsLoadingDetail] = useState(true); 19 | let [isOpenPlayBox, setIsOpenPlayBox] = useState(false); 20 | let galleryBox: HTMLElement | null = null; 21 | 22 | function reCalcGalleryBoxWidth() { 23 | // 重新计算图片画廊的宽度 24 | if (!galleryBox) return; 25 | let galleryBoxEl: HTMLElement | null = galleryBox; 26 | if (galleryBoxEl) { 27 | let width = 0; 28 | Array.from(galleryBoxEl.children).map((child: Element) => { 29 | let w = +(getComputedStyle(child).width || '0').replace("px", ""); 30 | w = Math.ceil(w); 31 | width += w; 32 | }); 33 | galleryBoxEl.style.width = `${width}px`; 34 | } 35 | } 36 | function playThisVideo(src: string, title?: string) { 37 | ipcRenderer.send("open-page-video", { 38 | src, 39 | title, 40 | }); 41 | } 42 | 43 | function openPlayBox(src: string, title?: string) { 44 | playThisVideo(src, title); 45 | } 46 | 47 | function closePlayBox() { 48 | setIsOpenPlayBox(false); 49 | } 50 | 51 | useEffect(() => { 52 | 53 | getDetail(params.id) 54 | .then(({ data }: AxiosResponse) => { 55 | // 处理下数据 56 | let average = data.rating.average; 57 | let [$units, $decimal] = ("" + average).split("."); 58 | 59 | data.$units = $units || 0; 60 | data.$decimal = $decimal || 0; 61 | 62 | setDetailData(data); 63 | setIsLoadingDetail(false); 64 | 65 | }); 66 | }, [props.match.params.id]); 67 | 68 | useEffect(() => { 69 | if (isLoadingDetail === true) return; 70 | 71 | // BUG 需要等待所有图片加载完,才能获取到宽度 72 | let id = setTimeout(() => { 73 | clearTimeout(id); 74 | reCalcGalleryBoxWidth(); 75 | }, 3e3); 76 | 77 | }, [isLoadingDetail]); 78 | 79 | 80 | if (isLoadingDetail) { 81 | return ; 82 | } 83 | if (!detailData.id) return ''; 84 | return ( 85 | <> 86 |
87 |
88 |
89 |
90 |
91 | {detailData.$units} 92 | { 93 | detailData.$units > 0 && 94 | .{detailData.$decimal} 95 | } 96 |
97 | 101 |

{detailData.original_title}

102 |
103 |
104 | 105 |
106 |
107 |

{detailData.title}

108 |
109 | { 110 | detailData.tags.map((tag: string, index: number) => { 111 | return {tag} 112 | }) 113 | } 114 |
115 |
116 | 117 | { 118 | detailData.directors.map((item: any, index: number) => { 119 | return {item.name} 120 | }) 121 | } 122 |
123 |
124 | 125 | { 126 | detailData.casts.map((item: any, index: number) => { 127 | let split = ""; 128 | if (index !== 0) { 129 | split = "/"; 130 | } 131 | return ( 132 | 133 | {split} 134 | {item.name} 135 | 136 | ); 137 | }) 138 | } 139 |
140 |
141 | 148 | {detailData.summary} 149 | 150 |
151 |
152 |
153 |

剧照({detailData.photos_count})

154 |
155 |
galleryBox = current}> 156 | { 157 | detailData.photos.map((item: any, index: number) => { 158 | let { image, alt } = item; 159 | 160 | return ( 161 |
162 | {index === 0 && detailData.trailer_urls.length > 0 && ( 163 | <> 164 | 预告片 165 | { openPlayBox(detailData.trailer_urls[currentPlayData.index], detailData.title) }} /> 170 | 171 | )} 172 | {alt} 173 |
174 | ); 175 | }) 176 | } 177 |
178 |
179 |
180 |
181 |
182 |

影评({detailData.reviews_count})

183 | { 184 | detailData.popular_reviews.map((item: any, index: number) => { 185 | let { 186 | author, 187 | rating, 188 | title, 189 | summary, 190 | created_at, 191 | } = item; 192 | 193 | return ( 194 | 199 | {author.name} 200 | 205 | 206 | )} 207 | avatar={( 208 | 212 | )} 213 | content={( 214 | <> 215 | {title} 216 |

{summary}

217 | 218 | )} 219 | datetime={( 220 | 221 | {moment(created_at).fromNow()} 222 | 223 | )} 224 | /> 225 | ); 226 | }) 227 | } 228 |
229 |
230 |

热评({detailData.comments_count})

231 | { 232 | detailData.popular_comments.map((item: any, index: number) => { 233 | let { 234 | author, 235 | rating, 236 | content, 237 | created_at, 238 | useful_count, 239 | useless_count, 240 | } = item; 241 | 242 | const actions = [ 243 | 244 | 245 | 249 | 250 | 251 | {useful_count || 0} 252 | 253 | , 254 | 255 | 256 | 260 | 261 | 262 | {useless_count || 0} 263 | 264 | 265 | ]; 266 | 267 | return ( 268 | 274 | {author.name} 275 | 280 | 281 | )} 282 | avatar={( 283 | 287 | )} 288 | content={( 289 |

{content}

290 | )} 291 | datetime={( 292 | 293 | {moment(created_at).fromNow()} 294 | 295 | )} 296 | /> 297 | ); 298 | }) 299 | } 300 |
301 |
302 |
303 |
304 |
305 | {/* 抽屉 */} 306 | 313 |
314 |
315 | 316 |
317 | ( 323 | } > 327 | 330 | 331 | )} 332 | /> 333 |
334 |
335 |
336 |
337 | 338 | ); 339 | }; 340 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Link } from 'react-router-dom'; 3 | import { Card, Tag, Carousel } from 'antd'; 4 | import { CardListTop250Skeleton, CardListSkeleton, ListSkeleton } from "../skeletons/Home"; 5 | import { getHotShowing, getNew, getGoodbox, getWeeklyMovie, getTop250 } from "../api"; 6 | import * as _ from "lodash"; 7 | import LazyLoad from "react-lazy-load"; 8 | import '../css/Home.css'; 9 | 10 | // temp banner 11 | import imgBanner001 from '../assets/banner-001.jpg'; 12 | import imgBanner002 from '../assets/banner-002.jpg'; 13 | import imgBanner003 from '../assets/banner-003.jpg'; 14 | import imgBanner004 from '../assets/banner-004.jpg'; 15 | import imgBanner005 from '../assets/banner-005.jpg'; 16 | 17 | 18 | // 懒加载的图片高度 19 | const IMG_HEIGHT = 220; 20 | const IMG_BIG_HEIGHT = IMG_HEIGHT * 2; 21 | 22 | export default function () { 23 | 24 | let [hotShowList, setHotShowList] = useState([]); // 热映 25 | let [newMovieList, setNewMovieList] = useState([]); // 新片 26 | let [goodBoxList, setGoodBoxList] = useState([]); // 票房榜 27 | let [weeklyBox, setWeeklyBox] = useState([]); // 口碑榜 28 | let [top250List, setTop250List] = useState([]); // top250 29 | let [boxLastDate, setBoxLastDate] = useState(""); 30 | let [isLoadingHotShow, setIsLoadingHotShow] = useState(true); 31 | let [isLoadingNewMovie, setIsLoadingNewMovie] = useState(true); 32 | let [isLoadingGoodBox, setIsLoadingGoodBox] = useState(true); 33 | let [isLoadingWeeklyBox, setIsLoadingWeeklyBox] = useState(true); 34 | let [isLoadingTop250, setIsLoadingTop250] = useState(true); 35 | let [searchHistory] = useState(getSearchHistory().slice(0)); 36 | 37 | // temp 38 | let bannerList = [ 39 | imgBanner001, 40 | imgBanner002, 41 | imgBanner003, 42 | imgBanner004, 43 | imgBanner005 44 | ]; 45 | 46 | function getSearchHistory() { 47 | const KEY = "SEARCH_H"; 48 | let cache = JSON.parse(localStorage.getItem(KEY) || "[]"); 49 | return cache; 50 | } 51 | 52 | function renderTop250() { 53 | let len = top250List.length; 54 | let count = len / 9 | 0; 55 | let groupList = new Array(count).fill(0); 56 | 57 | groupList = groupList.map((item: number, index: number) => { 58 | let s = 9 * index; 59 | let e = s + 9; 60 | return top250List.slice(s, e); 61 | }); 62 | 63 | return ( 64 | groupList.map((g: any, n: number) => { 65 | return ( 66 |
67 | { 68 | g.map((item: any, index: number) => { 69 | let isFirst = index === 0; 70 | return ( 71 |
72 | 77 | 78 | 79 | 80 | 81 | } 82 | > 83 | {item.rating.average} 84 | 88 | 89 |
90 | ); 91 | }) 92 | } 93 |
94 | ); 95 | }) 96 | ); 97 | 98 | } 99 | 100 | useEffect(() => { 101 | getHotShowing({ 102 | start: 0, 103 | count: 12, 104 | }) 105 | .then(({ data }: AxiosResponse) => { 106 | let { subjects } = data; 107 | 108 | setHotShowList(subjects); 109 | setIsLoadingHotShow(false); 110 | }); 111 | 112 | getNew() 113 | .then(({ data }: AxiosResponse) => { 114 | let { subjects } = data; 115 | 116 | setNewMovieList(subjects); 117 | setIsLoadingNewMovie(false); 118 | }); 119 | 120 | getGoodbox({ 121 | count: 10, 122 | }) 123 | .then(({ data }: AxiosResponse) => { 124 | let { subjects, date } = data; 125 | 126 | setBoxLastDate(date); 127 | setGoodBoxList(subjects); 128 | setIsLoadingGoodBox(false); 129 | }); 130 | 131 | getWeeklyMovie() 132 | .then(({ data }: AxiosResponse) => { 133 | let { subjects } = data; 134 | 135 | setWeeklyBox(subjects); 136 | setIsLoadingWeeklyBox(false); 137 | }); 138 | 139 | getTop250({ 140 | count: 36, 141 | }) 142 | .then(({ data }: AxiosResponse) => { 143 | let { subjects } = data; 144 | 145 | setTop250List(subjects); 146 | setIsLoadingTop250(false); 147 | }); 148 | }, []); 149 | 150 | 151 | return ( 152 | <> 153 |
154 |
155 | 156 | {bannerList.map((item: any, index: number) => { 157 | return ( 158 |
161 | banner 162 |
163 | ); 164 | })} 165 |
166 |
167 |
168 |

最近在看

169 |
    170 | { 171 | searchHistory.map((item: iSearchHistory, index: number) => { 172 | return ( 173 |
  • 174 | 175 |
    {item.title}
    176 | 177 |
  • 178 | ); 179 | }) 180 | } 181 |
182 |
183 |
184 |
185 |
186 |
187 |

正在热映

188 |
189 |
190 | { 191 | isLoadingHotShow ? 192 | : 193 | hotShowList.map((item: any, index: number) => { 194 | return ( 195 |
196 | 201 | 202 | 203 | 204 | 205 | } 206 | > 207 | {item.rating.average} 208 | 212 | 213 |
214 | ); 215 | })} 216 |
217 |
218 |
219 |
220 |

新片榜

221 |
222 |
223 |
224 | { 225 | isLoadingNewMovie ? 226 | : 227 | newMovieList.map((item: any, index: number) => { 228 | return ( 229 |
230 | 235 | 236 | 237 | 238 | 239 | } 240 | > 241 | {item.rating.average} 242 | 246 | 247 |
248 | ); 249 | })} 250 |
251 |
252 |
253 |

北美票房榜

254 |

{boxLastDate} 更新/美元

255 |
256 |
    257 | { 258 | isLoadingGoodBox ? 259 | : 260 | goodBoxList.map((item: any, index: number) => { 261 | let { rank, box, subject } = item; 262 | let { title, id, rating, collect_count } = subject; 263 | let { average } = rating; 264 | 265 | let isNew = item.new; 266 | 267 | let summaryList = []; 268 | let summary = ""; 269 | 270 | if (isNew) { 271 | summaryList.push("新上榜"); 272 | } 273 | 274 | summaryList.push(`${average || 0} 分`); 275 | summaryList.push(`${collect_count} 收藏`); 276 | summary = summaryList.join(" / "); 277 | 278 | return ( 279 |
  • 280 | 281 |

    {title}

    282 |

    283 | {rank} 284 | {box / 1e4} 万 285 | 286 |
  • 287 | ); 288 | }) 289 | } 290 |
291 |
292 |
293 |
294 |
295 |
296 |

一周口碑榜

297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 | { 305 | weeklyBox.slice(0, 6).map((item: any, index: number) => { 306 | let { subject } = item; 307 | let { rating, title } = subject; 308 | let { average } = rating; 309 | return ( 310 |
311 |
{average} 分
312 |
{title}
313 |
314 |
315 | ); 316 | }) 317 | } 318 |
319 |
320 | { 321 | isLoadingWeeklyBox ? 322 | : 323 | weeklyBox.slice(0, 6).map((item: any, index: number) => { 324 | let { subject } = item; 325 | let { rating, title, id, images, genres } = subject; 326 | let { average } = rating; 327 | return ( 328 |
329 | 334 | 335 | 336 | 337 | 338 | } 339 | > 340 | {average} 341 | 345 | 346 |
347 | ); 348 | })} 349 |
350 |
351 |
352 |
353 |

Top 250

354 |
355 | { 356 | isLoadingTop250 ? 357 | : 358 | renderTop250() 359 | } 360 |
361 |
362 | 363 | ); 364 | 365 | }; 366 | -------------------------------------------------------------------------------- /src/pages/Search.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { List, Pagination } from 'antd'; 4 | import { ListSkeleton } from '../skeletons/Home'; 5 | import { getContentBySearch, getHotShowing } from '../api'; 6 | import { reSerialize } from '../utils'; 7 | import '../css/Search.css'; 8 | 9 | 10 | export default function (props: iSearchProps) { 11 | 12 | let { location } = props; 13 | let { search } = location; 14 | search = search.slice(1); // ?q=123 -> q=123 15 | let query: any = reSerialize(search); 16 | let searchStr = query.q; 17 | let searchPageSize = 5; 18 | let [searchData, setSearchData] = useState({}); 19 | let [hotShowList, setHotShowList] = useState([]); 20 | let [isLoadingHotShow, setIsLoadingHotShow] = useState(true); 21 | 22 | 23 | function changeSearchData(current: number) { 24 | getContentBySearch(searchStr, { 25 | count: searchPageSize, 26 | start: (current - 1) * searchPageSize, 27 | }) 28 | .then(({ data }: AxiosResponse) => { 29 | setSearchData(data); 30 | }); 31 | } 32 | 33 | // componentDidMount 只执行一次(第二个参数设置了空数组) 34 | useEffect(() => { 35 | changeSearchData(1); 36 | 37 | getHotShowing({ 38 | start: 0, 39 | count: 12, 40 | }) 41 | .then(({ data }: AxiosResponse) => { 42 | let { subjects } = data; 43 | 44 | setHotShowList(subjects); 45 | setIsLoadingHotShow(false); 46 | 47 | }); 48 | }, []); 49 | 50 | return ( 51 |
52 |
53 |
54 | { 59 | let { images, title, id } = item; 60 | return ( 61 | 64 | 67 | logo 68 | } 69 | title={title} 70 | description={ 71 |
72 | {item.directors.length > 0 && 73 |
74 | 75 | { 76 | item.directors.map((item: any, index: number) => { 77 | return {item.name} 78 | }) 79 | } 80 |
81 | } 82 | {item.casts.length > 0 && 83 |
84 | 85 | { 86 | item.casts.map((item: any, index: number) => { 87 | let split = ""; 88 | if (index !== 0) { 89 | split = "/"; 90 | } 91 | return ( 92 | 93 | {split} 94 | {item.name} 95 | 96 | ); 97 | }) 98 | } 99 |
100 | } 101 | {item.genres.length > 0 && 102 |
103 | 104 | { 105 | item.genres.map((tag: string, index: number) => { 106 | let split = ""; 107 | if (index !== 0) { 108 | split = "/"; 109 | } 110 | return ( 111 | 112 | {split} 113 | {tag} 114 | 115 | ); 116 | }) 117 | } 118 |
119 | } 120 |
121 | } 122 | /> 123 |
124 | ) 125 | }} /> 126 | 127 |
128 |
129 |
130 |

热映榜

131 |
132 |
    133 | { 134 | isLoadingHotShow ? 135 | : 136 | hotShowList.map((item: any, index: number) => { 137 | let { id, title, rating } = item; 138 | let { average } = rating; 139 | return ( 140 |
  • 141 | 142 |

    {title}

    143 | {index + 1} 144 | {average} 分 145 | 146 |
  • 147 | ); 148 | }) 149 | } 150 |
151 |
152 |
153 |
154 | ); 155 | 156 | }; 157 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare module 'react-router-dom'; 6 | declare module 'react-lazy-load'; 7 | 8 | declare namespace NodeJS { 9 | interface ProcessEnv { 10 | NODE_ENV: 'development' | 'production' | 'test'; 11 | PUBLIC_URL: string; 12 | } 13 | } 14 | 15 | declare module '*.bmp' { 16 | const src: string; 17 | export default src; 18 | } 19 | 20 | declare module '*.gif' { 21 | const src: string; 22 | export default src; 23 | } 24 | 25 | declare module '*.jpg' { 26 | const src: string; 27 | export default src; 28 | } 29 | 30 | declare module '*.jpeg' { 31 | const src: string; 32 | export default src; 33 | } 34 | 35 | declare module '*.png' { 36 | const src: string; 37 | export default src; 38 | } 39 | 40 | declare module '*.webp' { 41 | const src: string; 42 | export default src; 43 | } 44 | 45 | declare module '*.svg' { 46 | import * as React from 'react'; 47 | 48 | export const ReactComponent: React.SFC>; 49 | 50 | const src: string; 51 | export default src; 52 | } 53 | 54 | declare module '*.module.css' { 55 | const classes: { [key: string]: string }; 56 | export default classes; 57 | } 58 | 59 | declare module '*.module.scss' { 60 | const classes: { [key: string]: string }; 61 | export default classes; 62 | } 63 | 64 | declare module '*.module.sass' { 65 | const classes: { [key: string]: string }; 66 | export default classes; 67 | } 68 | -------------------------------------------------------------------------------- /src/router/RouterView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | import routerMap from "./config"; 4 | 5 | 6 | function CustomRoute(props: iRouterViewProps) { 7 | let path: string = props.location.pathname; 8 | 9 | props.beforeEnter && props.beforeEnter(path); 10 | 11 | // '/'-> '/home 12 | if (path === '/') return 13 | 14 | // if can match 15 | let matchRoute: any = routerMap.find(item => { 16 | let url = item.path; 17 | // /detail/:id -> \\/detail\\/[^/+] 18 | url = url.replace(/(\:.+)/g, "[^/]+").replace(/\//g, "\\/"); 19 | 20 | return new RegExp(`${url}(\\/|\\/)?$`, 'gi').test(path); 21 | }); 22 | 23 | if (matchRoute) { 24 | return 25 | } 26 | return 27 | } 28 | 29 | export default function (props: iRouterViewProps) { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/router/config.ts: -------------------------------------------------------------------------------- 1 | import Home from '../pages/Home'; 2 | import Detail from '../pages/Detail'; 3 | import Box from '../pages/Box'; 4 | import Search from '../pages/Search'; 5 | 6 | export default [ 7 | { 8 | path: '/home', 9 | component: Home, 10 | }, 11 | { 12 | path: '/detail/:id', 13 | component: Detail, 14 | }, 15 | { 16 | path: '/box', 17 | component: Box, 18 | }, 19 | { 20 | path: '/search', 21 | component: Search, 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const proxy = require('http-proxy-middleware'); 2 | 3 | module.exports = function (app) { 4 | app.use(proxy('/api', { 5 | target: 'http://api.douban.com/', 6 | changeOrigin: true, 7 | pathRewrite: { 8 | '^/api': '/v2', // 重写路径 9 | }, 10 | })); 11 | app.use(proxy('/api2', { 12 | target: 'https://douban.uieee.com/', 13 | changeOrigin: true, 14 | pathRewrite: { 15 | '^/api2': '/v2', // 重写路径 16 | }, 17 | })); 18 | app.use(proxy('/bing', { 19 | target: 'https://www.bing.com/', 20 | changeOrigin: true, 21 | pathRewrite: { 22 | '^/bing': '/', // 重写路径 23 | }, 24 | })); 25 | }; 26 | -------------------------------------------------------------------------------- /src/skeletons/Detail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Statistic, Skeleton } from 'antd'; 3 | 4 | export default function () { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | 0 12 |
13 | 17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/skeletons/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, Skeleton } from 'antd'; 3 | import loadingSvg from '../assets/loading.svg'; 4 | 5 | export function CardListSkeleton(props: ICardList) { 6 | let { column } = props; 7 | let list = new Array(column || 6).fill(1); 8 | return ( 9 | <> 10 | { 11 | list.map((item: number, index: number) => { 12 | return ( 13 |
14 | 20 | loading 21 |
22 | } 23 | /> 24 | 25 | ); 26 | }) 27 | } 28 | 29 | ); 30 | } 31 | 32 | export function CardListTop250Skeleton() { 33 | let list = new Array(9).fill(1); 34 | return ( 35 |
36 | { 37 | list.map((item: number, index: number) => { 38 | return ( 39 |
40 | 46 | loading 47 |
48 | } 49 | /> 50 |
51 | ); 52 | }) 53 | } 54 | 55 | ); 56 | } 57 | 58 | export function ListSkeleton(props: IList) { 59 | let { row } = props; 60 | let list = new Array(row || 4).fill(1); 61 | return ( 62 | <> 63 | { 64 | list.map((item: number, index: number) => { 65 | return ( 66 |
  • 67 | 68 | 69 | 0 70 | 0 万 71 |
  • 72 | ); 73 | }) 74 | } 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { }; -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // serialize data e.g. {a:"abc",b:"123"} -> "a=abc&b=123" 2 | export function serialize(data: any, isTraditional: boolean = false): string { 3 | let arr = []; 4 | if (typeof data == "object") { 5 | for (let key in data) { 6 | if (data[key] != null) { 7 | let item = data[key]; 8 | if (isTraditional && item instanceof Array) { 9 | arr.push(item.map(function (field) { 10 | return encodeURIComponent(key) + "=" + encodeURIComponent(field); 11 | }).join("&")); 12 | } else { 13 | arr.push(encodeURIComponent(key) + "=" + encodeURIComponent(item)); 14 | } 15 | } 16 | } 17 | return arr.join("&"); 18 | } 19 | return ''; 20 | } 21 | /** 22 | * reverse serialize data e.g. "a=abc&b=123" -> {"a":"abc","b":"123"} 23 | * @param {string} str 24 | * @param {boolean} isTraditional if true e.g. "a=abc&b=123&b=456" -> {"a":"abc","b":["123","456"]} 25 | */ 26 | export function reSerialize(str: string, isTraditional = true): object { 27 | let s = decodeURIComponent(str); 28 | 29 | return s.split("&").reduce(function (prev:any, cur) { 30 | let flag = cur.indexOf("="); 31 | let key = cur.slice(0, flag); 32 | let val = cur.slice(flag + 1); 33 | 34 | if (isTraditional) { 35 | if (prev[key]) { 36 | prev[key] instanceof Array ? prev[key].push(val) : prev[key] = [prev[key], val]; 37 | } else { 38 | prev[key] = val; 39 | } 40 | } else { 41 | prev[key] = val; 42 | } 43 | 44 | return prev; 45 | }, {}); 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | "isolatedModules": true, 21 | "experimentalDecorators":true 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude":[ 27 | "src/setupProxy.js" 28 | ], 29 | } 30 | --------------------------------------------------------------------------------