├── .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 |
85 |
86 |
{moment(item.createdAt).format("YYYY-MM-DD HH:mm")}
87 |
88 | {(popupState) => (
89 |
90 |
91 |
92 |
93 |
94 |
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 |
47 |
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 |
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 |
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 |
86 |
87 |
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 |
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 |
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 |
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 |
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 |
:
} {...bindTrigger(popupState)}>{pubText}
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 |
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 |
}
61 | >
62 | 写文章
63 |
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 |
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
--------------------------------------------------------------------------------