├── .babelrc ├── .gitignore ├── README.md ├── config ├── babel │ └── plugin │ │ └── no-require-css.js ├── webpack.config.base.js ├── webpack.config.client.js ├── webpack.config.prod.js └── webpack.config.server.js ├── deploy.js ├── package.json ├── postcss.config.js ├── server ├── common │ └── assets.js ├── scripts │ ├── constant.js │ ├── free-port.js │ ├── open-browser.js │ ├── svr-code-watch.js │ ├── svr-dev-server.js │ ├── wds-start.js │ └── webpack-dev-server.config.js └── start.js ├── src ├── client │ ├── app │ │ ├── index.js │ │ ├── layout.js │ │ ├── layout.scss │ │ ├── reset.scss │ │ └── toast.scss │ ├── client-entry.js │ └── pages │ │ ├── 404 │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── draft-editor │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── draft-list │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── home │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── login │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── post-detail │ │ ├── add-comment-input.js │ │ ├── comment-input.js │ │ ├── comments-lists.js │ │ ├── comments-lists.scss │ │ ├── comments.scss │ │ ├── index.js │ │ ├── post-op-bars.js │ │ ├── redux.js │ │ ├── reply-input.js │ │ └── style.scss │ │ ├── post-editor │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── register │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss │ │ ├── user-center │ │ ├── activity-items.js │ │ ├── collection.js │ │ ├── follow.js │ │ ├── index.js │ │ ├── my-activites.js │ │ ├── my-post.js │ │ ├── redux.js │ │ └── style.scss │ │ └── user-setting │ │ ├── index.js │ │ ├── redux.js │ │ └── style.scss ├── componentCommon │ ├── confirm │ │ ├── index.js │ │ └── style.scss │ ├── editor │ │ ├── codemirror.scss │ │ ├── hljs.scss │ │ ├── index.js │ │ ├── language-list.js │ │ ├── preview-content.js │ │ ├── preview-content.scss │ │ ├── publish-post.js │ │ ├── publish-post.scss │ │ ├── style.scss │ │ ├── theme.scss │ │ ├── upload-header-image.js │ │ └── upload-header-image.scss │ ├── empty │ │ ├── index.js │ │ └── style.scss │ ├── post-card │ │ └── index.js │ ├── tdk.js │ └── toast │ │ ├── index.js │ │ ├── notice.js │ │ ├── notification.js │ │ └── toast.js ├── componentHOC │ ├── async-loader.js │ ├── form-create.js │ ├── with-initial-data.js │ └── withListenerScroll.js ├── componentLayout │ ├── header.js │ ├── redux.js │ ├── style.scss │ └── user-menu.js ├── router.js ├── server │ ├── app │ │ └── index.js │ ├── middlewares │ │ ├── get-static-routes.js │ │ └── react-ssr.js │ └── server-entry.js ├── share │ ├── pro-config.js │ ├── should-ssr-list.js │ ├── store.js │ └── theme.js └── utils │ ├── asyncHandler.js │ ├── composeHOC.js │ ├── events.js │ ├── helper.js │ ├── http │ └── axios.js │ ├── jsonSource.js │ └── redux │ ├── asyncMiddleware.js │ ├── createStore.js │ ├── index.js │ ├── location.js │ ├── preload.js │ └── reducer.js └── static └── favicon.ico /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node":{ 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets":{ 9 | "node":"current" 10 | } 11 | } 12 | ], 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [ 16 | "@babel/plugin-proposal-class-properties" 17 | ] 18 | }, 19 | "development": { 20 | "presets": [ 21 | [ 22 | "@babel/preset-env", 23 | { 24 | "targets": { 25 | "browsers": [ 26 | ">1%", 27 | "last 2 versions", 28 | "not ie <= 8" 29 | ] 30 | } 31 | } 32 | ], 33 | "@babel/preset-react" 34 | ], 35 | "plugins": [ 36 | "@babel/plugin-proposal-class-properties", 37 | "@babel/plugin-transform-runtime", 38 | "react-hot-loader/babel" //热更新组件状态不丢失 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | oss.json 4 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simplog-front-end 2 | 一个基于react ssr 的博客平台的前端项目 3 | -------------------------------------------------------------------------------- /config/babel/plugin/no-require-css.js: -------------------------------------------------------------------------------- 1 | module.exports = function ({ types: babelTypes }) { 2 | return { 3 | name: "no-require-css", 4 | visitor: { 5 | ImportDeclaration(path, state) { 6 | let importFile = path.node.source.value; 7 | if (importFile.indexOf('.scss') > -1) { 8 | //如果引入了 css文件,则删除此节点 9 | path.remove(); 10 | } 11 | } 12 | } 13 | }; 14 | }; -------------------------------------------------------------------------------- /config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); 4 | const resolvePath = (pathstr) => path.resolve(__dirname, pathstr); 5 | 6 | module.exports = { 7 | mode: 'development', 8 | devtool: "eval-source-map", 9 | resolve: { 10 | alias: { 11 | 'config': resolvePath('../config/'), 12 | 'dist': resolvePath('../dist/'), 13 | 'server': resolvePath('../server/'), 14 | 'src': resolvePath('../src/'), 15 | 'utils': resolvePath('../src/utils/'), 16 | } 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.jsx?$/, 22 | loader: 'babel-loader', 23 | exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.(sa|sc|c)ss$/, 27 | use: [ 28 | 'isomorphic-style-loader', 29 | { 30 | loader: "css-loader", 31 | options: { 32 | importLoaders: 2 33 | } 34 | // include: [ 35 | // resolvePath('../src'), 36 | // resolvePath('/node_modules/codemirror/lib/codemirror.css'), 37 | // resolvePath('/node_modules/codemirror/theme/material.css') 38 | // ] 39 | }, 40 | 'postcss-loader', 41 | 'sass-loader' 42 | ] 43 | }, 44 | { 45 | test: /\.(png|jpg|gif|ico)$/, 46 | use: [ 47 | { 48 | loader: 'file-loader', 49 | options: { 50 | name: 'images/[name].[ext]' 51 | } 52 | }] 53 | }, 54 | { 55 | test: /\.md$/, 56 | use: "raw-loader" 57 | } 58 | ] 59 | }, 60 | plugins: [ 61 | new webpack.HashedModuleIdsPlugin(), 62 | new HardSourceWebpackPlugin() 63 | ] 64 | } -------------------------------------------------------------------------------- /config/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseCongig = require('./webpack.config.base'); 4 | const autoprefixer = require('autoprefixer'); 5 | const webpack = require('webpack'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const proConfig = require('../src/share/pro-config'); 8 | 9 | const resolvePath = (pathstr) => path.resolve(__dirname, pathstr); 10 | 11 | module.exports = merge(baseCongig, { 12 | mode: 'development', 13 | entry: { 14 | main: ['react-hot-loader/patch', resolvePath('../src/client/client-entry.js')] 15 | }, 16 | output: { 17 | filename: '[name].js', 18 | path: resolvePath('../dist/static'), 19 | publicPath: 'http://localhost:' + proConfig.wdsPort + '/' 20 | 21 | }, 22 | resolve: { 23 | alias: { 24 | 'react-dom': '@hot-loader/react-dom' 25 | } 26 | }, 27 | 28 | plugins: [ 29 | new webpack.HotModuleReplacementPlugin(), 30 | new MiniCssExtractPlugin({ 31 | filename: '[name].css' //设置名称 32 | }), 33 | new webpack.DefinePlugin({ 34 | // 'process.env': { NODE_ENV: '"development"' }, 35 | 'process.env': { NODE_ENV: `"${process.env.NODE_ENV}"`}, 36 | '__IS_PROD__': false, 37 | '__SERVER__': false 38 | })], 39 | optimization: { 40 | splitChunks: { 41 | cacheGroups: { 42 | // styles: { 43 | // name: 'styles', 44 | // test: /\.scss$/, 45 | // chunks: 'all', 46 | // enforce: true, 47 | // }, 48 | libs: { // 抽离第三方库 49 | test: /node_modules/, // 指定是node_modules下的第三方包 50 | chunks: 'initial', 51 | name: 'libs'// 打包后的文件名,任意命名 52 | } 53 | } 54 | } 55 | }, 56 | devServer: { 57 | contentBase: path.resolve(__dirname, 'dist/static') 58 | } 59 | }) -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const ManifestPlugin = require('webpack-manifest-plugin'); 6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 8 | const baseCongig = require('./webpack.config.base'); 9 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 10 | const CompressionPlugin = require("compression-webpack-plugin"); 11 | 12 | const resolvePath = (pathstr) => path.resolve(__dirname, pathstr); 13 | process.env.BABEL_ENV = 'development';//指定 babel 编译环境 14 | 15 | module.exports = merge(baseCongig,{ 16 | performance: { 17 | hints: false 18 | }, 19 | mode: 'production', 20 | devtool: 'cheap-module-source-map', 21 | entry: { 22 | main: [resolvePath('../src/client/client-entry.js')] 23 | }, 24 | output: { 25 | filename: 'js/[name].[chunkhash:8].js', 26 | path: resolvePath('../dist/static'), 27 | publicPath: '/blog-cdn/' 28 | }, 29 | module: { 30 | rules: [{ 31 | test: /\.jsx?$/, 32 | exclude: /node_modules/, 33 | use:[{ 34 | loader: 'thread-loader', 35 | options: { 36 | workers: 3 37 | } 38 | }, 39 | 'babel-loader' 40 | ] 41 | }, 42 | { 43 | test: /\.(png|jpg|gif)$/, 44 | use: [{ 45 | loader: 'file-loader', 46 | options: { 47 | name: 'img/[name].[hash:8].[ext]', 48 | publicPath: '/blog-cdn/' 49 | } 50 | }] 51 | } 52 | ] 53 | }, 54 | 55 | plugins: [ 56 | // new MiniCssExtractPlugin({ 57 | // filename: 'css/[name].[contenthash:8].css' 58 | // }), 59 | // 清理上一次构建的文件 60 | new CleanWebpackPlugin(), 61 | //生成 manifest 方便定位对应的资源文件 62 | new ManifestPlugin({ 63 | fileName: '../server/asset-manifest.json', 64 | }), 65 | new webpack.DefinePlugin({ 66 | 'process.env': { NODE_ENV: '"production"'}, 67 | '__IS_PROD__': true, 68 | '__SERVER__': false 69 | }), 70 | // new BundleAnalyzerPlugin( 71 | // { 72 | // analyzerMode: 'server', 73 | // analyzerHost: '127.0.0.1', 74 | // analyzerPort: 8888, 75 | // reportFilename: 'report.html', 76 | // defaultSizes: 'parsed', 77 | // openAnalyzer: true, 78 | // generateStatsFile: false, 79 | // statsFilename: 'stats.json', 80 | // statsOptions: null, 81 | // logLevel: 'info' 82 | // } 83 | // ), 84 | new CompressionPlugin({ 85 | filename: "[path].gz[query]", 86 | algorithm: "gzip", 87 | test: /\.js$|\.html$/, 88 | threshold: 10240, 89 | minRatio: 0.8 90 | }) 91 | // new UglifyJsPlugin(), 92 | ], 93 | 94 | optimization: { 95 | minimize: true, 96 | minimizer: [ 97 | new TerserPlugin({ 98 | test: /\.js(\?.*)?$/i, 99 | }), 100 | new OptimizeCSSAssetsPlugin() 101 | ], 102 | splitChunks: { 103 | 104 | cacheGroups: { 105 | libs: { // 抽离第三方库 106 | maxSize: 0, 107 | test: /node_modules/, // 指定是node_modules下的第三方包 108 | chunks: 'initial', 109 | name: 'libs'// 打包后的文件名,任意命名 110 | }, 111 | default: { 112 | maxAsyncRequests: 10000, 113 | chunks: 'async', // 这里只是针对 async 的 chunk,因为 async 的 chunk 都是自动的异步加载的,分多少个都没关系,但是对于 initial 的 chunk,需要手动引入 114 | minChunks: 1, 115 | maxSize: 500000, 116 | } 117 | } 118 | }, 119 | }, 120 | }) -------------------------------------------------------------------------------- /config/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const webpack = require('webpack'); 4 | const nodeExternals = require('webpack-node-externals') 5 | const baseCongig = require('./webpack.config.base'); 6 | 7 | const resolvePath = (pathstr) => path.resolve(__dirname, pathstr); 8 | 9 | process.env.BABEL_ENV = 'node';//设置 babel 的运行的环境变量 10 | 11 | const isProd = process.env.NODE_ENV === 'production'; 12 | 13 | module.exports = merge(baseCongig, { 14 | target: 'node', 15 | mode: process.env.NODE_ENV, 16 | entry: resolvePath('../src/server/app/index.js'), 17 | output: { 18 | filename: 'app.js', 19 | path: resolvePath('../dist/server') 20 | }, 21 | externals: [nodeExternals()], 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': { NODE_ENV: `"${process.env.NODE_ENV}"`}, 25 | '__IS_PROD__':isProd, 26 | '__SERVER__': true 27 | }) 28 | ], 29 | devServer: { 30 | contentBase: path.resolve(__dirname, 'dist/static') 31 | } 32 | }) -------------------------------------------------------------------------------- /deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const util = require('util') 4 | const OSS = require('ali-oss') 5 | 6 | const config = require('./oss.json') 7 | 8 | const promisifyReaddir = util.promisify(fs.readdir) 9 | const promisifyStat = util.promisify(fs.stat) 10 | 11 | const ALIOSSKEY = { 12 | region: config.region, 13 | key: config.key, 14 | secret: config.secret, 15 | bucket: config.bucket 16 | } 17 | 18 | const client = new OSS({ 19 | region: ALIOSSKEY.region, 20 | accessKeyId: ALIOSSKEY.key, 21 | accessKeySecret: ALIOSSKEY.secret, 22 | bucket: ALIOSSKEY.bucket 23 | }) 24 | 25 | const publicPath = path.resolve(__dirname, './dist/static') 26 | const ossPath = 'blog-cdn' 27 | async function run(proPath = '') { 28 | const oldFiles = await client.list({ 29 | marker: 'blog-cdn' 30 | }); 31 | 32 | 33 | if (oldFiles.objects) { 34 | console.log('开始删除旧文件'); 35 | const oldFileLists = oldFiles.objects.map(file => file.name); 36 | await client.deleteMulti(oldFileLists); 37 | console.log('删除旧文件已全部删除'); 38 | } 39 | 40 | const dir = await promisifyReaddir(`${publicPath}${proPath}`); 41 | 42 | for (let i = 0; i < dir.length; i++) { 43 | const stat = await promisifyStat(path.resolve(`${publicPath}${proPath}`, dir[i])) 44 | 45 | if (stat.isFile()) { 46 | const fileStream = fs.createReadStream(path.resolve(`${publicPath}${proPath}`, dir[i])) 47 | console.log(`上传文件: ${ossPath}${proPath}/${dir[i]}`) 48 | const result = await client.putStream(`${ossPath + proPath}/${dir[i]}`, fileStream) 49 | } else if (stat.isDirectory()) { 50 | await run(`${proPath}/${dir[i]}`) 51 | } 52 | } 53 | 54 | console.log('文件已全部上传'); 55 | 56 | } 57 | 58 | run() 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplog-front", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node server/start.js", 8 | "client:watch": "webpack-dev-server --config ./config/webpack.config.client.js --open", 9 | "server:watch": "node ./server/scripts/svr-code-watch", 10 | "wds:watch": "cross-env BABEL_ENV=development node ./server/scripts/wds-start.js", 11 | "build": "cross-env NODE_ENV=production npm run build:client && npm run build:server", 12 | "build:client": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.prod.js", 13 | "build:server": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.server.js", 14 | "deploy": "node deploy.js", 15 | "analyz": "NODE_ENV=production npm_config_report=true npm run build" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@babel/cli": "^7.8.4", 22 | "@babel/core": "^7.8.7", 23 | "@babel/plugin-proposal-class-properties": "^7.8.3", 24 | "@babel/plugin-transform-runtime": "^7.9.0", 25 | "@babel/preset-env": "^7.8.7", 26 | "@babel/preset-react": "^7.8.3", 27 | "@hot-loader/react-dom": "^16.13.0+4.12.20", 28 | "ali-oss": "^6.8.0", 29 | "autoprefixer": "^9.7.5", 30 | "babel-loader": "^8.0.6", 31 | "babel-plugin-import": "^1.13.0", 32 | "babel-plugin-styled-components": "^1.10.7", 33 | "clean-webpack-plugin": "^3.0.0", 34 | "compression-webpack-plugin": "^4.0.0", 35 | "connect-timeout": "^1.9.0", 36 | "cross-env": "^7.0.2", 37 | "css-loader": "^3.4.2", 38 | "express": "^4.17.1", 39 | "file-loader": "^6.0.0", 40 | "hard-source-webpack-plugin": "^0.13.1", 41 | "http-proxy-middleware": "^1.0.3", 42 | "isomorphic-style-loader": "^5.1.0", 43 | "mini-css-extract-plugin": "^0.9.0", 44 | "node-sass": "^4.13.1", 45 | "open": "^7.0.3", 46 | "optimize-css-assets-webpack-plugin": "^5.0.3", 47 | "postcss": "^7.0.27", 48 | "postcss-loader": "^3.0.0", 49 | "raw-loader": "^4.0.1", 50 | "react-hot-loader": "^4.12.20", 51 | "redux-devtools-extension": "^2.13.8", 52 | "sass-loader": "^8.0.2", 53 | "style-loader": "^1.1.3", 54 | "terser-webpack-plugin": "^3.0.1", 55 | "thread-loader": "^2.1.3", 56 | "uglifyjs-webpack-plugin": "^2.2.0", 57 | "webpack": "^4.42.0", 58 | "webpack-aliyun-oss": "^0.2.5", 59 | "webpack-bundle-analyzer": "^3.7.0", 60 | "webpack-cli": "^3.3.11", 61 | "webpack-dev-server": "^3.10.3", 62 | "webpack-manifest-plugin": "^2.2.0", 63 | "webpack-merge": "^4.2.2", 64 | "webpack-node-externals": "^1.7.2" 65 | }, 66 | "dependencies": { 67 | "@babel/polyfill": "^7.8.7", 68 | "@babel/runtime": "^7.9.2", 69 | "@material-ui/core": "^4.9.10", 70 | "@material-ui/icons": "^4.9.1", 71 | "@material-ui/lab": "^4.0.0-alpha.49", 72 | "axios": "^0.19.2", 73 | "chalk": "^3.0.0", 74 | "codemirror": "^5.52.2", 75 | "cookie-parser": "^1.4.5", 76 | "crypto-js": "^4.0.0", 77 | "events": "^3.1.0", 78 | "express": "^4.17.1", 79 | "express-http-proxy": "^1.6.0", 80 | "highlight.js": "^9.18.1", 81 | "hoist-non-react-statics": "^3.3.2", 82 | "js-cookie": "^2.2.1", 83 | "lodash.has": "^4.5.2", 84 | "lodash.set": "^4.3.2", 85 | "marked": "^0.8.2", 86 | "material-ui-popup-state": "^1.5.4", 87 | "moment": "^2.24.0", 88 | "react": "^16.13.0", 89 | "react-codemirror2": "^7.1.0", 90 | "react-dom": "^16.13.0", 91 | "react-helmet": "^5.2.1", 92 | "react-hook-form": "^5.4.1", 93 | "react-infinite-scroller": "^1.2.4", 94 | "react-redux": "^7.2.0", 95 | "react-router": "^5.1.2", 96 | "react-router-dom": "^5.1.2", 97 | "react-transition-group": "^4.3.0", 98 | "redux": "^4.0.5", 99 | "redux-thunk": "^2.3.0", 100 | "styled-components": "^5.1.0" 101 | }, 102 | "browserslist": [ 103 | "defaults", 104 | "not ie < 11", 105 | "last 2 versions", 106 | "> 1%", 107 | "iOS 7", 108 | "last 3 iOS versions" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | }; -------------------------------------------------------------------------------- /server/common/assets.js: -------------------------------------------------------------------------------- 1 | //生产环境中 静态资源的处理 2 | const proConfig = require('src/share/pro-config'); 3 | module.exports = function () { 4 | let devHost = 'http://localhost:'+proConfig.wdsPort; 5 | let jsFiles = ['libs.js', 'main.js']; 6 | let cssFiles = ['main.css']; 7 | const assets = { 8 | js: [], 9 | css: [] 10 | }; 11 | 12 | if (!__IS_PROD__) {//开发环境 13 | // if (!isProd) {//开发环境 14 | assets.js.push(``); 15 | assets.js.push(``); 16 | assets.css.push(``); 17 | } else { 18 | //生产环境 从 asset-manifest.json 读取资源 19 | const map = require('dist/server/asset-manifest.json'); 20 | jsFiles.forEach(item => { 21 | if (map[item]) 22 | assets.js.push(``) 23 | }); 24 | cssFiles.forEach(item => { 25 | if (map[item]) 26 | assets.css.push(``) 27 | }); 28 | } 29 | 30 | return assets; 31 | } -------------------------------------------------------------------------------- /server/scripts/constant.js: -------------------------------------------------------------------------------- 1 | //用于开发环境的构建过程中的常量 2 | module.exports = { 3 | SVRCODECOMPLETED:'SVRCODECOMPLETED',//服务端代码编译完成 4 | } -------------------------------------------------------------------------------- /server/scripts/free-port.js: -------------------------------------------------------------------------------- 1 | module.exports = function (port) { 2 | if (process.platform && process.platform !== 'win32') { 3 | //mac linux等 4 | const args = process.argv.slice(2); 5 | 6 | let portArg = args && args[0]; 7 | if (portArg && portArg.indexOf('--') > 0) { 8 | port = portArg.split('--')[1]; 9 | } 10 | let order = `lsof -i :${port}`; 11 | let exec = require('child_process').exec; 12 | exec(order, (err, stdout, stderr) => { 13 | if (err) { 14 | // return console.log(`查看端口命令出错 ${err}`); 15 | } 16 | stdout.split('\n').filter(line => { 17 | let p = line.trim().split(/\s+/); 18 | let address = p[1]; 19 | if (address != undefined && address != "PID") { 20 | exec('kill -9 ' + address, (err, stdout, stderr) => { 21 | if (err) { 22 | return console.log('释放指定端口失败!!'); 23 | } 24 | console.log('port kill'); 25 | }); 26 | } 27 | }); 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /server/scripts/open-browser.js: -------------------------------------------------------------------------------- 1 | var open = require('open'); 2 | /** 3 | * 用open库自动打开浏览器 4 | * @param {url地址} url 5 | */ 6 | function openBrowser(url) { 7 | //默认谷歌 有需要请自行修改 8 | //open('http://sindresorhus.com', {app: 'firefox'}); 9 | open(url).catch(err => { console.log(err); }); 10 | } 11 | module.exports = openBrowser; -------------------------------------------------------------------------------- /server/scripts/svr-code-watch.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const config = require('../../config/webpack.config.server'); 3 | const constantCode = require('./constant'); 4 | 5 | config.mode = 'development' //设置编译模式 6 | 7 | const compiler = webpack(config); 8 | const watching = compiler.watch({ 9 | aggregateTimeout: 300, 10 | ignored: /node_modules/, 11 | poll: 2000, 12 | 'info-verbosity': 'verbose' 13 | },(err,stats) => { 14 | let json = stats.toJson('minimal'); 15 | 16 | if(json.errors){ 17 | json.errors.forEach(item => { 18 | console.log(item); 19 | }) 20 | } 21 | 22 | if(json.warnings){ 23 | json.warnings.forEach(item => { 24 | console.log(item); 25 | }) 26 | } 27 | try{ 28 | console.log(constantCode.SVRCODECOMPLETED) 29 | } 30 | catch(error){ 31 | console.log(error) 32 | } 33 | }); 34 | 35 | compiler.hooks.done.tap('done',function(data){ 36 | console.log(constantCode.SVRCODECOMPLETED) 37 | console.log('\n svr code done 123' ); //编译完成的时候 可以监听每次的监听 38 | }); 39 | 40 | //收到退出信号 退出自身进程 41 | process.stdin.on('data', function (data) { 42 | if (data.toString() === 'exit') { 43 | process.exit(); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /server/scripts/svr-dev-server.js: -------------------------------------------------------------------------------- 1 | const proConfig = require('../../src/share/pro-config'); 2 | const nodeServerPort = proConfig.nodeServerPort; 3 | 4 | require('./free-port')(nodeServerPort); 5 | try{require('../../dist/server/app')} 6 | catch(error){ 7 | console.log(error) 8 | } 9 | 10 | -------------------------------------------------------------------------------- /server/scripts/wds-start.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const webpack = require('webpack'); 3 | const freePort = require('./free-port'); 4 | const WebpackDevServer = require('webpack-dev-server'); 5 | const open = require('./open-browser'); 6 | const proConfig = require('../../src/share/pro-config'); 7 | const clientConfig = require('../../config/webpack.config.client'); 8 | 9 | //wds 配置 10 | const getWdsConfig = require('./webpack-dev-server.config'); 11 | 12 | let compilationTime=0;//编译次数 13 | 14 | const WDS_PORT = proConfig.wdsPort;//wds 服务端口 15 | 16 | const NODE_SERVER_PORT=proConfig.nodeServerPort;//node 服务端口 17 | const HOST='localhost'; 18 | 19 | //释放wds端口 20 | freePort(proConfig.wdsPort); 21 | 22 | function getWebPackCompiler() { 23 | return webpack(clientConfig) 24 | } 25 | 26 | function createWdsServer(port) { 27 | let compiler = getWebPackCompiler(); 28 | compiler.hooks.done.tap('done',function(data){ 29 | console.log('\n wds server compile done'); //编译完成的时候 30 | }); 31 | if (compilationTime===0){//第一次编译完成的时,自动打开浏览器 32 | open(`http://localhost:${NODE_SERVER_PORT}/`); 33 | } 34 | compilationTime+=1; 35 | return new WebpackDevServer(compiler,getWdsConfig(port,clientConfig.output.publicPath)); 36 | } 37 | 38 | function runWdsServer() { 39 | let devServer = createWdsServer(WDS_PORT); 40 | devServer.listen(WDS_PORT,HOST,err => { 41 | if(err){ 42 | return console.log(err); 43 | } 44 | console.log(chalk.cyan('🚀 Starting the development node server,please wait....\n')) 45 | }) 46 | } 47 | 48 | runWdsServer(); -------------------------------------------------------------------------------- /server/scripts/webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function (port,publicPath) { 4 | return { 5 | quiet: true,//不显示构建日志 6 | port,//wds 服务端口 7 | contentBase: path.resolve(__dirname, '../../dist/static'), 8 | publicPath: publicPath,//必须和 webpack.dev.config 配置一致 9 | hot: true, 10 | progress:true, 11 | open: false, 12 | compress: true, 13 | watchContentBase: true, 14 | watchOptions: { 15 | ignored: /node_modules/, 16 | //当第一个文件更改,会在重新构建前增加延迟。 17 | //这个选项允许 webpack 将这段时间内进行的任何其他更改都聚合到一次重新构建里。以毫秒为单位: 18 | aggregateTimeout: 500, 19 | //指定毫秒为单位进行轮询 20 | poll: 500 21 | }, 22 | headers: { 23 | 'Access-Control-Allow-Origin': '*' 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const constantCode = require('./scripts/constant'); 3 | const chalk = require('chalk'); //为控制台输出的信息增加点色彩 4 | const log = console.log; 5 | const proConfig = require('../src/share/pro-config'); 6 | const nodeServerPort = proConfig.nodeServerPort; 7 | 8 | log(chalk.red('servers starting...')); 9 | 10 | const feCodeWatchProcess = spawn('npm',['run','wds:watch'],{stdio:'inherit',shell: process.platform === 'win32'}); 11 | const svrCodeWatchProcess = spawn('npm',['run','server:watch'],{shell: process.platform === 'win32'}); 12 | 13 | //node 服务 14 | let nodeServerProcess = null; 15 | const startNodeServer = () => { 16 | nodeServerProcess && nodeServerProcess.kill(); 17 | try{ 18 | // nodeServerProcess = spawn('node',['-v'],{shell: process.platform === 'win32'}); 19 | nodeServerProcess = spawn('node',['./server/scripts/svr-dev-server.js'],{shell: process.platform === 'win32'}); 20 | } 21 | catch(error){ 22 | console.error(error) 23 | } 24 | nodeServerProcess.stdout.on('data', print); 25 | } 26 | 27 | function print(data){ 28 | let str = data.toString(); 29 | // console.log(str.indexOf(constantCode.SVRCODECOMPLETED) > -1) 30 | if(str.indexOf(constantCode.SVRCODECOMPLETED) > -1){ 31 | startNodeServer();//重启 node 服务 32 | }else{ 33 | console.log(str); 34 | } 35 | } 36 | 37 | svrCodeWatchProcess.stdout.on('data',print); 38 | 39 | const killChild = () => { 40 | nodeServerProcess && nodeServerProcess.kill(); 41 | feCodeWatchProcess && feCodeWatchProcess.kill(); 42 | svrCodeWatchProcess && svrCodeWatchProcess.kill(); 43 | } 44 | 45 | //主进程关闭退出子进程 46 | process.on('close', (code) => { 47 | console.log('main process close', code); 48 | killChild(); 49 | }); 50 | //主进程关闭退出子进程 51 | process.on('exit', (code) => { 52 | console.log('main process exit', code); 53 | killChild(); 54 | }); 55 | 56 | //非正常退出情况 57 | process.on('SIGINT', function () { 58 | svrCodeWatchProcess.stdin.write('exit', (error) => { 59 | console.log('svr code watcher process exit!'); 60 | }); 61 | killChild(); 62 | }); 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/client/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Layout from './layout'; 3 | import { updateLocation } from 'src/utils/redux/location' 4 | import { Route, Switch, withRouter } from 'react-router-dom'; 5 | import withStyles from 'isomorphic-style-loader/withStyles'; 6 | import css from './layout.scss'; 7 | 8 | const noHeaderList = ['/register', '/login', '/editor/post/:id', '/editor/draft/:id', '/404'] 9 | 10 | function App({ routeList, history }) { 11 | const [count, setCount] = useState(0); 12 | useEffect(() => { 13 | if (count === 0) { 14 | setCount(count + 1); 15 | updateLocation(window.__STORE__)(history.location) 16 | history.listen((args) => { 17 | updateLocation(window.__STORE__)(args) 18 | }) 19 | } 20 | }); 21 | 22 | function hasHeader(item) { 23 | return noHeaderList.indexOf(item.path) === -1; 24 | } 25 | 26 | function noHeader(item) { 27 | return noHeaderList.indexOf(item.path) > -1; 28 | } 29 | 30 | return ( 31 | 32 | { 33 | routeList.filter(noHeader).map(item => { 34 | return 35 | }) 36 | } 37 | 38 | { 39 | routeList.filter(hasHeader).map(item => { 40 | return 41 | }) 42 | } 43 | 44 | 45 | ); 46 | } 47 | 48 | export default withStyles(css)(withRouter(App)); -------------------------------------------------------------------------------- /src/client/app/layout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { hot } from 'react-hot-loader/root'; 4 | import Header from 'src/componentLayout/header'; 5 | 6 | class Index extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | render() { 11 | return ( 12 | [ 13 |
, 14 |
{this.props.children}
15 | ] 16 | ) 17 | } 18 | } 19 | 20 | export default hot(Index); -------------------------------------------------------------------------------- /src/client/app/layout.scss: -------------------------------------------------------------------------------- 1 | @import './reset.scss'; 2 | @import './toast.scss'; 3 | 4 | #root { 5 | padding-top: 64px; 6 | height: 100vh; 7 | overflow: auto; 8 | position: relative; 9 | 10 | .MuiBackdrop-root.async-loading{ 11 | background-color: rgba(255, 255, 255, 0.5); 12 | z-index: 9; 13 | .loading-tips{ 14 | margin-left: 10px; 15 | } 16 | } 17 | 18 | // & > div > div{ 19 | // background: #eee; 20 | // } 21 | } 22 | 23 | a{ 24 | cursor: pointer; 25 | &:hover{ 26 | text-decoration: none!important; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/client/app/reset.scss: -------------------------------------------------------------------------------- 1 | /* Box sizing rules */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* Remove default padding */ 9 | ul[class], 10 | ol[class] { 11 | padding: 0; 12 | } 13 | 14 | /* Remove default margin */ 15 | body, 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | p, 21 | ul[class], 22 | ol[class], 23 | figure, 24 | blockquote, 25 | dl, 26 | dd { 27 | margin: 0; 28 | } 29 | 30 | /* Set core body defaults */ 31 | body { 32 | min-height: 100vh; 33 | scroll-behavior: smooth; 34 | text-rendering: optimizeSpeed; 35 | line-height: 1.5; 36 | } 37 | 38 | /* Remove list styles on ul, ol elements with a class attribute */ 39 | ul[class], 40 | ol[class] { 41 | list-style: none; 42 | } 43 | 44 | /* A elements that don't have a class get default styles */ 45 | a:not([class]) { 46 | text-decoration-skip-ink: auto; 47 | } 48 | 49 | /* Make images easier to work with */ 50 | img { 51 | max-width: 100%; 52 | display: block; 53 | } 54 | 55 | /* Natural flow and rhythm in articles by default */ 56 | article > * + * { 57 | margin-top: 1em; 58 | } 59 | 60 | /* Inherit fonts for inputs and buttons */ 61 | input, 62 | button, 63 | textarea, 64 | select { 65 | font: inherit; 66 | } 67 | 68 | html{ 69 | font-family: -apple-system, "PingFang SC","Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei", "Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif; 70 | // font-family: -apple-system,system-ui,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif; 71 | font-size: 16px; 72 | line-height: 1.2; 73 | text-rendering: optimizeLegibility; 74 | } 75 | 76 | body{ 77 | font-size: 0.875rem; 78 | } 79 | 80 | /* Remove all animations and transitions for people that prefer not to see them */ 81 | @media (prefers-reduced-motion: reduce) { 82 | * { 83 | animation-duration: 0.01ms !important; 84 | animation-iteration-count: 1 !important; 85 | transition-duration: 0.01ms !important; 86 | scroll-behavior: auto !important; 87 | } 88 | } -------------------------------------------------------------------------------- /src/client/app/toast.scss: -------------------------------------------------------------------------------- 1 | 2 | .icon { 3 | width: 1em; 4 | height: 1em; 5 | vertical-align: -0.15em; 6 | fill: currentColor; 7 | overflow: hidden; 8 | } 9 | 10 | .toast-notification { 11 | position: fixed; 12 | top: 20px; 13 | left: 0; 14 | right: 0; 15 | display: flex; 16 | flex-direction: column; 17 | z-index: 1111; 18 | } 19 | 20 | .toast-notice-wrapper.notice-enter { 21 | opacity: 0.01; 22 | transform: scale(0); 23 | } 24 | 25 | .toast-notice-wrapper.notice-enter-active { 26 | opacity: 1; 27 | transform: scale(1); 28 | transition: all 300ms ease-out; 29 | } 30 | 31 | .toast-notice-wrapper.notice-exit { 32 | opacity: 1; 33 | transform: translateY(0); 34 | } 35 | 36 | .toast-notice-wrapper.notice-exit-active { 37 | opacity: 0.01; 38 | transform: translateY(-40%); 39 | transition: all 300ms ease-out; 40 | } 41 | 42 | .toast-notice { 43 | background: #FFFFFF; 44 | padding: 16px 32px; 45 | margin: 8px auto; 46 | box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, .1); 47 | border-radius: 6px; 48 | color: #454545; 49 | font-size: 15px; 50 | display: flex; 51 | align-items: center; 52 | } 53 | 54 | .toast-notice>span { 55 | margin-left: 6px; 56 | line-height: 100%; 57 | } 58 | 59 | .toast-notice>svg { 60 | font-size: 18px; 61 | } 62 | 63 | .toast-notice.info>svg { 64 | color: #1890FF; 65 | } 66 | 67 | .toast-notice.success>svg { 68 | color: #52C41A; 69 | } 70 | 71 | .toast-notice.warning>svg { 72 | color: #FAAD14; 73 | } 74 | 75 | .toast-notice.error>svg { 76 | color: #F74A53; 77 | } 78 | 79 | .toast-notice.loading>svg { 80 | color: #1890FF; 81 | animation: rotating 1s linear infinite; 82 | 83 | } 84 | 85 | @keyframes rotating { 86 | 0% { 87 | transform: rotate(0); 88 | } 89 | 100% { 90 | transform: rotate(360deg); 91 | } 92 | } -------------------------------------------------------------------------------- /src/client/client-entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import StyleContext from 'isomorphic-style-loader/StyleContext'; 5 | import App from './app'; 6 | import { ThemeProvider } from '@material-ui/core/styles'; 7 | import theme from 'src/share/theme'; 8 | import routeList, { matchRoute } from '../router'; 9 | import { decrypt } from '../utils/helper'; 10 | import proConfig from '../share/pro-config'; 11 | import { Provider } from 'react-redux'; 12 | import getStore from '../share/store'; 13 | 14 | let targetRoute = matchRoute(document.location.pathname); 15 | let initialData = JSON.parse(decrypt(JSON.parse(document.getElementById('ssrTextInitData').value).initialData)); 16 | 17 | window.__INITIAL_DATA__ = initialData; 18 | 19 | function renderDom() { 20 | const insertCss = (...styles) => { 21 | const removeCss = styles.map(style => style._insertCss());//客户端执行,插入style 22 | return () => removeCss.forEach(dispose => dispose());//组件卸载时 移除当前的 style 标签 23 | } 24 | 25 | const store = getStore(initialData); 26 | //将store 放入全局,方便后期的使用 27 | window.__STORE__ = store; 28 | function Main() { 29 | React.useEffect(() => { 30 | const jssStyles = document.querySelector('#jss-server-side'); 31 | if (jssStyles) { 32 | jssStyles.parentElement.removeChild(jssStyles); 33 | } 34 | }, []); 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | ReactDom.hydrate(
, document.getElementById('root')); 49 | } 50 | 51 | if (targetRoute) { 52 | if (targetRoute.component[proConfig.asyncComponentKey]) { 53 | targetRoute.component().props.load().then(res => { 54 | renderDom(); 55 | }); 56 | } 57 | } else { 58 | renderDom(); 59 | } 60 | 61 | //开发环境才会开启 62 | if (process.env.NODE_ENV === 'development' && module.hot) { 63 | module.hot.accept(); 64 | } 65 | -------------------------------------------------------------------------------- /src/client/pages/404/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getInitialData: action({ 9 | type: 'notFoundPage.getPage', 10 | action: () => ({ 11 | page:{ 12 | tdk: { 13 | title: '404 Not Found', 14 | keywords: '404 Not Found', 15 | description: '404 Not Found' 16 | } 17 | } 18 | }), 19 | handler: (state, result) => { 20 | return { 21 | ...state, 22 | ...result 23 | } 24 | } 25 | },reducerHandler), 26 | }; 27 | 28 | let initState = { 29 | key: 'notFoundPage', 30 | state: {page:{}} 31 | }; 32 | 33 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/client/pages/404/style.scss: -------------------------------------------------------------------------------- 1 | .notfound-page-wrap{ 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: fixed; 6 | top: 0; 7 | right: 0; 8 | bottom: 0; 9 | left: 0; 10 | & > div{ 11 | display: flex; 12 | } 13 | 14 | 15 | .text-content{ 16 | max-width: 387px; 17 | padding-left: 32px; 18 | 19 | header{ 20 | margin-bottom: 32px; 21 | line-height: 1; 22 | font-size: 120px; 23 | font-weight: 500; 24 | } 25 | 26 | .second-header{ 27 | font-size: 32px; 28 | 29 | .chinese-text{ 30 | font-weight: 500; 31 | } 32 | 33 | } 34 | .tips{ 35 | margin-bottom: 12px; 36 | font-size: 16px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/client/pages/draft-editor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import withInitialData from 'src/componentHOC/with-initial-data'; 6 | import withStyles from 'isomorphic-style-loader/withStyles'; 7 | import composeHOC from 'src/utils/composeHOC'; 8 | import Editor from 'src/componentCommon/editor'; 9 | import css from './style.scss'; 10 | 11 | //组件 12 | class EditorDraft extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | static state() { 18 | return ( 19 | { list: [], page: {} } 20 | ) 21 | } 22 | 23 | static async getInitialProps({ store }) { 24 | return store.dispatch(actions.getInitialData()); 25 | } 26 | 27 | render() { 28 | return ( 29 | 32 | ) 33 | } 34 | } 35 | 36 | const mapStateToProps = state => ({ 37 | initialData: state.editorDraftPage, 38 | userInfo: state.userInfo 39 | }); 40 | 41 | //将获取数据的方法也做为 props传递给组件 42 | const mapDispatchToProps = dispatch => ( 43 | bindActionCreators({ ...actions }, dispatch) 44 | ) 45 | 46 | export default composeHOC( 47 | withStyles(css), 48 | withInitialData, 49 | connect(mapStateToProps, mapDispatchToProps, null) 50 | )(EditorDraft); -------------------------------------------------------------------------------- /src/client/pages/draft-editor/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getInitialData: action({ 9 | type: 'editorDraftPage.getInitialData', 10 | action: async (http,dispatch) => { 11 | const path = __SERVER__ ? global.REQUEST_PATH : location.pathname; 12 | const urlInfo = path.split('/'); 13 | const postId = urlInfo[urlInfo.length-1]; 14 | 15 | const res = postId === 'new' ? {data:{post:{}}} : await dispatch(actions.getPost(postId)); 16 | const page = { 17 | tdk: { 18 | title: '', 19 | keywords: '', 20 | description: '' 21 | } 22 | } 23 | 24 | const post = res.data.post; 25 | 26 | page.tdk.title = '草稿-'+ (post.title || ''); 27 | 28 | return ({ 29 | post, 30 | page 31 | }) 32 | }, 33 | handler: (state, result) => { 34 | console.log({result}) 35 | return { 36 | ...state, 37 | ...result 38 | } 39 | } 40 | },reducerHandler), 41 | 42 | getPost: action({ 43 | type: 'editorDraftPage.getPost', 44 | action: (id,http) => { 45 | return http.get(`/api/getEditPost/${id}`) 46 | }, 47 | handler: (state, result) => { 48 | return { 49 | ...state 50 | } 51 | } 52 | },reducerHandler), 53 | 54 | createPost: action({ 55 | type: 'editorDraftPage.createPost', 56 | action: (params,http) => { 57 | return http.post('/api/createPost',params) 58 | }, 59 | handler: (state, result) => { 60 | return { 61 | ...state 62 | } 63 | } 64 | },reducerHandler), 65 | 66 | updatePost: action({ 67 | type: 'editorDraftPage.updatePost', 68 | action: (params,http) => { 69 | const { id } = params; 70 | delete params.id; 71 | return http.put(`/api/posts/${id}`,params) 72 | }, 73 | handler: (state, result) => { 74 | return { 75 | ...state 76 | } 77 | } 78 | },reducerHandler), 79 | 80 | publishPost: action({ 81 | type: 'editorDraftPage.publishPost', 82 | action: (params,http) => { 83 | const { id } = params; 84 | delete params.id; 85 | return http.post(`/api/publishPost/${id}`,params) 86 | }, 87 | handler: (state, result) => { 88 | return { 89 | ...state 90 | } 91 | } 92 | },reducerHandler), 93 | 94 | uploadImage: action({ 95 | type: 'editorDraftPage.uploadImage', 96 | action: (params,http) => { 97 | return http.post('/api/upload/post',params) 98 | }, 99 | handler: (state, result) => { 100 | return { 101 | ...state 102 | } 103 | } 104 | },reducerHandler), 105 | 106 | uploadHeaderImage: action({ 107 | type: 'editorDraftPage.uploadHeaderImage', 108 | action: (params,http) => { 109 | return http.post('/api/upload/header',params) 110 | }, 111 | handler: (state, result) => { 112 | return { 113 | ...state 114 | } 115 | } 116 | },reducerHandler), 117 | }; 118 | 119 | let initState = { 120 | key: 'editorDraftPage', 121 | state: {post:{},page:{}} 122 | }; 123 | 124 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/client/pages/draft-editor/style.scss: -------------------------------------------------------------------------------- 1 | .user-list{ 2 | li{ 3 | margin: 10px 0; 4 | } 5 | } -------------------------------------------------------------------------------- /src/client/pages/draft-list/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import withInitialData from 'src/componentHOC/with-initial-data'; 6 | import withStyles from 'isomorphic-style-loader/withStyles'; 7 | import { openInNewTab } from 'src/utils/helper'; 8 | import composeHOC from 'src/utils/composeHOC'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 11 | import Menu from '@material-ui/core/Menu'; 12 | import MenuItem from '@material-ui/core/MenuItem'; 13 | import Comfirm from 'src/componentCommon/confirm'; 14 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'; 15 | import Empty from 'src/componentCommon/empty'; 16 | import moment from 'moment'; 17 | import css from './style.scss'; 18 | 19 | //组件 20 | class DraftList extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | loading: false 25 | } 26 | } 27 | 28 | static state() { 29 | return ( 30 | { 31 | postData: { 32 | datas: [], 33 | page: {} 34 | }, 35 | page: {} 36 | } 37 | ) 38 | } 39 | 40 | static async getInitialProps({ store }) { 41 | return store.dispatch(actions.getInitialData()); 42 | } 43 | 44 | editPost = (popupState, id) => { 45 | popupState && popupState.close(); 46 | openInNewTab('/editor/draft/' + id, true); 47 | } 48 | 49 | deletePost = async (popupState, id) => { 50 | popupState.close(); 51 | await this.props.deletePost(id); 52 | this.props.updateListt(id); 53 | } 54 | 55 | getMore = () => { 56 | const { draftData } = this.props.initialData; 57 | const wrapBottom = this.scrolWrap.getBoundingClientRect().bottom; 58 | const contentBottom = this.scrolContent.getBoundingClientRect().bottom; 59 | 60 | if (wrapBottom >= contentBottom) { 61 | if (draftData.page.pageCount === draftData.page.currentPage) return; 62 | if (this.state.loading) return; 63 | this.setState({ loading: true }) 64 | this.props.getMoreDrafts({ pageNO: draftData.page.nextPage }).then(res => { 65 | this.setState({ loading: false }) 66 | }); 67 | } 68 | } 69 | 70 | render() { 71 | const { draftData } = this.props.initialData; 72 | const { loading } = this.state; 73 | return ( 74 |
this.scrolWrap = ref} 77 | > 78 | {!draftData || !draftData.datas || !draftData.datas.length ? 79 | 80 | : 81 |
    this.scrolContent = ref}> 82 | {draftData.datas.map((item, idx) => ( 83 |
  • 84 |
    {item.title || '无标题'}
    85 |
    86 | {moment(item.createdAt).format("YYYY-MM-DD HH:mm")} 87 | 88 | {(popupState) => ( 89 | 90 | 91 | 92 | 93 | 94 | 100 | 编辑 101 | 102 | 删除 103 | 104 | 105 | 106 | )} 107 | 108 |
    109 |
  • 110 | ))} 111 | {loading &&
  • 加载中...
  • } 112 |
