├── .babelrc ├── .gitignore ├── README.md ├── config-overrides.js ├── images.d.ts ├── nodemon.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── server ├── README.md ├── controller │ ├── todo.js │ └── user.js ├── db │ └── connect.js ├── index.js ├── models │ ├── todo.js │ └── user.js ├── routers │ ├── index.js │ ├── todo.js │ └── user.js └── utils │ ├── response.js │ └── utils.js ├── src ├── App.module.less ├── App.test.tsx ├── App.tsx ├── actions │ ├── todo.tsx │ └── user.tsx ├── constance │ ├── todo.tsx │ ├── user.tsx │ └── utils.tsx ├── index.css ├── index.tsx ├── logo.svg ├── reducer │ ├── index.tsx │ ├── todo.tsx │ └── user.tsx ├── registerServiceWorker.ts ├── routes │ ├── Login │ │ ├── index.module.less │ │ └── index.tsx │ ├── Register │ │ ├── index.module.less │ │ └── index.tsx │ └── Todo │ │ ├── Todo.tsx │ │ ├── TodoList.tsx │ │ └── index.module.less ├── services │ ├── todo.tsx │ └── user.tsx └── utils │ ├── connect.tsx │ ├── notification.tsx │ ├── render.tsx │ ├── request.tsx │ └── utils.tsx ├── tsconfig.json ├── tsconfig.prod.json ├── tsconfig.test.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":[ 3 | [ 4 | "env", 5 | { 6 | "target":{ 7 | "node":"current" 8 | } 9 | } 10 | ], 11 | "stage-3" 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 写英文md是我想学英语, 如果不习惯中式英文, [中文md点这里](https://github.com/binyellow/TS-TodoList-Koa/tree/develop-zh) 2 | # TS-TodoList-Koa 3 | todolist with TS Koa MongoDB Webpack 4 | 5 | ### plan 6 | - [x] Redux 7 | - [x] Router 8 | - [x] Koa 9 | - [x] learn mongoDB index 10 | - [x] encapsulated return params 11 | - [x] nginx to agency the local request 12 | - [x] add delete and toggle completed 13 | - [ ] add editRow,Cell. add Search 14 | - [x] register and login with token 15 | - [ ] docker,jenkins 16 | 17 | ### Some problem in the progress 18 | - [x] less-loader support css modules: react-app-rewire-less-with-modules 19 | - [x] tslint: import order problem 20 | ```js 21 | "rules": { 22 | "ordered-imports": false 23 | }, 24 | ``` 25 | - [x] antd problem about input>TextArea: 26 | - Open: node_modules/antd/lib/input/TextArea.d.ts 27 | - Insert import: import ResizeObserver from 'resize-observer-polyfill'; 28 | - [link to antd](https://github.com/ant-design/ant-design/issues/13405) 29 | - [x] asy import the Component: [antd official document](https://ant.design/docs/react/use-in-typescript-cn) 30 | - [x] nodemon and babel-cli to translate ES6 to lower version and hot replace, use nodemon.json to config the ignore about hot watch file: 31 | ```js 32 | { 33 | "ignore": ["lib/*.js", "README"] 34 | } 35 | ``` 36 | - [x] implicitly has an 'any' type: to install @types/moduleName to resolve this problem 37 | - [x] connect problem: [decorator and no](https://stackoverflow.com/questions/46861839/typescript-connect-react-redux-decorator-with-stateful-component) 38 | - [x] [params with shadow](https://stackoverflow.com/questions/52968903/shadowed-name-in-typescript-and-react-redux) 39 | - [x] Router props problem: import RouteComponentProps from react-router-dom to define the Component's props 40 | - [x] alias problem: Resolve alias issues by adding a path configuration in tsconfig 41 | - [x] static function problem: first, is should be put before constructor; second, specific the type of the method return values -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | // const { injectBabelPlugin } = require('react-app-rewired'); 2 | const path = require('path'); 3 | const tsImportPluginFactory = require('ts-import-plugin'); 4 | const { getLoader } = require("react-app-rewired"); 5 | const rewireLessWithModule = require('react-app-rewire-less-with-modules'); 6 | 7 | module.exports = function override (config, env) { 8 | // config = injectBabelPlugin(['import', { 9 | // libraryDirectory: 'es', 10 | // libraryName: 'antd', 11 | // style: true 12 | // }], config) 13 | const tsLoader = getLoader( 14 | config.module.rules, 15 | rule => 16 | rule.loader && 17 | typeof rule.loader === 'string' && 18 | rule.loader.includes('ts-loader') 19 | ); 20 | 21 | tsLoader.options = { 22 | getCustomTransformers: () => ({ 23 | before: [ tsImportPluginFactory({ 24 | libraryDirectory: 'es', 25 | libraryName: 'antd', 26 | style: 'css', 27 | }) ] 28 | }) 29 | }; 30 | config = rewireLessWithModule(config, env, { 31 | // modifyVars: { 32 | // '@primary-color': 'red', 33 | // '@link-color': '#1DA57A', 34 | // '@border-radius-base': '2px', 35 | // '@font-size-base': '16px', 36 | // '@line-height-base': '1.2' 37 | // }, 38 | }) 39 | // config.resolve = { 40 | // alias: { 41 | // // services: path.resolve(__dirname, 'src/services/'), 42 | // utils: path.resolve(__dirname, 'src/utils/') 43 | // }, 44 | // extensions: [ '.ts', '.js', '.json', '.css', '.tsx', '.less' ], 45 | // }; 46 | return config 47 | } -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.gif' 6 | declare module '*.bmp' 7 | declare module '*.tiff' 8 | declare module '*.less' 9 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "src/*.js", 4 | "src/**/*.js", 5 | "README", 6 | "src/*.tsx", 7 | "src/**/*.tsx", 8 | "node_modules", 9 | "public" 10 | ] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todolist", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "D": "^1.0.0", 7 | "react": "^16.6.3", 8 | "react-dom": "^16.6.3", 9 | "react-scripts-ts": "3.1.0" 10 | }, 11 | "scripts": { 12 | "start": "react-app-rewired start --scripts-version react-scripts-ts", 13 | "build": "react-app-rewired build --scripts-version react-scripts-ts", 14 | "test": "react-app-rewired test --scripts-version react-scripts-ts --env=jsdom", 15 | "eject": "react-scripts eject", 16 | "koa": "nodemon --exec babel-node server/index.js" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^23.3.10", 20 | "@types/koa": "^2.0.47", 21 | "@types/node": "^10.12.12", 22 | "@types/react": "^16.7.13", 23 | "@types/react-dom": "^16.0.11", 24 | "@types/react-redux": "^6.0.11", 25 | "@types/react-router": "^4.4.2", 26 | "@types/react-router-dom": "^4.3.1", 27 | "@types/redux-logger": "^3.0.6", 28 | "@types/uuid": "^3.4.4", 29 | "antd": "3.10.9", 30 | "axios": "^0.18.0", 31 | "babel-cli": "^6.26.0", 32 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 33 | "babel-preset-env": "^1.7.0", 34 | "babel-preset-stage-3": "^6.24.1", 35 | "jsonwebtoken": "^8.4.0", 36 | "koa": "^2.6.2", 37 | "koa-bodyparser": "^4.2.1", 38 | "koa-jwt": "^3.5.1", 39 | "koa-router": "^7.4.0", 40 | "koa2-cors": "^2.0.6", 41 | "less": "^3.9.0", 42 | "lodash-decorators": "^6.0.0", 43 | "mongoose": "^5.3.15", 44 | "querystringify": "^2.1.0", 45 | "react-app-rewire-less": "^2.1.3", 46 | "react-app-rewire-less-with-modules": "^0.0.4", 47 | "react-app-rewired": "^1.6.2", 48 | "react-redux": "^6.0.0", 49 | "react-router-dom": "^4.3.1", 50 | "redux": "^4.0.1", 51 | "redux-devtools-extension": "^2.13.7", 52 | "redux-logger": "^3.0.6", 53 | "redux-thunk": "^2.3.0", 54 | "ts-import-plugin": "^1.5.5", 55 | "typescript": "^3.2.2", 56 | "uuid": "^3.3.2" 57 | }, 58 | "proxy": "http://127.0.0.1:3001" 59 | } 60 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binyellow/TS-TodoList-Koa/eda75c3beb830b29889d99e287e81a094ea38e4f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ### response body format 2 | ```js 3 | { 4 | failed: Boolean, // status of request 5 | message: String, 6 | content?: Array, // fetch list's content 7 | total?: Number, // total number of contents 8 | current?: Number, // current page number 9 | pageSize?: Number, // page size 10 | result?: String, // object of request 11 | } 12 | ``` -------------------------------------------------------------------------------- /server/controller/todo.js: -------------------------------------------------------------------------------- 1 | import todo from '../models/todo'; 2 | import { successResponse, failedResponse } from '../utils/response'; 3 | import { toSafeNumber } from '../utils/utils'; 4 | 5 | async function add(ctx, next) { 6 | const { userId, content, completed } = ctx.request.body; 7 | const res = await todo.create({ userId, content, completed }); 8 | if(res.length!==0) { 9 | ctx.body = { 10 | failed: false, 11 | message: res 12 | } 13 | } else { 14 | ctx.body = { 15 | failed: true, 16 | message: '新增失败' 17 | } 18 | } 19 | } 20 | 21 | async function fetchList(ctx, next) { 22 | const { userId, current = 0, pageSize = 10 } = toSafeNumber(ctx.query, ['current', 'pageSize']); 23 | const query = await todo.find({ userId }); 24 | let res = await todo.find({ userId }).sort({time: -1}).skip((current)*pageSize).limit(pageSize); 25 | if(res instanceof Array && res.length>=0) { 26 | ctx.body = successResponse({ 27 | current, 28 | pageSize, 29 | total: query.length, 30 | message: "查询成功", 31 | content: res, 32 | }); 33 | } else { 34 | ctx.body = { 35 | failed: true, 36 | message: '无list', 37 | content: [] 38 | } 39 | } 40 | } 41 | 42 | async function deleteItem(ctx, next) { 43 | const { _id } = ctx.request.body; 44 | const res = await todo.findOneAndDelete({ _id }).exec(); 45 | if(res) { 46 | ctx.body = successResponse(); 47 | } else { 48 | ctx.body = failedResponse(); 49 | } 50 | } 51 | 52 | /** 53 | * 循环更新效率很低,Todo updateMany 54 | * @param {*} ctx 55 | * @param {function} next 56 | */ 57 | async function toggleForeach(ctx, next) { 58 | const { toggleList = [] } = ctx.request.body; 59 | console.log('toggleList===>', toggleList); 60 | await new Promise((resolve, reject)=>{ 61 | let res = undefined; 62 | toggleList.forEach(async (item, index)=> { 63 | const { _id, completed } = item; 64 | const result = todo.find({ _id }); 65 | res = await result.update({ $set: { completed: !completed }}).exec(); 66 | if(res) { 67 | if(index === toggleList.length - 1) { 68 | resolve(res); 69 | } 70 | } else { 71 | reject(res); 72 | } 73 | }) 74 | }).then(res=> { 75 | ctx.body = successResponse(); 76 | }, res=> { 77 | ctx.body = failedResponse(); 78 | }).catch(err=> { 79 | ctx.body = failedResponse(); 80 | }) 81 | } 82 | 83 | async function toggle(ctx, next) { 84 | const { toggleList = [] } = ctx.request.body; 85 | console.log('toggleList===>', toggleList); 86 | const res = await todo.bulkWrite(toggleList.map(item=>{ 87 | const { _id, completed } = item; 88 | return { 89 | updateOne: { 90 | filter: { _id }, 91 | update: { completed: !completed }, 92 | } 93 | } 94 | })); 95 | if(res) { 96 | ctx.body = successResponse(); 97 | } else { 98 | ctx.body = failedResponse(); 99 | } 100 | } 101 | module.exports = { add, fetchList, deleteItem, toggle } -------------------------------------------------------------------------------- /server/controller/user.js: -------------------------------------------------------------------------------- 1 | import user from '../models/user'; 2 | import { successResponse, failedResponse } from '../utils/response'; 3 | import jsonwebtoken from 'jsonwebtoken'; 4 | 5 | async function register(ctx, next) { 6 | const { name } = ctx.request.body; 7 | const res = await user.findOne({ name }); 8 | if(res) { 9 | ctx.body = failedResponse({message: '用户名已存在', content: res}); 10 | } else { 11 | const add = await user.create(ctx.request.body); 12 | ctx.body = successResponse({message: "注册成功", content: res}); 13 | } 14 | } 15 | 16 | async function login(ctx) { 17 | const { name, passWord } = ctx.request.body; 18 | const res = await user.findOne({ name }); 19 | if(res) { 20 | const passRes = await user.findOne({ name, passWord }); 21 | if(passRes) { 22 | ctx.body = successResponse({ 23 | message: "登录成功!", 24 | content: passRes, 25 | token: jsonwebtoken.sign({ 26 | data: name, 27 | exp: Math.floor(Date.now() / 1000) + (60), 28 | }, 'huangbin') 29 | }); 30 | } else { 31 | ctx.body = failedResponse({ message: "密码不正确!" }); 32 | } 33 | } else { 34 | ctx.body = failedResponse({ message: '账号不存在!' }); 35 | } 36 | } 37 | 38 | async function fetchUserList(ctx, next) { 39 | const content = await user.find(); 40 | if(content) { 41 | ctx.body = { 42 | failed: false, 43 | message: '查询成功', 44 | content 45 | } 46 | } else { 47 | ctx.body = { 48 | failed: true, 49 | message: '无list', 50 | content: [] 51 | } 52 | } 53 | } 54 | module.exports = { register, login, fetchUserList } -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const DB_URL = 'mongodb://localhost:27017/TodoList'; 4 | export default () => { 5 | const connect = () => { 6 | // mongoose.Promise = require('bluebird'); 7 | mongoose.connect(DB_URL, err => { 8 | if (err) { 9 | console.log(`===> Error connecting to mongoDB`); 10 | console.log(`Reason: ${err}`); 11 | } else { 12 | console.log(`===> Succeeded in connecting to mongoDB`); 13 | } 14 | }); 15 | }; 16 | connect(); 17 | 18 | mongoose.connection.on('error', console.log); 19 | mongoose.connection.on('disconnected', connect); 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * server root file 3 | * @date: 2018-12-05 4 | * @author: HB 5 | * @version: 0.0.1 6 | * @copyright Copyright (c) 2018, Hand 7 | */ 8 | 9 | import Koa from 'koa'; 10 | import connect from './db/connect'; 11 | import Todo from './models/todo'; 12 | import bodyParser from 'koa-bodyparser'; 13 | import cors from 'koa2-cors'; 14 | import qs from 'querystring'; 15 | import jwt from 'koa-jwt'; 16 | import router from './routers/index'; 17 | const app = new Koa(); 18 | 19 | connect(); 20 | app.use(jwt({ 21 | secret: 'huangbin' 22 | }).unless({ path: [/^\/user/] })) 23 | .use(cors()) 24 | .use(bodyParser()) 25 | .use(router.routes()).use(router.allowedMethods()); 26 | app.listen(3001, ()=>console.log('listen on 3001')); -------------------------------------------------------------------------------- /server/models/todo.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | 4 | const TodoSchema = Schema({ 5 | userId: { type: String, required: true }, 6 | content: { type: String, required: true }, 7 | completed: { type: Boolean, required: true, default: false }, 8 | time: { type: Number, default: Date.now } 9 | }) 10 | 11 | export default mongoose.model('Todo', TodoSchema); -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = Schema({ 5 | name: { type: String, required: true }, 6 | passWord: { type: String, required: true }, 7 | registerTime: { type: Number, default: Date.now } 8 | }) 9 | 10 | export default mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /server/routers/index.js: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'koa-router'; 2 | import todo from './todo'; 3 | import user from './user'; 4 | 5 | const router = new KoaRouter(); 6 | router.use('/todo', todo.routes(), todo.allowedMethods()); 7 | router.use('/user', user.routes(), user.allowedMethods()); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/routers/todo.js: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'koa-router'; 2 | import { add, fetchList, deleteItem, toggle } from '../controller/todo'; 3 | const todo = new KoaRouter(); 4 | 5 | todo.post('/add', add); 6 | todo.get('/fetch', fetchList); 7 | todo.del('/delete', deleteItem); 8 | todo.put('/toggle', toggle); 9 | 10 | module.exports = todo; -------------------------------------------------------------------------------- /server/routers/user.js: -------------------------------------------------------------------------------- 1 | import KoaRouter from 'koa-router'; 2 | import { register, login, fetchUserList } from '../controller/user'; 3 | const user = new KoaRouter(); 4 | 5 | user.post('/register', register); 6 | user.post('/login', login); 7 | user.get('/fetchUserList', fetchUserList); 8 | 9 | module.exports = user; -------------------------------------------------------------------------------- /server/utils/response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 成功返回数据 3 | * @param {Object} {content, total, current, pageSize} 4 | * @param content 内容 5 | * @param total? 总条数 6 | * @param current? 当前页 7 | * @param pageSize? 分页大小 8 | * @returns Object 9 | */ 10 | function successResponse({message = '操作成功', content, token, total = 10, current = 0, pageSize = 10} = {}) { 11 | const result = JSON.parse(JSON.stringify({content, total, current, pageSize, token})); 12 | return { 13 | message, 14 | failed: false, 15 | ...result 16 | } 17 | } 18 | 19 | /** 20 | * 失败返回数据 21 | * @param {Object} {content, message} 22 | * @param content? 内容 23 | * @param message 消息 24 | * @returns Object 25 | */ 26 | function failedResponse({content = undefined, message = '操作失败'} = {}) { 27 | const result = JSON.parse(JSON.stringify({content, message})); 28 | return { 29 | failed: true, 30 | message, 31 | ...result 32 | } 33 | } 34 | 35 | module.exports = { successResponse, failedResponse } 36 | -------------------------------------------------------------------------------- /server/utils/utils.js: -------------------------------------------------------------------------------- 1 | function toSafeNumber(params, arr) { 2 | let newPrams = { ...params }; 3 | arr.forEach(item=>{ 4 | if(params[item]){ 5 | newPrams[item] = +params[item] 6 | } 7 | }) 8 | return newPrams; 9 | } 10 | 11 | module.exports = { toSafeNumber } -------------------------------------------------------------------------------- /src/App.module.less: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | justify-content: center; 4 | color: red; 5 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './App.module.less'; 3 | 4 | class App extends React.Component { 5 | public render() { 6 | return ( 7 |
8 | 1 9 |
10 | ); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/actions/todo.tsx: -------------------------------------------------------------------------------- 1 | import { ADD, DELETE, COMPLETED } from '../constance/todo'; 2 | 3 | export function updateState(payload: object) { 4 | return { 5 | payload, 6 | type: ADD, 7 | } 8 | } 9 | export function deleteTodo(payload: object) { 10 | return { 11 | payload, 12 | type: DELETE, 13 | } 14 | } 15 | 16 | export function toggleCompleted(payload: any) { 17 | return { 18 | payload, 19 | type: COMPLETED, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/actions/user.tsx: -------------------------------------------------------------------------------- 1 | import { QUERY } from '../constance/user'; 2 | 3 | export function updateState(payload: object) { 4 | return { 5 | payload, 6 | type: QUERY, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/constance/todo.tsx: -------------------------------------------------------------------------------- 1 | const ADD = "ADD"; 2 | const DELETE = "DELETE"; 3 | const COMPLETED = "COMPLETED"; 4 | 5 | export { ADD, DELETE, COMPLETED } -------------------------------------------------------------------------------- /src/constance/user.tsx: -------------------------------------------------------------------------------- 1 | const QUERY = "QUERY"; 2 | 3 | export { QUERY } -------------------------------------------------------------------------------- /src/constance/utils.tsx: -------------------------------------------------------------------------------- 1 | export const PAGE_SIZE_OPTIONS = ['10', '20', '50', '100']; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { createStore, compose, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | import { composeWithDevTools } from 'redux-devtools-extension'; 7 | import logger from 'redux-logger'; 8 | import { LocaleProvider } from 'antd'; 9 | import zh_CN from 'antd/lib/locale-provider/zh_CN'; 10 | import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; 11 | 12 | import reducer from './reducer'; 13 | import Todo from './routes/Todo/Todo'; 14 | import Login from './routes/Login'; 15 | import Register from './routes/Register'; 16 | import './index.css'; 17 | 18 | const middleware = [logger, thunk]; 19 | // const middleware = [thunk].concat(process.env.NODE_ENV === `development`?logger:[]); 20 | const store = createStore(reducer, compose( 21 | applyMiddleware(...middleware), 22 | composeWithDevTools() 23 | )) 24 | ReactDOM.render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | document.getElementById('root') as HTMLElement 38 | ); 39 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/reducer/index.tsx: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import todo from './todo'; 3 | import user from './user'; 4 | 5 | export default combineReducers({ todo, user }); -------------------------------------------------------------------------------- /src/reducer/todo.tsx: -------------------------------------------------------------------------------- 1 | import { ADD, DELETE, COMPLETED } from '../constance/todo'; 2 | 3 | interface ActionProps { 4 | payload: any; 5 | type: string; 6 | } 7 | interface S { 8 | pagination: object, 9 | todoList: any[]; 10 | } 11 | const initialState = { 12 | pagination: {}, 13 | todoList: [], 14 | } 15 | export default function todo(state: S = initialState, action: ActionProps) { 16 | switch(action.type) { 17 | case ADD: 18 | return { 19 | ...state, 20 | ...action.payload 21 | } 22 | case DELETE: 23 | return { 24 | ...state, 25 | todoList: state.todoList.filter((item: any,index: number)=>action.payload.index!==index) 26 | } 27 | case COMPLETED: 28 | const { todoList } = state; 29 | const { selectedRowKeys } = action.payload; 30 | return { 31 | ...state, 32 | todoList: todoList.map((item: { _id: any, completed: boolean })=>{ 33 | if(selectedRowKeys.includes(item._id)) { 34 | return { 35 | ...item, 36 | completed: true, 37 | } 38 | } else { 39 | if(item.completed) { 40 | return { 41 | ...item, 42 | completed: false, 43 | } 44 | } 45 | return item; 46 | } 47 | }) 48 | } 49 | default: 50 | return state; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/reducer/user.tsx: -------------------------------------------------------------------------------- 1 | import { QUERY } from '../constance/user'; 2 | 3 | interface ActionProps { 4 | payload: any; 5 | type: string; 6 | } 7 | interface S { 8 | userList: any[]; 9 | } 10 | const initialState = { 11 | userList: [], 12 | } 13 | export default function user(state: S = initialState, action: ActionProps) { 14 | switch(action.type) { 15 | case QUERY: 16 | return { 17 | ...state, 18 | ...action.payload 19 | } 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log('New content is available; please refresh.'); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log('Content is cached for offline use.'); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type')!.indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | 'No internet connection found. App is running in offline mode.' 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ('serviceWorker' in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/routes/Login/index.module.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | flex-direction: column; 5 | padding: 20px; 6 | :global { 7 | .ant-form { 8 | width: 300px; 9 | .ant-row { 10 | display: flex; 11 | margin: 10px 0 0 0; 12 | justify-content: center; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/routes/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | import { withRouter, RouteComponentProps } from 'react-router-dom'; 4 | import { Bind } from 'lodash-decorators'; 5 | import { login } from '../../services/user'; 6 | import notification from '../../utils/notification'; 7 | import { getResponse } from '../../utils/utils'; 8 | 9 | import styles from './index.module.less'; 10 | 11 | interface LoginProps { 12 | form: any 13 | } 14 | const FormItem = Form.Item; 15 | 16 | class Login extends Component { 17 | @Bind() 18 | public handleLogin() { 19 | const { form: { validateFields } } = this.props; 20 | validateFields((err: object, values: object)=>{ 21 | if(!err) { 22 | login(values).then(res=>{ 23 | const result = getResponse(res); 24 | if(result) { 25 | const { _id } = result.content; 26 | notification.success(); 27 | sessionStorage.setItem('token',result.token); 28 | const path = { 29 | pathname: `/todo/${_id}`, 30 | // state: { 31 | // name: 'huangbin', 32 | // age: 23 33 | // }, 34 | // search: 'name=huang&age=23' 35 | } 36 | this.props.history.push(path); 37 | } 38 | }) 39 | } 40 | }) 41 | } 42 | public render() { 43 | const { form: { getFieldDecorator } } = this.props; 44 | return ( 45 |
46 |
47 | 48 | {getFieldDecorator('name',{ 49 | rules: [ 50 | {required: true} 51 | ] 52 | })( 53 | 54 | )} 55 | 56 | 57 | {getFieldDecorator('passWord',{ 58 | rules: [ 59 | {required: true} 60 | ] 61 | })( 62 | 63 | )} 64 | 65 | 66 | 67 | 68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default withRouter(Form.create()(Login)); -------------------------------------------------------------------------------- /src/routes/Register/index.module.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | flex-direction: column; 5 | padding: 20px; 6 | :global { 7 | .ant-form { 8 | width: 300px; 9 | .ant-row { 10 | display: flex; 11 | margin: 10px 0 0 0; 12 | justify-content: center; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/routes/Register/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; 4 | 5 | import notification from '../../utils/notification'; 6 | import { getResponse } from '../../utils/utils'; 7 | import { register } from '../../services/user'; 8 | import { Bind } from 'lodash-decorators'; 9 | import styles from './index.module.less'; 10 | 11 | interface RegisterProps { 12 | form: any 13 | } 14 | const FormItem = Form.Item; 15 | class Register extends Component { 16 | @Bind() 17 | public handleRegister() { 18 | const { form: { validateFields } } = this.props; 19 | validateFields((err: any,values: any)=>{ 20 | if(!err) { 21 | register(values).then(res=>{ 22 | const result = getResponse(res); 23 | if(result) { 24 | notification.success(); 25 | } 26 | }) 27 | } 28 | }) 29 | } 30 | public render() { 31 | const { form: { getFieldDecorator } } = this.props; 32 | return ( 33 |
34 |
35 | 36 | {getFieldDecorator('name',{ 37 | rules: [ 38 | {required: true} 39 | ] 40 | })( 41 | 42 | )} 43 | 44 | 45 | {getFieldDecorator('passWord',{ 46 | rules: [ 47 | {required: true} 48 | ] 49 | })( 50 | 51 | )} 52 | 53 | 54 | 55 | 登录 56 | 57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | export default withRouter(Form.create()(Register)); -------------------------------------------------------------------------------- /src/routes/Todo/Todo.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Input, Form } from 'antd'; 3 | import uuid from 'uuid/v4'; 4 | import { connect } from 'react-redux'; 5 | import { Bind } from 'lodash-decorators'; 6 | import { withRouter, RouteComponentProps } from 'react-router-dom'; 7 | 8 | import { fetchUserList } from 'service/user'; 9 | import { getResponse } from 'utils/utils'; 10 | import notification from 'utils/notification'; 11 | import * as actions from '../../actions/todo'; // shadow name problem must to import * 12 | import * as userActions from '../../actions/user'; 13 | import { add } from '../../services/todo'; 14 | import TodoList from './TodoList'; 15 | import styles from './index.module.less'; 16 | 17 | interface TodoProps { 18 | updateState: any, 19 | updateUser: any, 20 | form: any, 21 | todoState: any, 22 | userId: number, 23 | } 24 | interface S { 25 | userId: number; 26 | listDom: any; 27 | } 28 | const FormItem = Form.Item; 29 | // @connect( 30 | // (state: any)=>state, 31 | // { updateState } 32 | // ) 33 | class Todo extends Component { 34 | constructor(props: TodoProps & RouteComponentProps) { 35 | super(props); 36 | const { userId } = this.props.match.params as TodoProps; 37 | this.state = { 38 | userId, 39 | listDom: undefined, 40 | } 41 | } 42 | public componentDidMount() { 43 | const { updateUser } = this.props; 44 | fetchUserList({}).then(res=>{ 45 | const result = getResponse(res); 46 | if(result) { 47 | updateUser({ userList: result.content }); 48 | } 49 | }) 50 | } 51 | @Bind() 52 | public handleAdd() { 53 | const { 54 | updateState, 55 | form: { validateFieldsAndScroll, resetFields }, 56 | todoState: { todoList = [] } 57 | } = this.props; 58 | const { userId, listDom } = this.state; 59 | validateFieldsAndScroll((err: any, values: any)=>{ 60 | if(!err) { 61 | const { content } = values; 62 | content && updateState({ 63 | todoList: [ 64 | { userId, content, completed: false, time: Date.now(), _id: uuid() }, 65 | ...todoList 66 | ] 67 | }); 68 | add({ userId, content, completed: false }).then(res=>{ 69 | if(res) { 70 | notification.success(); 71 | listDom && listDom.handleSearchList({ userId }); 72 | } 73 | }) 74 | resetFields(); 75 | } 76 | }) 77 | } 78 | 79 | @Bind() 80 | public handleRef(node: any) { 81 | this.setState({ 82 | listDom: node 83 | }) 84 | } 85 | public render() { 86 | const { userId } = this.state; 87 | const { form } = this.props; 88 | const { getFieldDecorator } = form; 89 | return ( 90 |
91 |
92 | 93 | { 94 | getFieldDecorator('content',{ 95 | rules: [ 96 | {required: true} 97 | ] 98 | })( 99 | 100 | ) 101 | } 102 | 103 | 104 |
105 | 106 |
107 | ); 108 | } 109 | } 110 | 111 | export default withRouter(connect( 112 | (state: any)=>({todoState: state.todo}), 113 | { updateState: actions.updateState, updateUser: userActions.updateState } 114 | )(Form.create()(Todo))); -------------------------------------------------------------------------------- /src/routes/Todo/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table, Button, Badge } from 'antd'; 3 | import { connect } from 'react-redux'; 4 | import { Bind } from 'lodash-decorators'; 5 | import { getResponse, createPagination } from 'utils/utils'; 6 | import { renderTimeStamp } from 'utils/render'; 7 | import { fetchTodoList, deleteTodo, toggleTodo } from 'service/todo'; 8 | import * as actions from '../../actions/todo'; 9 | import notification from 'utils/notification'; 10 | 11 | interface TodoListProps { 12 | user: any, 13 | todoState: any, 14 | updateState: any, 15 | toggleCompleted: any, 16 | userId: number, 17 | onRef: any, 18 | } 19 | interface S { 20 | loading: boolean; 21 | selectedRowKeys: string[]; 22 | } 23 | interface RecordProps { 24 | _id: any; 25 | completed: boolean, 26 | } 27 | class TodoList extends Component { 28 | // public static getDerivedStateFromProps(props: TodoListProps, state: S): any { 29 | // const { loading } = state; 30 | // const { loading: nextLoading } = props; 31 | // if( !loading && nextLoading !== loading ) { 32 | // return { 33 | // loading: nextLoading, 34 | // } 35 | // } 36 | // return null; 37 | // } 38 | constructor(props: TodoListProps) { 39 | super(props); 40 | props.onRef(this); 41 | this.state = { 42 | loading: true, 43 | selectedRowKeys: [], 44 | } 45 | } 46 | public componentDidMount() { 47 | const { userId } = this.props; 48 | this.handleSearchList({ userId }); 49 | } 50 | @Bind() 51 | public handleSearchList(params: object) { 52 | this.setState({ loading: true }); 53 | const { updateState } = this.props; 54 | fetchTodoList(params).then(res=>{ 55 | const result = getResponse(res); 56 | if(result) { 57 | const { content } = result; 58 | this.setState({ loading: false }); 59 | updateState({ todoList: content, pagination: createPagination(result) }); 60 | this.handleSetSelectedRowKeys(content); 61 | } 62 | }) 63 | } 64 | @Bind() 65 | public handleSetSelectedRowKeys(content: any[]) { 66 | const selectedRowKeys: any[] = []; 67 | content.forEach((item: { _id: any, completed: boolean })=>{ 68 | if(item.completed) { 69 | selectedRowKeys.push(item._id); 70 | } 71 | }) 72 | console.log(selectedRowKeys); 73 | this.setState({ selectedRowKeys }); 74 | } 75 | @Bind() 76 | public handleDelete(record: RecordProps) { 77 | deleteTodo({_id: record._id}).then(res=>{ 78 | const result = getResponse(res); 79 | if(result) { 80 | notification.success(); 81 | const { userId } = this.props; 82 | this.handleSearchList({ userId }); 83 | } 84 | }) 85 | } 86 | @Bind() 87 | public handleChangeRow(selectedRowKeys: string[], selectedRows: any[]) { 88 | const { userId, todoState: { todoList = [] } } = this.props; 89 | // this.setState({ selectedRowKeys }); 90 | // toggleCompleted({ selectedRowKeys }); 91 | const toggleList: object[] = []; 92 | todoList.forEach((item: RecordProps)=> { 93 | if(selectedRowKeys.indexOf(item._id) >= 0) { 94 | if(!item.completed) { 95 | toggleList.push(item); // 如果被选中但是先前没完成 96 | } 97 | } else if(item.completed) { 98 | toggleList.push(item); // 或者之前没被选中 现在是完成 99 | } 100 | }) 101 | toggleTodo({ toggleList }).then((res: object)=> { 102 | const result = getResponse(res); 103 | if(result) { 104 | notification.success(); 105 | this.handleSearchList({ userId }); 106 | } 107 | }) 108 | } 109 | @Bind() 110 | public handleChangePage(pagination: { current: number, pageSize: number }) { 111 | const { updateState, userId } = this.props; 112 | const { current, pageSize } = pagination; 113 | this.setState({ loading: true }); 114 | fetchTodoList({ userId, current, pageSize }).then(res=>{ 115 | const result = getResponse(res); 116 | if(result) { 117 | updateState({ todoList: result.content, pagination: createPagination(result) }); 118 | this.setState({ loading: false }); 119 | } 120 | }) 121 | } 122 | public render() { 123 | const { loading = false, selectedRowKeys } = this.state; 124 | const { todoState: { todoList = [], pagination = {} }, user, userId } = this.props; 125 | const rowSelection = { 126 | selectedRowKeys, 127 | onChange: this.handleChangeRow, 128 | } 129 | const { name } = user.userList.length > 0 && user.userList.find((item: { _id: any })=> item._id === userId); 130 | const columns = [ 131 | { 132 | title: '用户名', 133 | dataIndex: 'userId', 134 | width: 250, 135 | render: ()=> {name}, 136 | }, 137 | { 138 | title: '内容', 139 | dataIndex: 'content', 140 | width: 250, 141 | render: (val: string, record: RecordProps) => 142 | record.completed ? 143 | {val} : 144 | val, 145 | }, 146 | { 147 | title: '是否已完成', 148 | dataIndex: 'completed', 149 | width: 120, 150 | align: 'center' as 'center', 151 | render: (value: boolean) => value ? : , 152 | }, 153 | { 154 | title: '备忘时间', 155 | dataIndex: 'time', 156 | width: 250, 157 | render: renderTimeStamp, 158 | }, 159 | { 160 | title: '操作', 161 | dataIndex: 'operation', 162 | align: 'center' as 'center', 163 | width: 100, 164 | render: (val: string, record: RecordProps, index: number) => 165 | , 166 | } 167 | ]; 168 | const tableProps = { 169 | columns, 170 | loading, 171 | pagination, 172 | rowSelection, 173 | rowKey: '_id', 174 | scroll: { y: 400 }, 175 | bordered: true, 176 | dataSource: todoList, 177 | onChange: this.handleChangePage, 178 | } 179 | return ( 180 | 181 | ) 182 | } 183 | } 184 | export default connect( 185 | (state: any)=>({todoState: state.todo, user: state.user}), 186 | { updateState: actions.updateState, toggleCompleted: actions.toggleCompleted } 187 | )(TodoList); 188 | -------------------------------------------------------------------------------- /src/routes/Todo/index.module.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | flex-flow: column nowrap; 5 | :global { 6 | p { 7 | margin: 0; 8 | } 9 | } 10 | .todo { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | padding: 20px; 15 | color: rgba(24, 144, 255, .8); 16 | :global { 17 | .ant-row,.ant-form-item { 18 | display: flex; 19 | flex-direction: row; 20 | margin-bottom: 0; 21 | } 22 | .ant-input { 23 | width: 150px; 24 | margin: 0 20px; 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/services/todo.tsx: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | import { parseParams } from 'utils/utils'; 3 | 4 | export async function add(params: any) { 5 | return request(`/todo/add`, { 6 | method: 'POST', 7 | data: params, 8 | }) 9 | } 10 | 11 | export async function fetchTodoList(params: any) { 12 | return request(`/todo/fetch`, { 13 | method: 'GET', 14 | params: parseParams(params), 15 | }) 16 | } 17 | 18 | export async function deleteTodo(params: any) { 19 | return request(`/todo/delete`, { 20 | method: 'DELETE', 21 | data: params, 22 | }) 23 | } 24 | 25 | export async function toggleTodo(params: any) { 26 | return request(`/todo/toggle`, { 27 | method: 'PUT', 28 | data: params, 29 | }) 30 | } -------------------------------------------------------------------------------- /src/services/user.tsx: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | 3 | export async function register(params: any) { 4 | return request(`/user/register`, { 5 | method: 'POST', 6 | data: params, 7 | }) 8 | } 9 | 10 | export async function login(params: any) { 11 | return request(`/user/login`, { 12 | method: 'POST', 13 | data: params, 14 | }) 15 | } 16 | 17 | export async function fetchUserList(params: any) { 18 | return request(`/user/fetchUserList`, { 19 | method: 'GET', 20 | params, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/connect.tsx: -------------------------------------------------------------------------------- 1 | import { connect as connectComponent } from 'react-redux'; 2 | 3 | export const connect = (mapStateToProps: any, actions: any) => { 4 | return (target: any) => ( 5 | connectComponent(mapStateToProps, actions)(target) as any 6 | ); 7 | }; -------------------------------------------------------------------------------- /src/utils/notification.tsx: -------------------------------------------------------------------------------- 1 | import { notification } from 'antd'; 2 | 3 | interface ParamsProps { 4 | message: string 5 | description: string 6 | // [key: string]: any 7 | } 8 | /** 9 | * 操作成功通知提示 10 | * @function success 11 | * @param {object} params - 默认属性 12 | * @param {?string} [params.message=操作成功] - 提示信息 13 | * @param {?string} [params.description] - 详细描述 14 | */ 15 | function success(params?: ParamsProps) { 16 | const { message = "操作成功", description = "", ...others } = params || {}; 17 | notification.success({ 18 | message, 19 | description, 20 | duration: 2, 21 | ...others, 22 | }); 23 | } 24 | 25 | // /** 26 | // * 操作失败通知提示 27 | // * @function error 28 | // * @param {object} params - 默认属性 29 | // * @param {?string} [params.message=操作失败] - 提示信息 30 | // * @param {?string} [params.description] - 详细描述 31 | // */ 32 | // function error({ message:string = "操作失败", description: string = "", ...others: any }) { 33 | // notification.error({ 34 | // message: message, 35 | // description, 36 | // duration: 2, 37 | // ...others, 38 | // }); 39 | // } 40 | 41 | // /** 42 | // * 操作异常通知提示 43 | // * @function warning 44 | // * @param {object} params - 默认属性 45 | // * @param {?string} [params.message=操作异常] - 提示信息 46 | // * @param {?string} [params.description] - 详细描述 47 | // */ 48 | // function warning({ message: string, description: string, ...others: any }) { 49 | // notification.warning({ 50 | // message: message || "操作异常", 51 | // description, 52 | // duration: 2, 53 | // ...others, 54 | // }); 55 | // } 56 | 57 | // /** 58 | // * 操作信息通知提示 59 | // * @function info 60 | // * @param {object} config - 默认属性 61 | // * @param {?string} [params.message] - 提示信息 62 | // * @param {?string} [params.description] - 详细描述 63 | // */ 64 | // function info({ message: string, description: string, ...others: any }) { 65 | // notification.info({ 66 | // message, 67 | // description, 68 | // duration: 2, 69 | // ...others, 70 | // }); 71 | // } 72 | 73 | export default { 74 | success, 75 | // error, 76 | // warning, 77 | // info, 78 | }; 79 | -------------------------------------------------------------------------------- /src/utils/render.tsx: -------------------------------------------------------------------------------- 1 | function renderTimeStamp(time: number) { 2 | return new Date(time).toLocaleString(); 3 | } 4 | 5 | export { renderTimeStamp } -------------------------------------------------------------------------------- /src/utils/request.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { notification } from 'antd'; 3 | 4 | notification.config({ 5 | placement: 'bottomRight', 6 | }); 7 | 8 | function checkStatus(response: any) { 9 | if (response.status >= 200 && response.status < 300) { 10 | return response; 11 | } 12 | 13 | const errorText = response.statusText; 14 | const error = new Error(errorText); 15 | // error.name = response.status; 16 | // error.response = response; 17 | throw error; 18 | } 19 | 20 | export default function request(url: string, options: any) { 21 | const newOptions = { responseType: 'json', ...options }; 22 | // newOptions.body = JSON.stringify(newOptions.body); 23 | const token = sessionStorage.getItem('token'); 24 | return axios({ 25 | url, 26 | headers: { 27 | 'Authorization': `bearer ${token}` 28 | }, 29 | ...newOptions 30 | }) 31 | .then(checkStatus) 32 | .then(response => { 33 | if (response.status === 204) { 34 | return {}; 35 | } 36 | if (newOptions.responseType === 'blob') { 37 | return response.blob(); 38 | } 39 | return response; 40 | }) 41 | .catch(e => { 42 | const status = e.name; 43 | if (status === 401) { 44 | // removeAccessToken(); 45 | window.location.href = "localhost:3000"; 46 | return; 47 | } 48 | 49 | notification.error({ 50 | message: `请求错误 ${status}`, 51 | description: e.message, 52 | }); 53 | }); 54 | } -------------------------------------------------------------------------------- /src/utils/utils.tsx: -------------------------------------------------------------------------------- 1 | import { notification } from 'antd'; 2 | import { PAGE_SIZE_OPTIONS } from 'constance/utils'; 3 | 4 | interface ResponseProps { 5 | current: number; 6 | pageSize: number; 7 | total: number; 8 | } 9 | function getResponse(response: any) { 10 | if (response) { 11 | if(response.data.failed) { 12 | notification.error({ 13 | message: '请求错误', 14 | description: response.data.message, 15 | }); 16 | } else { 17 | return response.data; 18 | } 19 | } 20 | } 21 | 22 | function createPagination(response: ResponseProps) { 23 | if(response) { 24 | return { 25 | showSizeChanger: true, 26 | pageSizeOptions: [...PAGE_SIZE_OPTIONS], 27 | current: response.current + 1, 28 | pageSize: response.pageSize, 29 | total: response.total, 30 | showTotal: (total: number, range: any) => `显示 ${range[0]} - ${range[1]} 共 ${total} 条` 31 | } 32 | } 33 | return {} 34 | } 35 | 36 | function parseParams(params: ResponseProps) { 37 | const { current = 1, pageSize = 10, ...other } = params; 38 | return { 39 | current: current - 1, 40 | pageSize, 41 | ...other, 42 | } 43 | } 44 | 45 | export { getResponse, createPagination, parseParams } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | // "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "importHelpers": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "noUnusedLocals": true, 21 | "experimentalDecorators": true, 22 | "allowSyntheticDefaultImports": true, 23 | "paths": { 24 | "utils/*": ["./src/utils/*"], 25 | "service/*": ["./src/services/*"], 26 | "constance/*": ["./src/constance/*"], 27 | "actions/*": ["./src/actions/*"], 28 | } 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "build", 33 | "scripts", 34 | "acceptance-tests", 35 | "webpack", 36 | "jest", 37 | "src/setupTests.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "rules": { 4 | "ordered-imports": false, 5 | "interface-name" : [true, "never-prefix"], 6 | "no-console": [true, "warning"], 7 | "no-unused-expression": false, 8 | "jsx-boolean-value": false, 9 | "jsx-no-lambda": false, 10 | "object-literal-sort-keys": false 11 | }, 12 | "linterOptions": { 13 | "exclude": [ 14 | "config/**/*.js", 15 | "node_modules/**/*.ts", 16 | "coverage/lcov-report/*.js", 17 | "server/**/*.js" 18 | ] 19 | } 20 | } 21 | --------------------------------------------------------------------------------