├── .dockerignore
├── src
├── router
│ ├── links.js
│ ├── index.js
│ └── routes.js
├── views
│ ├── Home
│ │ ├── store
│ │ │ ├── actionTypes.js
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ └── actionCreators.js
│ │ ├── components
│ │ │ └── Card
│ │ │ │ └── index.js
│ │ ├── index.less
│ │ └── index.js
│ ├── Summary
│ │ ├── store
│ │ │ ├── actionTypes.js
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ └── actionCreators.js
│ │ ├── index.less
│ │ └── index.js
│ └── Detail
│ │ ├── store
│ │ ├── actionTypes.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── actionCreators.js
│ │ ├── index.less
│ │ └── index.js
├── favicon.ico
├── components
│ ├── index.js
│ ├── WithStyles
│ │ └── index.js
│ ├── Header
│ │ ├── index.js
│ │ └── index.less
│ └── Footer
│ │ ├── index.less
│ │ └── index.js
├── utils
│ ├── utils.js
│ └── request.js
├── app.js
├── layouts
│ ├── index.js
│ └── basicLayout
│ │ ├── index.js
│ │ └── index.less
├── .editorConfig
├── common
│ └── style
│ │ ├── common.css
│ │ └── reset.css
├── store
│ ├── reducers.js
│ └── index.js
├── server-entry.js
├── client-entry.js
└── index.ejs
├── config
├── dev.env.js
├── prod.env.js
├── httpsConfig.js
├── devServer.js
├── webpack.config.server.js
├── util.js
├── webpack.config.base.js
└── webpack.config.client.js
├── static
└── images
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 14.png
│ ├── 15.png
│ ├── 16.png
│ ├── 17.png
│ ├── 18.png
│ ├── 19.png
│ ├── 2.png
│ ├── 20.png
│ ├── 21.png
│ ├── 22.png
│ ├── 23.png
│ ├── 24.png
│ ├── 25.png
│ ├── 26.png
│ ├── 27.png
│ ├── 28.png
│ ├── 29.png
│ ├── 3.png
│ ├── 30.png
│ ├── 31.png
│ ├── 32.png
│ ├── 33.png
│ ├── 34.png
│ ├── 35.png
│ ├── 36.png
│ ├── 37.png
│ ├── 38.png
│ ├── 39.png
│ ├── 4.png
│ ├── 40.png
│ ├── 41.png
│ ├── 42.png
│ ├── 43.png
│ ├── 44.png
│ ├── 45.png
│ ├── 46.png
│ ├── 47.png
│ ├── 48.png
│ ├── 49.png
│ ├── 5.png
│ ├── 50.png
│ ├── 51.png
│ ├── 52.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ ├── 9.png
│ ├── rss.png
│ └── zoo-team.png
├── bash.sh
├── .babelrc
├── .gitignore
├── server
├── ignore.js
├── process.json
├── index.js
└── render.js
├── Dockerfile
├── nginx.conf
├── LICENSE
├── README.md
├── .eslintrc
└── package.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | npm-debug.log
4 | dist
--------------------------------------------------------------------------------
/src/router/links.js:
--------------------------------------------------------------------------------
1 | export default [{ title: 'zoo weekly', path: '/' }];
2 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"development"',
3 | };
4 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"',
3 | };
4 |
--------------------------------------------------------------------------------
/src/views/Home/store/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const HOME_LIST = 'home/getList';
2 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/views/Summary/store/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const SUMMARY = 'summary/overview';
2 |
--------------------------------------------------------------------------------
/static/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/1.png
--------------------------------------------------------------------------------
/static/images/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/10.png
--------------------------------------------------------------------------------
/static/images/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/11.png
--------------------------------------------------------------------------------
/static/images/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/12.png
--------------------------------------------------------------------------------
/static/images/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/13.png
--------------------------------------------------------------------------------
/static/images/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/14.png
--------------------------------------------------------------------------------
/static/images/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/15.png
--------------------------------------------------------------------------------
/static/images/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/16.png
--------------------------------------------------------------------------------
/static/images/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/17.png
--------------------------------------------------------------------------------
/static/images/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/18.png
--------------------------------------------------------------------------------
/static/images/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/19.png
--------------------------------------------------------------------------------
/static/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/2.png
--------------------------------------------------------------------------------
/static/images/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/20.png
--------------------------------------------------------------------------------
/static/images/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/21.png
--------------------------------------------------------------------------------
/static/images/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/22.png
--------------------------------------------------------------------------------
/static/images/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/23.png
--------------------------------------------------------------------------------
/static/images/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/24.png
--------------------------------------------------------------------------------
/static/images/25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/25.png
--------------------------------------------------------------------------------
/static/images/26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/26.png
--------------------------------------------------------------------------------
/static/images/27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/27.png
--------------------------------------------------------------------------------
/static/images/28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/28.png
--------------------------------------------------------------------------------
/static/images/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/29.png
--------------------------------------------------------------------------------
/static/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/3.png
--------------------------------------------------------------------------------
/static/images/30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/30.png
--------------------------------------------------------------------------------
/static/images/31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/31.png
--------------------------------------------------------------------------------
/static/images/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/32.png
--------------------------------------------------------------------------------
/static/images/33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/33.png
--------------------------------------------------------------------------------
/static/images/34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/34.png
--------------------------------------------------------------------------------
/static/images/35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/35.png
--------------------------------------------------------------------------------
/static/images/36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/36.png
--------------------------------------------------------------------------------
/static/images/37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/37.png
--------------------------------------------------------------------------------
/static/images/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/38.png
--------------------------------------------------------------------------------
/static/images/39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/39.png
--------------------------------------------------------------------------------
/static/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/4.png
--------------------------------------------------------------------------------
/static/images/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/40.png
--------------------------------------------------------------------------------
/static/images/41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/41.png
--------------------------------------------------------------------------------
/static/images/42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/42.png
--------------------------------------------------------------------------------
/static/images/43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/43.png
--------------------------------------------------------------------------------
/static/images/44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/44.png
--------------------------------------------------------------------------------
/static/images/45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/45.png
--------------------------------------------------------------------------------
/static/images/46.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/46.png
--------------------------------------------------------------------------------
/static/images/47.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/47.png
--------------------------------------------------------------------------------
/static/images/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/48.png
--------------------------------------------------------------------------------
/static/images/49.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/49.png
--------------------------------------------------------------------------------
/static/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/5.png
--------------------------------------------------------------------------------
/static/images/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/50.png
--------------------------------------------------------------------------------
/static/images/51.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/51.png
--------------------------------------------------------------------------------
/static/images/52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/52.png
--------------------------------------------------------------------------------
/static/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/6.png
--------------------------------------------------------------------------------
/static/images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/7.png
--------------------------------------------------------------------------------
/static/images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/8.png
--------------------------------------------------------------------------------
/static/images/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/9.png
--------------------------------------------------------------------------------
/static/images/rss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/rss.png
--------------------------------------------------------------------------------
/static/images/zoo-team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcy-inc/weekly-front/HEAD/static/images/zoo-team.png
--------------------------------------------------------------------------------
/bash.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | rsync -av --delete -e ssh --exclude={'./node_modules','.*'} ./dist/* 用户@服务器ip:/root/weekly/weekly-client/
3 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import routesConfig from './routes';
2 | import links from './links';
3 |
4 | export { routesConfig, links };
5 |
--------------------------------------------------------------------------------
/src/views/Detail/store/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const WEEKLY_DETAIL = 'detail/getDetail';
2 |
3 | export const RESET_DETAIL = 'detail/resetDetail';
4 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Header from './Header';
2 | import Footer from './Footer';
3 | import WithStyles from './WithStyles';
4 |
5 | export { Header, Footer, WithStyles };
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react", "stage-0"],
3 | "plugins": [
4 | "syntax-dynamic-import",
5 | "transform-decorators-legacy",
6 | "add-module-exports"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | export const isNumeric = (value) => {
2 | if (typeof value === 'object') {
3 | return false;
4 | } else {
5 | return !Number.isNaN(Number(value));
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/config/httpsConfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | isHttps: false, // 是否开启https
3 | // https key
4 | httpsOption: {
5 | key: 'weekly.zoo.team.key',
6 | cert: 'weekly.zoo.team_bundle.crt',
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/views/Detail/store/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducer';
2 | import * as actionCreators from './actionCreators';
3 | import * as actionTypes from './actionTypes';
4 |
5 | export { reducer, actionCreators, actionTypes };
6 |
--------------------------------------------------------------------------------
/src/views/Home/store/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducer';
2 | import * as actionCreators from './actionCreators';
3 | import * as actionTypes from './actionTypes';
4 |
5 | export { reducer, actionCreators, actionTypes };
6 |
--------------------------------------------------------------------------------
/src/views/Summary/store/index.js:
--------------------------------------------------------------------------------
1 | import reducer from './reducer';
2 | import * as actionCreators from './actionCreators';
3 | import * as actionTypes from './actionTypes';
4 |
5 | export { reducer, actionCreators, actionTypes };
6 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | // 后端服务的ip和端口。3030在后端项目中写死,可以到后端项目中修改
3 | const BASE_URL = 'http://localhost:3030';
4 | const instance = axios.create({
5 | baseURL: BASE_URL,
6 | });
7 |
8 | export default instance;
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | router-config.js*
5 | npm-debug.log*
6 | package-lock.json*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | .history
14 | jsconfig.json
15 | *.suo
16 | *.ntvs*
17 | *.njsproj
18 | *.sln
19 |
--------------------------------------------------------------------------------
/src/views/Summary/index.less:
--------------------------------------------------------------------------------
1 | .info-wrapper {
2 | padding: 20px;
3 | text-align: center;
4 | font-size: 20px;
5 | line-height: 24px;
6 | color: #796e6e;
7 | }
8 |
9 | .summary-title {
10 | font-size: 30px;
11 | line-height: 36px;
12 | text-align: center;
13 | padding: 20px;
14 | color: brown;
15 | }
16 |
--------------------------------------------------------------------------------
/server/ignore.js:
--------------------------------------------------------------------------------
1 | const ignore = () => {
2 | const extensions = ['.css', '.scss', '.less', '.png', '.jpg', '.gif']; // 服务端渲染不加载的文件类型
3 | for (let i = 0, len = extensions.length; i < len; i++) {
4 | require.extensions[extensions[i]] = () => {
5 | return false;
6 | };
7 | }
8 | };
9 | module.exports = ignore;
10 |
--------------------------------------------------------------------------------
/src/views/Home/store/reducer.js:
--------------------------------------------------------------------------------
1 | import { HOME_LIST } from './actionTypes';
2 |
3 | const initState = {
4 | cardList: [],
5 | };
6 |
7 | export default (state = initState, action) => {
8 | switch (action.type) {
9 | case HOME_LIST:
10 | return {
11 | ...state,
12 | cardList: [...action.payload.list],
13 | };
14 | default:
15 | return state;
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/views/Summary/store/reducer.js:
--------------------------------------------------------------------------------
1 | import { SUMMARY } from './actionTypes';
2 |
3 | const initState = {
4 | overview: {},
5 | };
6 |
7 | export default (state = initState, action) => {
8 | switch (action.type) {
9 | case SUMMARY:
10 | return {
11 | ...state,
12 | overview: action.payload.overview,
13 | };
14 | default:
15 | return state;
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Layout from './layouts';
5 |
6 | const createApp = ({ store }) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default createApp;
17 |
--------------------------------------------------------------------------------
/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 | import BasicLayout from './basicLayout';
4 |
5 | class Layout extends Component {
6 | render() {
7 | return (
8 |
9 | } />
10 |
11 | );
12 | }
13 | }
14 |
15 | export default Layout;
16 |
--------------------------------------------------------------------------------
/src/.editorConfig:
--------------------------------------------------------------------------------
1 | root = true # 表示当前是项目根目录
2 |
3 | [*] # 所有文件都使用配置
4 | charset = utf-8 # 编码格式
5 | indent_style = space # Tab键缩进的样式,由space和table两种 一般代码中是space
6 | indent_size = 2 # 缩进size为2
7 | end_of_line = lf # 以lf换行 默认win为crlf mac和linux为lf
8 | insert_final_newline = true # 末尾加一行空行
9 | trim_trailing_whitespace = true # 去掉行尾多余空格
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-alpine AS builder
2 | COPY . /code
3 | WORKDIR /code
4 | RUN npm install && npm run build
5 |
6 | FROM nginx:1.21.0-alpine
7 | COPY --from=builder /code/dist/ /root/weekly/weekly-client/
8 |
9 | COPY ./nginx.conf /etc/nginx
10 |
11 | EXPOSE 80
12 |
13 | CMD sed -i "s/localhost/$SERVER_IP/g" `grep http://localhost:3030 -rl /root/weekly/weekly-client/`; nginx -g "daemon off;"
14 | # CMD nginx -g "daemon off;"
15 |
--------------------------------------------------------------------------------
/src/common/style/common.css:
--------------------------------------------------------------------------------
1 | p.cnzz_p_wrapper {
2 | background: #2e3032;
3 | color: #cccccc;
4 | font-size: 14px;
5 | position: absolute;
6 | width: 100%;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | bottom: 3rem;
11 | }
12 |
13 | #cnzz_stat_icon_1275915533 a {
14 | text-decoration: none;
15 | color: #cccccc;
16 | }
17 |
18 | .record-filling {
19 | display: block;
20 | white-space: pre;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/WithStyles/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default styles => (DecoratedComponent) => {
4 | return class NewComponent extends Component {
5 | componentWillMount() {
6 | if (this.props.staticContext) {
7 | this.props.staticContext.css.push(styles._getCss());
8 | }
9 | }
10 | render() {
11 | return ;
12 | }
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as homeReducer } from '../views/Home/store';
3 | import { reducer as detailReducer } from '../views/Detail/store';
4 | import { reducer as summaryReducer } from '../views/Summary/store';
5 |
6 | const reducer = combineReducers({
7 | homeStore: homeReducer,
8 | detailStore: detailReducer,
9 | summaryStore: summaryReducer,
10 | });
11 |
12 | export default reducer;
13 |
--------------------------------------------------------------------------------
/src/views/Detail/store/reducer.js:
--------------------------------------------------------------------------------
1 | import { WEEKLY_DETAIL, RESET_DETAIL } from './actionTypes';
2 |
3 | const initState = {
4 | weeklyDetail: [],
5 | };
6 |
7 | export default (state = initState, action) => {
8 | switch (action.type) {
9 | case WEEKLY_DETAIL:
10 | return {
11 | ...state,
12 | weeklyDetail: [...action.payload.detail],
13 | };
14 | case RESET_DETAIL:
15 | return {
16 | ...state,
17 | weeklyDetail: [],
18 | };
19 | default:
20 | return state;
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/server/process.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "weekly-ssr",
5 | "script": "./index.js",
6 | "watch": false,
7 | "args": "--web --port 4092",
8 | "interpreter": "node",
9 | "env": {
10 | "NODE_ENV": "development"
11 | },
12 | "env_production": {
13 | "NODE_ENV": "production"
14 | },
15 | "log_date_format": "YYYY-MM-DD HH:mm Z",
16 | "error_file": "./logs/process/error_file.log",
17 | "out_file": "./logs/process/out_file.log"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/views/Home/store/actionCreators.js:
--------------------------------------------------------------------------------
1 | import request from '../../../utils/request';
2 | import { HOME_LIST } from './actionTypes';
3 |
4 | const weeklyListUrl = '/api/weeks/list';
5 |
6 | export const getList = list => ({
7 | type: HOME_LIST,
8 | payload: { list },
9 | });
10 |
11 | export const getListEffect = () => (dispatch) => {
12 | return request.get(weeklyListUrl).then((res) => {
13 | dispatch(getList(res.data));
14 | });
15 | };
16 |
17 | /* 服务端请求数据 */
18 | export const homeLoadData = (store) => {
19 | return store.dispatch(getListEffect());
20 | };
21 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import rootReducer from './reducers.js';
4 |
5 | const composeEnhancers =
6 | process.env.NODE_ENV === 'development'
7 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
8 | : compose;
9 |
10 | const middleware = [thunkMiddleware];
11 |
12 | const configureStore = initialState =>
13 | createStore(
14 | rootReducer,
15 | initialState,
16 | composeEnhancers(applyMiddleware(...middleware))
17 | );
18 |
19 | export default configureStore;
20 |
--------------------------------------------------------------------------------
/src/views/Summary/store/actionCreators.js:
--------------------------------------------------------------------------------
1 | import request from '../../../utils/request';
2 | import { SUMMARY } from './actionTypes';
3 |
4 | const weeklySummaryUrl = '/api/summary/overview';
5 |
6 | export const getSummary = overview => ({
7 | type: SUMMARY,
8 | payload: { overview },
9 | });
10 |
11 | export const getSummaryEffect = () => (dispatch) => {
12 | return request.get(weeklySummaryUrl).then((res) => {
13 | dispatch(getSummary(res.data));
14 | });
15 | };
16 |
17 | /* 服务端请求数据 */
18 | export const SummaryLoadData = (store) => {
19 | return store.dispatch(getSummaryEffect());
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './index.less';
3 |
4 | export default class Header extends Component {
5 | render() {
6 | return (
7 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/server-entry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { StaticRouter } from 'react-router';
4 | import Loadable from 'react-loadable';
5 | import Layout from './layouts';
6 | import configureStore from './store';
7 | import { routesConfig } from './router';
8 |
9 | const createApp = ({ modules, store, context, url }) => {
10 | return (
11 | modules.push(moduleName)}>
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export { createApp, configureStore, routesConfig };
22 |
--------------------------------------------------------------------------------
/src/views/Home/components/Card/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | export default class Card extends Component {
5 | render() {
6 | const {
7 | data: { count = '' },
8 | } = this.props;
9 | return (
10 |
11 |
16 |
21 | 前端小报总第 {count} 期
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/layouts/basicLayout/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { NavLink, Route } from 'react-router-dom';
3 | import { Header, Footer } from '@components';
4 | import { links, routesConfig } from '../../router/';
5 | import './index.less';
6 |
7 | export default class BasicLayout extends Component {
8 | render() {
9 | return (
10 |
11 |
12 | {links.map(l => (
13 |
14 | {l.title}
15 |
16 | ))}
17 |
18 |
19 | {routesConfig.map(r => (
20 |
21 | ))}
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/config/devServer.js:
--------------------------------------------------------------------------------
1 | /* webpack-dev-server配置 */
2 | const path = require('path');
3 |
4 | const isDev = process.env.NODE_ENV === 'development';
5 |
6 | module.exports = {
7 | /* 服务根目录 默认指向项目根目录 */
8 | contentBase: isDev
9 | ? path.join(__dirname, '../')
10 | : path.join(__dirname, '../dist'),
11 | /* 所有404 定位到根路径 */
12 | historyApiFallback: true,
13 | /* 服务端口 */
14 | port: 3000,
15 | /* 热更新 */
16 | hot: true,
17 | /* 自动打开页面 */
18 | open: true,
19 | /* 数据Mock */
20 | before() {
21 | // apiMocker(app, path.resolve(__dirname, mockDataPath), {})
22 | },
23 | proxy: {
24 | // '/api': {
25 | // target: 'http://119.29.241.71:3000/',
26 | // pathRewrite: {
27 | // '^/api': ''
28 | // },
29 | // /* 支持https */
30 | // secure: false
31 | // }
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Header/index.less:
--------------------------------------------------------------------------------
1 | .hidden-title {
2 | display: none;
3 | }
4 |
5 | .zoo-header,
6 | .zoo-footer,
7 | .about {
8 | padding: 0 6%;
9 | }
10 |
11 | /* site head */
12 | .zoo-header {
13 | height: 70px;
14 | line-height: 70px;
15 | background-color: #2b303b;
16 | }
17 |
18 | .logo a {
19 | display: inline-block;
20 | color: #fff;
21 | text-decoration: none;
22 | font-size: 24px;
23 | font-family: Pacifico, cursive;
24 |
25 | &:hover {
26 | color: gainsboro;
27 | }
28 | }
29 |
30 | .logo .router-links {
31 | float: right;
32 | height: 65px;
33 |
34 | a {
35 | font-size: 18px;
36 | margin-left: 20px;
37 | }
38 |
39 | a:hover {
40 | color: gainsboro;
41 | }
42 | .rss {
43 | width: 32px;
44 | position: relative;
45 | top: 10px;
46 | &:hover{
47 | -webkit-transform:rotate(360deg);
48 | transform:rotate(360deg);
49 | -webkit-transition:-webkit-transform 0.5s linear;
50 | transition:transform 0.5s linear;
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | user root;
2 | worker_processes auto;
3 |
4 | error_log /var/log/nginx/error.log notice;
5 | pid /var/run/nginx.pid;
6 |
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 | http {
13 | include /etc/nginx/mime.types;
14 | default_type application/octet-stream;
15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16 | '$status $body_bytes_sent "$http_referer" '
17 | '"$http_user_agent" "$http_x_forwarded_for"';
18 | access_log /var/log/nginx/access.log main;
19 | sendfile on;
20 | keepalive_timeout 65;
21 | server {
22 | listen 80;
23 | listen [::]:80;
24 | server_name localhost;
25 | root /root/weekly/weekly-client/app;
26 | location / {
27 | try_files $uri /index.html;
28 | }
29 | location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
30 | {
31 | expires 30d;
32 | }
33 | location ~ .*\.(js|css)?$
34 | {
35 | expires 1h;
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/views/Home/index.less:
--------------------------------------------------------------------------------
1 | .card-wrapper {
2 | padding: 0 5%;
3 | background-color: #f4f4f4;
4 | .weekly__list {
5 | padding: 2rem 0;
6 | text-align: center;
7 | }
8 | .weekly__item {
9 | display: inline-block;
10 | margin: 1.5rem;
11 | padding: 1rem;
12 | background: #fff;
13 | border-radius: 4px;
14 | transition: transform 0.2s linear;
15 | box-shadow: 0 2px 0 rgba(170, 170, 170, 0.1);
16 | }
17 | .weekly__item__url {
18 | display: block;
19 | text-decoration: none;
20 | }
21 | .weekly__item:hover {
22 | transform: translate3d(0, -3px, 0);
23 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.1);
24 | }
25 | .weekly__item__cover {
26 | display: block;
27 | width: 240px;
28 | height: 180px;
29 | border-top-right-radius: 4px;
30 | border-top-left-radius: 4px;
31 | }
32 | .weekly__item__title {
33 | margin-top: 1rem;
34 | font-size: 1rem;
35 | font-family: Pacifico, cursive;
36 | color: #666;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/client-entry.js:
--------------------------------------------------------------------------------
1 | import { hydrate } from 'react-dom';
2 | import Loadable from 'react-loadable';
3 | import createApp from './app';
4 | import configureStore from './store';
5 | import './common/style/reset.css';
6 | import './common/style/common.css';
7 |
8 | const initialState = window && window.__INITIAL_STATE__;
9 |
10 | const store = configureStore(initialState);
11 |
12 | Loadable.preloadReady().then(() => {
13 | const App = createApp({ store });
14 | hydrate(App, document.getElementById('root'));
15 | });
16 |
17 | if (process.env.NODE_ENV === 'development') {
18 | if (module.hot) {
19 | module.hot.accept('./store/reducers.js', () => {
20 | const newReducer = require('./store/reducers.js');
21 | store.replaceReducer(newReducer);
22 | });
23 | module.hot.accept('./app.js', () => {
24 | const newReducer = require('./store/reducers.js');
25 | store.replaceReducer(newReducer);
26 | const App = require('./app.js')({ store });
27 | hydrate(App, document.getElementById('root'));
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/views/Detail/store/actionCreators.js:
--------------------------------------------------------------------------------
1 | import request from '@utils/request';
2 | import { isNumeric } from '@utils/utils';
3 | import { WEEKLY_DETAIL, RESET_DETAIL } from './actionTypes';
4 |
5 | const weeklyListUrl = '/api/list';
6 | const tagListUrl = '/api/articles/category';
7 | export const getDetail = detail => ({
8 | type: WEEKLY_DETAIL,
9 | payload: { detail },
10 | });
11 |
12 | export const resetDetail = () => ({
13 | type: RESET_DETAIL,
14 | });
15 |
16 | export const getDetailEffect = week => (dispatch) => {
17 | let url = '';
18 | let params = null;
19 | if (isNumeric(week)) {
20 | url = weeklyListUrl;
21 | params = {
22 | week,
23 | };
24 | } else {
25 | url = tagListUrl;
26 | params = {
27 | category: week,
28 | };
29 | }
30 | return request
31 | .get(url, {
32 | params,
33 | })
34 | .then((res) => {
35 | dispatch(getDetail(res.data));
36 | });
37 | };
38 |
39 | /* 服务端请求数据 */
40 | export const detailLoadData = (store, match) => {
41 | const { week } = match.params;
42 | return store.dispatch(getDetailEffect(week));
43 | };
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 政采云有限公司
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Footer/index.less:
--------------------------------------------------------------------------------
1 | /* site about */
2 |
3 | .about {
4 | display: flex;
5 | flex-wrap: wrap;
6 | justify-content: space-between;
7 | padding-top: 3rem;
8 | padding-bottom: 3rem;
9 | border-top: 1px solid #d6d6d6;
10 | background: #e8e8e8;
11 | }
12 |
13 | .about dl {
14 | width: 24%;
15 | }
16 |
17 | .about h3 {
18 | font-size: 1rem;
19 | line-height: 1.6;
20 | color: #555;
21 | text-shadow: 1px 1px 1px #fff;
22 | }
23 |
24 | .about dd {
25 | margin: 1.2rem 0;
26 | font-size: 0.9rem;
27 | line-height: 1.4;
28 | color: #555;
29 | }
30 |
31 | /* site foot */
32 |
33 | .zoo-footer {
34 | padding: 3rem;
35 | padding-bottom: 5rem;
36 | background: #2e3033;
37 | color: #ccc;
38 | font-size: 0.9rem;
39 | text-align: center;
40 | }
41 |
42 | .footer__logo {
43 | margin-left: 0.3rem;
44 | }
45 |
46 | .zoo-team__site {
47 | display: inline-block;
48 | width: 120px;
49 | height: 16px;
50 | overflow: hidden;
51 | color: transparent;
52 | background: url("/static/images/zoo-team.png") no-repeat 0 0;
53 | background-size: contain;
54 | vertical-align: bottom;
55 | }
56 |
57 | /* mobile */
58 |
59 | @media screen and (max-width: 414px) {
60 | header {
61 | text-align: center;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/router/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loadable from 'react-loadable';
3 | import { homeLoadData } from '../views/Home/store/actionCreators';
4 | import { detailLoadData } from '../views/Detail/store/actionCreators';
5 | import { summaryLoadData } from '../views/Summary/store/actionCreators';
6 |
7 | const Loading = () => {
8 | return ;
9 | };
10 |
11 | const LoadableHome = Loadable({
12 | loader: () => import(/* webpackChunkName: 'Home' */ '../views/Home'),
13 | loading: Loading,
14 | });
15 |
16 | const LoadableDetail = Loadable({
17 | loader: () => import(/* webpackChunkName: 'Detail' */ '../views/Detail'),
18 | loading: Loading,
19 | });
20 |
21 | const LoadableSummary = Loadable({
22 | loader: () => import(/* webpackChunkName: 'Summary' */ '../views/Summary'),
23 | loading: Loading,
24 | });
25 | const routesConfig = [
26 | {
27 | path: '/',
28 | exact: true,
29 | component: LoadableHome,
30 | loadData: homeLoadData,
31 | },
32 | {
33 | path: '/detail/:week',
34 | component: LoadableDetail,
35 | loadData: detailLoadData,
36 | },
37 | {
38 | path: '/summary',
39 | component: LoadableSummary,
40 | loadData: summaryLoadData,
41 | },
42 | ];
43 |
44 | export default routesConfig;
45 |
--------------------------------------------------------------------------------
/src/views/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { WithStyles } from '@components';
4 | import Card from './components/Card';
5 | import styles from './index.less';
6 | import { actionCreators } from './store';
7 |
8 | @WithStyles(styles)
9 | @connect(
10 | state => ({ homeStore: state.homeStore }),
11 | dispatch => ({
12 | getHomeList: () => dispatch(actionCreators.getListEffect()),
13 | })
14 | )
15 | export default class Home extends Component {
16 | componentDidMount() {
17 | const { cardList } = this.props.homeStore;
18 | if (!cardList.length) {
19 | this.props.getHomeList();
20 | }
21 | }
22 |
23 | render() {
24 | const { cardList } = this.props.homeStore;
25 | if (!cardList.length) return null;
26 | const reverseList = cardList.slice().reverse();
27 | return (
28 |
29 |
Weekly List
30 |
31 | {reverseList.map(card => (
32 |
37 | ))}
38 |
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import './index.less';
3 |
4 | export const whatIsThisData = [
5 | {
6 | title: '关于前端小报',
7 | info: 'Zoo Weekly! 前端小报,是政采云前端团队的周度前端文章推荐汇总。',
8 | },
9 | {
10 | title: '文章来源构成',
11 | info: ' 前端小报的所有文章来源,都是由政采云的前端同学自发推荐汇总而得。',
12 | },
13 | {
14 | title: '为什么是Zoo',
15 | info:
16 | 'Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo有生物圈的含义,希望后续政采云的前端团队,不论是人才梯队,还是技术体系,都能各面兼备,逐渐成长为一个生态。',
17 | },
18 | ];
19 |
20 | export default class Footer extends Component {
21 | render() {
22 | return (
23 |
24 |
25 | About Zoo Weekly
26 | {whatIsThisData.map(d => (
27 |
28 | -
29 |
{d.title}
30 |
31 | - {d.info}
32 |
33 | ))}
34 |
35 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require('./ignore.js')();
2 | require('babel-polyfill');
3 |
4 | const isProd = true;
5 |
6 | if (isProd) {
7 | require('babel-register')();
8 | } else {
9 | require('babel-register')({
10 | presets: ['react', 'stage-0', ['env', { targets: { node: 'current' } }]],
11 | plugins: [
12 | 'dynamic-import-node',
13 | 'react-loadable/babel',
14 | 'transform-decorators-legacy',
15 | ],
16 | });
17 | }
18 | const Koa = require('koa');
19 |
20 | const app = new Koa();
21 |
22 | const fs = require('fs');
23 | const path = require('path');
24 | const https = require('https');
25 | const http = require('http');
26 | const {
27 | isHttps,
28 | httpsOption: { key, cert },
29 | } = require('./httpsConfig');
30 |
31 | const options = {};
32 | isHttps &&
33 | Object.assign(options, {
34 | key: fs.readFileSync(key),
35 | cert: fs.readFileSync(cert),
36 | });
37 |
38 | const render = require('./render.js');
39 |
40 | const port = process.env.port || 3002;
41 | const staticCache = require('koa-static-cache');
42 | const cors = require('koa2-cors');
43 | const Loadable = require('react-loadable');
44 |
45 | app.use(cors());
46 | app.use(render);
47 | app.use(
48 | staticCache(path.resolve(__dirname, '../app'), {
49 | maxAge: 365 * 24 * 60 * 60,
50 | gzip: true,
51 | })
52 | );
53 |
54 | console.log(
55 | `\n==> 🌎 Listening on port ${port}. Open up ${
56 | isHttps ? 'https' : 'http'
57 | }://localhost:${port}/ in your browser.\n`
58 | );
59 | Loadable.preloadAll().then(() => {
60 | // app.listen(port);
61 | (isHttps ? https : http).createServer(options, app.callback()).listen(port);
62 | });
63 |
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
15 |
19 | <% if(htmlWebpackPlugin.options.isHttps) { %>
20 |
24 | <% } %>
25 | <%= htmlWebpackPlugin.options.title %>
26 |
27 |
28 |
29 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 项目描述
2 |
3 | 政采云小报前端项目,收录插件投稿的前端小报内容,便于查看,并进行相关数据统计。
4 |
5 | ## 包含的页面
6 |
7 | 前端小报首页:启动页
8 | 
9 |
10 | 前端小报详情页:/detail/${id}
11 | 
12 |
13 | 前端小报的数据统计页:/summary
14 | 
15 |
16 |
17 | # 启动
18 |
19 | 本地启动需要安装 Chrome 扩展 [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=zh-CN) (点击链接安装)
20 | 执行 `npm i` 安装依赖
21 | 在 `src/utils/request.js` 下的 `BASE_URL` 中配置您的 `ip`
22 | 执行 `npm run dev` 启动项目,该命令可以同时启动本地服务端和客户端。
23 |
24 |
25 | ## 配置您的信息
26 |
27 | 修改 `/src/components/Footer/index.js` 的组件代码、`/src/index.ejs` 的 `script` 内容,替换成您的网站信息。
28 |
29 | 
30 |
31 | # 技术选型
32 |
33 |
34 | ## 前端同构渲染
35 |
36 | 为了便于 SEO 爬取信息以及减少白屏时间的考量,我们选择前端同构渲染,使用一套代码,用户首次请求的页面在服务端渲染好,然后再是客户端渲染,也就是前端同构渲染原理。
37 |
38 | 一个填充好的 html => html string => 数据 store + 模板 route
39 |
40 | ### [ReactDOMServer](https://react.docschina.org/docs/react-dom-server.html#___gatsby)
41 |
42 | ReactDOMServer 类可以让你在服务端渲染你的组件
43 |
44 | ### [react-router](https://reacttraining.com/react-router/web/guides/server-rendering)
45 |
46 | 服务端 react 代码需要根据请求的 path 知晓渲染哪一个页面
47 |
48 | ### [redux](http://cn.redux.js.org/docs/recipes/ServerRendering.html)
49 |
50 | 服务端需要提前将不同页面对应的数据请求操作执行,提供应用所需的初始 state,并且由于在服务端渲染时已经请求过对应数据,需要避免客户端再次请求。
51 |
52 | ## 其他依赖项
53 |
54 | [React Helmet](https://github.com/nfl/react-helmet) 一个HTML文档head管理工具,管理对文档头的所有更改。
55 |
56 |
57 |
--------------------------------------------------------------------------------
/config/webpack.config.server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const nodeExternals = require('webpack-node-externals');
5 | const baseWebpackConfig = require('./webpack.config.base');
6 | const CopyWebpackPlugin = require('copy-webpack-plugin');
7 |
8 | const webpackConfig = merge(baseWebpackConfig, {
9 | entry: {
10 | app: path.resolve(__dirname, '../src/server-entry'),
11 | },
12 | output: {
13 | path: path.resolve(__dirname, '../dist/server'),
14 | filename: 'server-entry.js',
15 | libraryTarget: 'commonjs2', // 打包成commonjs2规范
16 | },
17 | target: 'node', // 指定node运行环境
18 | externals: [nodeExternals()],
19 | module: {
20 | rules: [
21 | {
22 | test: /\.jsx?$/,
23 | exclude: /node_modules/,
24 | include: path.resolve(__dirname, '../src'),
25 | use: {
26 | loader: 'babel-loader',
27 | options: {
28 | presets: [
29 | 'react',
30 | 'stage-0',
31 | ['env', { targets: { node: 'current' } }],
32 | ],
33 | plugins: [
34 | 'dynamic-import-node',
35 | 'react-loadable/babel',
36 | 'transform-decorators-legacy',
37 | ],
38 | cacheDirectory: true,
39 | },
40 | },
41 | },
42 | {
43 | test: /\.(le|c)ss$/,
44 | use: ['isomorphic-style-loader', 'css-loader', 'less-loader'],
45 | },
46 | ],
47 | },
48 | plugins: [
49 | new webpack.DefinePlugin({
50 | 'process.env.REACT_ENV': JSON.stringify('server'), // 指定React环境为服务端
51 | }),
52 | new CopyWebpackPlugin([
53 | {
54 | from: path.resolve(__dirname, '../server'),
55 | },
56 | {
57 | from: path.resolve(__dirname, '../package.json'),
58 | },
59 | {
60 | from: path.resolve(__dirname, '../.babelrc'),
61 | },
62 | {
63 | from: path.resolve(__dirname, '../config/httpsConfig.js'),
64 | },
65 | ]),
66 | ],
67 | });
68 |
69 | module.exports = webpackConfig;
70 |
--------------------------------------------------------------------------------
/config/util.js:
--------------------------------------------------------------------------------
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
2 | const autoprefixer = require('autoprefixer');
3 |
4 | const cssLoaders = (options) => {
5 | options = options || {};
6 |
7 | const cssLoader = {
8 | loader: 'css-loader',
9 | options: {
10 | sourceMap: options.sourceMap,
11 | minimize: process.env.NODE_ENV === 'production',
12 | },
13 | };
14 |
15 | const postcssLoader = {
16 | loader: 'postcss-loader',
17 | options: {
18 | sourceMap: options.sourceMap,
19 | ident: 'postcss',
20 | plugins: () => [
21 | require('postcss-flexbugs-fixes'),
22 | autoprefixer({
23 | browsers: [
24 | '>1%',
25 | 'last 4 versions',
26 | 'Firefox ESR',
27 | 'not ie < 9', // Doesn"t support IE8 anyway
28 | ],
29 | flexbox: 'no-2009',
30 | }),
31 | ],
32 | },
33 | };
34 |
35 | function generateLoaders(loader, loaderOptions) {
36 | const loaders = options.usePostCSS
37 | ? [cssLoader, postcssLoader]
38 | : [cssLoader];
39 |
40 | if (loader) {
41 | loaders.push({
42 | loader: `${loader}-loader`,
43 | options: Object.assign({}, loaderOptions, {
44 | sourceMap: options.sourceMap,
45 | }),
46 | });
47 | }
48 |
49 | // Extract CSS when that option is specified
50 | // (which is the case during production build)
51 | if (options.extract) {
52 | return ExtractTextPlugin.extract({
53 | use: loaders,
54 | fallback: 'style-loader',
55 | });
56 | } else {
57 | return ['style-loader'].concat(loaders);
58 | }
59 | }
60 |
61 | return {
62 | css: generateLoaders(),
63 | postcss: generateLoaders(),
64 | less: generateLoaders('less'),
65 | sass: generateLoaders('sass', { indentedSyntax: true }),
66 | scss: generateLoaders('sass'),
67 | stylus: generateLoaders('stylus'),
68 | styl: generateLoaders('stylus'),
69 | };
70 | };
71 |
72 | module.exports.styleLoaders = (options) => {
73 | const output = [];
74 | const loaders = cssLoaders(options);
75 |
76 | for (const extension in loaders) {
77 | if (Object.prototype.hasOwnProperty.call(loaders, extension)) {
78 | const loader = loaders[extension];
79 | output.push({
80 | test: new RegExp(`\\.${extension}$`),
81 | use: loader,
82 | });
83 | }
84 | }
85 | return output;
86 | };
87 |
--------------------------------------------------------------------------------
/config/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | /**
5 | * webpack-manifest-plugin
6 | * 该插件将生成一个manifest.json文件在输出文件夹下
7 | * 其中包含所有源文件名到其对应输出文件的映射
8 | */
9 | const ManifestPlugin = require('webpack-manifest-plugin');
10 |
11 | /**
12 | * progress-bar-webpack-plugin
13 | * 以百分比显示打包进度
14 | */
15 | const ProgressBarPlugin = require('progress-bar-webpack-plugin');
16 |
17 | /**
18 | * uglifyjs-webpack-plugin
19 | * 压缩js文件
20 | */
21 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
22 |
23 | /**
24 | * extract-text-webpack-plugin
25 | * 抽取css文件
26 | */
27 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
28 |
29 | let env = 'dev';
30 | let isProd = false;
31 | let prodPlugins = [];
32 |
33 | if (process.env.NODE_ENV === 'production') {
34 | env = 'prod';
35 | isProd = true;
36 | prodPlugins = [
37 | new ManifestPlugin(),
38 | /**
39 | * 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。这样可以确保输出资源不会包含错误。
40 | */
41 | new webpack.NoEmitOnErrorsPlugin(),
42 | new UglifyJsPlugin({ sourceMap: true }),
43 | new ExtractTextPlugin({
44 | filename: 'static/css/[name].[contenthash:8].css',
45 | }),
46 | ];
47 | }
48 |
49 | module.exports = {
50 | devtool: isProd ? 'source-map' : 'cheap-module-source-map',
51 | resolve: {
52 | extensions: ['.js', '.jsx'],
53 | alias: {
54 | '@components': path.join(__dirname, '../src/components'),
55 | '@utils': path.join(__dirname, '../src/utils'),
56 | },
57 | },
58 | module: {
59 | rules: [
60 | {
61 | test: /\.(js|jsx|mjs)?$/,
62 | loader: 'babel-loader',
63 | options: {
64 | /* cacheDirectory是用来缓存编译结果,下次编译加速 */
65 | cacheDirectory: true,
66 | },
67 | /* 指定src文件下的内容 */
68 | include: path.join(__dirname, '../src'),
69 | },
70 | {
71 | test: /\.(png|jpe?g|gif|svg)$/,
72 | loader: 'url-loader',
73 | options: {
74 | limit: 10000,
75 | name: 'static/images/[name].[ext]',
76 | },
77 | },
78 | {
79 | test: /\.(woff2?|eot|ttf|otf)$/,
80 | loader: 'url-loader',
81 | options: {
82 | limit: 10000,
83 | name: 'static/fonts/[name].[hash:7].[ext]',
84 | },
85 | },
86 | ],
87 | },
88 | plugins: [
89 | new webpack.DefinePlugin({
90 | 'process.env': require(`./${env}.env`),
91 | }),
92 | new ProgressBarPlugin(),
93 | ...prodPlugins,
94 | ],
95 | };
96 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "plugins": ["compat"],
5 | "globals": {
6 | "__DEV__": true
7 | },
8 | "env": {
9 | "browser": true,
10 | "node": true,
11 | "es6": true,
12 | "mocha": true,
13 | "jest": true,
14 | "jasmine": true
15 | },
16 | "rules": {
17 | "no-shadow": [0],
18 | "no-underscore-dangle": [0],
19 | "array-callback-return": [0],
20 | "react/sort-comp": [0],
21 | "prefer-destructuring": [0],
22 | "generator-star-spacing": [0],
23 | "no-param-reassign": [0],
24 | "consistent-return": [0],
25 | "indent": [2, 2],
26 | "react/forbid-prop-types": [0],
27 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
28 | "global-require": [0],
29 | "import/prefer-default-export": [0],
30 | "react/jsx-no-bind": [0],
31 | "react/prop-types": [0],
32 | "react/prefer-stateless-function": [0],
33 | "react/jsx-first-prop-new-line": [0],
34 | "react/jsx-wrap-multilines": [
35 | "error",
36 | {
37 | "declaration": "parens-new-line",
38 | "assignment": "parens-new-line",
39 | "return": "parens-new-line",
40 | "arrow": "parens-new-line",
41 | "condition": "parens-new-line",
42 | "logical": "parens-new-line",
43 | "prop": "ignore"
44 | }
45 | ],
46 | "no-unused-expressions": [0],
47 | "no-else-return": [0],
48 | "no-restricted-syntax": [0],
49 | "import/no-extraneous-dependencies": [0],
50 | "import/no-dynamic-require": [0],
51 | "no-use-before-define": [0],
52 | "jsx-a11y/no-static-element-interactions": [0],
53 | "jsx-a11y/no-noninteractive-element-interactions": [0],
54 | "jsx-a11y/click-events-have-key-events": [0],
55 | "jsx-a11y/anchor-is-valid": [0],
56 | "no-nested-ternary": [0],
57 | "arrow-body-style": [0],
58 | "import/extensions": [0],
59 | "no-bitwise": [0],
60 | "no-cond-assign": [0],
61 | "import/no-unresolved": [0],
62 | "comma-dangle": [
63 | "error",
64 | {
65 | "arrays": "always-multiline",
66 | "objects": "always-multiline",
67 | "imports": "always-multiline",
68 | "exports": "always-multiline",
69 | "functions": "ignore"
70 | }
71 | ],
72 | "object-curly-newline": [0],
73 | "function-paren-newline": [0],
74 | "no-restricted-globals": [0],
75 | "require-yield": [1],
76 | "compat/compat": "error",
77 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
78 | "no-console": [0]
79 | },
80 | "parserOptions": {
81 | "ecmaFeatures": {
82 | "experimentalObjectRestSpread": true,
83 | "legacyDecorators": true
84 | }
85 | },
86 | "settings": {
87 | "polyfills": ["fetch", "promises"]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/config/webpack.config.client.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const CopyWebpackPlugin = require('copy-webpack-plugin');
6 | const baseWebpackConfig = require('./webpack.config.base');
7 | /**
8 | * react-loadable 提供的 webpack 插件
9 | * 该插件将生成源文件与打包后的chunk文件的映射关系
10 | * 结合服务端渲染用于预加载
11 | */
12 | const { ReactLoadablePlugin } = require('react-loadable/webpack');
13 | const util = require('./util');
14 | const { isHttps } = require('../config/httpsConfig');
15 |
16 | const isProd = process.env.NODE_ENV === 'production';
17 |
18 | const webpackConfig = merge(baseWebpackConfig, {
19 | entry: {
20 | app: path.resolve(__dirname, '../src/client-entry.js'),
21 | },
22 | output: {
23 | publicPath: '/',
24 | path: path.resolve(__dirname, '../dist/app'),
25 | filename: `static/js/[name].[${isProd ? 'chunkhash' : 'hash'}:8].js`,
26 | chunkFilename: 'static/js/[name].[chunkhash:8].js',
27 | },
28 | module: {
29 | rules: util.styleLoaders({
30 | sourceMap: isProd,
31 | usePostCSS: true,
32 | extract: isProd,
33 | }),
34 | },
35 | plugins: [
36 | new webpack.optimize.CommonsChunkPlugin({
37 | name: 'vendor',
38 | minChunks: ({ resource }) =>
39 | resource &&
40 | resource.indexOf('node_modules') >= 0 &&
41 | resource.match(/\.js$/),
42 | }),
43 | new webpack.optimize.CommonsChunkPlugin({
44 | async: 'common-in-lazy',
45 | minChunks: ({ resource } = {}) =>
46 | resource && resource.includes('node_modules') && /axios/.test(resource),
47 | }),
48 | new webpack.optimize.CommonsChunkPlugin({
49 | async: 'used-twice',
50 | minChunks: (module, count) => count >= 2,
51 | }),
52 | new CopyWebpackPlugin([
53 | { from: path.resolve(__dirname, '../src/favicon.ico') },
54 | {
55 | from: path.resolve(__dirname, '../static/images'),
56 | to: 'static/images',
57 | },
58 | ]),
59 | new HtmlWebpackPlugin({
60 | title: 'Zoo Weekly - 政采云前端小报',
61 | filename: 'index.html',
62 | template: path.resolve(__dirname, '../src/index.ejs'),
63 | isHttps, // 是否开启https
64 | }),
65 | ],
66 | });
67 |
68 | if (!isProd) {
69 | webpackConfig.module.rules.unshift({
70 | enforce: 'pre',
71 | test: /\.js$/,
72 | exclude: /node_modules/,
73 | loader: 'eslint-loader',
74 | options: {
75 | // eslint options (if necessary)
76 | },
77 | });
78 | webpackConfig.devServer = require('./devServer');
79 | webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
80 | }
81 |
82 | if (isProd) {
83 | webpackConfig.plugins.push(
84 | new ReactLoadablePlugin({
85 | filename: path.join(__dirname, '../dist/react-loadable.json'),
86 | })
87 | );
88 | }
89 |
90 | module.exports = webpackConfig;
91 |
--------------------------------------------------------------------------------
/src/views/Detail/index.less:
--------------------------------------------------------------------------------
1 | .detail-container {
2 | padding: 3rem 6%;
3 | display: flex;
4 | background-color: #f4f4f4;
5 |
6 | .detail {
7 | width: 100%;
8 | }
9 |
10 | .title {
11 | font-size: 24px;
12 | margin-bottom: 2rem;
13 | }
14 |
15 | .title time {
16 | margin-left: 0.3rem;
17 | font-size: 0.9rem;
18 | font-weight: normal;
19 | color: #333;
20 | }
21 |
22 | .list {
23 | padding-right: 12rem;
24 | }
25 |
26 | .item {
27 | list-style: circle inside;
28 | margin-bottom: 1.6rem;
29 | }
30 |
31 | .item h4 {
32 | display: inline-block;
33 | line-height: 1.6;
34 | font-weight: normal;
35 | max-width: 90%;
36 | vertical-align: middle;
37 | }
38 |
39 | .item a {
40 | margin-right: 0.5rem;
41 | text-decoration: none;
42 | color: #2466dc;
43 | vertical-align: middle;
44 | }
45 |
46 | .item a:hover {
47 | color: #1d25dc;
48 | }
49 |
50 | .item__tag {
51 | display: inline-block;
52 | font-size: 12px;
53 | padding: 5px 9px;
54 | line-height: 1;
55 | background: #eaeaea;
56 | border-radius: 3px;
57 | color: #555;
58 | vertical-align: top;
59 | margin-left: 0.4rem;
60 | }
61 |
62 | .item blockquote {
63 | font-size: 0.9rem;
64 | line-height: 1.4;
65 | color: #444;
66 | }
67 |
68 | /* aside */
69 | aside {
70 | width: 400px;
71 | }
72 |
73 | .aside__title {
74 | font-size: 24px;
75 | margin-bottom: 2rem;
76 | }
77 |
78 | .weekly_list {
79 | margin-bottom: 3rem;
80 | }
81 |
82 | .weekly_list li {
83 | list-style: circle inside;
84 | line-height: 2;
85 | }
86 |
87 | .weekly_list a {
88 | text-decoration: none;
89 | color: #2484a0;
90 | }
91 |
92 | .weekly_list a:hover {
93 | color: #1359a0;
94 | }
95 |
96 | .weekly_list h4 {
97 | display: inline-block;
98 | font-weight: normal;
99 | }
100 |
101 | .tags {
102 | padding-right: 2rem;
103 | }
104 |
105 | .tags li {
106 | margin: 5px;
107 | display: inline-block;
108 | }
109 |
110 | .tags a {
111 | display: inline-block;
112 | padding: 0 10px;
113 | height: 28px;
114 | line-height: 26px;
115 | font-size: 12px;
116 | border-radius: 4px;
117 | box-sizing: border-box;
118 | background-color: cadetblue;
119 | border: 1px solid rgba(64, 158, 255, 0.2);
120 | white-space: nowrap;
121 | color: #fff;
122 | cursor: pointer;
123 | text-decoration: none;
124 | transition: transform 0.2s linear;
125 | }
126 |
127 | .tags a:hover {
128 | transform: translate3d(0, -2px, 0);
129 | }
130 | }
131 |
132 | /* mobile */
133 | @media screen and (max-width: 414px) {
134 | .detail-container {
135 | display: block;
136 |
137 | .list {
138 | padding-right: 0;
139 | margin-bottom: 3rem;
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/layouts/basicLayout/index.less:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI",
3 | "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue",
4 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
5 | "Segoe UI Symbol";
6 | -webkit-font-smoothing: antialiased;
7 | }
8 |
9 | main {
10 | background-color: #f4f4f4;
11 | overflow: hidden;
12 | }
13 |
14 | .lmask {
15 | position: absolute;
16 | height: 100%;
17 | width: 100%;
18 | background-color: #000;
19 | bottom: 0;
20 | left: 0;
21 | right: 0;
22 | top: 0;
23 | z-index: 9999;
24 | opacity: 0.4;
25 | &.fixed {
26 | position: fixed;
27 | }
28 | &:before {
29 | content: "";
30 | background-color: rgba(0, 0, 0, 0);
31 | border: 5px solid rgba(0, 183, 229, 0.9);
32 | opacity: 0.9;
33 | border-right: 5px solid rgba(0, 0, 0, 0);
34 | border-left: 5px solid rgba(0, 0, 0, 0);
35 | border-radius: 50px;
36 | box-shadow: 0 0 35px #2187e7;
37 | width: 50px;
38 | height: 50px;
39 | -moz-animation: spinPulse 1s infinite ease-in-out;
40 | -webkit-animation: spinPulse 1s infinite linear;
41 | margin: -25px 0 0 -25px;
42 | position: absolute;
43 | top: 50%;
44 | left: 50%;
45 | }
46 | &:after {
47 | content: "";
48 | background-color: rgba(0, 0, 0, 0);
49 | border: 5px solid rgba(0, 183, 229, 0.9);
50 | opacity: 0.9;
51 | border-left: 5px solid rgba(0, 0, 0, 0);
52 | border-right: 5px solid rgba(0, 0, 0, 0);
53 | border-radius: 50px;
54 | box-shadow: 0 0 15px #2187e7;
55 | width: 30px;
56 | height: 30px;
57 | -moz-animation: spinoffPulse 1s infinite linear;
58 | -webkit-animation: spinoffPulse 1s infinite linear;
59 | margin: -15px 0 0 -15px;
60 | position: absolute;
61 | top: 50%;
62 | left: 50%;
63 | }
64 | }
65 |
66 | @-moz-keyframes spinPulse {
67 | 0% {
68 | -moz-transform: rotate(160deg);
69 | opacity: 0;
70 | box-shadow: 0 0 1px #2187e7;
71 | }
72 | 50% {
73 | -moz-transform: rotate(145deg);
74 | opacity: 1;
75 | }
76 | 100% {
77 | -moz-transform: rotate(-320deg);
78 | opacity: 0;
79 | }
80 | }
81 |
82 | @-moz-keyframes spinoffPulse {
83 | 0% {
84 | -moz-transform: rotate(0deg);
85 | }
86 | 100% {
87 | -moz-transform: rotate(360deg);
88 | }
89 | }
90 |
91 | @-webkit-keyframes spinPulse {
92 | 0% {
93 | -webkit-transform: rotate(160deg);
94 | opacity: 0;
95 | box-shadow: 0 0 1px #2187e7;
96 | }
97 | 50% {
98 | -webkit-transform: rotate(145deg);
99 | opacity: 1;
100 | }
101 | 100% {
102 | -webkit-transform: rotate(-320deg);
103 | opacity: 0;
104 | }
105 | }
106 |
107 | @-webkit-keyframes spinoffPulse {
108 | 0% {
109 | -webkit-transform: rotate(0deg);
110 | }
111 | 100% {
112 | -webkit-transform: rotate(360deg);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/common/style/reset.css:
--------------------------------------------------------------------------------
1 | html {
2 | line-height: 1.15;
3 | -webkit-text-size-adjust: 100%;
4 | }
5 | body {
6 | margin: 0;
7 | position: relative;
8 | }
9 | main {
10 | display: block;
11 | }
12 | h1 {
13 | font-size: 2em;
14 | }
15 | h1,
16 | h2,
17 | h3,
18 | h4,
19 | h5,
20 | h6 {
21 | margin: 0;
22 | }
23 | hr {
24 | box-sizing: content-box;
25 | height: 0;
26 | overflow: visible;
27 | }
28 | pre {
29 | font-family: monospace, monospace;
30 | font-size: 1em;
31 | }
32 | a {
33 | background-color: transparent;
34 | }
35 | abbr[title] {
36 | border-bottom: none;
37 | text-decoration: underline;
38 | text-decoration: underline dotted;
39 | }
40 | b,
41 | strong {
42 | font-weight: bolder;
43 | }
44 | ol,
45 | ul,
46 | dl,
47 | dd,
48 | p {
49 | margin: 0;
50 | padding: 0;
51 | }
52 | ol,
53 | ul {
54 | list-style: none;
55 | }
56 | code,
57 | kbd,
58 | samp {
59 | font-family: monospace, monospace;
60 | font-size: 1em;
61 | }
62 | small {
63 | font-size: 80%;
64 | }
65 | sub,
66 | sup {
67 | font-size: 75%;
68 | line-height: 0;
69 | position: relative;
70 | vertical-align: baseline;
71 | }
72 | sub {
73 | bottom: -0.25em;
74 | }
75 | sup {
76 | top: -0.5em;
77 | }
78 | img {
79 | border-style: none;
80 | }
81 | button,
82 | input,
83 | optgroup,
84 | select,
85 | textarea {
86 | font-family: inherit;
87 | font-size: 100%;
88 | line-height: 1.15;
89 | margin: 0;
90 | }
91 | button,
92 | input {
93 | overflow: visible;
94 | }
95 | button,
96 | select {
97 | text-transform: none;
98 | }
99 | button,
100 | [type="button"],
101 | [type="reset"],
102 | [type="submit"] {
103 | -webkit-appearance: button;
104 | }
105 | button::-moz-focus-inner,
106 | [type="button"]::-moz-focus-inner,
107 | [type="reset"]::-moz-focus-inner,
108 | [type="submit"]::-moz-focus-inner {
109 | border-style: none;
110 | padding: 0;
111 | }
112 | button:-moz-focusring,
113 | [type="button"]:-moz-focusring,
114 | [type="reset"]:-moz-focusring,
115 | [type="submit"]:-moz-focusring {
116 | outline: 1px dotted ButtonText;
117 | }
118 | fieldset {
119 | padding: 0.35em 0.75em 0.625em;
120 | }
121 | legend {
122 | box-sizing: border-box;
123 | color: inherit;
124 | display: table;
125 | max-width: 100%;
126 | padding: 0;
127 | white-space: normal;
128 | }
129 | progress {
130 | vertical-align: baseline;
131 | }
132 | textarea {
133 | overflow: auto;
134 | }
135 | [type="checkbox"],
136 | [type="radio"] {
137 | box-sizing: border-box;
138 | padding: 0;
139 | }
140 | [type="number"]::-webkit-inner-spin-button,
141 | [type="number"]::-webkit-outer-spin-button {
142 | height: auto;
143 | }
144 | [type="search"] {
145 | -webkit-appearance: textfield;
146 | outline-offset: -2px;
147 | }
148 | [type="search"]::-webkit-search-decoration {
149 | -webkit-appearance: none;
150 | }
151 | ::-webkit-file-upload-button {
152 | -webkit-appearance: button;
153 | font: inherit;
154 | }
155 | details {
156 | display: block;
157 | }
158 | summary {
159 | display: list-item;
160 | }
161 | template {
162 | display: none;
163 | }
164 | [hidden] {
165 | display: none;
166 | }
167 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-universal-ssr",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.config.client.js ",
8 | "dev:server": "nodemon ./server/index.js --watch config --watch server",
9 | "dev": "npm run dev:client && npm run dev:server",
10 | "build:client": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.client.js",
11 | "build:server": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.server.js",
12 | "build": "npm run clear && npm run build:client && npm run build:server",
13 | "clear": "rimraf dist",
14 | "start": "cross-env NODE_ENV=production node dist/server/index.js",
15 | "lint": "eslint --ext .js --ext .jsx src/",
16 | "ssr": "pm2 start process.json --env production"
17 | },
18 | "author": "",
19 | "license": "ISC",
20 | "devDependencies": {
21 | "babel-core": "^6.26.0",
22 | "babel-eslint": "^8.2.2",
23 | "babel-loader": "^7.1.2",
24 | "babel-plugin-add-module-exports": "^0.2.1",
25 | "babel-plugin-dynamic-import-node": "^1.2.0",
26 | "babel-plugin-import": "^1.11.0",
27 | "babel-plugin-remove-webpack": "^1.1.0",
28 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
29 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
30 | "babel-plugin-transform-runtime": "^6.23.0",
31 | "babel-polyfill": "^6.26.0",
32 | "babel-preset-env": "^1.6.1",
33 | "babel-preset-react": "^6.24.1",
34 | "babel-preset-stage-0": "^6.24.1",
35 | "babel-register": "^6.26.0",
36 | "copy-webpack-plugin": "^4.6.0",
37 | "cross-env": "^5.1.1",
38 | "css-loader": "^0.28.7",
39 | "eslint": "^4.18.2",
40 | "eslint-config-airbnb": "^16.1.0",
41 | "eslint-loader": "^2.0.0",
42 | "eslint-plugin-babel": "^4.1.2",
43 | "eslint-plugin-compat": "^2.4.0",
44 | "eslint-plugin-import": "^2.12.0",
45 | "eslint-plugin-jsx-a11y": "^6.0.3",
46 | "eslint-plugin-react": "^7.9.1",
47 | "extract-text-webpack-plugin": "^3.0.2",
48 | "file-loader": "^1.1.6",
49 | "html-webpack-plugin": "^2.30.1",
50 | "husky": "^1.3.0",
51 | "isomorphic-style-loader": "^4.0.0",
52 | "less": "^2.7.3",
53 | "less-loader": "^4.1.0",
54 | "nodemon": "^1.12.5",
55 | "npm-run-all": "^4.1.2",
56 | "pm2": "^4.5.6",
57 | "postcss-flexbugs-fixes": "^4.1.0",
58 | "postcss-loader": "^2.0.9",
59 | "progress-bar-webpack-plugin": "^1.10.0",
60 | "rimraf": "^2.6.2",
61 | "style-loader": "^0.19.1",
62 | "uglifyjs-webpack-plugin": "^1.3.0",
63 | "url-loader": "^0.6.2",
64 | "webpack": "^3.12.0",
65 | "webpack-dev-server": "^2.11.3",
66 | "webpack-manifest-plugin": "^1.3.2",
67 | "webpack-merge": "^4.1.4",
68 | "webpack-node-externals": "^1.7.2"
69 | },
70 | "dependencies": {
71 | "axios": "^0.18.0",
72 | "echarts": "^4.2.1",
73 | "history": "^4.7.2",
74 | "koa": "^2.4.1",
75 | "koa-static": "^5.0.0",
76 | "koa-static-cache": "^5.1.1",
77 | "koa2-cors": "^2.0.5",
78 | "prop-types": "^15.6.2",
79 | "qs": "^6.6.0",
80 | "react": "^16.7.0-alpha.2",
81 | "react-dom": "^16.7.0-alpha.2",
82 | "react-helmet": "^5.2.0",
83 | "react-loadable": "^5.5.0",
84 | "react-redux": "^5.0.6",
85 | "react-router": "^4.3.1",
86 | "react-router-config": "^1.0.0-beta.4",
87 | "react-router-dom": "^4.3.1",
88 | "redux": "^3.7.2",
89 | "redux-thunk": "^2.2.0"
90 | },
91 | "husky": {
92 | "hooks": {
93 | "pre-commit": "npm run lint"
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/server/render.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { renderToString } = require('react-dom/server');
4 | const { Helmet } = require('react-helmet');
5 | const { getBundles } = require('react-loadable/webpack');
6 | const { matchRoutes } = require('react-router-config');
7 | const { matchPath } = require('react-router-dom');
8 | const stats = require('../react-loadable.json');
9 | const { configureStore, createApp, routesConfig } = require('./server-entry');
10 |
11 | const createStore = (configureStore) => {
12 | const store = configureStore();
13 | return store;
14 | };
15 |
16 | const createTags = (modules) => {
17 | /**
18 | * 根据生产环境下 webpack 打包生成的 react-loadable.json 数据
19 | * 挑选要加载的chunks
20 | */
21 | const bundles = getBundles(stats, modules);
22 | const scriptfiles = bundles.filter(bundle => bundle.file.endsWith('.js'));
23 | console.log('n\n\n\n\n\\n***************', scriptfiles);
24 |
25 | const stylefiles = bundles.filter(bundle => bundle.file.endsWith('.css'));
26 | const scripts = scriptfiles
27 | .map(script => ``)
28 | .join('\n');
29 | const styles = stylefiles
30 | .map(style => ``)
31 | .join('\n');
32 | return {
33 | scripts,
34 | styles,
35 | };
36 | };
37 |
38 | const prepHTML = (
39 | data,
40 | { html, head, rootString, scripts, styles, initState, cssStr }
41 | ) => {
42 | console.log(data, html, head);
43 | data = data.replace('',
46 | `${head} \n ${styles}`
47 | );
48 | data = data.replace(
49 | '',
50 | `${rootString}
`
51 | );
52 | data = data.replace(
53 | '',
54 | ` \n `
57 | );
58 | data = data.replace('', `${scripts}`);
59 | return data;
60 | };
61 |
62 | const getMatch = (routesArray, url) => {
63 | return routesArray.some(router =>
64 | matchPath(url, {
65 | path: router.path,
66 | exact: router.exact,
67 | })
68 | );
69 | };
70 |
71 | const makeup = (ctx, store, createApp, html) => {
72 | /* 获取初始state */
73 | const initState = store.getState();
74 | const context = {
75 | css: [],
76 | };
77 |
78 | /* 当前页面要加载的chunk[App 中的 Loadable.Capture 会告诉你的] */
79 | const modules = [];
80 |
81 | const rootString = renderToString(
82 | createApp({
83 | modules,
84 | store,
85 | context,
86 | url: ctx.url,
87 | })
88 | );
89 |
90 | const { scripts, styles } = createTags(modules);
91 |
92 | /* SEO优化 */
93 | const helmet = Helmet.renderStatic();
94 |
95 | // const cssStr = context.css ? context.css : '';
96 | const cssStr = context.css ? context.css.join('') : '';
97 |
98 | /* 填充好html */
99 | const renderedHtml = prepHTML(html, {
100 | html: helmet.htmlAttributes.toString(),
101 | head: helmet.meta.toString() + helmet.link.toString(),
102 | rootString,
103 | scripts,
104 | styles,
105 | initState,
106 | cssStr,
107 | });
108 |
109 | return renderedHtml;
110 | };
111 |
112 | module.exports = async (ctx, next) => {
113 | /* 读取打包后的html文件 */
114 | const html = fs.readFileSync(
115 | path.join(path.resolve(__dirname, '../app'), 'index.html'),
116 | 'utf-8'
117 | );
118 |
119 | /* 创建store */
120 | const store = createStore(configureStore);
121 |
122 | /* 匹配上的路径 */
123 | const branch = matchRoutes(routesConfig, ctx.url);
124 |
125 | /* 在服务端拉取数据 */
126 | const promises = branch.map(({ route }) => {
127 | /* 拿到url params query等 */
128 | const matchInfo = matchPath(ctx.url, route);
129 | return route.loadData
130 | ? route.loadData(store, matchInfo)
131 | : Promise.resolve(null);
132 | });
133 |
134 | /* 等待拉取数据结束 */
135 | await Promise.all(promises).catch(err => console.log('err:---', err));
136 |
137 | /* 是否匹配上 */
138 | const isMatch = getMatch(routesConfig, ctx.url);
139 |
140 | /* 匹配上 */
141 | if (isMatch) {
142 | /* 服务端react渲染 */
143 | const renderedHtml = await makeup(ctx, store, createApp, html);
144 | ctx.body = renderedHtml;
145 | }
146 |
147 | await next();
148 | };
149 |
--------------------------------------------------------------------------------
/src/views/Detail/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { WithStyles } from '@components';
5 | import request from '@utils/request';
6 | import { isNumeric } from '@utils/utils';
7 | import { Helmet } from 'react-helmet';
8 | import { actionCreators } from './store';
9 | import styles from './index.less';
10 |
11 | @WithStyles(styles)
12 | @connect(
13 | state => ({ detailStore: state.detailStore }),
14 | dispatch => ({
15 | getDetail: week => dispatch(actionCreators.getDetailEffect(week)),
16 | resetDetail: () => dispatch(actionCreators.resetDetail()),
17 | })
18 | )
19 | export default class Detail extends Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | date: null,
24 | homeList: [],
25 | tags: [],
26 | };
27 | }
28 |
29 | componentDidMount = () => {
30 | this.getDate();
31 | this.getHomeList();
32 | this.getTags();
33 | if (!this.props.detailStore.weeklyDetail.length) {
34 | const { week } = this.props.match.params;
35 | this.props.getDetail(week);
36 | }
37 | };
38 |
39 | componentDidUpdate = (prevProps) => {
40 | const { week } = this.props.match.params;
41 | if (week !== prevProps.match.params.week) {
42 | this.props.getDetail(week);
43 | this.getDate();
44 | }
45 | };
46 |
47 | componentWillUnmount = () => {
48 | this.props.resetDetail();
49 | };
50 |
51 | getDate = () => {
52 | const week = this.props.match.params.week;
53 | if (!isNumeric(week)) return;
54 | request
55 | .get(`/api/weeks/list?count=${this.props.match.params.week}`)
56 | .then((res) => {
57 | if (res.data && res.data[0]) {
58 | this.setState({
59 | date: new Date(res.data[0].datetime).toLocaleDateString(),
60 | });
61 | }
62 | });
63 | };
64 |
65 | getHomeList = () => {
66 | request.get('/api/weeks/list').then((res) => {
67 | this.setState({
68 | homeList: res.data,
69 | });
70 | });
71 | };
72 |
73 | getTags = () => {
74 | request.get('/api/categories/list').then((res) => {
75 | this.setState({
76 | tags: res.data,
77 | });
78 | });
79 | };
80 |
81 | renderDetail = (weeklyDetail) => {
82 | if (!weeklyDetail || !weeklyDetail.length) return '暂无数据';
83 | return weeklyDetail.map(card => (
84 |
85 |
86 |
87 | {card.title} {card.category}
88 |
89 |
90 |
91 | {card.description}
92 |
93 |
94 | ));
95 | };
96 |
97 | renderHistoryIndex = (homeList) => {
98 | if (!homeList || !homeList.length) return '暂无数据';
99 | const reverseList = homeList.slice().reverse().slice(0, 10);
100 | return reverseList.map(card => (
101 |
102 |
107 | {card.title}
108 |
109 |
110 | ));
111 | };
112 |
113 | renderTags = (tags) => {
114 | if (!tags || !tags.length) return '暂无数据';
115 | return tags.map(tag => (
116 |
117 |
122 | {tag.name}
123 |
124 |
125 | ));
126 | };
127 |
128 | render() {
129 | const { date, homeList, tags } = this.state;
130 | const { weeklyDetail } = this.props.detailStore;
131 | const { week } = this.props.match.params;
132 | return (
133 |
134 |
135 | {isNumeric(week) ? (
136 | 政采云前端小报第{week}期 - Zoo Weekly!
137 | ) : (
138 | {week} - Zoo Weekly! 政采云前端小报
139 | )}
140 |
141 |
142 | {isNumeric(week) ? (
143 |
144 | 政采云前端小报第{week}期
145 |
146 | ) : (
147 |
#{week}
148 | )}
149 |
{this.renderDetail(weeklyDetail)}
150 |
151 |
152 |
159 |
160 | );
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/views/Summary/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import echarts from 'echarts/lib/echarts';
4 | import 'echarts/lib/chart/bar';
5 | import 'echarts/lib/chart/pie';
6 | import 'echarts/lib/component/tooltip';
7 | import 'echarts/lib/component/title';
8 | import { actionCreators } from './store';
9 |
10 | import './index.less';
11 |
12 | @connect(
13 | state => ({ summaryStore: state.summaryStore }),
14 | dispatch => ({
15 | getSummary: () => dispatch(actionCreators.getSummaryEffect()),
16 | })
17 | )
18 | export default class Summary extends Component {
19 | componentDidMount() {
20 | const { overview } = this.props.summaryStore;
21 | if (!overview.articleTotal) {
22 | this.props.getSummary().then(() => {
23 | this.initBarEcharts(this.props.summaryStore.overview);
24 | this.initCategoryChart(this.props.summaryStore.overview);
25 | });
26 | } else {
27 | this.initBarEcharts(overview);
28 | this.initCategoryChart(overview);
29 | }
30 | }
31 |
32 | initBarEcharts = (overview) => {
33 | const { weekMap = [], categoryMap = [], monthMap = [] } = overview;
34 | const xAxis = [];
35 | const data = [];
36 | const CategoryAxis = [];
37 | const CategoryData = [];
38 | const monthAxis = [];
39 | const monthData = [];
40 |
41 | weekMap.map((item) => {
42 | xAxis.push(`第 ${item.week} 周`);
43 | data.push(item.count);
44 | });
45 | categoryMap.map((item) => {
46 | CategoryAxis.push(`${item.category}`);
47 | CategoryData.push(item.count);
48 | });
49 | monthMap.map((item) => {
50 | monthAxis.push(`${item.month} 月`);
51 | monthData.push(item.count);
52 | });
53 |
54 | const categoryOption = {
55 | title: {
56 | text: '小报分类数据',
57 | },
58 | tooltip: {},
59 | xAxis: {
60 | data: CategoryAxis,
61 | },
62 | yAxis: {},
63 | series: [
64 | {
65 | name: '分类统计',
66 | type: 'bar',
67 | data: CategoryData,
68 | },
69 | ],
70 | };
71 |
72 | const monthOption = {
73 | title: {
74 | text: '小报月度数据',
75 | },
76 | tooltip: {},
77 | xAxis: {
78 | data: monthAxis,
79 | },
80 | yAxis: {},
81 | series: [
82 | {
83 | name: '分类统计',
84 | type: 'bar',
85 | data: monthData,
86 | },
87 | ],
88 | };
89 |
90 | const option = {
91 | title: {
92 | text: '小报周度数据',
93 | },
94 | legend: {
95 | data: ['bar'],
96 | align: 'left',
97 | },
98 | toolbox: {
99 | feature: {
100 | magicType: {
101 | type: ['stack', 'tiled'],
102 | },
103 | dataView: {},
104 | saveAsImage: {
105 | pixelRatio: 2,
106 | },
107 | },
108 | },
109 | tooltip: {},
110 | xAxis: {
111 | data: xAxis,
112 | silent: false,
113 | splitLine: {
114 | show: false,
115 | },
116 | },
117 | yAxis: {},
118 | series: [
119 | {
120 | name: '周数:发布文章数',
121 | type: 'bar',
122 | data,
123 | animationDelay(idx) {
124 | return idx * 10;
125 | },
126 | },
127 | ],
128 | animationEasing: 'elasticOut',
129 | animationDelayUpdate(idx) {
130 | return idx * 5;
131 | },
132 | };
133 |
134 | const WChart = echarts.init(document.getElementById('bar-chart'));
135 | WChart.setOption(option);
136 |
137 | const CChart = echarts.init(document.getElementById('category-chart-all'));
138 | CChart.setOption(categoryOption);
139 |
140 | const MChart = echarts.init(document.getElementById('month-chart'));
141 | MChart.setOption(monthOption);
142 | };
143 |
144 | initCategoryChart = (overview) => {
145 | const { categoryMap = [] } = overview;
146 | const xAxis = [];
147 | const data = [];
148 |
149 | categoryMap.slice(0, 6).map((item) => {
150 | xAxis.push(item.category);
151 | data.push({
152 | value: item.count,
153 | name: item.category,
154 | });
155 | });
156 | const option = {
157 | title: {
158 | text: '小报分类排名前六数据',
159 | },
160 | tooltip: {
161 | trigger: 'item',
162 | formatter: '{a}
{b}: {c} ({d}%)',
163 | },
164 | legend: {
165 | orient: 'vertical',
166 | x: 'left',
167 | data: xAxis,
168 | },
169 | series: [
170 | {
171 | name: '分类',
172 | type: 'pie',
173 | radius: ['40%', '55%'],
174 | label: {
175 | normal: {
176 | formatter: '{a|{a}}{abg|}\n{hr|}\n {b|{b}:}{c} {per|{d}%} ',
177 | backgroundColor: '#eee',
178 | borderColor: '#aaa',
179 | borderWidth: 1,
180 | borderRadius: 4,
181 | rich: {
182 | a: {
183 | color: '#999',
184 | lineHeight: 22,
185 | align: 'center',
186 | },
187 | hr: {
188 | borderColor: '#aaa',
189 | width: '100%',
190 | borderWidth: 0.5,
191 | height: 0,
192 | },
193 | b: {
194 | fontSize: 16,
195 | lineHeight: 33,
196 | },
197 | per: {
198 | color: '#eee',
199 | backgroundColor: '#334455',
200 | padding: [2, 4],
201 | borderRadius: 2,
202 | },
203 | },
204 | },
205 | },
206 | data,
207 | },
208 | ],
209 | };
210 |
211 | const myChart = echarts.init(document.getElementById('category-chart'));
212 | myChart.setOption(option);
213 | };
214 |
215 | render() {
216 | const { overview } = this.props.summaryStore;
217 | return (
218 |
219 | 政采云前端小报半年期数据
220 |
221 |
222 | 累计文章:
223 |
224 | {overview ? ((overview.articleTotal || [])[0] || {}).count : null}
225 |
226 | 篇
227 |
228 |
229 | 累计分类:
230 |
231 | {overview
232 | ? ((overview.categoryTotal || [])[0] || {}).count
233 | : null}
234 |
235 | 类
236 |
237 |
238 |
239 |
243 |
247 |
251 |
255 |
256 | );
257 | }
258 | }
259 |
--------------------------------------------------------------------------------