113 | } 114 |
115 | ) 116 | } 117 | } 118 | 119 | const mapStateToProps = state => ({ 120 | initialData: state.draftPage, 121 | }); 122 | 123 | //将获取数据的方法也做为 props传递给组件 124 | const mapDispatchToProps = dispatch => ( 125 | bindActionCreators({ ...actions }, dispatch) 126 | ) 127 | 128 | export default composeHOC( 129 | withStyles(css), 130 | withInitialData, 131 | connect(mapStateToProps, mapDispatchToProps, null) 132 | )(DraftList); -------------------------------------------------------------------------------- /src/client/pages/draft-list/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getDraftLists: action({ 9 | type: 'draftPage.getPosts', 10 | action: (http,dispatch,getstate) => { 11 | return http.get('/api/getDraftLists') 12 | }, 13 | handler: (state, result) => { 14 | return { 15 | ...state 16 | } 17 | } 18 | },reducerHandler), 19 | 20 | getMoreDrafts: action({ 21 | type: 'draftPage.getMoreDrafts', 22 | action: (params,http,dispatch,getstate) => { 23 | return http.get('/api/getDraftLists?pageNo='+params.pageNO) 24 | }, 25 | handler: (state, result) => { 26 | const newDraftData = JSON.parse(JSON.stringify(state.draftData)); 27 | newDraftData.datas = [...newDraftData.datas, ...result.data.datas]; 28 | newDraftData.page = result.data.page; 29 | return { 30 | ...state, 31 | draftData:newDraftData 32 | } 33 | } 34 | },reducerHandler), 35 | 36 | getPage: action({ 37 | type: 'draftPage.getPage', 38 | action: () => ({ 39 | tdk: { 40 | title: '草稿', 41 | keywords: 'draft', 42 | description: '极简博客平台' 43 | } 44 | }), 45 | handler: (state, result) => { 46 | return { 47 | ...state 48 | } 49 | } 50 | },reducerHandler), 51 | 52 | deletePost: action({ 53 | type: 'draftPage.deletePost', 54 | action: (id,http) => { 55 | return http.delete(`/api/posts/${id}`) 56 | }, 57 | handler: (state, result) => { 58 | return { 59 | ...state, 60 | } 61 | } 62 | },reducerHandler), 63 | 64 | updateListt: action({ 65 | type: 'draftPage.updateListt', 66 | action: id => id, 67 | handler: (state, id) => { 68 | return { 69 | ...state, 70 | draftData: {...state.draftData,datas: state.draftData.datas.filter(data => data._id !== id)} 71 | } 72 | } 73 | },reducerHandler), 74 | 75 | getInitialData: action({ 76 | type: 'draftPage.getInitialData', 77 | action: async (http,dispatch) => { 78 | const res = await dispatch(actions.getDraftLists()); 79 | const page = await dispatch(actions.getPage()); 80 | return ({ 81 | draftData: res.data, 82 | page 83 | }) 84 | }, 85 | handler: (state, result) => { 86 | return { 87 | ...state, 88 | ...result 89 | } 90 | } 91 | },reducerHandler), 92 | }; 93 | 94 | let initState = { 95 | key: 'draftPage', 96 | state: {draftData:{datas:[],page:{}},page:{}} 97 | }; 98 | 99 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/client/pages/draft-list/style.scss: -------------------------------------------------------------------------------- 1 | .draft-page { 2 | height: calc(100vh - 64px); 3 | overflow-y: auto; 4 | background: #f8f8f8; 5 | 6 | .draft-list-block { 7 | max-width: 920px; 8 | margin: 20px auto 0; 9 | padding: 0 12px; 10 | background: #fff; 11 | box-shadow: 7px 12px 18px #00000008; 12 | 13 | .draft-list-item{ 14 | padding: 24px 0 14px; 15 | border-top: 1px solid rgba(0,0,0,.05); 16 | 17 | &:first-child{ 18 | border: none; 19 | } 20 | 21 | header{ 22 | font-size: 16px; 23 | font-weight: 700; 24 | line-height: 2; 25 | text-decoration: none; 26 | color: #333; 27 | cursor: pointer; 28 | } 29 | 30 | .info-box{ 31 | display: flex; 32 | align-items: center; 33 | margin: 3.6px 0; 34 | font-size: 14.4px; 35 | color: #909090; 36 | cursor: default; 37 | 38 | .MuiIconButton-root{ 39 | padding: 8px; 40 | margin-left: 12px; 41 | } 42 | 43 | .MuiSvgIcon-root{ 44 | width: 0.7em; 45 | height: 0.7em; 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/client/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import { openInNewTab } from 'src/utils/helper'; 6 | import withInitialData from 'src/componentHOC/with-initial-data'; 7 | import withStyles from 'isomorphic-style-loader/withStyles'; 8 | import composeHOC from 'src/utils/composeHOC'; 9 | import PostCard from 'src/componentCommon/post-card/'; 10 | import css from './style.scss'; 11 | 12 | //组件 13 | class List extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | loading: false 18 | } 19 | } 20 | 21 | static state() { 22 | return ( 23 | { 24 | postData: { 25 | datas: [], 26 | page: {} 27 | }, 28 | page: {} 29 | } 30 | ) 31 | } 32 | 33 | static async getInitialProps({ store }) { 34 | return store.dispatch(actions.getInitialData()); 35 | } 36 | 37 | toDetail = id => { 38 | openInNewTab('/post/' + id,true); 39 | } 40 | 41 | getMore = () => { 42 | const { postData } = this.props.initialData; 43 | const wrapBottom = this.scrolWrap.getBoundingClientRect().bottom; 44 | const contentBottom = this.scrolContent.getBoundingClientRect().bottom; 45 | 46 | if(wrapBottom === contentBottom){ 47 | if(postData.page.pageCount === postData.page.currentPage) return; 48 | if(this.state.loading) return; 49 | this.setState({loading:true}) 50 | this.props.getMorePosts({pageNO: postData.page.nextPage}).then(res => { 51 | this.setState({loading:false}) 52 | }); 53 | } 54 | } 55 | 56 | render() { 57 | const { postData } = this.props.initialData; 58 | const { loading } = this.state; 59 | return ( 60 |
this.scrolWrap = ref} 63 | > 64 | {!postData || !postData.datas || !postData.datas.length ? 65 | '暂无数据' 66 | : 67 |
    this.scrolContent = ref}> 68 | {postData.datas.map((item, idx) => )} 69 | {loading &&
  • 加载中...
  • } 70 |
71 | } 72 |
73 | ) 74 | } 75 | } 76 | 77 | const mapStateToProps = state => ({ 78 | initialData: state.listPage, 79 | }); 80 | 81 | //将获取数据的方法也做为 props传递给组件 82 | const mapDispatchToProps = dispatch => ( 83 | bindActionCreators({ ...actions }, dispatch) 84 | ) 85 | 86 | export default composeHOC( 87 | withStyles(css), 88 | withInitialData, 89 | connect(mapStateToProps, mapDispatchToProps, null) 90 | )(List); -------------------------------------------------------------------------------- /src/client/pages/home/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getPosts: action({ 9 | type: 'listPage.getPosts', 10 | action: (http,dispatch,getstate) => { 11 | return http.get('/api/posts') 12 | }, 13 | handler: (state, result) => { 14 | return { 15 | ...state 16 | } 17 | } 18 | },reducerHandler), 19 | 20 | getMorePosts: action({ 21 | type: 'listPage.getMorePosts', 22 | action: (params,http,dispatch,getstate) => { 23 | return http.get('/api/posts?pageNo='+params.pageNO) 24 | }, 25 | handler: (state, result) => { 26 | const newPostData = JSON.parse(JSON.stringify(state.postData)); 27 | newPostData.datas = [...newPostData.datas, ...result.data.datas]; 28 | newPostData.page = result.data.page; 29 | return { 30 | ...state, 31 | postData:newPostData 32 | } 33 | } 34 | },reducerHandler), 35 | 36 | getPage: action({ 37 | type: 'listPage.getPage', 38 | action: () => ({ 39 | tdk: { 40 | title: '首页', 41 | keywords: 'simple blog', 42 | description: '极简博客平台' 43 | } 44 | }), 45 | handler: (state, result) => { 46 | return { 47 | ...state 48 | } 49 | } 50 | },reducerHandler), 51 | 52 | getInitialData: action({ 53 | type: 'listPage.getInitialData', 54 | action: async (http,dispatch) => { 55 | const res = await dispatch(actions.getPosts()); 56 | const page = await dispatch(actions.getPage()); 57 | return ({ 58 | postData: res.data, 59 | page 60 | }) 61 | }, 62 | handler: (state, result) => { 63 | return { 64 | ...state, 65 | ...result 66 | } 67 | } 68 | },reducerHandler), 69 | 70 | initCompoentReduxFromServer: action({ 71 | type: 'listPage.initCompoentReduxFromServer', 72 | action: async (params) => { 73 | console.log(params); 74 | 75 | return 76 | }, 77 | handler: (state, result) => { 78 | return { 79 | ...state, 80 | // ...result 81 | } 82 | } 83 | },reducerHandler), 84 | }; 85 | 86 | let initState = { 87 | key: 'listPage', 88 | state: {postData:{datas:[],page:{}},page:{}} 89 | }; 90 | 91 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/client/pages/home/style.scss: -------------------------------------------------------------------------------- 1 | .home-page { 2 | height: calc(100vh - 64px); 3 | overflow-y: auto; 4 | background: #f8f8f8; 5 | 6 | 7 | .user-list { 8 | max-width: 920px; 9 | margin: 20px auto 0; 10 | background: #fff; 11 | box-shadow: 7px 12px 18px #00000008; 12 | } 13 | } 14 | 15 | .post-preview-card { 16 | position: relative; 17 | display: flex; 18 | width: 100%; 19 | padding: 12px 18px; 20 | 21 | justify-content: space-between; 22 | cursor: pointer; 23 | // &::before{ 24 | // display: block; 25 | // height: 0px; 26 | // border:1px 27 | 28 | // } 29 | 30 | .post-info{ 31 | display: flex; 32 | align-items: baseline; 33 | white-space: nowrap; 34 | font-size: 14px; 35 | color: #b2bac2; 36 | } 37 | 38 | .post-title { 39 | margin-bottom: 0; 40 | font-weight: 400; 41 | text-shadow: 0 0 1px rgba(0, 0, 0, .15); 42 | } 43 | 44 | .active-info{ 45 | display: flex; 46 | padding-top: 8px; 47 | 48 | & > span{ 49 | display: flex; 50 | align-items: center; 51 | padding: 0 6px; 52 | color: #b2bac2; 53 | border: 1px solid #eee; 54 | 55 | &:first-child{ 56 | border-right: none; 57 | } 58 | 59 | .MuiSvgIcon-root{ 60 | width: 0.5em; 61 | height: 0.5em; 62 | margin-right: 4px; 63 | } 64 | } 65 | } 66 | 67 | .post-header-bg{ 68 | img, .no-bg{ 69 | width: 80px; 70 | height: 80px; 71 | } 72 | } 73 | 74 | hr { 75 | position: absolute; 76 | margin: 0; 77 | margin-top: 10px; 78 | border: 0; 79 | border-top: 1px solid #eee; 80 | height: 0; 81 | bottom: 0; 82 | 83 | left: 18px; 84 | right: 18px; 85 | } 86 | } 87 | 88 | @media only screen and (min-width: 768px) { 89 | .post-preview-card { 90 | .post-title { 91 | font-size: 18px; 92 | line-height: 1.4; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/client/pages/login/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import withStyles from 'isomorphic-style-loader/withStyles'; 4 | import { withRouter } from "react-router-dom"; 5 | import { Grid, Button, Divider, Link } from '@material-ui/core'; 6 | import withInitialData from 'src/componentHOC/with-initial-data'; 7 | import composeHOC from 'src/utils/composeHOC'; 8 | import createForm, { InputFormItem } from 'src/componentHOC/form-create'; 9 | import { actions } from './redux'; 10 | import { actions as getUserInfo } from 'src/componentLayout/redux'; 11 | import css from './style.scss'; 12 | 13 | class Login extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | static state() { 19 | return ({ 20 | page: {} 21 | }) 22 | } 23 | static async getInitialProps({ store }) { 24 | return store.dispatch(actions.getInitialData()); 25 | } 26 | 27 | handleSubmit = () => { 28 | const { data, valid } = this.props.handleSubmit(); 29 | if (!valid) return; 30 | this.props.fetchLogin(data).then(res => { 31 | if (res && res.success) { 32 | this.props.getUserInfo(); 33 | this.props.history.push('/') 34 | } 35 | }); 36 | } 37 | 38 | render() { 39 | return ( 40 |
41 |
42 | 43 |
44 |
登录
45 | 46 |
47 |
48 | 49 | 50 | 53 |
还没账户? this.props.history.push('/register')}>去注册
54 | 55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | const mapStateToProps = state => ({ 62 | initialData: state.loginPage, 63 | }); 64 | 65 | export default composeHOC( 66 | createForm, 67 | withRouter, 68 | withStyles(css), 69 | withInitialData, 70 | connect(mapStateToProps, { ...actions, getUserInfo: getUserInfo.getInitialData }, null) 71 | )(Login); -------------------------------------------------------------------------------- /src/client/pages/login/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | import Cookie from 'js-cookie'; 4 | 5 | const { action, createReducer, injectReducer } = enRedux.default; 6 | const reducerHandler = createReducer(); 7 | 8 | export const actions = { 9 | getInitialData: action({ 10 | type: 'loginPage.getPage', 11 | action: () => ({ 12 | page:{ 13 | tdk: { 14 | title: '登录', 15 | keywords: 'simplog', 16 | description: 'simplog 简约博客' 17 | } 18 | } 19 | }), 20 | handler: (state, result) => { 21 | return { 22 | ...state, 23 | ...result 24 | } 25 | } 26 | },reducerHandler), 27 | 28 | fetchLogin: action({ 29 | type: 'loginPage.fetchLogin', 30 | action: (params,http) => { 31 | return http.post('/api/user/login',params) 32 | }, 33 | handler: (state, result) => { 34 | const { success } = result; 35 | if(success) { 36 | Cookie.set('token',result.data.token); 37 | } 38 | return { 39 | ...state 40 | } 41 | } 42 | },reducerHandler), 43 | }; 44 | 45 | let initState = { 46 | key: 'loginPage', 47 | state: {page:{}} 48 | }; 49 | 50 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/client/pages/login/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .register-page-wrap{ 3 | display: flex; 4 | position: fixed; 5 | width: 100%; 6 | top:0; 7 | bottom: 0; 8 | // height: calc(100vh - 64px); 9 | z-index: 1; 10 | background: #e7f2f3; 11 | 12 | img.register-logo{ 13 | position: absolute; 14 | top: -100px; 15 | left: 0; 16 | width: 100%; 17 | } 18 | .form-wrap{ 19 | position: relative; 20 | margin: auto; 21 | max-width: 480px; 22 | min-width: 320px; 23 | padding: 40px 20px; 24 | background: #fff; 25 | box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12); 26 | 27 | } 28 | 29 | .form-content{ 30 | > div{ 31 | margin-bottom: 16px; 32 | } 33 | 34 | > div:last-child{ 35 | margin-bottom: 0; 36 | } 37 | > button{ 38 | margin-top: 12px; 39 | } 40 | } 41 | 42 | .get-verify-code-wrap{ 43 | text-align: right; 44 | > button { 45 | margin-top: 12px; 46 | } 47 | } 48 | 49 | .op-title{ 50 | padding-bottom: 16px; 51 | header{ 52 | font-size: 24px; 53 | padding-bottom: 12px; 54 | } 55 | } 56 | 57 | .switch-op{ 58 | padding-top: 20px; 59 | text-align: center; 60 | } 61 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/add-comment-input.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import { withRouter } from 'react-router-dom'; 4 | import withStyles from 'isomorphic-style-loader/withStyles'; 5 | import PersonIcon from '@material-ui/icons/Person'; 6 | import CommentInput from './comment-input'; 7 | import css from './comments.scss'; 8 | 9 | class AddCommentInput extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {} 13 | } 14 | 15 | getAvatar = () => { 16 | if(!this.props.currentUser.avatar) return 'https://simplog.oss-cn-beijing.aliyuncs.com/system/unlogin-avatar.svg'; 17 | return this.props.currentUser.avatar; 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |
24 | 25 | 31 |
32 |
33 | ) 34 | } 35 | } 36 | 37 | export default withRouter(withStyles(css)(AddCommentInput)) -------------------------------------------------------------------------------- /src/client/pages/post-detail/comment-input.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import Button from '@material-ui/core/Button'; 4 | import Collapse from '@material-ui/core/Collapse'; 5 | import Toast from 'src/componentCommon/toast'; 6 | 7 | export default ({createComment, getComment,id, currentUser}) => { 8 | const [value, setValue] = useState(''); 9 | const [visible, setVisible] = useState(false); 10 | 11 | function handlerTextFocus (){ 12 | setVisible(true) 13 | } 14 | 15 | async function submitComment(){ 16 | if(!currentUser._id){ 17 | Toast.error('还没登录!') 18 | return; 19 | } 20 | const createRes = await createComment({id,body:value}); 21 | if(createRes && createRes.success){ 22 | Toast.success(createRes.data.message); 23 | setValue('') 24 | await getComment(id); 25 | } 26 | } 27 | 28 | 29 | return ( 30 |
31 | setValue(e.target.value)} 40 | onFocus={handlerTextFocus} 41 | /> 42 | 43 |
44 | 45 | 46 |
47 |
48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/comments-lists.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState } from 'react'; 2 | import moment from 'moment'; 3 | import withStyles from 'isomorphic-style-loader/withStyles'; 4 | import { Link, Avatar, Button } from '@material-ui/core'; 5 | import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; 6 | import DeleteIcon from '@material-ui/icons/Delete'; 7 | import ReplyInput from './reply-input'; 8 | import Comfirm from 'src/componentCommon/confirm'; 9 | import Toast from 'src/componentCommon/toast' 10 | import css from './comments-lists.scss'; 11 | 12 | const Commentitem = ({ setVisible, setPid, parentId, _id, postAuthor, parentAuthor, fromUser, replyToUser, currentUser, isAuthor, body, createdAt, setReply, deleteComment, getComment, match }) => { 13 | function handlerReply() { 14 | if (!currentUser._id) { 15 | Toast.error('请先登录!'); 16 | return; 17 | } 18 | setPid(parentId || _id); 19 | setVisible(true); 20 | // setReply(parentId ? { id: fromUser._id, name: fromUser.username } : {}); 21 | setReply({ id: fromUser._id, name: fromUser.username }); 22 | } 23 | 24 | return ( 25 |
26 | 27 |
28 |
{fromUser.username} {isAuthor ? 作者 : ''} {replyToUser && replyToUser.username ? 回复 {replyToUser.username}: : ''}
29 |
{moment(createdAt).format("YYYY.MM.DD HH:mm:ss")}
30 |
{body}
31 |
32 | 回复 33 | { 34 | currentUser._id && (currentUser._id === fromUser._id || currentUser._id === postAuthor || currentUser._id === parentAuthor) 35 | && 36 | getComment(match.params.id)}> 37 | 删除 38 | 39 | } 40 |
41 |
42 |
43 | ) 44 | } 45 | 46 | const RenderComments = ({ comment, currentUser, postAuthor, createComment, deleteComment, getComment, match }) => { 47 | const [visible, setVisible] = useState(false); 48 | const [pid, setPid] = useState(''); 49 | const [reply, setReply] = useState({}); 50 | 51 | return ( 52 |
53 | 64 | { 65 | comment.children.length 66 | ? 67 | comment.children.map(item => ( 68 | 81 | )) 82 | : 83 | '' 84 | } 85 | 86 |
87 | 96 |
97 |
98 | ) 99 | } 100 | 101 | class CommentsLists extends Component { 102 | constructor(props) { 103 | super(props); 104 | this.state = {}; 105 | } 106 | render() { 107 | const { comments } = this.props; 108 | return ( 109 |
110 |

全部评论

111 | { 112 | comments.map(comment => ) 113 | } 114 |
115 | ) 116 | } 117 | } 118 | 119 | export default withStyles(css)(CommentsLists); -------------------------------------------------------------------------------- /src/client/pages/post-detail/comments-lists.scss: -------------------------------------------------------------------------------- 1 | .comments-lists-wrap{ 2 | padding: 20px; 3 | // margin-top: 20px; 4 | max-width: 920px; 5 | margin: 20px auto 0; 6 | border: none; 7 | box-shadow: 7px 12px 18px rgba(0, 0, 0, .03); 8 | background: #fff; 9 | 10 | .comments-lists-header{ 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | margin-bottom: 16px; 15 | padding-left: 12px; 16 | border-left: 4px solid #07a2b9; 17 | font-size: 18px; 18 | font-weight: 500; 19 | height: 20px; 20 | line-height: 20px; 21 | } 22 | 23 | .comment-cell{ 24 | .comment-cell-item:not(:first-child){ 25 | margin-left: 62px; 26 | } 27 | } 28 | 29 | .comment-cell-item{ 30 | display: flex; 31 | margin-left: 10px; 32 | margin-bottom: 20px; 33 | padding-bottom: 16px; 34 | border-bottom: 1px solid #eee; 35 | 36 | .comment-content{ 37 | width: 100%; 38 | padding-left: 12px; 39 | 40 | .auth-info{ 41 | font-size: 15px; 42 | font-weight: 500; 43 | 44 | & > span { 45 | padding: 0 3px; 46 | color: #888; 47 | font-size: 12px; 48 | 49 | &.is-author{ 50 | color: #07a2b9; 51 | border: 1px solid; 52 | border-radius: 4px; 53 | font-weight: 400; 54 | } 55 | } 56 | } 57 | 58 | .comment-date{ 59 | margin-top: 2px; 60 | font-size: 12px; 61 | color: #969696; 62 | } 63 | 64 | .comment-body{ 65 | margin-top: 10px; 66 | font-size: 16px; 67 | line-height: 1.5; 68 | word-break: break-word; 69 | } 70 | 71 | .comment-op-wrap{ 72 | display: flex; 73 | justify-content: space-between; 74 | padding-top: 10px; 75 | 76 | .delete-op{ 77 | display: none; 78 | } 79 | 80 | &:hover{ 81 | .delete-op{ 82 | display: flex; 83 | } 84 | } 85 | 86 | span { 87 | cursor: pointer; 88 | margin-right: 16px; 89 | color: #b0b0b0; 90 | 91 | display: flex; 92 | align-items: center; 93 | 94 | & > .MuiSvgIcon-root { 95 | margin-right: 4px; 96 | width: 0.65em; 97 | height: 0.65em; 98 | } 99 | 100 | &:hover{ 101 | color: #07a2b9; 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/comments.scss: -------------------------------------------------------------------------------- 1 | .post-comments { 2 | max-width: 920px; 3 | margin: 0 auto; 4 | border: none; 5 | box-shadow: 7px 12px 18px rgba(0, 0, 0, .03); 6 | } 7 | 8 | .post-comments { 9 | background: #fff; 10 | padding: 20px; 11 | margin-top: 20px; 12 | 13 | .main-comment-input-wrap { 14 | display: flex; 15 | } 16 | } 17 | 18 | .add-comments-content{ 19 | width: 100%; 20 | 21 | .comment-btns-wrap{ 22 | text-align: right; 23 | padding: 10px 0; 24 | 25 | button{ 26 | margin-left: 6px; 27 | } 28 | } 29 | } 30 | 31 | .reply-input-wrap{ 32 | margin-left: 62px; 33 | } 34 | 35 | .MuiAvatar-root { 36 | color: #fff; 37 | background-color: #bdbdbd; 38 | margin-right: 10px; 39 | } 40 | 41 | .comment-input { 42 | background: #f9fbfb; 43 | 44 | .MuiOutlinedInput-root { 45 | &.Mui-focused{ 46 | .MuiOutlinedInput-notchedOutline { 47 | border-width: 1px; 48 | } 49 | } 50 | 51 | border-color: rgba(0, 0, 0, 0.17); 52 | font-size: 12px; 53 | } 54 | 55 | .MuiInputBase-input:focus { 56 | outline: none; 57 | outline-color: transparent; 58 | } 59 | } 60 | 61 | .reply-to-wrap{ 62 | padding-bottom: 6px; 63 | color: #888; 64 | font-size: 14px; 65 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/post-op-bars.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import Menu from '@material-ui/core/Menu'; 4 | import MenuItem from '@material-ui/core/MenuItem'; 5 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 6 | import CheckIcon from '@material-ui/icons/Check'; 7 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'; 8 | 9 | export default ({handlerCollection, hasCollectioned}) => { 10 | 11 | return ( 12 | 13 | {(popupState) => ( 14 | 15 | 16 | 17 | 18 | 24 | {hasCollectioned ? 已收藏 : 收藏文章} 25 | {/* Death */} 26 | 27 | 28 | )} 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | const { action, createReducer, injectReducer } = enRedux.default; 4 | 5 | const reducerHandler = createReducer(); 6 | export const actions = { 7 | getInitialData: action({ 8 | type: 'editPostPage.getInitialData', 9 | action: async (http,dispatch) => { 10 | const path = __SERVER__ ? global.REQUEST_PATH : location.pathname; 11 | const urlInfo = path.split('/'); 12 | const postId = urlInfo[urlInfo.length-1]; 13 | 14 | const resPost = await dispatch(actions.getPost(postId)); 15 | const resComment = await dispatch(actions.getComment(postId)); 16 | const resLikers = await dispatch(actions.getPostLikers(postId)); 17 | const resCollectioned = await dispatch(actions.getCollectioned(postId)); 18 | const resFollowedauthor = await dispatch(actions.hasFollowedAuthor(postId)); 19 | const page = { 20 | tdk: { 21 | title: '', 22 | keywords: '', 23 | description: '' 24 | } 25 | } 26 | 27 | const post = resPost.data.post; 28 | const comments = resComment.data.comments; 29 | const likers = resLikers.data.likers; 30 | const hasCollectioned = resCollectioned.data.hasCollectioned; 31 | const hasFollowedAuthor = resFollowedauthor.data.hasFollowed; 32 | 33 | page.tdk.title = post.title; 34 | page.tdk.keywords = post.tags .join(','); 35 | page.tdk.description = post.title; 36 | 37 | return ({ 38 | post, 39 | comments, 40 | likers, 41 | hasCollectioned, 42 | hasFollowedAuthor, 43 | page 44 | }) 45 | }, 46 | handler: (state, result) => { 47 | return { 48 | ...state, 49 | ...result 50 | } 51 | } 52 | },reducerHandler), 53 | 54 | getPost: action({ 55 | type: 'postDetailPage.getPost', 56 | action: (id,http) => { 57 | return http.get(`/api/posts/${id}`) 58 | }, 59 | handler: (state, result) => { 60 | return { 61 | ...state 62 | } 63 | } 64 | },reducerHandler), 65 | 66 | getComment: action({ 67 | type: 'postDetailPage.getComments', 68 | action: (id,http) => { 69 | return http.get(`/api/posts/${id}/comment`) 70 | }, 71 | handler: (state, result) => { 72 | const comments = result.data.comments; 73 | return { 74 | ...state, 75 | comments 76 | } 77 | } 78 | },reducerHandler), 79 | 80 | createComment: action({ 81 | type: 'postDetailPage.createComment', 82 | action: (params,http) => { 83 | let id = params.id; 84 | delete params.id; 85 | return http.post(`/api/posts/${id}/comment`,params) 86 | }, 87 | handler: (state, result) => { 88 | 89 | return { 90 | ...state, 91 | } 92 | } 93 | },reducerHandler), 94 | 95 | deleteComment: action({ 96 | type: 'postDetailPage.deleteComment', 97 | action: (commentId,http) => { 98 | return http.post(`/api/deleteComment/${commentId}`) 99 | }, 100 | handler: (state, result) => { 101 | 102 | return { 103 | ...state, 104 | } 105 | } 106 | },reducerHandler), 107 | 108 | likePost: action({ 109 | type: 'postDetailPage.likePost', 110 | action: (postId,http) => { 111 | return http.post(`/api/likePost/${postId}`) 112 | }, 113 | handler: (state, result) => { 114 | 115 | return { 116 | ...state, 117 | } 118 | } 119 | },reducerHandler), 120 | 121 | getPostLikers: action({ 122 | type: 'postDetailPage.getPostLikers', 123 | action: (postId,http) => { 124 | return http.get(`/api/getPostLikers/${postId}`) 125 | }, 126 | handler: (state, result) => { 127 | return { 128 | ...state, 129 | ...result.data 130 | } 131 | } 132 | },reducerHandler), 133 | 134 | collectionPost: action({ 135 | type: 'postDetailPage.collectionPost', 136 | action: (postId,http) => { 137 | return http.post(`/api/collectionPost/${postId}`) 138 | }, 139 | handler: (state, result) => { 140 | return { 141 | ...state, 142 | } 143 | } 144 | },reducerHandler), 145 | 146 | getCollectioned: action({ 147 | type: 'postDetailPage.getCollectioned', 148 | action: (postId,http) => { 149 | return http.get(`/api/hasCollectioned/${postId}`) 150 | }, 151 | handler: (state, result) => { 152 | return { 153 | ...state, 154 | ...result.data 155 | } 156 | } 157 | },reducerHandler), 158 | 159 | followauthor: action({ 160 | type: 'postDetailPage.followauthor', 161 | action: (userId,http) => { 162 | return http.post(`/api/follow/${userId}`) 163 | }, 164 | handler: (state, result) => { 165 | return { 166 | ...state, 167 | } 168 | } 169 | },reducerHandler), 170 | 171 | hasFollowedAuthor: action({ 172 | type: 'postDetailPage.hasFollowedAuthor', 173 | action: (postId,http) => { 174 | return http.get(`/api/hasFollowedAuthor/${postId}`) 175 | }, 176 | handler: (state, result) => { 177 | return { 178 | ...state, 179 | ...result.data 180 | } 181 | } 182 | },reducerHandler), 183 | }; 184 | 185 | const inintStates = { 186 | post: { 187 | author: {}, 188 | body:'', 189 | headerBg:'', 190 | tags:[] 191 | }, 192 | likers: [], 193 | hasCollectioned:false, 194 | hasFollowed: false, 195 | comments:[], 196 | page:{} 197 | } 198 | 199 | let initState = { 200 | key: 'postDetailPage', 201 | state: inintStates 202 | }; 203 | 204 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 205 | 206 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/client/pages/post-detail/reply-input.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import Button from '@material-ui/core/Button'; 4 | import Collapse from '@material-ui/core/Collapse'; 5 | import Toast from 'src/componentCommon/toast' 6 | 7 | export default ({visible, setVisible, createComment, getComment, id, parentId,reply}) => { 8 | const [value, setValue] = useState(''); 9 | 10 | async function submitComment(){ 11 | const createRes = await createComment({id,body:value,parentId,replyId:reply.id}); 12 | if(createRes && createRes.success){ 13 | Toast.success(createRes.data.message); 14 | setValue('') 15 | await getComment(id); 16 | } 17 | } 18 | 19 | return ( 20 | 21 | {reply.name ?
回复 {reply.name } :
: ''} 22 |
23 | setValue(e.target.value)} 32 | /> 33 |
34 | 35 | 36 |
37 |
38 |
39 | ) 40 | } -------------------------------------------------------------------------------- /src/client/pages/post-detail/style.scss: -------------------------------------------------------------------------------- 1 | // @import 'src/componentCommon/editor/codemirror.scss'; 2 | @import 'src/componentCommon/editor/theme.scss'; 3 | @import 'src/componentCommon/editor/hljs.scss'; 4 | @import 'src/componentCommon/editor/preview-content.scss'; 5 | 6 | #root { 7 | padding: 0; 8 | } 9 | 10 | #post-op-popup-menu{ 11 | .MuiMenu-list{ 12 | .collection-show{ 13 | display: flex; 14 | 15 | svg { 16 | width: 0.8em; 17 | margin-right: 4px; 18 | } 19 | } 20 | } 21 | } 22 | 23 | .app-header { 24 | opacity: 0 !important; 25 | transition: opacity 600ms; 26 | 27 | &.header-visible { 28 | opacity: 1 !important; 29 | } 30 | } 31 | 32 | .post-detail-page { 33 | padding-bottom: 20px; 34 | background: #f8f8f8; 35 | } 36 | .hover-bars-wrap, 37 | .side-auth-info { 38 | position: fixed; 39 | display: none; 40 | top: 140px; 41 | z-index: 9; 42 | } 43 | .hover-bars-wrap { 44 | 45 | .MuiFab-root{ 46 | background: #fff; 47 | 48 | .MuiSvgIcon-root{ 49 | width: 0.7em; 50 | height: 0.7em; 51 | } 52 | } 53 | 54 | .like-num{ 55 | text-align: center; 56 | padding: 8px 0; 57 | } 58 | 59 | &.hover-bars-wrap-visible{ 60 | display: block; 61 | } 62 | 63 | .liked{ 64 | color:#ec7259; 65 | 66 | .MuiFab-root{ 67 | background: #ec7259; 68 | } 69 | 70 | .MuiSvgIcon-root{ 71 | fill: #fff; 72 | } 73 | 74 | } 75 | } 76 | 77 | .side-auth-info{ 78 | width: 200px; 79 | background: #fff; 80 | box-shadow: 0 1px 2px 0 rgba(0,0,0,.05); 81 | 82 | &.side-auth-info-visible{ 83 | display: block; 84 | } 85 | 86 | .title{ 87 | display: flex; 88 | justify-content: space-between; 89 | font-weight: 400; 90 | color: #ccc; 91 | padding: 12px 16px; 92 | color: #333; 93 | border-bottom: 1px solid hsla(0,0%,58.8%,.1); 94 | 95 | .follow-author{ 96 | color: #de5900; 97 | border: 1px solid; 98 | padding: 0 4px; 99 | cursor: pointer; 100 | font-weight: 700; 101 | } 102 | } 103 | 104 | .user-avater{ 105 | width: 60px; 106 | height: 60px; 107 | } 108 | 109 | .auth-info-content{ 110 | padding: 12px; 111 | cursor: pointer; 112 | } 113 | 114 | .avater-work{ 115 | display: flex; 116 | } 117 | 118 | .auth-info-work{ 119 | display: flex; 120 | flex-direction: column; 121 | justify-content: center; 122 | 123 | div:first-child{ 124 | font-size: 16px; 125 | font-weight: 600; 126 | color: #000; 127 | } 128 | 129 | div:nth-child(2){ 130 | color: #72777b; 131 | } 132 | } 133 | 134 | .active-info{ 135 | display: flex; 136 | padding: 5px 0; 137 | 138 | & > span { 139 | display: flex; 140 | justify-content: center; 141 | align-items: center; 142 | width: 20px; 143 | height: 20px; 144 | margin-right: 6px; 145 | background: #defdff; 146 | border-radius: 50%; 147 | 148 | 149 | } 150 | svg{ 151 | width: 0.7em; 152 | height: 0.7em; 153 | } 154 | } 155 | } 156 | 157 | .post-detail-header { 158 | background-attachment: scroll; 159 | height: 300px; 160 | display: table; 161 | width: 100%; 162 | margin-bottom: 20px; 163 | color: #fff; 164 | 165 | &>.container { 166 | display: flex; 167 | padding: 124px 15px 15px; 168 | justify-content: center; 169 | align-items: center; 170 | 171 | .post-heading { 172 | text-align: center; 173 | 174 | h1 { 175 | margin-top: 0; 176 | margin-bottom: 24px; 177 | font-size: 36px; 178 | font-weight: 300; 179 | } 180 | 181 | .meta { 182 | font-weight: 300; 183 | font-size: 16px; 184 | color: #eee; 185 | 186 | &>span { 187 | margin-left: 16px; 188 | } 189 | } 190 | 191 | .post-tags { 192 | text-align: center; 193 | margin-top: 20px; 194 | 195 | .tag-item { 196 | font-weight: 300; 197 | border-radius: 4px; 198 | border: 1px solid; 199 | margin: 4px; 200 | padding: 2px 10px; 201 | font-size: 12px; 202 | } 203 | } 204 | } 205 | 206 | } 207 | } 208 | 209 | .post-detail{ 210 | max-width: 920px; 211 | margin: 0 auto; 212 | border: none; 213 | box-shadow: 7px 12px 18px rgba(0, 0, 0, .03); 214 | } 215 | 216 | 217 | @media only screen and (min-width: 768px) { 218 | .post-detail-header { 219 | height: 370px; 220 | } 221 | } -------------------------------------------------------------------------------- /src/client/pages/post-editor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import withInitialData from 'src/componentHOC/with-initial-data'; 6 | import withStyles from 'isomorphic-style-loader/withStyles'; 7 | import composeHOC from 'src/utils/composeHOC'; 8 | import Editor from 'src/componentCommon/editor'; 9 | import css from './style.scss'; 10 | 11 | //组件 12 | class EditorPost extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | static state() { 18 | return ( 19 | { page: {} } 20 | ) 21 | } 22 | 23 | static async getInitialProps({ store }) { 24 | return store.dispatch(actions.getInitialData()); 25 | } 26 | 27 | render() { 28 | 29 | return ( 30 | 34 | ) 35 | } 36 | } 37 | 38 | const mapStateToProps = state => ({ 39 | initialData: state.editPostPage, 40 | userInfo: state.userInfo 41 | }); 42 | 43 | //将获取数据的方法也做为 props传递给组件 44 | const mapDispatchToProps = dispatch => ( 45 | bindActionCreators({ ...actions }, dispatch) 46 | ) 47 | 48 | export default composeHOC( 49 | withStyles(css), 50 | withInitialData, 51 | connect(mapStateToProps, mapDispatchToProps, null) 52 | )(EditorPost); -------------------------------------------------------------------------------- /src/client/pages/post-editor/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | export const actions = { 7 | getInitialData: action({ 8 | type: 'editPostPage.getInitialData', 9 | action: async (http, dispatch) => { 10 | const path = __SERVER__ ? global.REQUEST_PATH : location.pathname; 11 | const urlInfo = path.split('/'); 12 | const postId = urlInfo[urlInfo.length - 1]; 13 | 14 | const res = await dispatch(actions.getPost(postId)); 15 | const page = { 16 | tdk: { 17 | title: '', 18 | keywords: '', 19 | description: '' 20 | } 21 | } 22 | 23 | const post = res.data.post; 24 | 25 | page.tdk.title = '写文章-' + (post.title || ''); 26 | page.tdk.keywords = post.tags ? post.tags.join(',') : ''; 27 | page.tdk.description = post.title || ''; 28 | 29 | return ({ 30 | post, 31 | page 32 | }) 33 | }, 34 | handler: (state, result) => { 35 | return { 36 | ...state, 37 | ...result 38 | } 39 | } 40 | }, reducerHandler), 41 | 42 | getPost: action({ 43 | type: 'editPostPage.getPost', 44 | action: (id, http) => { 45 | return http.get(`/api/getEditPost/${id}`) 46 | }, 47 | handler: (state, result) => { 48 | return { 49 | ...state 50 | } 51 | } 52 | }, reducerHandler), 53 | 54 | updatePost: action({ 55 | type: 'editPostPage.updatePost', 56 | action: (params, http) => { 57 | const { id } = params; 58 | delete params.id; 59 | return http.put(`/api/posts/${id}`, params) 60 | }, 61 | handler: (state, result) => { 62 | return { 63 | ...state 64 | } 65 | } 66 | }, reducerHandler), 67 | 68 | publishPost: action({ 69 | type: 'editPostPage.publishPost', 70 | action: (params, http) => { 71 | const { id } = params; 72 | delete params.id; 73 | return http.post(`/api/publishPost/${id}`, params) 74 | }, 75 | handler: (state, result) => { 76 | return { 77 | ...state 78 | } 79 | } 80 | }, reducerHandler), 81 | 82 | uploadImage: action({ 83 | type: 'editPostPage.uploadImage', 84 | action: (params, http) => { 85 | return http.post('/api/upload/post', params) 86 | }, 87 | handler: (state, result) => { 88 | return { 89 | ...state 90 | } 91 | } 92 | }, reducerHandler), 93 | 94 | uploadHeaderImage: action({ 95 | type: 'editPostPage.uploadHeaderImage', 96 | action: (params, http) => { 97 | return http.post('/api/upload/header', params) 98 | }, 99 | handler: (state, result) => { 100 | return { 101 | ...state 102 | } 103 | } 104 | }, reducerHandler), 105 | }; 106 | 107 | let initState = { 108 | key: 'editPostPage', 109 | state: { post: {}, page: {} } 110 | }; 111 | 112 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState)) }); -------------------------------------------------------------------------------- /src/client/pages/post-editor/style.scss: -------------------------------------------------------------------------------- 1 | .user-list{ 2 | li{ 3 | margin: 10px 0; 4 | } 5 | } -------------------------------------------------------------------------------- /src/client/pages/register/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import withStyles from 'isomorphic-style-loader/withStyles'; 4 | import { withRouter } from "react-router-dom"; 5 | import { Grid, Button, Divider, Link } from '@material-ui/core'; 6 | import withInitialData from 'src/componentHOC/with-initial-data'; 7 | import composeHOC from 'src/utils/composeHOC'; 8 | import createForm, { InputFormItem } from 'src/componentHOC/form-create'; 9 | import { actions } from './redux'; 10 | import { actions as getUserInfo } from 'src/componentLayout/redux'; 11 | import Toast from 'src/componentCommon/toast'; 12 | import css from './style.scss'; 13 | 14 | class Register extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | btnText: '获取验证码', 19 | seconds: 60, //称数初始化 20 | liked: true //获取验证码文案 21 | } 22 | } 23 | 24 | static state() { 25 | return ({ 26 | page: {} 27 | }) 28 | } 29 | 30 | static async getInitialProps({ store }) { 31 | return store.dispatch(actions.getInitialData()); 32 | } 33 | 34 | startCountDown = () => { 35 | this.setState({ 36 | liked: false, 37 | seconds: this.state.seconds - 1, 38 | }, () => { 39 | if (this.state.seconds <= 0) { 40 | this.setState({ 41 | liked: true, 42 | seconds: 60 43 | }); 44 | clearInterval(this.siv); 45 | this.siv = null; 46 | } 47 | }) 48 | } 49 | 50 | sendCode = () => { 51 | const { getFieldValues, validFields } = this.props; 52 | if (!validFields('email')) return; 53 | const email = getFieldValues('email'); 54 | this.props.sendVerifyCode({ email }).then(res => { 55 | Toast.success(res.message); 56 | this.startCountDown(); 57 | this.siv = setInterval(this.startCountDown, 1000); 58 | }) 59 | } 60 | 61 | handleSubmit = () => { 62 | const { data, valid } = this.props.handleSubmit(); 63 | if (!valid) return; 64 | this.props.fetchRegister(data).then(res => { 65 | if (res && res.success) { 66 | this.props.getUserInfo(); 67 | this.props.history.push('/') 68 | } 69 | }); 70 | } 71 | 72 | getVerifyCode = () => { 73 | this.sendCode(); 74 | } 75 | 76 | render() { 77 | const { liked } = this.state; 78 | return ( 79 |
80 |
81 | 82 |
83 |
注册
84 | 85 |
86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 103 |
已有账户? this.props.history.push('/login')}>去登录
104 | 105 |
106 |
107 | ) 108 | } 109 | } 110 | 111 | const mapStateToProps = state => ({ 112 | initialData: state.registerPage, 113 | }); 114 | 115 | export default composeHOC( 116 | createForm, 117 | withRouter, 118 | withStyles(css), 119 | withInitialData, 120 | connect(mapStateToProps, { ...actions, getUserInfo: getUserInfo.getInitialData }, null) 121 | )(Register); -------------------------------------------------------------------------------- /src/client/pages/register/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | const { action, createReducer, injectReducer } = enRedux.default; 4 | import Cookie from 'js-cookie'; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | 9 | getInitialData: action({ 10 | type: 'registerPage.getPage', 11 | action: () => ({ 12 | page:{ 13 | tdk: { 14 | title: '注册', 15 | keywords: 'simplog', 16 | description: 'simplog 简约博客' 17 | } 18 | } 19 | }), 20 | handler: (state, result) => { 21 | return { 22 | ...state, 23 | ...result 24 | } 25 | } 26 | },reducerHandler), 27 | 28 | sendVerifyCode: action({ 29 | type: 'registerPage.sendVerifyCode', 30 | action: (params,http) => { 31 | return http.post('/api/email',params) 32 | }, 33 | handler: (state, result) => { 34 | return { 35 | ...state 36 | } 37 | } 38 | },reducerHandler), 39 | 40 | fetchRegister: action({ 41 | type: 'registerPage.fetchRegister', 42 | action: (params,http) => { 43 | return http.post('/api/user/register',params) 44 | }, 45 | handler: (state, result) => { 46 | const { success } = result; 47 | if(success) { 48 | Cookie.set('token',result.data.token); 49 | } 50 | return { 51 | ...state 52 | } 53 | } 54 | },reducerHandler), 55 | }; 56 | 57 | let initState = { 58 | key: 'registerPage', 59 | state: {page:{}} 60 | }; 61 | 62 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/client/pages/register/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .register-page-wrap{ 3 | display: flex; 4 | position: fixed; 5 | width: 100%; 6 | top:0; 7 | bottom: 0; 8 | // height: calc(100vh - 64px); 9 | z-index: 1; 10 | background: #e7f2f3; 11 | 12 | img.register-logo{ 13 | position: absolute; 14 | top: -100px; 15 | left: 0; 16 | width: 100%; 17 | } 18 | .form-wrap{ 19 | position: relative; 20 | margin: auto; 21 | max-width: 480px; 22 | min-width: 320px; 23 | padding: 40px 20px; 24 | background: #fff; 25 | box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12); 26 | 27 | } 28 | 29 | .form-content{ 30 | > div{ 31 | margin-bottom: 16px; 32 | } 33 | 34 | > div:last-child{ 35 | margin-bottom: 0; 36 | } 37 | > button{ 38 | margin-top: 12px; 39 | } 40 | } 41 | 42 | .get-verify-code-wrap{ 43 | text-align: right; 44 | > button { 45 | margin-top: 12px; 46 | } 47 | } 48 | 49 | .op-title{ 50 | padding-bottom: 16px; 51 | header{ 52 | font-size: 24px; 53 | padding-bottom: 12px; 54 | } 55 | } 56 | 57 | .switch-op{ 58 | padding-top: 20px; 59 | text-align: center; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/client/pages/user-center/activity-items.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { timeTransfor, openInNewTab } from 'src/utils/helper'; 3 | import { Avatar } from '@material-ui/core'; 4 | import Link from '@material-ui/core/Link'; 5 | import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; 6 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 7 | import VisibilityIcon from '@material-ui/icons/Visibility'; 8 | 9 | const CommonCard = post => ( 10 |
11 |
12 | 13 |
14 | {post.author.username} 15 | 16 | {post.author.jobTitle || '无'} 17 | 18 | {timeTransfor(new Date(post.createdAt))} 19 | 20 |
21 |
22 |
openInNewTab('/post/' + post._id, true)}> 23 |
{post.title}
24 |
25 |

{post.main.length <= 120 ? post.main : post.main.substr(0, 120)}...

26 | { 27 | post.headerBg ? : '' 28 | } 29 |
30 |
31 | 32 |
33 | {post.read} 34 | {post.likes} 35 | {post.comments} 36 |
37 |
38 | ) 39 | 40 | const LikePostItem = ({ data }) => ( 41 |
42 |
{data.user.username}赞了这篇文章
43 | {CommonCard(data.likePost)} 44 |
45 | ) 46 | 47 | const CommentItem = ({ data }) => ( 48 |
49 |
50 |
{data.user.username}评论了这篇文章
51 |
“{data.addComment.replyToUser && openInNewTab('/user/' + data.addComment.replyToUser._id, true)}>@{data.addComment.replyToUser.username}} {data.addComment.body}”
52 |
53 | {CommonCard(data.addComment.post)} 54 |
55 | ) 56 | 57 | const PublishPostItem = ({ data }) => ( 58 |
59 |
{data.user.username}发布了新文章
60 | {CommonCard(data.publish)} 61 |
62 | ) 63 | 64 | const CollectionPostItem = ({ data }) => ( 65 |
66 |
{data.user.username}收藏了这文章
67 | {CommonCard(data.collectionPost)} 68 |
69 | ) 70 | 71 | const FollowAuthorItem = ({ data }) => ( 72 |
73 | 74 |
75 | {data.user.username} 关注了 openInNewTab('/user/' + data.followAuthor._id, true)}>{data.followAuthor.username} 76 | {'无'} 77 |
78 |
79 | ) 80 | 81 | export { 82 | CommonCard, 83 | LikePostItem, 84 | CommentItem, 85 | PublishPostItem, 86 | CollectionPostItem, 87 | FollowAuthorItem 88 | } -------------------------------------------------------------------------------- /src/client/pages/user-center/collection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { openInNewTab } from 'src/utils/helper'; 3 | import withListenerScroll from 'src/componentHOC/withListenerScroll'; 4 | import { CommonCard } from './activity-items'; 5 | 6 | class MyCollection extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | static method = 'getUserCollections'; 11 | 12 | switchDetail = (id) => { 13 | openInNewTab('/post/'+id,true); 14 | } 15 | 16 | editPost = (id) => { 17 | openInNewTab('/editor/post/'+id,true); 18 | } 19 | 20 | render() { 21 | const { userInfo, datas } = this.props; 22 | return ( 23 |
24 | { 25 | datas.map(data =>
{CommonCard(data.post)}
) 26 | } 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default withListenerScroll(MyCollection); -------------------------------------------------------------------------------- /src/client/pages/user-center/follow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import { openInNewTab } from 'src/utils/helper'; 4 | import Divider from '@material-ui/core/Divider'; 5 | import withListenerScroll from 'src/componentHOC/withListenerScroll'; 6 | 7 | class Follow extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | } 12 | 13 | static method = 'getFollowedUsers'; 14 | 15 | deletePost = async (popupState, id) => { 16 | const res = await this.props.deletePost(id); 17 | const { params } = this.props.match; 18 | popupState.close(); 19 | this.props.refreshDatas(); 20 | } 21 | 22 | switchDetail = (id) => { 23 | openInNewTab('/post/' + id, true); 24 | } 25 | 26 | editPost = (id) => { 27 | openInNewTab('/editor/post/' + id, true); 28 | } 29 | 30 | render() { 31 | const { userInfo, datas, followType } = this.props; 32 | 33 | return ( 34 |
35 |
36 | 关注了 37 | 38 | 关注者 39 |
40 | { 41 | datas.map(data => ( 42 |
43 |
openInNewTab('/user/' + data.user._id, true)}> 44 | 45 |
46 | {data.user.username} 47 | {data.user.jobTitle || '无'} 48 |
49 |
50 |
51 | )) 52 | } 53 |
54 | ) 55 | } 56 | } 57 | 58 | export default withListenerScroll(Follow); -------------------------------------------------------------------------------- /src/client/pages/user-center/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import { Button } from '@material-ui/core'; 6 | import withInitialData from 'src/componentHOC/with-initial-data'; 7 | import withStyles from 'isomorphic-style-loader/withStyles'; 8 | import WorkIcon from '@material-ui/icons/Work'; 9 | import VisibilityIcon from '@material-ui/icons/Visibility'; 10 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 11 | import Tabs from '@material-ui/core/Tabs'; 12 | import Tab from '@material-ui/core/Tab'; 13 | import composeHOC from 'src/utils/composeHOC'; 14 | import emitter from 'src/utils/events'; 15 | import moment from 'moment'; 16 | import MyPost from './my-post'; 17 | import MyActivites from './my-activites'; 18 | import Follow from './follow'; 19 | import Collection from './collection'; 20 | import { openInNewTab } from 'src/utils/helper'; 21 | import Toast from 'src/componentCommon/toast'; 22 | import css from './style.scss'; 23 | 24 | function TabPanel({ menuValue, value, children }) { 25 | return ( 26 | 37 | ); 38 | } 39 | 40 | function a11yProps(index) { 41 | return { 42 | id: `scrollable-auto-tab-${index}`, 43 | 'aria-controls': `scrollable-auto-tabpanel-${index}`, 44 | }; 45 | } 46 | 47 | //组件 48 | class UserCenter extends React.Component { 49 | constructor(props) { 50 | super(props); 51 | this.state = { 52 | menuValue: 'post' 53 | } 54 | } 55 | 56 | static state() { 57 | return ( 58 | { 59 | userInfo: {}, 60 | page: {} 61 | } 62 | ) 63 | } 64 | 65 | static async getInitialProps({ store }) { 66 | return store.dispatch(actions.getInitialData()); 67 | } 68 | 69 | handerFollow = async () => { 70 | const { currentUser, initialData } = this.props; 71 | const { _id } = initialData.userInfo; 72 | 73 | if (!currentUser._id) { 74 | Toast.error('请先登录!'); 75 | return; 76 | } 77 | 78 | const res = await this.props.followauthor(_id); 79 | 80 | if (res && res.success) { 81 | this.props.getOtherUserInfo(_id); 82 | Toast.success(res.data.message); 83 | } 84 | } 85 | 86 | handlerTabChange = () => { 87 | const contentPosition = this.majorContent.getBoundingClientRect(); 88 | const userViewPosition = this.userView.getBoundingClientRect(); 89 | const scrollTop = contentPosition.top; 90 | 91 | if (userViewPosition.height - scrollTop + 65 >= contentPosition.height) { 92 | emitter.emit('scrollToBottom'); 93 | } 94 | 95 | if (scrollTop <= -64) { 96 | this.tabsRef.classList.add('position-status') 97 | } else { 98 | this.tabsRef.classList.remove('position-status') 99 | } 100 | } 101 | 102 | ifFollowAuthor = () => { 103 | const { currentUser, initialData } = this.props; 104 | return initialData.userInfo.totalFollowTo && initialData.userInfo.totalFollowTo.findIndex(item => item.followFrom === currentUser._id) > -1; 105 | } 106 | 107 | handlerBtn = () => { 108 | const { currentUser, initialData } = this.props; 109 | if (currentUser._id === initialData.userInfo._id) { 110 | return 111 | } 112 | 113 | if (this.ifFollowAuthor()) { 114 | return 115 | } 116 | 117 | return 118 | } 119 | 120 | render() { 121 | const { userInfo } = this.props.initialData; 122 | const { menuValue } = this.state; 123 | 124 | return ( 125 |
126 |
127 |
this.userView = ref}> 128 |
129 |
130 |
this.majorContent = ref}> 131 |
132 |
133 | 134 |
135 |
136 |

{userInfo.username}

137 | {userInfo.jobTitle || '无'} 138 |
139 |
140 | {this.handlerBtn()} 141 |
142 |
143 | 144 |
145 |
146 | this.setState({ menuValue: value })} 151 | variant="fullWidth" 152 | aria-label="user menu" 153 | ref={ref => this.tabsRef = ref} 154 | id='user-center-tabs' 155 | > 156 | 157 | 158 | 159 | 160 | 161 |
162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |
175 |
176 |
177 |
178 | 179 |
180 |
181 |
个人成就
182 |
183 |
184 | 185 | 文章被阅读 {userInfo.totalReads} 186 |
187 |
188 | 189 | 获得点赞 {userInfo.totalLikes} 190 |
191 |
192 |
193 |
194 |
195 |
关注了
196 |
{userInfo.totalFollowFrom ? userInfo.totalFollowFrom.length : 0}
197 |
198 |
199 |
专注者
200 |
{userInfo.totalFollowTo ? userInfo.totalFollowTo.length : 0}
201 |
202 |
203 |
204 |
205 |
加入于
206 |
{moment(userInfo.createdAt).format("YYYY-MM-DD")}
207 |
208 |
209 |
210 |
211 |
212 |
213 | ) 214 | } 215 | } 216 | 217 | const mapStateToProps = state => ({ 218 | initialData: state.userCenterPage, 219 | currentUser: state.userInfo 220 | }); 221 | 222 | //将获取数据的方法也做为 props传递给组件 223 | const mapDispatchToProps = dispatch => ( 224 | bindActionCreators({ ...actions }, dispatch) 225 | ) 226 | 227 | export default composeHOC( 228 | withStyles(css), 229 | withInitialData, 230 | connect(mapStateToProps, mapDispatchToProps, null) 231 | )(UserCenter); -------------------------------------------------------------------------------- /src/client/pages/user-center/my-activites.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { openInNewTab } from 'src/utils/helper'; 3 | import { LikePostItem, CommentItem, PublishPostItem, CollectionPostItem, FollowAuthorItem } from './activity-items'; 4 | import withListenerScroll from 'src/componentHOC/withListenerScroll'; 5 | 6 | class MyActivities extends Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | static method = 'getActivites'; 11 | 12 | deletePost = async (popupState, id) => { 13 | const res = await this.props.deletePost(id); 14 | const { params } = this.props.match; 15 | popupState.close(); 16 | this.props.refreshDatas(); 17 | } 18 | 19 | switchDetail = (id) => { 20 | openInNewTab('/post/'+id,true); 21 | } 22 | 23 | editPost = (id) => { 24 | openInNewTab('/editor/post/'+id,true); 25 | } 26 | 27 | render() { 28 | const { userInfo, datas } = this.props; 29 | 30 | return ( 31 |
32 | { 33 | datas.map(data => { 34 | if(data.activeType === 'LIKE_POST') return ; 35 | if(data.activeType === 'COMMENT') return ; 36 | if(data.activeType === 'PUBLISH') return ; 37 | if(data.activeType === 'COLLECTION') return ; 38 | if(data.activeType === 'FOLLOW') return ; 39 | }) 40 | } 41 |
42 | ) 43 | } 44 | } 45 | 46 | export default withListenerScroll(MyActivities); -------------------------------------------------------------------------------- /src/client/pages/user-center/my-post.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import { timeTransfor, openInNewTab } from 'src/utils/helper'; 4 | import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; 5 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 6 | import VisibilityIcon from '@material-ui/icons/Visibility'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 9 | import Menu from '@material-ui/core/Menu'; 10 | import MenuItem from '@material-ui/core/MenuItem'; 11 | import Comfirm from 'src/componentCommon/confirm'; 12 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'; 13 | import withListenerScroll from 'src/componentHOC/withListenerScroll'; 14 | 15 | class MyPost extends Component { 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | static method = 'getUserPosts'; 21 | 22 | deletePost = async (popupState, id) => { 23 | const res = await this.props.deletePost(id); 24 | const { params } = this.props.match; 25 | popupState.close(); 26 | this.props.refreshDatas(); 27 | } 28 | 29 | switchDetail = (id) => { 30 | openInNewTab('/post/' + id, true); 31 | } 32 | 33 | editPost = (id) => { 34 | this.props.history.push('/editor/post/' + id); 35 | } 36 | 37 | render() { 38 | const { userInfo, datas } = this.props; 39 | 40 | return ( 41 |
42 | { 43 | datas.map(post => ( 44 |
45 |
46 | 47 | {post.author.username} 48 | 49 | {timeTransfor(new Date(post.createdAt))} 50 |
51 | {post.headerBg &&
} 52 |
53 |
{post.title}
54 |
{post.main}
55 |
56 | 57 |
58 |
59 | {post.read} 60 | {post.likes} 61 | {post.comments} 62 |
63 |
e.stopPropagation()}> 64 | { 65 | userInfo._id === post.author._id 66 | && 67 | 68 | {(popupState) => ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 编辑 82 | 83 | 删除 84 | 85 | 86 | 87 | )} 88 | 89 | } 90 | 91 |
92 |
93 |
94 | )) 95 | } 96 |
97 | ) 98 | } 99 | } 100 | 101 | export default withListenerScroll(MyPost); -------------------------------------------------------------------------------- /src/client/pages/user-center/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getInitialData: action({ 9 | type: 'userCenterPage.getInitialData', 10 | action: async (http, dispatch) => { 11 | 12 | const path = __SERVER__ ? global.REQUEST_PATH : location.pathname; 13 | const urlInfo = path.split('/'); 14 | const userId = urlInfo[urlInfo.length - 1]; 15 | const page = { 16 | tdk: { 17 | title: '', 18 | keywords: '', 19 | description: '' 20 | } 21 | } 22 | 23 | const resUserInfo = await dispatch(actions.getOtherUserInfo(userId)); 24 | page.tdk.title = resUserInfo.data.user.username + '的主页'; 25 | return ({ 26 | page 27 | }) 28 | }, 29 | handler: (state, result) => { 30 | return { 31 | ...state, 32 | ...result 33 | } 34 | } 35 | }, reducerHandler), 36 | 37 | getPage: action({ 38 | type: 'userCenterPage.getPage', 39 | action: () => ({ 40 | tdk: { 41 | title: '个人主页', 42 | keywords: 'simple blog', 43 | description: '个人主页' 44 | } 45 | }), 46 | handler: (state, result) => { 47 | return { 48 | ...state 49 | } 50 | } 51 | }, reducerHandler), 52 | 53 | followauthor: action({ 54 | type: 'userCenterPage.followauthor', 55 | action: (userId, http) => { 56 | return http.post(`/api/follow/${userId}`) 57 | }, 58 | handler: (state, result) => { 59 | return { 60 | ...state, 61 | } 62 | } 63 | }, reducerHandler), 64 | 65 | getOtherUserInfo: action({ 66 | type: 'userCenterPage.getOtherUserInfo', 67 | action: (id, http) => { 68 | return http.get(`/api/getOtherUserInfo/${id}`) 69 | }, 70 | handler: (state, result) => { 71 | return { 72 | ...state, 73 | userInfo: result.data.user 74 | } 75 | } 76 | }, reducerHandler), 77 | 78 | getUserPosts: action({ 79 | type: 'userCenterPage.getUserPosts', 80 | action: (params, http) => { 81 | const { id, pageNo } = params; 82 | return http.get(`/api/userPosts/${id}?pageNo=${pageNo}`) 83 | }, 84 | handler: (state, result) => { 85 | return { 86 | ...state 87 | } 88 | } 89 | }, reducerHandler), 90 | 91 | getActivites: action({ 92 | type: 'userCenterPage.getActivites', 93 | action: (params, http) => { 94 | const { id, pageNo } = params; 95 | return http.get(`/api/getActivites/${id}?pageNo=${pageNo}`) 96 | }, 97 | handler: (state, result) => { 98 | return { 99 | ...state 100 | } 101 | } 102 | }, reducerHandler), 103 | 104 | deletePost: action({ 105 | type: 'userCenterPage.deletePost', 106 | action: (id, http) => { 107 | return http.delete(`/api/posts/${id}`) 108 | }, 109 | handler: (state, result) => { 110 | return { 111 | ...state 112 | } 113 | } 114 | }, reducerHandler), 115 | 116 | getFollowedUsers: action({ 117 | type: 'userCenterPage.getFollowedUsers', 118 | action: (params, http) => { 119 | if (!params.followType) { 120 | params['followType'] = 'FOLLOW_TO' 121 | } 122 | const { id, pageNo, followType } = params; 123 | 124 | return http.get(`/api/getFollowedUsers/${id}?pageNo=${pageNo}&followType=${followType}`) 125 | }, 126 | handler: (state, result) => { 127 | return { 128 | ...state 129 | } 130 | } 131 | }, reducerHandler), 132 | 133 | getUserCollections: action({ 134 | type: 'userCenterPage.getActivites', 135 | action: (params, http) => { 136 | const { id, pageNo } = params; 137 | return http.get(`/api/getUserCollections/${id}?pageNo=${pageNo}`) 138 | }, 139 | handler: (state, result) => { 140 | return { 141 | ...state 142 | } 143 | } 144 | }, reducerHandler), 145 | }; 146 | 147 | let initState = { 148 | key: 'userCenterPage', 149 | state: { userInfo: {}, page: {} } 150 | }; 151 | 152 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState)) }); 153 | -------------------------------------------------------------------------------- /src/client/pages/user-setting/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import { actions } from './redux'; 5 | import { actions as layoutAction } from 'src/componentLayout/redux'; 6 | import withInitialData from 'src/componentHOC/with-initial-data'; 7 | import withStyles from 'isomorphic-style-loader/withStyles'; 8 | import composeHOC from 'src/utils/composeHOC'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import EditIcon from '@material-ui/icons/Edit'; 11 | import Link from '@material-ui/core/Link'; 12 | import Backdrop from '@material-ui/core/Backdrop'; 13 | import CircularProgress from '@material-ui/core/CircularProgress'; 14 | import Button from '@material-ui/core/Button'; 15 | import Toast from 'src/componentCommon/toast'; 16 | import css from './style.scss'; 17 | 18 | const ChangeItem = ({label,placeholder,name,userInfo, updateUserInfo}) => { 19 | const [status, setStatus] = useState('blur'); 20 | const [value, setValue] = useState(userInfo[name]); 21 | const inputRef = React.createRef(); 22 | const handlerEdit = () => { 23 | setStatus('focus'); 24 | inputRef.current.focus(); 25 | } 26 | const handlerBlur = () => { 27 | setStatus('blur'); 28 | inputRef.current.bliur(); 29 | } 30 | const handlerSaveForm = (e) => { 31 | e.stopPropagation(); 32 | updateUserInfo({[name]:value}) 33 | } 34 | 35 | return ( 36 |
37 | 38 | setValue(e.target.value)} 44 | onFocus={() => setStatus('focus')} 45 | onBlur={() => setStatus('blur')} 46 | /> 47 |
48 | { 49 | status === 'blur' 50 | ? 51 | 52 | 53 | 54 | : 55 | [ 保存, 56 | 取消 57 | ] 58 | } 59 | 60 |
61 |
62 | ) 63 | } 64 | 65 | //组件 66 | class UserSetting extends React.Component { 67 | constructor(props) { 68 | super(props); 69 | this.state = { 70 | 71 | } 72 | } 73 | 74 | static state() { 75 | return ( 76 | { 77 | postData: { 78 | datas: [], 79 | page: {} 80 | }, 81 | page: {} 82 | } 83 | ) 84 | } 85 | 86 | static async getInitialProps({ store }) { 87 | return store.dispatch(actions.getInitialData()); 88 | } 89 | 90 | handerUploadHeaderImage = async (e) => { 91 | try{ 92 | const { avatar } = this.props.userInfo; 93 | const formData = new FormData(); 94 | const image = e.target.files[0]; 95 | this.setState({uploading: true}); 96 | formData.append('images', image); 97 | const res = await this.props.uploadAvatar(formData); 98 | if (res && res.success) { 99 | this.updateUserInfo({avatar: res.data.url}) 100 | } 101 | }finally{ 102 | this.setState({uploading: false}); 103 | } 104 | } 105 | 106 | updateUserInfo = async params => { 107 | const updateUserRes = await this.props.updateUserInfo(params); 108 | if(updateUserRes.success){ 109 | Toast.success(updateUserRes.data.message); 110 | this.props.getUserInfo(); 111 | }else{ 112 | Toast.error(updateUserRes.data.message); 113 | } 114 | } 115 | 116 | render() { 117 | const { avatar } = this.props.userInfo; 118 | const { uploading } =this.state; 119 | return ( 120 |
121 | 122 | 123 | 头像上传中... 124 | 125 |
126 |
用户设置
127 |
128 | 129 |
130 | 131 |
132 | 支持 jpg、png 格式大小 5M 以内的图片 133 |
134 | 141 | 146 |
147 |
148 |
149 |
150 | 157 | 164 | 171 | 178 |
179 |
180 | ) 181 | } 182 | } 183 | 184 | const mapStateToProps = state => ({ 185 | initialData: state.userSettingPage, 186 | userInfo: state.userInfo 187 | }); 188 | 189 | //将获取数据的方法也做为 props传递给组件 190 | const mapDispatchToProps = dispatch => ( 191 | bindActionCreators({ ...actions, getUserInfo: layoutAction.getInitialData }, dispatch) 192 | ) 193 | 194 | export default composeHOC( 195 | withStyles(css), 196 | withInitialData, 197 | connect(mapStateToProps, mapDispatchToProps, null) 198 | )(UserSetting); -------------------------------------------------------------------------------- /src/client/pages/user-setting/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | 4 | const { action, createReducer, injectReducer } = enRedux.default; 5 | const reducerHandler = createReducer(); 6 | 7 | export const actions = { 8 | getPage: action({ 9 | type: 'userSettingPage.getPage', 10 | action: () => ({ 11 | tdk: { 12 | title: '用户设置', 13 | keywords: 'user-setting', 14 | description: 'simplog' 15 | } 16 | }), 17 | handler: (state, result) => { 18 | return { 19 | ...state 20 | } 21 | } 22 | }, reducerHandler), 23 | 24 | uploadAvatar: action({ 25 | type: 'userSettingPage.uploadAvatar', 26 | action: (params, http) => { 27 | return http.post('/api/upload/avatar', params) 28 | }, 29 | handler: (state, result) => { 30 | return { 31 | ...state 32 | } 33 | } 34 | }, reducerHandler), 35 | 36 | updateUserInfo: action({ 37 | type: 'userSettingPage.uploadAvatar', 38 | action: (params, http) => { 39 | return http.put('/api/userinfo', params) 40 | }, 41 | handler: (state, result) => { 42 | return { 43 | ...state 44 | } 45 | } 46 | }, reducerHandler), 47 | 48 | getInitialData: action({ 49 | type: 'userSettingPage.getInitialData', 50 | action: async (http, dispatch) => { 51 | const page = await dispatch(actions.getPage()); 52 | return ({ 53 | page 54 | }) 55 | }, 56 | handler: (state, result) => { 57 | return { 58 | ...state, 59 | ...result 60 | } 61 | } 62 | }, reducerHandler), 63 | }; 64 | 65 | let initState = { 66 | key: 'userSettingPage', 67 | state: { page: {} } 68 | }; 69 | 70 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState)) }); 71 | -------------------------------------------------------------------------------- /src/client/pages/user-setting/style.scss: -------------------------------------------------------------------------------- 1 | .user-setting-page { 2 | height: calc(100vh - 64px); 3 | overflow-y: auto; 4 | background: #f8f8f8; 5 | 6 | .setting-block { 7 | max-width: 920px; 8 | margin: 20px auto 0; 9 | padding: 0 12px; 10 | 11 | 12 | position: relative; 13 | padding: 32px 48px 84px; 14 | background-color: #fff; 15 | 16 | header{ 17 | margin: 16px 0; 18 | font-size: 24px; 19 | font-weight: 700; 20 | } 21 | 22 | .upload-avatar{ 23 | display: flex; 24 | padding: 12px 0; 25 | border-top: 1px solid #eee; 26 | 27 | label.item-label{ 28 | line-height: 72px; 29 | } 30 | 31 | .avater-content{ 32 | display: flex; 33 | & > img{ 34 | width: 72px; 35 | height: 72px; 36 | } 37 | 38 | .upload-block{ 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | margin-left: 24px; 43 | 44 | .upload-tips{ 45 | color: #909090; 46 | font-size: 12px; 47 | margin-bottom: 10px; 48 | } 49 | .MuiButtonBase-root{ 50 | border-radius: 0; 51 | } 52 | 53 | .upload-item{ 54 | input{ 55 | display: none; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | .other-info{ 63 | display: flex; 64 | padding: 12px 0; 65 | border-top: 1px solid #eee; 66 | 67 | &:last-child{ 68 | border-bottom: 1px solid #eee; 69 | } 70 | 71 | .change-input{ 72 | width: calc(100% - 200px); 73 | border: none; 74 | line-height: 16px; 75 | 76 | &:focus{ 77 | outline: none; 78 | } 79 | } 80 | 81 | .op-bars{ 82 | display: flex; 83 | justify-content: flex-end; 84 | align-items: center; 85 | width: 100px; 86 | text-align: right; 87 | 88 | & > a{ 89 | margin-left: 4px; 90 | } 91 | } 92 | } 93 | 94 | label.item-label{ 95 | width: 100px; 96 | line-height: 44px; 97 | font-size: 14.4px; 98 | color: #333; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/componentCommon/confirm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state'; 3 | import withStyles from 'isomorphic-style-loader/withStyles'; 4 | import { Button, Popover } from '@material-ui/core'; 5 | import ErrorIcon from '@material-ui/icons/Error'; 6 | import css from './style.scss'; 7 | 8 | class CustConfirm extends Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | const { children, click, header, successCb } = this.props; 15 | return ( 16 | 17 | {(popupState) => { 18 | const handlerClick = async () => { 19 | const res = await click(); 20 | if (res) { 21 | popupState.close(); 22 | successCb && successCb(); 23 | } 24 | } 25 | return ( 26 |
27 | 28 | {children} 29 | 30 | 41 |
42 |
43 | {header} 44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 | ) 53 | }} 54 |
55 | ) 56 | } 57 | } 58 | 59 | export default withStyles(css)(CustConfirm); -------------------------------------------------------------------------------- /src/componentCommon/confirm/style.scss: -------------------------------------------------------------------------------- 1 | .confirm-delete-wrap{ 2 | padding: 0 24px 12px; 3 | .confirm-header{ 4 | padding: 18px 0; 5 | display: flex; 6 | 7 | svg{ 8 | margin-right: 4px; 9 | } 10 | } 11 | 12 | .delete-op-wrap{ 13 | text-align: right; 14 | 15 | .MuiButtonBase-root{ 16 | min-width: 44px; 17 | padding: 0; 18 | border-radius: 0; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/componentCommon/editor/hljs.scss: -------------------------------------------------------------------------------- 1 | .hljs { 2 | display: block; 3 | overflow-x: auto; 4 | padding: 0.5em; 5 | color: #383a42; 6 | background: #f8f8f8; 7 | } 8 | 9 | .hljs-comment, 10 | .hljs-quote { 11 | color: #a0a1a7; 12 | font-style: italic; 13 | } 14 | 15 | .hljs-doctag, 16 | .hljs-keyword, 17 | .hljs-formula { 18 | color: #a626a4; 19 | } 20 | 21 | .hljs-section, 22 | .hljs-name, 23 | .hljs-selector-tag, 24 | .hljs-deletion, 25 | .hljs-subst { 26 | color: #e45649; 27 | } 28 | 29 | .hljs-literal { 30 | color: #0184bb; 31 | } 32 | 33 | .hljs-string, 34 | .hljs-regexp, 35 | .hljs-addition, 36 | .hljs-attribute, 37 | .hljs-meta-string { 38 | color: #50a14f; 39 | } 40 | 41 | .hljs-built_in, 42 | .hljs-class .hljs-title { 43 | color: #c18401; 44 | } 45 | 46 | .hljs-attr, 47 | .hljs-variable, 48 | .hljs-template-variable, 49 | .hljs-type, 50 | .hljs-selector-class, 51 | .hljs-selector-attr, 52 | .hljs-selector-pseudo, 53 | .hljs-number { 54 | color: #986801; 55 | } 56 | 57 | .hljs-symbol, 58 | .hljs-bullet, 59 | .hljs-link, 60 | .hljs-meta, 61 | .hljs-selector-id, 62 | .hljs-title { 63 | color: #4078f2; 64 | } 65 | 66 | .hljs-emphasis { 67 | font-style: italic; 68 | } 69 | 70 | .hljs-strong { 71 | font-weight: bold; 72 | } 73 | 74 | .hljs-link { 75 | text-decoration: underline; 76 | } -------------------------------------------------------------------------------- /src/componentCommon/editor/language-list.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'AngelScript', 3 | 'Apache', 4 | 'ARM Assembly', 5 | 'Bash', 6 | 'Basic', 7 | 'CoffeeScript', 8 | 'C++', 9 | 'C#', 10 | 'Dart', 11 | 'Diff', 12 | 'Django', 13 | 'DNS Zone file', 14 | 'Dockerfile', 15 | 'DOS .bat', 16 | 'Device Tree', 17 | 'Dust', 18 | 'Excel', 19 | 'Go', 20 | 'HTTP', 21 | 'Java', 22 | 'JavaScript', 23 | 'JSON', 24 | 'Less', 25 | 'Markdown', 26 | 'Nginx', 27 | 'Oxygene', 28 | 'PHP', 29 | 'PowerShell', 30 | 'Python', 31 | 'Ruby', 32 | 'Scheme', 33 | 'SCSS', 34 | 'Shell Session', 35 | 'Stylus', 36 | 'Swift', 37 | 'TypeScript', 38 | 'Vim Script', 39 | 'HTML', 40 | 'XML' 41 | ] -------------------------------------------------------------------------------- /src/componentCommon/editor/preview-content.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import marked from 'marked'; 3 | import hljs from 'highlight.js'; 4 | import languageList from './language-list'; 5 | import { debounce } from 'src/utils/helper'; 6 | 7 | marked.setOptions({ 8 | langPrefix: "hljs language-", 9 | highlight: function (code) { 10 | return hljs.highlightAuto(code, languageList).value; 11 | } 12 | }); 13 | 14 | export default class PreviewContent extends Component { 15 | constructor(props){ 16 | super(props); 17 | this.state = { 18 | doms: marked(props.code || '', { breaks: true }) 19 | } 20 | this.debounceGetContent = debounce(this.onContentChange,200); 21 | } 22 | 23 | componentWillReceiveProps(nextProps){ 24 | this.debounceGetContent(); 25 | } 26 | 27 | componentWillUnmount(){ 28 | this.debounceGetContent = null; 29 | } 30 | 31 | onContentChange = () => { 32 | const { code } = this.props; 33 | this.setState({ 34 | doms: marked(code || '', { breaks: true }) 35 | }) 36 | } 37 | render(){ 38 | const { getPreviewRef, className, code} = this.props; 39 | const { doms } = this.state; 40 | return ( 41 |
42 |
43 |
44 |
45 |
46 |
预览
47 |
{code ? code.length : 0}字
48 |
49 |
50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/componentCommon/editor/preview-content.scss: -------------------------------------------------------------------------------- 1 | .preview-content { 2 | display: flex; 3 | flex-direction: column; 4 | position: relative; 5 | background-color: #fff; 6 | overflow: hidden; 7 | border-left: 1px solid #ddd; 8 | flex: 1 1 50%; 9 | height: 100%; 10 | overflow-y: auto; 11 | font-family: -apple-system, "PingFang SC", "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei", "Microsoft JhengHei", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "WenQuanYi Micro Hei", SimSun, sans-serif; 12 | 13 | p, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | a { 21 | word-break: break-word; 22 | } 23 | 24 | &.fold{ 25 | display: none; 26 | } 27 | 28 | .preview-content-html { 29 | padding: 20px 32px; 30 | height: calc(100% - 40px); 31 | overflow: auto; 32 | } 33 | 34 | code { 35 | padding: .5em 1em; 36 | display: block; 37 | overflow-x: auto; 38 | padding: 0.5em; 39 | color: #383a42; 40 | background: #f8f8f8; 41 | } 42 | 43 | p { 44 | code { 45 | margin: 1em 0px; 46 | } 47 | } 48 | 49 | 50 | h1 { 51 | font-size: 36px; 52 | } 53 | 54 | h2 { 55 | font-size: 22px; 56 | } 57 | 58 | h3 { 59 | font-size: 20px; 60 | } 61 | 62 | h4 { 63 | font-size: 18px; 64 | } 65 | 66 | h5 { 67 | font-size: 16px; 68 | } 69 | 70 | h6 { 71 | font-size: 15px; 72 | } 73 | 74 | h1, 75 | h2, 76 | h3, 77 | h4, 78 | h5, 79 | h6 { 80 | margin-top: -90px; 81 | padding: 100px 0 0; 82 | font-weight: 500; 83 | } 84 | 85 | h1,h2,h3{ 86 | margin-bottom: 10px; 87 | } 88 | 89 | p { 90 | color: #333; 91 | margin: 0 0 20px; 92 | font-size: 16px; 93 | } 94 | 95 | a{ 96 | color: #099da6; 97 | text-decoration: none; 98 | &:focus{ 99 | color: #099da6; 100 | } 101 | } 102 | 103 | 104 | 105 | ul,li{ 106 | text-decoration: none; 107 | // list-style: none; 108 | } 109 | 110 | ul{ 111 | padding-left: 20px; 112 | } 113 | 114 | li{ 115 | color: #333; 116 | font-size: 16px; 117 | line-height: 1.6; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/componentCommon/editor/publish-post.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import withStyles from 'isomorphic-style-loader/withStyles'; 3 | import { IconButton, Button, Popover, ButtonBase, Input, Chip, Divider } from '@material-ui/core'; 4 | import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; 5 | import ArrowDropUp from '@material-ui/icons/ArrowDropUp'; 6 | import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state'; 7 | import Toast from 'src/componentCommon/toast' 8 | import { postCategorys } from 'utils/jsonSource'; 9 | import css from './publish-post.scss'; 10 | 11 | class PublishPost extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | tagInputValue: '', 17 | pubText: props.published ? '更新' : '发布' 18 | } 19 | this.tagInput = createRef(); 20 | } 21 | handleDelete = deleTag => { 22 | const { tags } = this.props; 23 | this.props.updatePostAndCb({ tags: tags.filter(tag => tag !== deleTag) }); 24 | } 25 | 26 | handlerAddTag = e => { 27 | if (e.nativeEvent.keyCode === 13) { //e.nativeEvent获取原生的事件对像 28 | const { tagInputValue } = this.state; 29 | const { tags } = this.props; 30 | 31 | this.props.updatePostAndCb({ tags: Array.from(new Set([...tags, tagInputValue])) }); 32 | this.setState({ tagInputValue: '' }); 33 | } 34 | } 35 | 36 | selectCategory = category => { 37 | this.props.updatePostAndCb({ category }); 38 | } 39 | 40 | submitPublish = () => { 41 | const { tags, category } = this.props; 42 | if (!category) { 43 | Toast.error('请选择文章分类!'); 44 | return; 45 | } 46 | 47 | if (!tags.length) { 48 | Toast.error('请至少添加一个标签!'); 49 | return; 50 | } 51 | this.props.publishPost(); 52 | } 53 | render() { 54 | const { tagInputValue, pubText } = this.state; 55 | const { tags, category, saving, published } = this.props; 56 | 57 | return ( 58 | 59 | {(popupState) => ( 60 |
61 | 62 | 73 |
74 |
{pubText}文章
75 |
76 |
分类
77 |
78 | { 79 | postCategorys.map((postCategory) => ( 80 | 83 | )) 84 | } 85 |
86 |
87 |
88 |
标签
89 | = 10} 95 | inputProps={{ 'aria-label': 'description' }} 96 | onChange={e => { 97 | this.setState({ tagInputValue: e.target.value }) 98 | }} 99 | value={tagInputValue} 100 | onKeyUp={this.handlerAddTag} 101 | /> 102 |
103 | { 104 | tags.map(tag => ) 105 | } 106 |
107 |
108 |
109 | 112 |
113 |
114 |
115 |
116 | ) 117 | } 118 |
119 | ) 120 | } 121 | } 122 | 123 | export default withStyles(css)(PublishPost) -------------------------------------------------------------------------------- /src/componentCommon/editor/publish-post.scss: -------------------------------------------------------------------------------- 1 | 2 | #publish-post-img-popup-popover{ 3 | .MuiDivider-root{ 4 | margin: 0 -24px; 5 | } 6 | .MuiPaper-root{ 7 | width: 400px; 8 | } 9 | 10 | .post-submit-content{ 11 | padding-top: 14px; 12 | text-align: center; 13 | } 14 | 15 | .content-wraps{ 16 | header{ 17 | font-size: 16px; 18 | color: #909090; 19 | } 20 | } 21 | } 22 | 23 | .tags-list{ 24 | padding: 10px 0; 25 | & > div{ 26 | margin: 2px 4px 2px 0; 27 | } 28 | } 29 | 30 | // .category-wrap{ 31 | // padding-bottom: 12px; 32 | // } 33 | 34 | .category-btn-wrap{ 35 | & > button:not(.MuiButton-outlinedPrimary){ 36 | color: #aaa; 37 | } 38 | & > button { 39 | padding: 0 8px; 40 | min-width: 42px; 41 | margin: 2px 4px 2px 0; 42 | } 43 | } 44 | .publish-post-op-btn{ 45 | .MuiButton-endIcon{ 46 | margin-left: 0; 47 | } 48 | } -------------------------------------------------------------------------------- /src/componentCommon/editor/style.scss: -------------------------------------------------------------------------------- 1 | @import './codemirror.scss'; 2 | @import './theme.scss'; 3 | @import './hljs.scss'; 4 | @import './preview-content.scss'; 5 | 6 | .editor-header { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | padding: 0 1.4rem; 15 | height: 4rem; 16 | background-color: #fff; 17 | border-bottom: 1px solid #ddd; 18 | z-index: 100; 19 | 20 | .editor-title { 21 | margin: 0; 22 | padding: 0; 23 | font-size: 1.5rem; 24 | font-weight: 700; 25 | color: #000; 26 | border: none; 27 | outline: none; 28 | } 29 | 30 | .header-right-area{ 31 | display: flex; 32 | align-items: center; 33 | 34 | .save-tips{ 35 | color: #aaa; 36 | 37 | .MuiButton-root{ 38 | min-width: 44px; 39 | margin-left: 6px; 40 | font-size: 14px; 41 | padding:0; 42 | color: #aaa; 43 | } 44 | } 45 | } 46 | 47 | 48 | } 49 | 50 | .editor-main { 51 | display: flex; 52 | position: absolute; 53 | top: 4rem; 54 | left: 0; 55 | right: 0; 56 | bottom: 0; 57 | overflow: hidden; 58 | 59 | .editor-box { 60 | display: flex; 61 | flex-direction: column; 62 | background-color: #f8f9fa; 63 | flex: 1 1 50%; 64 | height: 100%; 65 | overflow-y: hidden; 66 | 67 | .react-codemirror2{ 68 | height: calc(100% - 40px); 69 | overflow: auto; 70 | 71 | &.editor-fullscreen{ 72 | width: 920px; 73 | margin: 0 auto; 74 | } 75 | } 76 | 77 | .raw-editor-content { 78 | height: calc(100% - 40px); 79 | padding: 20px 32px; 80 | overflow-y: auto; 81 | } 82 | } 83 | 84 | 85 | 86 | .bottom-tool-bar{ 87 | display: flex; 88 | justify-content: space-between; 89 | line-height: 40px; 90 | height: 40px; 91 | padding: 0 8px; 92 | background: #fff; 93 | border-top: 1px solid #ddd; 94 | 95 | .MuiIconButton-root{ 96 | color: #aaa; 97 | padding: 8px; 98 | } 99 | 100 | .upload-content{ 101 | input{ 102 | display: none; 103 | } 104 | } 105 | 106 | 107 | &.toRight{ 108 | .fullscreen{ 109 | transform: rotate(90deg); 110 | } 111 | } 112 | 113 | &.toLeft{ 114 | .fullscreen{ 115 | transform: rotate(-90deg); 116 | } 117 | } 118 | 119 | &.preview-tool-bar{ 120 | color: #ddd; 121 | } 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /src/componentCommon/editor/upload-header-image.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import withStyles from 'isomorphic-style-loader/withStyles'; 3 | import { IconButton, Button, Box, Popover, ButtonBase } from '@material-ui/core'; 4 | import ImageIcon from '@material-ui/icons/Image'; 5 | import DeleteIcon from '@material-ui/icons/Delete'; 6 | import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state'; 7 | import css from './upload-header-image.scss'; 8 | 9 | class UploadHeaderImage extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | } 14 | handerUploadHeaderImage = async (e) => { 15 | const formData = new FormData(); 16 | const image = e.target.files[0]; 17 | formData.append('images', image); 18 | // Toast.loading('上传中'); 19 | const res = await this.props.uploadHeaderImage(formData); 20 | if (res && res.success) { 21 | this.props.updatePostAndCb({ headerBg: res.data.url }); 22 | } 23 | } 24 | 25 | handlerDeleteHeaderImage = () => { 26 | this.props.updatePostAndCb({ headerBg: '' }); 27 | } 28 | render() { 29 | const { headerBg } = this.props; 30 | return ( 31 | 32 | {(popupState) => ( 33 |
34 | 35 | 36 | 37 | 48 |
49 |
添加封面大图
50 | { 51 | headerBg 52 | ? 53 |
54 | 55 | 56 | 57 | 58 |
59 | : 60 |
61 | 67 | 76 |
77 | } 78 |
79 |
80 |
81 | )} 82 |
83 | ) 84 | } 85 | } 86 | 87 | export default withStyles(css)(UploadHeaderImage) -------------------------------------------------------------------------------- /src/componentCommon/editor/upload-header-image.scss: -------------------------------------------------------------------------------- 1 | .MuiPopover-paper{ 2 | padding: 24px; 3 | 4 | .title{ 5 | font-size: 18px; 6 | font-weight: 700; 7 | color: hsla(218,9%,51%,.8); 8 | margin-bottom: 14px; 9 | } 10 | 11 | .upload-content{ 12 | & > .image-content { 13 | position: relative; 14 | & > .delete-header-image{ 15 | position: absolute; 16 | right: 10px; 17 | top: 10px; 18 | color: #fff; 19 | background: rgba(0,0,0,.5); 20 | 21 | &:hover{ 22 | background: rgba(0,0,0,1); 23 | } 24 | } 25 | & > img{ 26 | width: 240px!important; 27 | display: block; 28 | } 29 | } 30 | } 31 | 32 | #upload-header-image{ 33 | display: none; 34 | } 35 | } 36 | .MuiButtonBase-root{ 37 | 38 | &.upload-header-btn{ 39 | width: 240px; 40 | padding: 48px 20px; 41 | 42 | color: rgba(51,51,51,.4); 43 | font-size: 16px; 44 | background-color: hsla(0,0%,87%,.6); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/componentCommon/empty/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import withStyles from 'isomorphic-style-loader/withStyles'; 3 | import css from './style.scss'; 4 | 5 | const Empty = () => ( 6 |
7 |
8 | 9 |
10 |
暂无数据
11 |
12 | ) 13 | 14 | export default withStyles(css)(Empty) -------------------------------------------------------------------------------- /src/componentCommon/empty/style.scss: -------------------------------------------------------------------------------- 1 | .empty-wrap{ 2 | padding: 12px; 3 | background: #fff; 4 | } 5 | .svg-wrap{ 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | opacity: 0.6; 11 | } 12 | 13 | .tips-text{ 14 | padding-top: 6px; 15 | text-align: center; 16 | } 17 | .empty-img-default{ 18 | max-height: 100px; 19 | } 20 | 21 | .empty-img-default-ellipse { 22 | fill-opacity: .8; 23 | fill: #f5f5f5; 24 | } 25 | 26 | .empty-img-default-path-1 { 27 | fill: #aeb8c2; 28 | } 29 | 30 | .empty-img-default-path-2 { 31 | fill: url(#linearGradient-1); 32 | } 33 | 34 | .empty-img-default-path-3 { 35 | fill: #f5f5f7; 36 | } 37 | 38 | .empty-img-default-path-4 { 39 | fill: #dce0e6; 40 | } 41 | 42 | .empty-img-default-path-5 { 43 | fill: #dce0e6; 44 | } 45 | 46 | .empty-img-default-g { 47 | fill: #fff; 48 | } 49 | -------------------------------------------------------------------------------- /src/componentCommon/post-card/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { timeTransfor } from 'src/utils/helper'; 3 | import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; 4 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 5 | import VisibilityIcon from '@material-ui/icons/Visibility'; 6 | 7 | export default ({ toDetail, item }) => { 8 | return ( 9 |
  • 10 |
    11 |
    12 | {item.author.username} 13 | 14 | 15 | {timeTransfor(new Date(item.createdAt))} 16 | 17 | 18 | {item.category+'/'+item.tags.join('/')} 19 |
    20 |

    21 | {item.title} 22 |

    23 |
    24 | {item.read} 25 | {item.likes} 26 | {item.comments} 27 |
    28 |
    29 |
    30 | {item.headerBg ? :
    } 31 |
    32 |
    33 |
  • 34 | ) 35 | } -------------------------------------------------------------------------------- /src/componentCommon/tdk.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | export default page => { 5 | return ( 6 | 7 | {page.title} 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/componentCommon/toast/index.js: -------------------------------------------------------------------------------- 1 | import Toast from './toast' 2 | // import './icons' 3 | export default Toast -------------------------------------------------------------------------------- /src/componentCommon/toast/notice.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Notice extends Component { 4 | render() { 5 | const icons = { 6 | info: 'icon-info-circle-fill', 7 | success: 'icon-check-circle-fill', 8 | warning: 'icon-warning-circle-fill', 9 | error: 'icon-close-circle-fill', 10 | loading: 'icon-loading' 11 | } 12 | const { type, content } = this.props 13 | return ( 14 |
    15 | 18 | {content} 19 |
    20 | ) 21 | } 22 | } 23 | 24 | export default Notice -------------------------------------------------------------------------------- /src/componentCommon/toast/notification.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { CSSTransition, TransitionGroup } from 'react-transition-group' 4 | import Notice from './notice' 5 | 6 | class Notification extends Component { 7 | constructor() { 8 | super() 9 | this.transitionTime = 300 10 | this.state = { notices: [] } 11 | this.removeNotice = this.removeNotice.bind(this) 12 | } 13 | 14 | getNoticeKey() { 15 | const { notices } = this.state 16 | return `notice-${new Date().getTime()}-${notices.length}` 17 | } 18 | 19 | addNotice(notice) { 20 | const { notices } = this.state 21 | notice.key = this.getNoticeKey() 22 | if (notices.every(item => item.key !== notice.key)) { 23 | if (notices.length > 0 && notices[notices.length - 1].type === 'loading') { 24 | notices.push(notice) 25 | setTimeout(() => { 26 | this.setState({ notices }) 27 | }, this.transitionTime) 28 | } else { 29 | notices.push(notice) 30 | this.setState({ notices }) 31 | } 32 | if (notice.duration > 0) { 33 | setTimeout(() => { 34 | this.removeNotice(notice.key) 35 | }, notice.duration) 36 | } 37 | } 38 | return () => { 39 | this.removeNotice(notice.key) 40 | } 41 | } 42 | 43 | removeNotice(key) { 44 | const { notices } = this.state 45 | this.setState({ 46 | notices: notices.filter(notice => { 47 | if (notice.key === key) { 48 | if (notice.onClose) setTimeout(notice.onClose, this.transitionTime) 49 | return false 50 | } 51 | return true 52 | }) 53 | }) 54 | } 55 | 56 | render() { 57 | const { notices } = this.state 58 | return ( 59 | 60 | {notices.map((notice) => ( 61 | 66 | 67 | 68 | ))} 69 | 70 | ) 71 | } 72 | } 73 | 74 | function createNotification() { 75 | if(__SERVER__) return; 76 | const div = document.createElement('div') 77 | document.body.appendChild(div) 78 | const ref = React.createRef() 79 | ReactDOM.render(, div) 80 | return { 81 | addNotice(notice) { 82 | return ref.current.addNotice(notice) 83 | }, 84 | destroy() { 85 | ReactDOM.unmountComponentAtNode(div) 86 | document.body.removeChild(div) 87 | } 88 | } 89 | } 90 | 91 | export default createNotification() -------------------------------------------------------------------------------- /src/componentCommon/toast/toast.js: -------------------------------------------------------------------------------- 1 | import notificationDOM from './notification' 2 | let notification 3 | const notice = (type, content, duration = 2000, onClose) => { 4 | if (!notification) notification = notificationDOM 5 | return notification.addNotice({ type, content, duration, onClose }) 6 | } 7 | 8 | export default { 9 | info(content, duration, onClose) { 10 | return notice('info', content, duration, onClose) 11 | }, 12 | success(content, duration, onClose) { 13 | return notice('success', content, duration, onClose) 14 | }, 15 | warning(content, duration, onClose) { 16 | return notice('warning', content, duration, onClose) 17 | }, 18 | error(content, duration, onClose) { 19 | return notice('error', content, duration, onClose) 20 | }, 21 | loading(content, duration = 0, onClose) { 22 | return notice('loading', content, duration, onClose) 23 | } 24 | } -------------------------------------------------------------------------------- /src/componentHOC/async-loader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Backdrop from '@material-ui/core/Backdrop'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import proConfig from 'src/share/pro-config'; 5 | 6 | class AsyncLoader extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | COMPT: null 11 | }; 12 | } 13 | 14 | UNSAFE_componentWillMount() { 15 | //执行组件加载 16 | if (!this.state.COMPT) { 17 | this.load(this.props); 18 | } 19 | } 20 | 21 | load(props) { 22 | this.setState({ 23 | COMPT: null 24 | }); 25 | //注意这里,返回Promise对象; C.default 指向按需组件 26 | props.load().then((C) => { 27 | this.setState({ 28 | COMPT: C.default ? C.default : COMPT 29 | }); 30 | }); 31 | } 32 | 33 | render() { 34 | return this.state.COMPT ? this.props.children(this.state.COMPT) : ( 35 | 36 | 37 | 模块加载中... 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default loader => { 44 | const asyncFn = props => ( 45 | 46 | {(Comp) => } 47 | 48 | ) 49 | 50 | asyncFn[proConfig.asyncComponentKey] = true; 51 | return asyncFn 52 | } 53 | -------------------------------------------------------------------------------- /src/componentHOC/form-create.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { FormControl, Input, InputLabel, InputAdornment, FormHelperText } from '@material-ui/core'; 3 | import { validateEmail } from 'src/utils/helper'; 4 | 5 | const validateFunc = { 6 | 'email': validateEmail 7 | } 8 | export class InputFormItem extends Component { 9 | 10 | componentDidMount() { 11 | const { getField, name, label, rules, required, initState } = this.props; 12 | initState({ name, label, required, rules }); 13 | } 14 | render() { 15 | const { name, label, type, required, fullWidth, errors } = this.props; 16 | return ( 17 | 18 | {label} 19 | 20 | {errors[name] && errors[name].error && {errors[name].errorMsg}} 21 | 22 | ) 23 | } 24 | } 25 | 26 | const stateObj = { 27 | fields: {}, 28 | validation: {}, 29 | error: {} 30 | } 31 | 32 | export default WrappedComponent => 33 | class extends Component { 34 | constructor() { 35 | super(); 36 | this.state = JSON.parse(JSON.stringify(stateObj)); 37 | this.saveState = JSON.parse(JSON.stringify(stateObj)); 38 | } 39 | 40 | componentDidMount() { 41 | this.setState(this.saveState); 42 | } 43 | 44 | onChange = key => e => { 45 | const errorInfo = this.onValidData(key, e.target.value); 46 | this.setState({ 47 | fields: { 48 | ...this.state.fields, 49 | [key]: e.target.value 50 | }, 51 | error: { 52 | ...this.state.error, 53 | ...errorInfo 54 | } 55 | }); 56 | }; 57 | 58 | onValidData = (fieldName, value) => { 59 | const { validation } = this.state; 60 | const rules = validation[fieldName].rules; 61 | const required = validation[fieldName].required; 62 | let errorMsg = ''; 63 | let error = false; 64 | if (required) { 65 | if (!value) { 66 | errorMsg = `${validation[fieldName]['label']} 为必填项!`; 67 | error = true; 68 | } 69 | }; 70 | 71 | if (rules && value) { 72 | if (!validateFunc[rules](value)) { 73 | errorMsg = `${validation[fieldName]['label']} 格式不正确!`; 74 | error = true; 75 | } 76 | }; 77 | return ({ 78 | [fieldName]: { 79 | errorMsg, 80 | error 81 | } 82 | }) 83 | } 84 | 85 | validFields = fieldName => { 86 | const { fields, error } = this.state; 87 | if (fieldName) { 88 | this.setState({ 89 | error: { 90 | ...error, 91 | ...this.onValidData(fieldName, fields[fieldName]) 92 | } 93 | }); 94 | 95 | return !this.onValidData(fieldName, fields[fieldName])[fieldName].error 96 | } 97 | else { 98 | return Object.values(this.onValidAll()).every(item => !item.error); 99 | } 100 | } 101 | 102 | onValidAll = () => { 103 | const { fields } = this.state; 104 | let validResult = {}; 105 | for (let name in fields) { 106 | validResult = { ...validResult, ...this.onValidData(name, fields[name]) }; 107 | } 108 | this.setState({ 109 | error: validResult 110 | }); 111 | return validResult; 112 | } 113 | 114 | handleSubmit = () => { 115 | return ({ 116 | data: this.state.fields, 117 | valid: Object.values(this.onValidAll()).every(item => !item.error) 118 | }) 119 | }; 120 | 121 | getField = (fieldName) => { 122 | return { 123 | onChange: this.onChange(fieldName), 124 | value: this.state.fields[fieldName] 125 | }; 126 | }; 127 | 128 | getFieldValues = fieldName => fieldName ? this.state.fields[fieldName] : this.state.fields; 129 | 130 | initState = ({ name, label, required, rules }) => { 131 | this.saveState = { 132 | validation: { 133 | ...this.saveState.validation, 134 | [name]: { 135 | required, 136 | rules, 137 | label 138 | } 139 | }, 140 | fields: { 141 | ...this.saveState.fields, 142 | [name]: '' 143 | }, 144 | error: { 145 | ...this.saveState.error, 146 | [name]: {} 147 | } 148 | } 149 | } 150 | 151 | render() { 152 | const props = { 153 | ...this.props, 154 | initState: this.initState, 155 | handleSubmit: this.handleSubmit, 156 | getField: this.getField, 157 | getFieldValues: this.getFieldValues, 158 | validFields: this.validFields, 159 | errors: this.state.error 160 | }; 161 | 162 | return ; 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /src/componentHOC/with-initial-data.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { decrypt } from 'src/utils/helper'; 3 | import Tdk from 'src/componentCommon/tdk'; 4 | import Backdrop from '@material-ui/core/Backdrop'; 5 | import CircularProgress from '@material-ui/core/CircularProgress'; 6 | 7 | let _this = null; 8 | 9 | const popStateCallback = () => { 10 | // 使用popStateFn保存函数防止addEventListener重复注册 11 | if (_this && _this.getInitialProps) { 12 | _this.getInitialProps(); 13 | } 14 | }; 15 | 16 | export default SourceComponent => { 17 | return class HocComponent extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | initialData: {}, 22 | canClientFetch: false //浏览器端是否需要请求数据 23 | } 24 | } 25 | 26 | static async getInitialProps(props) { 27 | return SourceComponent.getInitialProps ? await SourceComponent.getInitialProps(props) : {}; 28 | } 29 | 30 | async getInitialProps() { 31 | const props = this.props; 32 | // const store = window.__STORE__;//从全局得到 store 33 | 34 | const initialData = props.getInitialData ? await props.getInitialData() : ( 35 | SourceComponent.getInitialProps ? await SourceComponent.getInitialProps() : {} 36 | ); 37 | 38 | this.setState({ 39 | initialData, 40 | canClientFetch: true 41 | }); 42 | } 43 | 44 | async componentDidMount() { //客户端渲染会执行 45 | _this = this; // 修正_this指向,保证_this指向当前渲染的页面组件 46 | window.addEventListener('popstate', popStateCallback); 47 | const canClientFetch = this.props.history && (this.props.history.action === 'PUSH');//路由跳转的时候可以异步请求数据 48 | if (canClientFetch) { 49 | //如果是 history PUSH 操作 则更新数据 50 | try { 51 | this.setState({ loading: true }); 52 | await this.getInitialProps(); 53 | } finally { 54 | this.setState({ loading: false }); 55 | } 56 | } 57 | } 58 | 59 | render() { 60 | const { loading } = this.state; 61 | const props = { 62 | initialData: {}, 63 | ...this.props 64 | }; 65 | 66 | if (this.state.canClientFetch) {//需要异步请求数据 67 | props.initialData = this.state.initialData || {}; 68 | } else { 69 | props.initialData = this.props.initialData; 70 | } 71 | 72 | props.initialData = this.props.initialData; 73 | 74 | if (JSON.stringify(props.initialData) === '{}') { 75 | props.initialData = SourceComponent.state(); 76 | } 77 | return ( 78 | loading ? 79 | 80 | 81 | 数据加载中... 82 | 83 | : 84 |
    85 | 86 | 87 |
    88 | ) 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/componentHOC/withListenerScroll.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from 'react-redux'; 4 | import composeHOC from 'src/utils/composeHOC'; 5 | import emitter from 'src/utils/events'; 6 | import { actions } from 'src/client/pages/user-center/redux'; 7 | import Skeleton from '@material-ui/lab/Skeleton'; 8 | import Empty from 'src/componentCommon/empty'; 9 | 10 | const HocWrap = SourceComponent => { 11 | return class HocComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | datas: { 17 | datas: [], 18 | page: {} 19 | }, 20 | loading: true, 21 | followType: '' 22 | } 23 | } 24 | 25 | async componentDidMount() { 26 | this.getDatas(1); 27 | emitter.addListener('scrollToBottom', this.loadMore); 28 | } 29 | 30 | changeFollowType = followType => { 31 | this.setState({ 32 | datas: { 33 | datas: [], 34 | page: {} 35 | }, 36 | loading: true, 37 | followType 38 | }, () => this.getDatas(1)) 39 | } 40 | 41 | loadMore = () => { 42 | const { page } = this.state.datas; 43 | if (!page.nextPage) return; 44 | 45 | this.getDatas(page.nextPage); 46 | } 47 | 48 | getDatas = async pageNo => { 49 | const { params } = this.props.match; 50 | const { datas } = this.state.datas; 51 | const { followType } = this.state; 52 | const queryData = { id: params.id, pageNo }; 53 | 54 | if (followType) { 55 | queryData['followType'] = followType; 56 | } 57 | const res = await this.props[SourceComponent.method](queryData); 58 | this.setState({ 59 | datas: { 60 | datas: [...datas, ...res.data.datas], 61 | page: res.data.page 62 | }, 63 | loading: false 64 | }); 65 | }; 66 | 67 | refreshDatas = async () => { 68 | const { params } = this.props.match; 69 | const res = await this.props[SourceComponent.method]({ id: params.id, pageNo: 1 }); 70 | this.setState({ 71 | datas: res.data 72 | }); 73 | } 74 | 75 | componentWillUnmount() { 76 | emitter.removeListener('scrollToBottom', this.loadMore); 77 | } 78 | render() { 79 | const { datas, loading, followType } = this.state; 80 | return ( 81 | loading 82 | ? 83 |
    84 |
    85 | 86 | 87 |
    88 | 89 | 90 | 91 | 92 |
    93 | : 94 | (datas.datas.length ? : ) 95 | ) 96 | } 97 | } 98 | } 99 | 100 | const mapStateToProps = state => ({ 101 | userInfo: state.userInfo 102 | }); 103 | 104 | 105 | export default SourceComponent => ( 106 | composeHOC( 107 | withRouter, 108 | connect(mapStateToProps, actions) 109 | )(HocWrap(SourceComponent)) 110 | ) -------------------------------------------------------------------------------- /src/componentLayout/header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { actions } from './redux'; 4 | import { withRouter } from "react-router-dom"; 5 | import withStyles from 'isomorphic-style-loader/withStyles'; 6 | import { AppBar, Toolbar, Typography, Button, Avatar } from '@material-ui/core'; 7 | import CreateIcon from '@material-ui/icons/Create'; 8 | import AcUnitIcon from '@material-ui/icons/AcUnit'; 9 | import composeHOC from 'src/utils/composeHOC'; 10 | import css from './style.scss'; 11 | import Toast from 'src/componentCommon/toast'; 12 | import UserMenu from './user-menu'; 13 | 14 | class Header extends Component { 15 | 16 | static async getInitialProps({ store }) { 17 | return store.dispatch(actions.getInitialData()); 18 | } 19 | 20 | componentDidMount() { 21 | if (this.props.userInfo['userInfo.getInitialData.pending'] !== false) { 22 | this.props.getInitialData(); 23 | } 24 | } 25 | 26 | goToDraft = () => { 27 | if (this.props.userInfo.username) { 28 | this.props.history.push('/editor/draft/new'); 29 | } else { 30 | Toast.error('请先登录!') 31 | this.props.history.push('/login'); 32 | } 33 | } 34 | 35 | goToHomePage = () => { 36 | this.props.history.push('/'); 37 | } 38 | 39 | render() { 40 | const { userInfo } = this.props; 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 |
    48 | { 49 | !userInfo.username ? ( 50 |
    51 | 52 | 53 |
    54 | ) : 55 | } 56 | 64 |
    65 |
    66 |
    67 | ) 68 | } 69 | } 70 | 71 | const mapStateToProps = state => ({ 72 | userInfo: state.userInfo, 73 | }); 74 | 75 | export default composeHOC( 76 | withStyles(css), 77 | withRouter, 78 | connect(mapStateToProps, actions) 79 | )(Header); -------------------------------------------------------------------------------- /src/componentLayout/redux.js: -------------------------------------------------------------------------------- 1 | import * as enRedux from 'utils/redux'; 2 | import { getInitState } from 'utils/helper'; 3 | const { action, createReducer, injectReducer } = enRedux.default; 4 | 5 | const reducerHandler = createReducer(); 6 | export const actions = { 7 | getInitialData: action({ 8 | type: 'userInfo.getInitialData', 9 | action: (http) => { 10 | return http.get('/api/userinfo') 11 | 12 | }, 13 | handler: (state, result) => { 14 | return { 15 | ...state, 16 | ...result.data.user 17 | } 18 | } 19 | },reducerHandler), 20 | }; 21 | 22 | let initState = { 23 | key: 'userInfo', 24 | state: {} 25 | }; 26 | 27 | injectReducer({ key: initState.key, reducer: reducerHandler(getInitState(initState))}); 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/componentLayout/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .app-header{ 3 | color: #07a2b9!important; 4 | background-color: rgba(255,255,255,.5)!important; 5 | backdrop-filter: blur(12px); 6 | 7 | .tool-bar{ 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | 12 | .logo-wrap{ 13 | img{ 14 | width: 150px; 15 | } 16 | } 17 | 18 | .right-tool-wrap{ 19 | display: flex; 20 | justify-content: space-between; 21 | 22 | & > button{ 23 | margin-left: 12px; 24 | } 25 | } 26 | 27 | .user-avater{ 28 | color: #fff; 29 | background-color: #673ab7; 30 | cursor: pointer; 31 | } 32 | } 33 | 34 | #user-menu-popup{ 35 | .MuiMenuItem-root{ 36 | color: #71777c; 37 | font-size: 15.6px; 38 | padding-top: 10px; 39 | padding-bottom: 10px; 40 | 41 | .MuiSvgIcon-root{ 42 | width: 0.8em; 43 | height: 0.8em; 44 | margin-right: 6px; 45 | } 46 | } 47 | 48 | } 49 | 50 | .block{ 51 | background-color: #fff; 52 | border-radius: 2px; 53 | } 54 | 55 | .shadow{ 56 | box-shadow: 0 1px 2px 0 rgba(0,0,0,.05); 57 | } 58 | 59 | .split-point{ 60 | padding: 0 4px; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/componentLayout/user-menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import Menu from '@material-ui/core/Menu'; 4 | import MenuItem from '@material-ui/core/MenuItem'; 5 | import PersonIcon from '@material-ui/icons/Person'; 6 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'; 7 | import BookmarkIcon from '@material-ui/icons/Bookmark'; 8 | import DraftsIcon from '@material-ui/icons/Drafts'; 9 | import SettingsIcon from '@material-ui/icons/Settings'; 10 | import ExitToAppIcon from '@material-ui/icons/ExitToApp'; 11 | import Cookie from 'js-cookie'; 12 | 13 | export default ({userInfo,history}) => { 14 | const switchPage = (popupState,path) => { 15 | history.push(path); 16 | popupState.close(); 17 | } 18 | 19 | const logout = (popupState) => { 20 | Cookie.set('token',''); 21 | switchPage(popupState,'/login'); 22 | } 23 | 24 | return ( 25 | 26 | {(popupState) => ( 27 | 28 | 29 | 35 | 我的主页 36 | 草稿 37 | 设置 38 | 退出登录 39 | 40 | 41 | )} 42 | 43 | ); 44 | } -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { matchPath } from "react-router"; 3 | import AsyncLoader from 'src/componentHOC/async-loader'; 4 | 5 | const routeList = [ 6 | { 7 | path: '/', 8 | component: AsyncLoader(() => import('./client/pages/home/')), 9 | exact: true 10 | }, 11 | { 12 | path: '/404', 13 | component: AsyncLoader(() => import('./client/pages/404/')), 14 | exact: true 15 | }, 16 | { 17 | path: '/register', 18 | component: AsyncLoader(() => import('./client/pages/register/')), 19 | exact: true 20 | }, 21 | { 22 | path: '/login', 23 | component: AsyncLoader(() => import('./client/pages/login/')), 24 | exact: true 25 | }, 26 | { 27 | path: '/editor/post/:id', 28 | component: AsyncLoader(() => import('./client/pages/post-editor/')), 29 | exact: true 30 | }, 31 | { 32 | path: '/editor/draft/:id', 33 | component: AsyncLoader(() => import('./client/pages/draft-editor/')), 34 | exact: true 35 | }, 36 | { 37 | path: '/post/:id', 38 | component: AsyncLoader(() => import('./client/pages/post-detail/')), 39 | exact: true 40 | }, 41 | { 42 | path: '/editor/draft', 43 | component: AsyncLoader(() => import('./client/pages/draft-list/')), 44 | exact: true 45 | }, 46 | { 47 | path: '/users/setting', 48 | component: AsyncLoader(() => import('./client/pages/user-setting/')), 49 | exact: true 50 | }, 51 | { 52 | path: '/user/:id', 53 | component: AsyncLoader(() => import('./client/pages/user-center/')), 54 | exact: true 55 | } 56 | ] 57 | 58 | const matchRoute = (path,list=routeList) =>{ 59 | let route; 60 | for(var item of list){ 61 | if(matchPath(path,item)){ 62 | route = item; 63 | break; 64 | } 65 | } 66 | return route; 67 | } 68 | 69 | 70 | export default routeList; 71 | export { matchRoute }; 72 | 73 | -------------------------------------------------------------------------------- /src/server/app/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const compression = require('compression') 3 | const timeout = require('connect-timeout'); 4 | const { createProxyMiddleware } = require('http-proxy-middleware'); 5 | const cookieParser = require('cookie-parser') ; 6 | const { asyncWrap } = require('src/utils/helper'); 7 | const reactSsr = require('src/server/middlewares/react-ssr').default; 8 | 9 | const app = express(); 10 | app.use(compression()); 11 | const TIME_OUT = 30 * 1e3; 12 | 13 | const render = async function(req,res){ 14 | global.__SERVER_TOKEN__ = req.cookies.token || ''; 15 | global.REQUEST_PATH = req.path; 16 | try{ 17 | const data = await reactSsr(req); 18 | const { html,template,context } = data; 19 | let htmlStr = template.replace("", `
    ${html}
    `); 20 | 21 | res.send(htmlStr); 22 | }catch(error){ 23 | if(error.message === '没有登录'){ 24 | res.redirect(302, '/login'); 25 | } 26 | if(error.message === '页面不存在'){ 27 | res.redirect(302, '/404'); 28 | } 29 | } 30 | 31 | } 32 | 33 | const proxyOption = { 34 | target: 'http://localhost:9999', 35 | changeOrigoin:true 36 | }; 37 | 38 | app.use(cookieParser()); 39 | 40 | if(process.env.BABEL_ENV !== 'production'){ 41 | app.use('/api', createProxyMiddleware(proxyOption)); 42 | } 43 | 44 | app.use(express.static('dist/static')); 45 | app.use(express.static('static')); 46 | 47 | // app.use('/static', express.static(__dirname + '/dist/static')); 48 | app.use(timeout(TIME_OUT)); 49 | app.use((req, res, next) => { 50 | if (!req.timedout) next(); 51 | }); 52 | 53 | app.get('*',asyncWrap(render)); 54 | app.listen(9001); 55 | 56 | console.log('server is start .9001'); -------------------------------------------------------------------------------- /src/server/middlewares/get-static-routes.js: -------------------------------------------------------------------------------- 1 | import routes from 'src/router';//得到动态路由的配置 2 | import proConfig from '../../share/pro-config'; 3 | 4 | const checkIsAsyncRoute = (component) => { 5 | return component[proConfig.asyncComponentKey]; 6 | } 7 | 8 | //将路由转换为静态路由 9 | async function getStaticRoutes() { 10 | 11 | const key ='__dynamics_route_to_static'; 12 | if (global[key]){ 13 | return global[key]; 14 | } 15 | 16 | let len = routes.length, 17 | i = 0; 18 | const staticRoutes = []; 19 | 20 | for (; i < len; i++) { 21 | let item = routes[i]; 22 | if (checkIsAsyncRoute(item.component)) { 23 | staticRoutes.push({ 24 | ...item, 25 | ...{ 26 | component: (await item.component().props.load()).default 27 | } 28 | }); 29 | } else { 30 | staticRoutes.push({ 31 | ...item 32 | }); 33 | } 34 | } 35 | global[key]=staticRoutes; 36 | return staticRoutes; //返回静态路由 37 | } 38 | 39 | export default getStaticRoutes; -------------------------------------------------------------------------------- /src/server/middlewares/react-ssr.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const { renderToString } = require('react-dom/server'); 3 | import { Provider } from "react-redux"; 4 | import { ServerStyleSheets, ThemeProvider } from '@material-ui/core/styles'; 5 | import { matchRoute } from '../../router'; 6 | import getAssets from '../../../server/common/assets'; 7 | import getStaticRoutes from '../middlewares/get-static-routes'; 8 | import { encrypt } from '../../utils/helper'; 9 | import shouldSsrList from '../../share/should-ssr-list'; 10 | 11 | import { StaticRouter, Route } from 'react-router'; 12 | import App from '../../client/app/index'; 13 | import getStore from '../../share/store'; 14 | 15 | import theme from '../../share/theme'; 16 | 17 | import StyleContext from 'isomorphic-style-loader/StyleContext'; 18 | 19 | const assetsMap = getAssets(); 20 | 21 | const shouldSsr = path => ['/login','/register'].indexOf(path) === -1; 22 | 23 | export default async (req) => { 24 | try{ 25 | let staticRoutes = await getStaticRoutes(); 26 | let targetRoute = matchRoute(req.path, staticRoutes); 27 | let template; 28 | let fetchDataFn = targetRoute ? targetRoute.component.getInitialProps : null; 29 | let fetchResult = {}; 30 | const store = getStore(); 31 | 32 | if (fetchDataFn) { 33 | fetchResult = await fetchDataFn({ store }); 34 | } 35 | 36 | if(shouldSsr(req.path)){ 37 | for (let key in shouldSsrList) { 38 | fetchResult[key] = await shouldSsrList[key].getInitialProps({ store }) 39 | } 40 | } 41 | 42 | let { page } = fetchResult || {}; 43 | 44 | let tdk = { 45 | title: '默认标题', 46 | keywords: '默认关键词', 47 | description: '默认描述' 48 | }; 49 | 50 | if (page && page.tdk) { 51 | tdk = page.tdk; 52 | } 53 | 54 | const context = { 55 | initialData: encrypt(store.getState()) 56 | }; 57 | const css = new Set(); 58 | const insertCss = (...styles) => styles.forEach(style => css.add(style._getContent())); 59 | const sheets = new ServerStyleSheets(); 60 | const html = renderToString( 61 | sheets.collect( 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | )); 72 | 73 | const materialCss = sheets.toString(); 74 | const styles = []; 75 | [...css].forEach(item => { 76 | let [mid, content] = item[0]; 77 | styles.push(``) 78 | }); 79 | 80 | template = ` 81 | 82 | 83 | 84 | ${tdk.title} 85 | 86 | 87 | 88 | ${styles.join('')} 89 | 90 | 91 | 92 | 93 | 94 | 95 | ${assetsMap.js.join('')} 96 | 99 | `; 100 | 101 | return { html, template, context, store } 102 | }catch(error){ 103 | console.log(error) 104 | throw Error(error.message); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/server/server-entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StaticRouter, Route} from 'react-router'; 3 | import App from '../client/app/index'; 4 | 5 | // import routeList from '../router'; 6 | 7 | export default ({location,context,routeList}) => { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | }; 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/share/pro-config.js: -------------------------------------------------------------------------------- 1 | //双端公用的配置文件 2 | 3 | module.exports = { 4 | wdsPort:9002,//wds 服务的运行端口 5 | nodeServerPort:9001,//node server 的监听端口 6 | asyncComponentKey:'__IS_ASYNC_COMP_FLAG__',//标志组件是否是按需加载 turn | false 7 | __IS_SSR__:true,//是否为 ssr 模式 8 | } -------------------------------------------------------------------------------- /src/share/should-ssr-list.js: -------------------------------------------------------------------------------- 1 | import Header from 'src/componentLayout/header'; 2 | 3 | export default { 4 | Header 5 | } -------------------------------------------------------------------------------- /src/share/store.js: -------------------------------------------------------------------------------- 1 | // import { createStore, applyMiddleware, compose } from 'redux'; 2 | // import thunk from 'redux-thunk'; 3 | require('@babel/polyfill'); 4 | import Cookie from 'js-cookie'; 5 | import * as enRedux from 'utils/redux'; 6 | const { createStore, asyncMiddleware, InjectReducerManager, locationReducer } = enRedux.default; 7 | import { axiosCreater } from 'utils/http/axios'; 8 | import asyncHandler from 'utils/asyncHandler'; 9 | 10 | export const axios = axiosCreater({ 11 | baseURL: __SERVER__ ? 'http://localhost:9999/' : '/', 12 | validateStatus: function (status) { 13 | return status >= 200 && status < 300 14 | }, 15 | activitySiteSuccessMiddleware: (response) => { 16 | 17 | if (response.staus === 200) { 18 | //to do 19 | } 20 | return response; 21 | }, 22 | failMiddleware: (error) => { 23 | // console.log('error.response====>>>',error.response.status); 24 | // console.log('没有登录====>>>>',error) 25 | 26 | 27 | if(!error.response ){ 28 | throw new Error('没有登录'); 29 | } 30 | if(error.response){ 31 | if(error.response.status === 401){ 32 | if (__SERVER__ === false) { 33 | location.href = '/login' 34 | } 35 | throw new Error('没有登录') 36 | } 37 | 38 | if(error.response.status === 404){ 39 | throw new Error('页面不存在') 40 | } 41 | } 42 | throw error 43 | 44 | // if (!error.response || (error.response && error.response.status === 401)) { 45 | // if (__SERVER__ === false) { 46 | // location.href = '/login' 47 | // } 48 | // throw new Error('没有登录') 49 | // } else { 50 | // throw error 51 | // } 52 | }, 53 | headers: { 54 | 'Content-Type': 'application/json' 55 | } 56 | }); 57 | 58 | axios.interceptors.request.use(function (config) { 59 | 60 | config.headers['Authorization'] = __SERVER__ ? global.__SERVER_TOKEN__ : (Cookie.get()['token'] || ''); 61 | return config; 62 | }) 63 | 64 | axios.interceptors.response.use(function (response) { 65 | const data = response.data 66 | return data 67 | }, function (error) { 68 | return Promise.reject(error) 69 | }) 70 | 71 | const middlewares = [asyncMiddleware({ 72 | http: axios, 73 | ...asyncHandler 74 | })] 75 | 76 | export default (initialState) => { 77 | // console.log(initialState) 78 | const store = createStore(null, initialState, middlewares); 79 | // console.log(store.getState()); 80 | 81 | InjectReducerManager.with(store); 82 | return store; 83 | } 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/share/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import red from '@material-ui/core/colors/red'; 3 | 4 | // 创建一个主题的实例。 5 | export default createMuiTheme({ 6 | palette: { 7 | primary: { 8 | main: '#07a2b9', 9 | }, 10 | secondary: { 11 | main: '#099da6', 12 | }, 13 | error: { 14 | main: red.A400, 15 | }, 16 | background: { 17 | default: '#fff', 18 | }, 19 | }, 20 | }); -------------------------------------------------------------------------------- /src/utils/asyncHandler.js: -------------------------------------------------------------------------------- 1 | 2 | import Toast from 'src/componentCommon/toast' 3 | 4 | function onSuccess({ type, result }) { 5 | } 6 | 7 | function onError({ type, error }) { 8 | if (typeof window === 'undefined') { 9 | global.window = null 10 | } 11 | if(window){ 12 | if (error.response) { 13 | 14 | Toast.error(error.response.data && (error.response.data.message || error.response.data.resultMsg || '服务端错误')) 15 | } else { 16 | if (error.data) { 17 | Toast.error(error.data.message || '服务端错误') 18 | // console.log(error.data.message || '服务端错误') 19 | }else{ 20 | Toast.error(error) 21 | } 22 | }; 23 | } 24 | 25 | } 26 | 27 | 28 | export default { 29 | onSuccess, 30 | onError 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/composeHOC.js: -------------------------------------------------------------------------------- 1 | import hoist from 'hoist-non-react-statics'; 2 | 3 | export default function(...fns) { 4 | return (Component) => { 5 | return fns.reduce((Cur, fn) => hoist(fn(Cur), Cur), Component); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/events.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export default new EventEmitter(); 4 | -------------------------------------------------------------------------------- /src/utils/helper.js: -------------------------------------------------------------------------------- 1 | 2 | import CryptoJS from "crypto-js"; 3 | 4 | const key = CryptoJS.enc.Utf8.parse("1234567890000000"); //16位 5 | const iv = CryptoJS.enc.Utf8.parse("1234567890000000"); 6 | 7 | const asyncWrap = fn => 8 | function asyncUtilWrap(req, res, next, ...args) { 9 | const fnReturn = fn(req, res, next, ...args) 10 | return Promise.resolve(fnReturn).catch(next) 11 | } 12 | 13 | const deepCopy = obj => JSON.parse(JSON.stringify(obj)); 14 | 15 | const encrypt = word => { 16 | let encrypted = ""; 17 | if (typeof word == "string") { 18 | const srcs = CryptoJS.enc.Utf8.parse(word); 19 | encrypted = CryptoJS.AES.encrypt(srcs, key, { 20 | iv: iv, 21 | mode: CryptoJS.mode.CBC, 22 | padding: CryptoJS.pad.Pkcs7 23 | }); 24 | } else if (typeof word == "object") { 25 | //对象格式的转成json字符串 26 | const data = JSON.stringify(word); 27 | const srcs = CryptoJS.enc.Utf8.parse(data); 28 | encrypted = CryptoJS.AES.encrypt(srcs, key, { 29 | iv: iv, 30 | mode: CryptoJS.mode.CBC, 31 | padding: CryptoJS.pad.Pkcs7 32 | }); 33 | } 34 | return encrypted.ciphertext.toString(); 35 | } 36 | 37 | const decrypt = word => { 38 | const encryptedHexStr = CryptoJS.enc.Hex.parse(word); 39 | const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr); 40 | const decrypt = CryptoJS.AES.decrypt(srcs, key, { 41 | iv: iv, 42 | mode: CryptoJS.mode.CBC, 43 | padding: CryptoJS.pad.Pkcs7 44 | }); 45 | const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); 46 | return decryptedStr.toString(); 47 | } 48 | 49 | const validateEmail = email => { 50 | let reg = /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/; 51 | return reg.test(email); 52 | } 53 | 54 | const debounce = (fun, delay) => { 55 | return function (args) { 56 | let that = this 57 | let _args = args 58 | clearTimeout(fun.id) 59 | fun.id = setTimeout(function () { 60 | fun.call(that, _args) 61 | }, delay) 62 | } 63 | } 64 | 65 | const throttle = (fun, delay) => { 66 | let last, deferTimer 67 | return function (args) { 68 | let that = this 69 | let _args = arguments 70 | let now = +new Date() 71 | if (last && now < last + delay) { 72 | clearTimeout(deferTimer) 73 | deferTimer = setTimeout(function () { 74 | last = now 75 | fun.apply(that, _args) 76 | }, delay) 77 | } else { 78 | last = now 79 | fun.apply(that, _args) 80 | } 81 | } 82 | } 83 | 84 | const timeTransfor = dateTimeStamp => { 85 | let result; 86 | let minute = 1000 * 60; 87 | let hour = minute * 60; 88 | let day = hour * 24; 89 | let halfamonth = day * 15; 90 | let month = day * 30; 91 | let now = new Date().getTime(); 92 | let diffValue = now - dateTimeStamp; 93 | if (diffValue < 0) { return; } 94 | let monthC = diffValue / month; 95 | let weekC = diffValue / (7 * day); 96 | let dayC = diffValue / day; 97 | let hourC = diffValue / hour; 98 | let minC = diffValue / minute; 99 | if (monthC >= 1) { 100 | result = "" + parseInt(monthC) + "月前"; 101 | } 102 | else if (weekC >= 1) { 103 | result = "" + parseInt(weekC) + "周前"; 104 | } 105 | else if (dayC >= 1) { 106 | result = "" + parseInt(dayC) + "天前"; 107 | } 108 | else if (hourC >= 1) { 109 | result = "" + parseInt(hourC) + "小时前"; 110 | } 111 | else if (minC >= 1) { 112 | result = "" + parseInt(minC) + "分钟前"; 113 | } else { 114 | result = "刚刚"; 115 | } 116 | return result; 117 | } 118 | 119 | const openInNewTab = (href, blank) => { 120 | const a = document.createElement('a'); 121 | a.setAttribute('href', href); 122 | if(blank){ 123 | a.setAttribute('target', '_blank'); 124 | } 125 | a.click(); 126 | } 127 | 128 | const getInitState = ({key,state}) => { 129 | 130 | if(typeof(window) !== 'undefined' && key === 'userInfo' ){ 131 | const { userInfo } = JSON.parse(decrypt(JSON.parse(document.getElementById('ssrTextInitData').value).initialData)); 132 | return userInfo 133 | } 134 | 135 | if(typeof(window) === 'undefined' || !window.__INITIAL_DATA__) return state; 136 | 137 | return __INITIAL_DATA__[key]; 138 | } 139 | 140 | export { 141 | asyncWrap, 142 | deepCopy, 143 | encrypt, 144 | decrypt, 145 | validateEmail, 146 | debounce, 147 | throttle, 148 | timeTransfor, 149 | openInNewTab, 150 | getInitState 151 | } -------------------------------------------------------------------------------- /src/utils/http/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const activitySiteSuccessMiddleware = (response) => { 4 | if (response.staus === 200) { 5 | //to do 6 | } 7 | return response; 8 | } 9 | 10 | const activitySiteFailMiddleware = (error) => { 11 | // 此处页面即使已经跳转到login仍然会进入。 12 | if (error.response.status === 403) { 13 | location.href= __BASENAME__ + 'login'; 14 | throw new Error('没有登录'); 15 | } 16 | return Promise.reject(error); 17 | } 18 | 19 | export const axiosCreater = (projectHttpConfig) => { 20 | const config = {}; 21 | const instance = axios.create({ 22 | ...config, 23 | ...projectHttpConfig 24 | }); 25 | // 添加各个项目的拦截器 26 | instance.interceptors.response.use( 27 | projectHttpConfig.successMiddleware || activitySiteSuccessMiddleware, 28 | projectHttpConfig.failMiddleware || activitySiteFailMiddleware 29 | ) 30 | return instance; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/jsonSource.js: -------------------------------------------------------------------------------- 1 | export const postCategorys = ['前端','后端','Android','iOS','人工智能','阅读','大数据','UI','产品']; -------------------------------------------------------------------------------- /src/utils/redux/asyncMiddleware.js: -------------------------------------------------------------------------------- 1 | import { types } from './reducer'; 2 | 3 | function noop() { }; 4 | 5 | export const asyncMiddleware = ({ http, onRequest = noop, onSuccess = noop, onError = noop }) => ({ dispatch, getState }) => next => async action => { 6 | let { promise, type, ...rests } = action; 7 | 8 | if (!promise) { 9 | return next(action); 10 | }; 11 | 12 | const { REQUEST, SUCCESS, FAILURE } = types(type); 13 | const promised = promise(http, dispatch, getState); 14 | 15 | if (!promised || !promised.then) { 16 | dispatch({ type: SUCCESS, result: promised }) 17 | onSuccess({ type: SUCCESS, result: promised, ...rests }); 18 | return promised; 19 | }; 20 | 21 | dispatch({ type: REQUEST }); 22 | onRequest({ type: REQUEST, ...rests }); 23 | 24 | try { 25 | const result = await promised 26 | dispatch({ type: SUCCESS, result }); 27 | onSuccess({ type: SUCCESS, result: result, ...rests }); 28 | return result; 29 | } catch (error) { 30 | onError({ type: FAILURE, error: error, ...rests }); 31 | if (!error.response || error.response.status != 401) { 32 | console.error(error); 33 | } 34 | 35 | if (error.response) { 36 | dispatch({ type: FAILURE, error: error.response.data }); 37 | } else { 38 | dispatch({ type: FAILURE, error: error }); 39 | } 40 | throw error; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/redux/createStore.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore as _createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { makeRootReducer } from './reducer'; 4 | 5 | export const createStore = (reducer, initialState, middlewares = []) => { 6 | const middls = [ thunk, ...middlewares ]; 7 | let composeEnhancers = compose; 8 | 9 | if (typeof(window) !== 'undefined' && !__IS_PROD__) { 10 | const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 11 | if (typeof composeWithDevToolsExtension === 'function') { 12 | composeEnhancers = composeWithDevToolsExtension; 13 | } 14 | } 15 | 16 | const store = _createStore( 17 | makeRootReducer(reducer), 18 | {}, 19 | composeEnhancers( 20 | applyMiddleware(...middls), 21 | ) 22 | ); 23 | 24 | store.asyncReducers = {}; 25 | return store; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/redux/index.js: -------------------------------------------------------------------------------- 1 | import * as createStore from './createStore'; 2 | import * as reducer from './reducer'; 3 | import * as asyncMiddleware from './asyncMiddleware'; 4 | import * as preload from './preload'; 5 | 6 | export default { 7 | ...createStore, 8 | ...reducer, 9 | ...asyncMiddleware, 10 | ...preload 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/utils/redux/location.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const LOCATION_CHANGE = 'LOCATION_CHANGE' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function locationChange (location = '/') { 10 | return { 11 | type : LOCATION_CHANGE, 12 | payload : location 13 | } 14 | } 15 | 16 | // ------------------------------------ 17 | // Specialized Action Creator 18 | // ------------------------------------ 19 | export const updateLocation = ({ dispatch }) => { 20 | return (nextLocation) => dispatch(locationChange(nextLocation)) 21 | } 22 | 23 | // ------------------------------------ 24 | // Reducer 25 | // ------------------------------------ 26 | const initialState = {} 27 | export function locationReducer(state = initialState, action) { 28 | return action.type === LOCATION_CHANGE 29 | ? action.payload 30 | : state 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/utils/redux/preload.js: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router'; 2 | export function preload({ history, routes, location}, cb) { 3 | routes = typeof routes == 'function' ? routes(dispatch, getState) : routes; 4 | 5 | matchPath({ history, routes, location }, (error, redirect, props) => { 6 | 7 | const { components, location, params } = props; 8 | 9 | dispatch({ type: 'page.preload.start' }); 10 | 11 | preload(components, { dispatch, getState, ...props }).then(() => { 12 | 13 | dispatch({ type: 'page.preload.finish' }); 14 | cb(); 15 | 16 | }).catch(error => { 17 | dispatch({ type: 'page.preload.error', error }); 18 | }) 19 | }); 20 | } 21 | 22 | export function preloadReducer(state = {prevs: []}, action = {}) { 23 | switch (action.type) { 24 | case 'page.preload.start': 25 | return { ...state, pending: true, error: false }; 26 | case 'page.preload.finish': 27 | return { ...state, pending: false, pended: true, error: false }; 28 | case 'page.preload.error': 29 | return { ...state, pending: false, error: action.error }; 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/utils/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import * as loca from './location'; 3 | 4 | // import { set, has } from 'lodash'; 5 | const set = require('lodash.set'); 6 | const has = require('lodash.has'); 7 | const combine = combineReducers; 8 | 9 | export let initReducers = { 10 | location: loca.locationReducer 11 | }; 12 | 13 | export const makeRootReducer = (reducers = {}) => { 14 | initReducers = { ...initReducers, ...reducers }; 15 | 16 | return combineReducers({ 17 | ...initReducers 18 | }); 19 | }; 20 | 21 | export const combineReducersRecurse = function (reducers) { 22 | if (typeof reducers === 'function') { 23 | return reducers 24 | } 25 | 26 | if (typeof reducers === 'object') { 27 | let combinedReducers = {} 28 | for (let key of Object.keys(reducers)) { 29 | combinedReducers[key] = combineReducersRecurse(reducers[key]) 30 | } 31 | return combine(combinedReducers) 32 | } 33 | 34 | throw new Error({ 35 | message: 'Invalid item in reducer tree', 36 | item: reducers 37 | }) 38 | } 39 | 40 | export let InjectReducerManager = { 41 | store: {}, 42 | loadReducer: [], 43 | with: function (store) { 44 | this.injectReducer = this.create(store) 45 | this.store = store; 46 | this.setLoadReducer(); 47 | }, 48 | create: (store) => ({ key, reducer, ...reducers }) => { 49 | if (has(store.asyncReducers, key)) return 50 | if (!key) { 51 | 52 | Object.keys(reducers).forEach(key => { 53 | set(store.asyncReducers, key, reducers[key]) 54 | }) 55 | } else { 56 | 57 | set(store.asyncReducers, key, reducer) 58 | } 59 | store.replaceReducer(combineReducersRecurse({ ...initReducers, ...store.asyncReducers })); 60 | 61 | // console.log(reducer) 62 | // if(key === 'listPage'){ 63 | // if(!__SERVER__){ 64 | // reducer(__INITIAL_DATA__.listPage,{type:'listPage.initCompoentReduxFromServer',result:__INITIAL_DATA__.listPage}) 65 | // } 66 | // } 67 | }, 68 | injectReducer: function ({ key, reducer, ...reducers }) { 69 | this.loadReducer.push({ key, reducer, ...reducers }); 70 | }, 71 | setLoadReducer: function () { 72 | this.loadReducer.forEach(reducer => { 73 | this.injectReducer(reducer); 74 | }); 75 | } 76 | } 77 | 78 | export const injectReducer = ({ key, reducer, ...reducers }) => { 79 | InjectReducerManager.injectReducer({ key, reducer, ...reducers }) 80 | } 81 | 82 | export const createReducer = () => { 83 | const reducer = (initState = {}) => (state = initState, action = {}) => { 84 | const handler = reducer[action.type] 85 | if (!handler) { return state } 86 | 87 | return handler(state, action.result || action.error) 88 | } 89 | 90 | return reducer; 91 | } 92 | 93 | 94 | export const action = ({ type, action, handler }, reducerCreator) => { 95 | const { REQUEST, SUCCESS, FAILURE } = types(type) 96 | 97 | reducerCreator[REQUEST] = (state, result) => ({ 98 | ...state, 99 | [`${type}.pending`]: true, 100 | [`${type}.error`]: undefined 101 | }) 102 | 103 | reducerCreator[SUCCESS] = (state, result) => ({ 104 | ...state, 105 | ...handler(state, result), 106 | [`${type}.pending`]: false, 107 | [`${type}.error`]: undefined 108 | }) 109 | 110 | reducerCreator[FAILURE] = (state, error) => ({ 111 | ...state, 112 | [`${type}.pending`]: false, 113 | [`${type}.error`]: error 114 | }) 115 | 116 | return (...args) => ({ 117 | type, 118 | promise: (http, dispatch, getState) => action.apply(this, args.concat([http, dispatch, getState])) 119 | }) 120 | } 121 | 122 | export const types = (type) => ({ 123 | REQUEST: `async.action.request.${type}`, 124 | SUCCESS: `async.action.success.${type}`, 125 | FAILURE: `async.action.failure.${type}` 126 | }) 127 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zev91/simplog-front-end/6d3802c9129b773545f65c4e5293c3d0be5c99c6/static/favicon.ico --------------------------------------------------------------------------------