Repository: B2D1/TodoList
Branch: master
Commit: 600ca9447531
Files: 54
Total size: 44.8 KB
Directory structure:
gitextract_ivoo60g2/
├── .gitignore
├── README.md
├── package.json
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── server/
│ ├── package.json
│ ├── src/
│ │ ├── app.ts
│ │ ├── config.ts
│ │ ├── db/
│ │ │ ├── index.ts
│ │ │ ├── models/
│ │ │ │ ├── todo.ts
│ │ │ │ └── user.ts
│ │ │ └── schemas/
│ │ │ ├── todo.ts
│ │ │ └── user.ts
│ │ ├── routes/
│ │ │ ├── todo.ts
│ │ │ └── user.ts
│ │ ├── services/
│ │ │ ├── todo.ts
│ │ │ └── user.ts
│ │ └── utils/
│ │ ├── enum.ts
│ │ └── response.ts
│ └── tsconfig.json
├── src/
│ ├── App.css
│ ├── App.tsx
│ ├── api/
│ │ ├── request.ts
│ │ ├── todo.ts
│ │ └── user.ts
│ ├── common/
│ │ ├── config/
│ │ │ └── index.ts
│ │ ├── enum/
│ │ │ └── index.ts
│ │ └── interface/
│ │ └── index.ts
│ ├── components/
│ │ ├── TodoItem/
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── TodoModal/
│ │ │ └── index.tsx
│ │ └── UserForm/
│ │ └── index.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── saga.ts
│ ├── store/
│ │ ├── index.ts
│ │ ├── todo/
│ │ │ ├── actions.ts
│ │ │ ├── reducers.ts
│ │ │ ├── saga.ts
│ │ │ └── types.ts
│ │ └── user/
│ │ ├── actions.ts
│ │ ├── reducers.ts
│ │ ├── saga.ts
│ │ └── types.ts
│ ├── utils/
│ │ └── index.ts
│ └── views/
│ ├── Home/
│ │ └── index.tsx
│ ├── Login/
│ │ ├── index.module.scss
│ │ └── index.tsx
│ └── Todo/
│ ├── index.module.scss
│ └── index.tsx
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/server/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/server/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.vscode
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: README.md
================================================
## 基于 TS + React + AntD + Koa + MongoDB 实现的 TodoList 全栈应用

### 应用特点
- 前后端均用 TypeScript 编写
- 接口统一遵循 RESTful 风格
- 实现服务端的优雅错误处理
### 技术栈
- 语言
- TypeScript(赋予 JS 强类型语言的特性)
- 前端
- React(当下最流行的前端框架)
- Axios(处理 HTTP 请求)
- Ant-Design(阿里开源的 UI 语言框架)
- React-Router(处理页面路由)
- Redux(数据状态管理)
- Redux-Saga(处理异步 Action)
- 后端
- Koa(基于 Node.js 平台的下一代 web 开发框架)
- Mongoose(内置数据验证, 查询构建,业务逻辑钩子等,开箱即用)
### 本地运行
```bash
# clone
git clone https://github.com/B2D1/TodoList.git
```
```bash
cd /TodoList/server
yarn
# 启动后端服务,监听本地 5000 端口,请自行下载 MongoDB,并开启数据库服务
yarn start
```
```bash
cd /TodoList
yarn
# 启动 react 项目
yarn start
```
### 相关链接
[TS + React + AntD + Koa2 + MongoDB 打造 TodoList 全栈应用](https://baobangdong.cn/todolist-full-stack-application/)
================================================
FILE: package.json
================================================
{
"name": "TodoList",
"version": "1.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"antd": "^4.15.0",
"axios": "^0.21.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"redux": "^4.0.5",
"redux-saga": "^1.1.3",
"sass": "^1.32.8",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-router-dom": "^5.1.7",
"redux-devtools-extension": "^2.13.9"
}
}
================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>TodoList</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
FILE: public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: server/package.json
================================================
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node ./src/app.ts",
"watch": "nodemon",
"build": "tsc",
"serve": "node dist/app.js"
},
"nodemonConfig": {
"ignore": [
"node_modules"
],
"watch": [
"src"
],
"exec": "yarn start",
"ext": "ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@koa/cors": "^3.1.0",
"koa": "^2.13.1",
"koa-bodyparser": "^4.3.0",
"koa-router": "^10.0.0",
"mongoose": "^5.12.2",
"nodemon": "^2.0.7",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"devDependencies": {
"@types/koa": "^2.13.1",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-router": "^7.4.1",
"@types/koa__cors": "^3.0.2",
"@types/node": "^12.0.8"
}
}
================================================
FILE: server/src/app.ts
================================================
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import Config from './config';
import connectDB from './db';
import todoRouter from './routes/todo';
import userRouter from './routes/user';
const app = new Koa();
connectDB(Config.MONGODB_URI);
app
.use(cors())
.use(bodyParser())
.use(userRouter.routes())
.use(todoRouter.routes());
app.listen(Config.PORT, () => {
console.log(`server starts successful: http://localhost:${Config.PORT}`);
});
================================================
FILE: server/src/config.ts
================================================
const Config = {
/**
* 监听端口
*/
PORT: 5000,
/**
* MongoDB 数据库地址
*/
MONGODB_URI: "mongodb://localhost:27017/todo",
};
export default Config;
================================================
FILE: server/src/db/index.ts
================================================
import mongoose from 'mongoose';
export default (db: string) => {
const connect = () => {
mongoose
.connect(db, {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
})
.then(() => {
return console.log(`Successfully connected to ${db}`);
})
.catch((error) => {
console.log('Error connecting to database: ', error);
return process.exit(1);
});
};
connect();
mongoose.connection.on('disconnected', connect);
};
================================================
FILE: server/src/db/models/todo.ts
================================================
import { model } from "mongoose";
import { ITodo, TodoSchema } from "../schemas/todo";
export default model<ITodo>("Todo", TodoSchema);
================================================
FILE: server/src/db/models/user.ts
================================================
import { model } from 'mongoose';
import { UserSchema, IUser } from '../schemas/user';
export default model<IUser>('User', UserSchema);
================================================
FILE: server/src/db/schemas/todo.ts
================================================
import { Document, Schema } from 'mongoose';
export interface ITodo extends Document {
content: string;
status: boolean;
}
export const TodoSchema: Schema = new Schema({
content: String,
status: {
type: Boolean,
default: false,
},
});
TodoSchema.index({ content: 'text' });
================================================
FILE: server/src/db/schemas/user.ts
================================================
import { Document, Schema } from 'mongoose';
import { ITodo } from './todo';
export interface IUser extends Document {
usr: string;
psd: string;
todos: ITodo[];
}
export const UserSchema: Schema = new Schema({
usr: {
type: String,
required: true,
unique: true,
},
psd: {
type: String,
required: true,
},
todos: [
{
type: Schema.Types.ObjectId,
ref: 'Todo',
},
],
});
================================================
FILE: server/src/routes/todo.ts
================================================
import { Context } from 'koa';
import Router from 'koa-router';
import TodoService from '../services/todo';
import { StatusCode } from '../utils/enum';
import createRes from '../utils/response';
const todoService = new TodoService();
const todoRouter = new Router({
prefix: '/api/todos',
});
todoRouter
.get('/search', async (ctx: Context) => {
const { userId, query } = ctx.query;
try {
const data = await todoService.searchTodo(
userId as string,
query as string
);
if (data) {
createRes({
ctx,
data,
});
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.get('/:userId', async (ctx: Context) => {
const userId = ctx.params.userId;
try {
const data = await todoService.getAllTodos(userId);
if (data) {
createRes({
ctx,
data,
});
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.put('/status', async (ctx: Context) => {
const payload = ctx.request.body;
const { todoId } = payload;
try {
const data = await todoService.updateTodoStatus(todoId);
if (data) {
createRes({ ctx, statusCode: StatusCode.Accepted });
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.put('/content', async (ctx: Context) => {
const payload = ctx.request.body;
const { todoId, content } = payload;
try {
const data = await todoService.updateTodoContent(todoId, content);
if (data) {
createRes({ ctx, statusCode: StatusCode.Accepted });
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.post('/', async (ctx: Context) => {
const payload = ctx.request.body;
const { userId, content } = payload;
try {
const data = await todoService.addTodo(userId, content);
if (data) {
createRes({
ctx,
statusCode: StatusCode.Created,
data,
});
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.delete('/:todoId', async (ctx: Context) => {
const todoId = ctx.params.todoId;
try {
const data = await todoService.deleteTodo(todoId);
if (data) {
createRes({ ctx, statusCode: StatusCode.NoContent });
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
});
export default todoRouter;
================================================
FILE: server/src/routes/user.ts
================================================
import { Context, Request } from 'koa';
import Router from 'koa-router';
import UserService from '../services/user';
import { StatusCode } from '../utils/enum';
import createRes from '../utils/response';
const userService = new UserService();
const userRouter = new Router({
prefix: '/api/users',
});
userRouter
.post('/login', async (ctx: Context) => {
const payload = ctx.request.body;
const { username, password } = payload;
try {
const user = await userService.validUser(username, password);
createRes({
ctx,
data: {
userId: user._id,
username: user.usr,
},
});
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
})
.post('/', async (ctx: Context) => {
const payload = ctx.request.body;
const { username, password } = payload;
try {
const data = await userService.addUser(username, password);
if (data) {
createRes({
ctx,
statusCode: StatusCode.Created,
});
}
} catch (error) {
createRes({
ctx,
errorCode: 1,
msg: error.message,
});
}
});
export default userRouter;
================================================
FILE: server/src/services/todo.ts
================================================
import Todo from '../db/models/todo';
import User from '../db/models/user';
export default class TodoService {
public async addTodo(userId: string, content: string) {
const todo = new Todo({
content,
});
try {
const res = await todo.save();
const user = await User.findById(userId);
user?.todos.push(res.id);
await user?.save();
return res;
} catch (error) {
throw new Error('新增失败 ( ̄o ̄).zZ');
}
}
public async deleteTodo(todoId: string) {
try {
return await Todo.findByIdAndDelete(todoId);
} catch (error) {
throw new Error('删除失败 ( ̄o ̄).zZ');
}
}
public async getAllTodos(userId: string) {
try {
const res = await User.findById(userId).populate('todos');
return res?.todos;
} catch (error) {
throw new Error('获取失败 ( ̄o ̄).zZ');
}
}
public async updateTodoStatus(todoId: string) {
try {
const oldRecord = await Todo.findById(todoId);
const record = await Todo.findByIdAndUpdate(todoId, {
status: !oldRecord?.status,
});
return record;
} catch (error) {
throw new Error('更新状态失败 ( ̄o ̄).zZ');
}
}
public async updateTodoContent(todoId: string, content: string) {
try {
return await Todo.findByIdAndUpdate(todoId, { content });
} catch (error) {
throw new Error('更新内容失败 ( ̄o ̄).zZ');
}
}
public async searchTodo(userId: string, query: string) {
try {
// MongoDB Text Search 对中文支持不佳
// e.g. 当 query 为“你好”,“你好张三"不匹配,”你好,张三“匹配
// return await User.findById(userId).populate({
// path: 'todos',
// match: { $text: { $search: query } },
// });
return await User.findById(userId).populate({
path: 'todos',
match: { content: { $regex: new RegExp(query), $options: 'i' } },
});
} catch (error) {
throw new Error('查询失败 ( ̄o ̄).zZ');
}
}
}
================================================
FILE: server/src/services/user.ts
================================================
import User from '../db/models/user';
export default class UserService {
public async addUser(usr: string, psd: string) {
try {
const user = new User({
usr,
psd,
todos: [],
});
return await user.save();
} catch (error) {
if (error.code === 11000) {
// MongoError: E11000 duplicate key error collection
throw new Error('用户名已存在 ( ̄o ̄).zZ');
} else {
throw error;
}
}
}
public async validUser(usr: string, psd: string) {
try {
const user = await User.findOne({
usr,
});
// 查询用户
if (!user) {
throw new Error('用户不存在 ( ̄o ̄).zZ');
}
// 校验密码
if (psd === user.psd) {
return user;
}
throw new Error('密码错误 ( ̄o ̄).zZ');
} catch (error) {
throw new Error(error.message);
}
}
}
================================================
FILE: server/src/utils/enum.ts
================================================
export enum StatusCode {
/**
* 成功
*/
OK = 200,
/**
* 更新成功
*/
Accepted = 202,
/**
* 删除成功
*/
NoContent = 204,
/**
* 创建成功
*/
Created = 201,
}
================================================
FILE: server/src/utils/response.ts
================================================
import { Context } from "koa";
import { StatusCode } from "./enum";
interface IRes {
ctx: Context;
statusCode?: number;
data?: any;
errorCode?: number;
msg?: string;
}
const createRes = (params: IRes) => {
params.ctx.status = params.statusCode! || StatusCode.OK;
params.ctx.body = {
error_code: params.errorCode || 0,
data: params.data || null,
msg: params.msg || "",
};
};
export default createRes;
================================================
FILE: server/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
},
"include": ["src/**/*"]
}
================================================
FILE: src/App.css
================================================
@import '~antd/dist/antd.css';
================================================
FILE: src/App.tsx
================================================
import './App.css';
import { message } from 'antd';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from 'views/Home';
import Login from './views/Login';
import Todo from './views/Todo';
// 配置全局 message
message.config({
duration: 1,
maxCount: 3,
});
const App = () => (
<>
<BrowserRouter>
<Route path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/todo" component={Todo} />
</BrowserRouter>
</>
);
export default App;
================================================
FILE: src/api/request.ts
================================================
import { message } from 'antd';
import axios from 'axios';
import Config from 'common/config';
import { IRes } from 'common/interface';
const request = axios.create({
baseURL: Config.API_URI,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
});
request.interceptors.response.use((response) => {
const res: IRes = response.data;
// 当 error_code 不为 0 时,统一弹出错误提示框,并抛出错误
if (res.error_code) {
message.warn(res.msg);
throw new Error(res.msg);
}
// 当 error_code 为 0 时,继续返回请求
return response.data;
});
export default request;
================================================
FILE: src/api/todo.ts
================================================
import request from './request';
class TodoAPI {
static PREFIX = '/todos';
static fetchTodo(userId: string) {
return request.get(`${TodoAPI.PREFIX}/${userId}`);
}
static addTodo(userId: string, content: string) {
return request.post(`${TodoAPI.PREFIX}`, {
userId,
content,
});
}
static searchTodo(userId: string, query: string) {
return request.get(
`${TodoAPI.PREFIX}/search?userId=${userId}&query=${query}`
);
}
static deleteTodo(todoId: string) {
return request.delete(`${TodoAPI.PREFIX}/${todoId}`);
}
static updateTodoStatus(todoId: string) {
return request.put(`${TodoAPI.PREFIX}/status`, {
todoId,
});
}
static updateTodoContent(todoId: string, content: string) {
return request.put(`${TodoAPI.PREFIX}/content`, {
todoId,
content,
});
}
}
export default TodoAPI;
================================================
FILE: src/api/user.ts
================================================
import request from './request';
class UserAPI {
static PREFIX = '/users';
static login(username: string, password: string) {
return request.post(`${UserAPI.PREFIX}/login`, {
username,
password,
});
}
static register(username: string, password: string) {
return request.post(`${UserAPI.PREFIX}`, {
username,
password,
});
}
}
export default UserAPI;
================================================
FILE: src/common/config/index.ts
================================================
enum Config {
API_URI = "http://localhost:5000/api/",
}
export default Config;
================================================
FILE: src/common/enum/index.ts
================================================
export enum ModalType {
Edit = 'EDIT',
Add = 'ADD',
}
================================================
FILE: src/common/interface/index.ts
================================================
export interface IRes {
data: any;
error_code: number;
msg: string;
}
================================================
FILE: src/components/TodoItem/index.module.scss
================================================
.item {
display: flex;
min-height: 3rem;
line-height: 3rem;
padding: 0.5rem 1rem;
background-color: #fff;
border-bottom: 1px solid #ddd;
justify-content: space-between;
align-items: center;
.content {
width: 300px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
font-size: 1.2rem;
@media screen and (max-width: 700px) {
width: 150px;
font-size: 1rem;
}
}
}
.icon {
display: inline-block;
font-size: 1.5rem;
transition: transform 0.2s ease;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
& + .icon {
margin-left: 1rem;
}
@media screen and (max-width: 700px) {
font-size: 1.2rem;
}
}
================================================
FILE: src/components/TodoItem/index.tsx
================================================
import {
CheckOutlined,
DeleteOutlined,
EditOutlined,
UndoOutlined,
} from '@ant-design/icons';
import { ModalType } from 'common/enum';
import { FC } from 'react';
import styles from './index.module.scss';
interface ITodoItem {
id: string;
type: string;
content: string;
finished: boolean;
onShowModal: (type: ModalType, todoId: string, content: string) => void;
onUpdateStatus: (todoId: string) => void;
onDelete: (todoId: string) => void;
}
const TodoItem: FC<ITodoItem> = ({
id,
content,
finished,
onUpdateStatus,
onDelete,
onShowModal,
}) => (
<li>
<div className={styles.item}>
<span className={styles.content}>{content}</span>
<div>
<EditOutlined
className={styles.icon}
onClick={() => onShowModal(ModalType.Edit, id, content)}
/>
{finished ? (
<UndoOutlined
className={styles.icon}
onClick={() => onUpdateStatus(id)}
/>
) : (
<CheckOutlined
className={styles.icon}
onClick={() => onUpdateStatus(id)}
/>
)}
<DeleteOutlined className={styles.icon} onClick={() => onDelete(id)} />
</div>
</div>
</li>
);
export default TodoItem;
================================================
FILE: src/components/TodoModal/index.tsx
================================================
import { Form, Input, Modal } from 'antd';
import { ModalType } from 'common/enum';
import { FC, useEffect } from 'react';
interface ITodoModal {
todoId: string;
modalType: string;
visible: boolean;
title: string;
content: string;
onClose: () => void;
onAdd: (content: string) => void;
onUpdateContent: (todoId: string, content: string) => void;
}
const TodoModal: FC<ITodoModal> = ({
content,
visible,
title,
modalType,
todoId,
onClose,
onAdd,
onUpdateContent,
}) => {
const [form] = Form.useForm();
const handleOK = () => {
form.submit();
};
const handleFinish = () => {
const content = form.getFieldValue('content');
if (modalType === ModalType.Add) {
onAdd(content);
}
if (modalType === ModalType.Edit) {
onUpdateContent(todoId, content);
}
handleCancel();
};
const handleCancel = () => {
form.resetFields();
onClose();
};
useEffect(() => {
form.setFieldsValue({ content });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [content]);
return (
<Modal
title={title}
visible={visible}
onOk={handleOK}
onCancel={handleCancel}
okText="提交"
cancelText="取消"
>
<Form layout="horizontal" form={form} onFinish={handleFinish}>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入内容' }]}
>
<Input placeholder="请输入内容" autoComplete="off" />
</Form.Item>
</Form>
</Modal>
);
};
export default TodoModal;
================================================
FILE: src/components/UserForm/index.tsx
================================================
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { AppState } from 'store';
import { login, register, setLoading } from 'store/user/actions';
const mapDispatch = {
register,
login,
setLoading,
};
const mapState = ({ user }: AppState) => ({
user,
});
interface OwnProps {
showLogin: boolean;
}
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
type Props = PropsFromRedux & OwnProps;
const UserForm: React.FC<Props> = ({
register,
login,
setLoading,
showLogin,
user: { loading },
}) => {
const [form] = Form.useForm();
const onFinish = (values: any) => {
setLoading(true);
const { username, password } = values;
if (showLogin) {
login({
username,
password,
});
} else {
register({
username,
password,
});
}
form.setFieldsValue({ username: '', password: '' });
};
return (
<Form onFinish={onFinish} form={form}>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入您的用户名!' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名"
autoComplete="off"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入您的密码!' }]}
>
<Input prefix={<LockOutlined />} type="password" placeholder="密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
{showLogin ? '登录' : '注册'}
</Button>
</Form.Item>
</Form>
);
};
export default connector(UserForm);
================================================
FILE: src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: src/index.tsx
================================================
import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import reportWebVitals from './reportWebVitals';
import App from './App';
import { store } from './store';
ReactDOM.render(
<Provider store={store}>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: src/react-app-env.d.ts
================================================
/// <reference types="react-scripts" />
================================================
FILE: src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: src/saga.ts
================================================
import { takeEvery } from 'redux-saga/effects';
import {
addTodo,
deleteTodo,
fetchTodo,
searchTodo,
updateTodoContent,
updateTodoStatus,
} from './store/todo/saga';
import {
ADD_TODO,
DELETE_TODO,
FETCH_TODO,
SEARCH_TODO,
UPDATE_TODO_CONTENT,
UPDATE_TODO_STATUS,
} from './store/todo/types';
import { login, logout, register } from './store/user/saga';
import { LOGIN, LOGOUT, REGISTER } from './store/user/types';
function* rootSaga() {
yield takeEvery(LOGIN, login);
yield takeEvery(LOGOUT, logout);
yield takeEvery(REGISTER, register);
yield takeEvery(FETCH_TODO, fetchTodo);
yield takeEvery(SEARCH_TODO, searchTodo);
yield takeEvery(ADD_TODO, addTodo);
yield takeEvery(DELETE_TODO, deleteTodo);
yield takeEvery(UPDATE_TODO_STATUS, updateTodoStatus);
yield takeEvery(UPDATE_TODO_CONTENT, updateTodoContent);
}
export default rootSaga;
================================================
FILE: src/store/index.ts
================================================
import { applyMiddleware, combineReducers, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../saga';
import todoReducer from './todo/reducers';
import userReducer from './user/reducers';
const sagaMiddleware = createSagaMiddleware();
const rootReducer = combineReducers({ todo: todoReducer, user: userReducer });
export const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
export type AppState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
sagaMiddleware.run(rootSaga);
================================================
FILE: src/store/todo/actions.ts
================================================
import {
ADD_TODO,
CLEAR_TODO,
DELETE_TODO,
FETCH_TODO,
SEARCH_TODO,
UPDATE_TODO_CONTENT,
UPDATE_TODO_STATUS,
} from './types';
export const addTodo = (userId: string, content: string) => ({
type: ADD_TODO,
payload: { userId, content },
});
export const fetchTodo = (userId: string) => ({
type: FETCH_TODO,
payload: { userId },
});
export const searchTodo = (userId: string, query: string) => ({
type: SEARCH_TODO,
payload: { userId, query },
});
export const deleteTodo = (todoId: string) => ({
type: DELETE_TODO,
payload: { todoId },
});
export const updateTodoStatus = (todoId: string) => ({
type: UPDATE_TODO_STATUS,
payload: { todoId },
});
export const updateTodoContent = (todoId: string, content: string) => ({
type: UPDATE_TODO_CONTENT,
payload: { todoId, content },
});
export const clearTodo = () => ({
type: CLEAR_TODO,
});
================================================
FILE: src/store/todo/reducers.ts
================================================
import {
ADD_TODO_SUC,
CLEAR_TODO,
DELETE_TODO_SUC,
FETCH_TODO_SUC,
ITodoState,
SEARCH_TODO_SUC,
TodoActionTypes,
UPDATE_TODO_CONTENT_SUC,
UPDATE_TODO_STATUS_SUC,
} from './types';
const initialState: ITodoState[] = [];
export default function todoReducer(
state = initialState,
action: TodoActionTypes
) {
switch (action.type) {
case ADD_TODO_SUC:
return [...state, action.payload];
case FETCH_TODO_SUC:
return [...action.payload];
case DELETE_TODO_SUC:
return state.filter((v) => v._id !== action.payload.todoId);
case UPDATE_TODO_STATUS_SUC:
return state.map((v) =>
v._id === action.payload.todoId ? { ...v, status: !v.status } : v
);
case SEARCH_TODO_SUC:
return [...action.payload];
case UPDATE_TODO_CONTENT_SUC:
return state.map((v) =>
v._id === action.payload.todoId
? { ...v, content: action.payload.content }
: v
);
case CLEAR_TODO:
return initialState;
default:
return state;
}
}
================================================
FILE: src/store/todo/saga.ts
================================================
import { message } from 'antd';
import TodoAPI from 'api/todo';
import { IRes } from 'common/interface';
import { call, put } from 'redux-saga/effects';
import {
ADD_TODO_SUC,
DELETE_TODO_SUC,
FETCH_TODO_SUC,
SEARCH_TODO_SUC,
UPDATE_TODO_CONTENT_SUC,
UPDATE_TODO_STATUS_SUC,
IAddAction,
IDeleteAction,
IFetchAction,
ISearchAction,
IUpdateContentAction,
IUpdateStatusAction,
} from './types';
export function* fetchTodo(action: IFetchAction) {
const { userId } = action.payload;
try {
const res: IRes = yield call(TodoAPI.fetchTodo, userId);
yield put({
type: FETCH_TODO_SUC,
payload: res.data,
});
} catch {}
}
export function* addTodo(action: IAddAction) {
const { userId, content } = action.payload;
try {
const res: IRes = yield call(TodoAPI.addTodo, userId, content);
yield put({
type: ADD_TODO_SUC,
payload: res.data,
});
message.success('新增成功');
} catch {}
}
export function* deleteTodo(action: IDeleteAction) {
const { todoId } = action.payload;
try {
yield call(TodoAPI.deleteTodo, todoId);
yield put({
type: DELETE_TODO_SUC,
payload: { todoId },
});
message.success('删除成功');
} catch {}
}
export function* searchTodo(action: ISearchAction) {
const { userId, query } = action.payload;
try {
const res: IRes = yield call(TodoAPI.searchTodo, userId, query);
yield put({
type: SEARCH_TODO_SUC,
payload: res.data.todos,
});
} catch {}
}
export function* updateTodoStatus(action: IUpdateStatusAction) {
const { todoId } = action.payload;
try {
yield call(TodoAPI.updateTodoStatus, todoId);
yield put({
type: UPDATE_TODO_STATUS_SUC,
payload: { todoId },
});
} catch {}
}
export function* updateTodoContent(action: IUpdateContentAction) {
const { todoId, content } = action.payload;
try {
yield call(TodoAPI.updateTodoContent, todoId, content);
yield put({
type: UPDATE_TODO_CONTENT_SUC,
payload: { todoId, content },
});
message.success('编辑成功');
} catch {}
}
================================================
FILE: src/store/todo/types.ts
================================================
// Constant
export const FETCH_TODO = 'FETCH_TODO';
export const FETCH_TODO_SUC = 'FETCH_TODO_SUC';
export const ADD_TODO = 'ADD_TODO';
export const ADD_TODO_SUC = 'ADD_TODO_SUC';
export const SEARCH_TODO = 'SEARCH_TODO';
export const SEARCH_TODO_SUC = 'SEARCH_TODO_SUC';
export const DELETE_TODO = 'DELETE_TODO';
export const DELETE_TODO_SUC = 'DELETE_TODO_SUC';
export const UPDATE_TODO_CONTENT = 'UPDATE_TODO_CONTENT';
export const UPDATE_TODO_CONTENT_SUC = 'UPDATE_TODO_CONTENT_SUC';
export const UPDATE_TODO_STATUS = 'UPDATE_TODO_STATUS';
export const UPDATE_TODO_STATUS_SUC = 'UPDATE_TODO_STATUS_SUC';
export const CLEAR_TODO = 'CLEAR_TODO';
// State
export interface ITodoState {
_id: string;
content: string;
userId: string;
status: boolean;
}
// Action
export interface IFetchAction {
type: typeof FETCH_TODO;
payload: { userId: string };
}
export interface IFetchSucAction {
type: typeof FETCH_TODO_SUC;
payload: ITodoState[];
}
export interface IAddAction {
type: typeof ADD_TODO;
payload: {
userId: string;
content: string;
};
}
export interface IAddSucAction {
type: typeof ADD_TODO_SUC;
payload: ITodoState;
}
export interface ISearchAction {
type: typeof SEARCH_TODO;
payload: { userId: string; query: string };
}
export interface ISearchSucAction {
type: typeof SEARCH_TODO_SUC;
payload: ITodoState[];
}
export interface IDeleteAction {
type: typeof DELETE_TODO;
payload: {
todoId: string;
};
}
export interface IDeleteSucAction {
type: typeof DELETE_TODO_SUC;
payload: {
todoId: string;
};
}
export interface IUpdateContentAction {
type: typeof UPDATE_TODO_CONTENT;
payload: {
todoId: string;
content: string;
};
}
export interface IUpdateContentSucAction {
type: typeof UPDATE_TODO_CONTENT_SUC;
payload: {
todoId: string;
content: string;
};
}
export interface IUpdateStatusAction {
type: typeof UPDATE_TODO_STATUS;
payload: {
todoId: string;
};
}
export interface IClearTodoAction {
type: typeof CLEAR_TODO;
}
export interface IUpdateStatusSucAction {
type: typeof UPDATE_TODO_STATUS_SUC;
payload: {
todoId: string;
};
}
export type TodoActionTypes =
| IFetchAction
| IFetchSucAction
| IAddAction
| IAddSucAction
| IUpdateContentAction
| IUpdateContentSucAction
| IUpdateStatusAction
| IUpdateStatusSucAction
| ISearchAction
| ISearchSucAction
| IDeleteAction
| IDeleteSucAction
| IClearTodoAction;
================================================
FILE: src/store/user/actions.ts
================================================
import {
IAuthState,
LOGIN,
REGISTER,
LOGOUT,
KEEP_LOGIN,
IUserState,
SET_LOADING,
} from './types';
export const login = (authState: IAuthState) => ({
type: LOGIN,
payload: authState,
});
export const register = (authState: IAuthState) => ({
type: REGISTER,
payload: authState,
});
export const logout = () => ({
type: LOGOUT,
});
export const keepLogin = (userState: Partial<IUserState>) => ({
type: KEEP_LOGIN,
payload: userState,
});
export const setLoading = (loading: boolean) => ({
type: SET_LOADING,
payload: { loading },
});
================================================
FILE: src/store/user/reducers.ts
================================================
import {
IUserState,
KEEP_LOGIN,
LOGIN_SUC,
LOGOUT_SUC,
REGISTER_SUC,
SET_LOADING,
UserActionTypes,
} from './types';
const initialState: IUserState = {
userId: '',
username: '',
errMsg: '',
loading: false,
};
export default function userReducer(
state = initialState,
action: UserActionTypes
) {
switch (action.type) {
case REGISTER_SUC:
return {
...state,
};
case LOGIN_SUC:
return {
...state,
...action.payload,
};
case LOGOUT_SUC:
return initialState;
case KEEP_LOGIN:
return {
...state,
...action.payload,
};
case SET_LOADING:
return {
...state,
loading: action.payload.loading,
};
default:
return state;
}
}
================================================
FILE: src/store/user/saga.ts
================================================
import { message } from 'antd';
import UserAPI from 'api/user';
import { IRes } from 'common/interface';
import { call, put } from 'redux-saga/effects';
import { CLEAR_TODO } from 'store/todo/types';
import { LocalStorage } from 'utils';
import {
ILoginAction,
IRegisterAction,
LOGIN_SUC,
LOGOUT_SUC,
REGISTER_SUC,
SET_LOADING,
} from './types';
export function* login(action: ILoginAction) {
const { username, password } = action.payload;
try {
const res: IRes = yield call(UserAPI.login, username, password);
yield call(LocalStorage.set, 'userId', res.data.userId);
yield call(LocalStorage.set, 'username', res.data.username);
yield put({
type: LOGIN_SUC,
payload: { ...res.data, errMsg: res.msg },
});
yield put({
type: SET_LOADING,
payload: { loading: false },
});
} catch {
yield put({
type: SET_LOADING,
payload: { loading: false },
});
}
}
export function* logout() {
try {
yield call(LocalStorage.remove, 'userId');
yield call(LocalStorage.remove, 'username');
yield put({
type: LOGOUT_SUC,
});
yield put({
type: CLEAR_TODO,
});
} catch {}
}
export function* register(action: IRegisterAction) {
const { username, password } = action.payload;
try {
yield call(UserAPI.register, username, password);
yield put({
type: REGISTER_SUC,
});
yield put({
type: SET_LOADING,
payload: { loading: false },
});
message.success('注册成功');
} catch {
yield put({
type: SET_LOADING,
payload: { loading: false },
});
}
}
================================================
FILE: src/store/user/types.ts
================================================
// Constant
export const REGISTER = 'REGISTER';
export const REGISTER_SUC = 'REGISTER_SUC';
export const LOGIN = 'LOGIN';
export const LOGIN_SUC = 'LOGIN_SUC';
export const LOGOUT = 'LOGOUT';
export const LOGOUT_SUC = 'LOGOUT_SUC';
export const KEEP_LOGIN = 'KEEP_LOGIN';
export const SET_LOADING = 'SET_LOADING';
// State
export interface IAuthState {
username: string;
password: string;
}
export interface IUserState {
userId: string;
username: string;
errMsg: string;
loading: boolean;
}
// Action
export interface ILoginAction {
type: typeof LOGIN;
payload: IAuthState;
}
export interface ILoginSucAction {
type: typeof LOGIN_SUC;
payload: IUserState;
}
export interface ILogoutAction {
type: typeof LOGOUT;
}
export interface ILogoutSucAction {
type: typeof LOGOUT_SUC;
}
export interface IRegisterAction {
type: typeof REGISTER;
payload: IAuthState;
}
export interface IRegSucAction {
type: typeof REGISTER_SUC;
}
export interface IKeepLogin {
type: typeof KEEP_LOGIN;
payload: IUserState;
}
export interface ISetLoadingAction {
type: typeof SET_LOADING;
payload: { loading: boolean };
}
export type UserActionTypes =
| ILoginAction
| ILoginSucAction
| ILogoutAction
| ILogoutSucAction
| IKeepLogin
| IRegisterAction
| IRegSucAction
| ISetLoadingAction;
================================================
FILE: src/utils/index.ts
================================================
export class LocalStorage {
static get(key: string) {
return localStorage.getItem(key);
}
static set(key: string, value: string) {
localStorage.setItem(key, value);
}
static remove(key: string) {
localStorage.removeItem(key);
}
}
================================================
FILE: src/views/Home/index.tsx
================================================
import { FC, useEffect } from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { LocalStorage } from 'utils';
import { connect, ConnectedProps } from 'react-redux';
import { AppState } from 'store';
import { keepLogin } from 'store/user/actions';
const mapState = ({ user }: AppState) => ({
user,
});
const mapDispatch = {
keepLogin,
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
const Home: FC<RouteComponentProps & PropsFromRedux> = ({
user,
keepLogin,
}) => {
useEffect(() => {
const userId = LocalStorage.get('userId');
const username = LocalStorage.get('username');
// local 有用户信息,但 session 无 userId,自动登录
if (userId && username && !user.userId) {
keepLogin({ userId, username });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return user.userId ? <Redirect to="/todo" /> : <Redirect to="/login" />;
};
export default connector(Home);
================================================
FILE: src/views/Login/index.module.scss
================================================
.wrapper {
width: 100vw;
height: 100vh;
background: #f6f6f6;
.container {
position: relative;
top: 30%;
width: 320px;
margin: auto;
}
.tip {
span:nth-child(2) {
color: #096dd9;
cursor: pointer;
&:hover {
border-bottom: 1px solid currentColor;
}
}
}
}
================================================
FILE: src/views/Login/index.tsx
================================================
import UserForm from 'components/UserForm';
import { FC, useState } from 'react';
import styles from './index.module.scss';
const Login: FC = () => {
const [showLogin, setShowLogin] = useState(true);
const toggleForm = () => {
setShowLogin(!showLogin);
};
return (
<div className={styles.wrapper}>
<div className={styles.container}>
<h1>To-Do List</h1>
<UserForm showLogin={showLogin} />
<p className={styles.tip}>
<span>Or </span>
<span onClick={toggleForm}>
{showLogin ? '现在注册!' : '已有账号!'}
</span>
</p>
</div>
</div>
);
};
export default Login;
================================================
FILE: src/views/Todo/index.module.scss
================================================
.wrapper {
width: 90vw;
max-width: 600px;
min-width: 300px;
display: flex;
flex-direction: column;
align-items: center;
margin: 0 auto;
padding: 6rem 0;
}
.user {
position: absolute;
right: 2rem;
top: 2rem;
> span {
margin-right: 1rem;
}
}
.newTodo {
margin-left: 2rem;
}
.main {
width: 100%;
}
.queryBar {
width: 80%;
display: flex;
margin-bottom: 30px;
}
.nav {
display: flex;
border-bottom: 2px solid rgba(114, 111, 112, 0.5);
li {
position: relative;
display: flex;
flex: 1;
padding: 1rem 0;
cursor: pointer;
.dot {
width: 1.5rem;
height: 1.5rem;
margin: 0 1rem;
border-radius: 100%;
}
&.active::after {
display: block;
content: '';
position: absolute;
background-color: #539fe6;
width: 100%;
bottom: -2px;
height: 2px;
}
}
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
.noData {
margin-top: 3rem;
}
.pending {
background-color: #726f70;
}
.resolved {
background-color: #f25f66;
}
================================================
FILE: src/views/Todo/index.tsx
================================================
import { Button, Empty, Input } from 'antd';
import { ModalType } from 'common/enum';
import TodoItem from 'components/TodoItem';
import TodoModal from 'components/TodoModal';
import { ChangeEvent, FC, useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { AppState } from 'store';
import {
addTodo,
deleteTodo,
fetchTodo,
searchTodo,
updateTodoContent,
updateTodoStatus,
} from 'store/todo/actions';
import { keepLogin, logout } from 'store/user/actions';
import styles from './index.module.scss';
const mapState = ({ todo, user }: AppState) => ({
todo,
user,
});
const mapDispatch = {
logout,
keepLogin,
addTodo,
deleteTodo,
fetchTodo,
searchTodo,
updateTodoContent,
updateTodoStatus,
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
interface ITodoProps extends PropsFromRedux {}
const Todo: FC<ITodoProps> = ({
todo,
user: { userId, username },
logout,
deleteTodo,
fetchTodo,
updateTodoContent,
updateTodoStatus,
addTodo,
searchTodo,
}) => {
const [visible, setVisible] = useState(false);
const [isFinished, setFinished] = useState(false);
const [modalType, setModalType] = useState<ModalType>(ModalType.Add);
const [modalTitle, setModalTitle] = useState('');
const [content, setContent] = useState('');
const [todoId, setTodoId] = useState('');
const handleAdd = (content: string) => {
addTodo(userId, content);
setFinished(false);
};
const handleUpdateContent = (todoId: string, content: string) => {
updateTodoContent(todoId, content);
};
const handleDelete = (todoId: string) => {
deleteTodo(todoId);
};
const handleUpdateStatus = (todoId: string) => {
updateTodoStatus(todoId);
};
const handleSearch = (ev: ChangeEvent<HTMLInputElement>) => {
searchTodo(userId, ev.target.value);
};
const handleCloseModal = () => {
setVisible(false);
setContent('');
};
const handleOpenModal = (
type: ModalType,
todoId?: string,
content?: string
) => {
setVisible(true);
if (type === ModalType.Add) {
setModalTitle('新增待办事项');
setContent('');
setModalType(ModalType.Add);
}
if (type === ModalType.Edit) {
setModalTitle('编辑待办事项');
setModalType(ModalType.Edit);
setContent(content!);
setTodoId(todoId!);
}
};
useEffect(() => {
userId && fetchTodo(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
return (
<div className={styles.wrapper}>
<div className={styles.user}>
<span>Hi,{username}</span>
<Button type="ghost" size="small" onClick={logout}>
退出
</Button>
</div>
<div className={styles.queryBar}>
<Input
allowClear
placeholder="请输入要查询的内容"
onChange={handleSearch}
/>
<Button
type="primary"
onClick={() => handleOpenModal(ModalType.Add)}
className={styles.newTodo}
>
新增
</Button>
</div>
<div className={styles.main}>
<ul className={styles.nav}>
<li
className={isFinished ? '' : styles.active}
onClick={() => setFinished(false)}
>
<i className={`${styles.dot} ${styles.pending}`} />
未完成
</li>
<li
className={isFinished ? styles.active : ''}
onClick={() => setFinished(true)}
>
<i className={`${styles.dot} ${styles.resolved}`} />
已完成
</li>
</ul>
<ul className={styles.list}>
{todo.length ? (
todo
.filter((v) => v.status === isFinished)
.map((v) => (
<TodoItem
key={v._id}
content={v.content}
id={v._id}
type={modalType}
finished={isFinished}
onShowModal={handleOpenModal}
onDelete={handleDelete}
onUpdateStatus={handleUpdateStatus}
/>
))
) : (
<Empty className={styles.noData} />
)}
</ul>
</div>
<TodoModal
todoId={todoId}
modalType={modalType}
content={content}
visible={visible}
title={modalTitle}
onClose={handleCloseModal}
onAdd={handleAdd}
onUpdateContent={handleUpdateContent}
/>
</div>
);
};
export default connector(Todo);
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": [
"src"
]
}
gitextract_ivoo60g2/ ├── .gitignore ├── README.md ├── package.json ├── public/ │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── server/ │ ├── package.json │ ├── src/ │ │ ├── app.ts │ │ ├── config.ts │ │ ├── db/ │ │ │ ├── index.ts │ │ │ ├── models/ │ │ │ │ ├── todo.ts │ │ │ │ └── user.ts │ │ │ └── schemas/ │ │ │ ├── todo.ts │ │ │ └── user.ts │ │ ├── routes/ │ │ │ ├── todo.ts │ │ │ └── user.ts │ │ ├── services/ │ │ │ ├── todo.ts │ │ │ └── user.ts │ │ └── utils/ │ │ ├── enum.ts │ │ └── response.ts │ └── tsconfig.json ├── src/ │ ├── App.css │ ├── App.tsx │ ├── api/ │ │ ├── request.ts │ │ ├── todo.ts │ │ └── user.ts │ ├── common/ │ │ ├── config/ │ │ │ └── index.ts │ │ ├── enum/ │ │ │ └── index.ts │ │ └── interface/ │ │ └── index.ts │ ├── components/ │ │ ├── TodoItem/ │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── TodoModal/ │ │ │ └── index.tsx │ │ └── UserForm/ │ │ └── index.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── saga.ts │ ├── store/ │ │ ├── index.ts │ │ ├── todo/ │ │ │ ├── actions.ts │ │ │ ├── reducers.ts │ │ │ ├── saga.ts │ │ │ └── types.ts │ │ └── user/ │ │ ├── actions.ts │ │ ├── reducers.ts │ │ ├── saga.ts │ │ └── types.ts │ ├── utils/ │ │ └── index.ts │ └── views/ │ ├── Home/ │ │ └── index.tsx │ ├── Login/ │ │ ├── index.module.scss │ │ └── index.tsx │ └── Todo/ │ ├── index.module.scss │ └── index.tsx └── tsconfig.json
SYMBOL INDEX (90 symbols across 22 files)
FILE: server/src/db/schemas/todo.ts
type ITodo (line 3) | interface ITodo extends Document {
FILE: server/src/db/schemas/user.ts
type IUser (line 4) | interface IUser extends Document {
FILE: server/src/services/todo.ts
class TodoService (line 4) | class TodoService {
method addTodo (line 5) | public async addTodo(userId: string, content: string) {
method deleteTodo (line 19) | public async deleteTodo(todoId: string) {
method getAllTodos (line 26) | public async getAllTodos(userId: string) {
method updateTodoStatus (line 34) | public async updateTodoStatus(todoId: string) {
method updateTodoContent (line 45) | public async updateTodoContent(todoId: string, content: string) {
method searchTodo (line 52) | public async searchTodo(userId: string, query: string) {
FILE: server/src/services/user.ts
class UserService (line 3) | class UserService {
method addUser (line 4) | public async addUser(usr: string, psd: string) {
method validUser (line 21) | public async validUser(usr: string, psd: string) {
FILE: server/src/utils/enum.ts
type StatusCode (line 1) | enum StatusCode {
FILE: server/src/utils/response.ts
type IRes (line 4) | interface IRes {
FILE: src/api/todo.ts
class TodoAPI (line 3) | class TodoAPI {
method fetchTodo (line 5) | static fetchTodo(userId: string) {
method addTodo (line 8) | static addTodo(userId: string, content: string) {
method searchTodo (line 14) | static searchTodo(userId: string, query: string) {
method deleteTodo (line 19) | static deleteTodo(todoId: string) {
method updateTodoStatus (line 22) | static updateTodoStatus(todoId: string) {
method updateTodoContent (line 27) | static updateTodoContent(todoId: string, content: string) {
FILE: src/api/user.ts
class UserAPI (line 3) | class UserAPI {
method login (line 5) | static login(username: string, password: string) {
method register (line 11) | static register(username: string, password: string) {
FILE: src/common/config/index.ts
type Config (line 1) | enum Config {
FILE: src/common/enum/index.ts
type ModalType (line 1) | enum ModalType {
FILE: src/common/interface/index.ts
type IRes (line 1) | interface IRes {
FILE: src/components/TodoItem/index.tsx
type ITodoItem (line 12) | interface ITodoItem {
FILE: src/components/TodoModal/index.tsx
type ITodoModal (line 5) | interface ITodoModal {
FILE: src/components/UserForm/index.tsx
type OwnProps (line 18) | interface OwnProps {
type PropsFromRedux (line 24) | type PropsFromRedux = ConnectedProps<typeof connector>;
type Props (line 25) | type Props = PropsFromRedux & OwnProps;
FILE: src/store/index.ts
type AppState (line 17) | type AppState = ReturnType<typeof store.getState>;
type AppDispatch (line 18) | type AppDispatch = typeof store.dispatch;
FILE: src/store/todo/reducers.ts
function todoReducer (line 15) | function todoReducer(
FILE: src/store/todo/types.ts
constant FETCH_TODO (line 2) | const FETCH_TODO = 'FETCH_TODO';
constant FETCH_TODO_SUC (line 3) | const FETCH_TODO_SUC = 'FETCH_TODO_SUC';
constant ADD_TODO (line 4) | const ADD_TODO = 'ADD_TODO';
constant ADD_TODO_SUC (line 5) | const ADD_TODO_SUC = 'ADD_TODO_SUC';
constant SEARCH_TODO (line 6) | const SEARCH_TODO = 'SEARCH_TODO';
constant SEARCH_TODO_SUC (line 7) | const SEARCH_TODO_SUC = 'SEARCH_TODO_SUC';
constant DELETE_TODO (line 8) | const DELETE_TODO = 'DELETE_TODO';
constant DELETE_TODO_SUC (line 9) | const DELETE_TODO_SUC = 'DELETE_TODO_SUC';
constant UPDATE_TODO_CONTENT (line 10) | const UPDATE_TODO_CONTENT = 'UPDATE_TODO_CONTENT';
constant UPDATE_TODO_CONTENT_SUC (line 11) | const UPDATE_TODO_CONTENT_SUC = 'UPDATE_TODO_CONTENT_SUC';
constant UPDATE_TODO_STATUS (line 12) | const UPDATE_TODO_STATUS = 'UPDATE_TODO_STATUS';
constant UPDATE_TODO_STATUS_SUC (line 13) | const UPDATE_TODO_STATUS_SUC = 'UPDATE_TODO_STATUS_SUC';
constant CLEAR_TODO (line 14) | const CLEAR_TODO = 'CLEAR_TODO';
type ITodoState (line 17) | interface ITodoState {
type IFetchAction (line 25) | interface IFetchAction {
type IFetchSucAction (line 30) | interface IFetchSucAction {
type IAddAction (line 35) | interface IAddAction {
type IAddSucAction (line 43) | interface IAddSucAction {
type ISearchAction (line 48) | interface ISearchAction {
type ISearchSucAction (line 53) | interface ISearchSucAction {
type IDeleteAction (line 58) | interface IDeleteAction {
type IDeleteSucAction (line 65) | interface IDeleteSucAction {
type IUpdateContentAction (line 72) | interface IUpdateContentAction {
type IUpdateContentSucAction (line 80) | interface IUpdateContentSucAction {
type IUpdateStatusAction (line 88) | interface IUpdateStatusAction {
type IClearTodoAction (line 95) | interface IClearTodoAction {
type IUpdateStatusSucAction (line 99) | interface IUpdateStatusSucAction {
type TodoActionTypes (line 106) | type TodoActionTypes =
FILE: src/store/user/reducers.ts
function userReducer (line 18) | function userReducer(
FILE: src/store/user/types.ts
constant REGISTER (line 2) | const REGISTER = 'REGISTER';
constant REGISTER_SUC (line 3) | const REGISTER_SUC = 'REGISTER_SUC';
constant LOGIN (line 4) | const LOGIN = 'LOGIN';
constant LOGIN_SUC (line 5) | const LOGIN_SUC = 'LOGIN_SUC';
constant LOGOUT (line 6) | const LOGOUT = 'LOGOUT';
constant LOGOUT_SUC (line 7) | const LOGOUT_SUC = 'LOGOUT_SUC';
constant KEEP_LOGIN (line 8) | const KEEP_LOGIN = 'KEEP_LOGIN';
constant SET_LOADING (line 9) | const SET_LOADING = 'SET_LOADING';
type IAuthState (line 12) | interface IAuthState {
type IUserState (line 17) | interface IUserState {
type ILoginAction (line 25) | interface ILoginAction {
type ILoginSucAction (line 30) | interface ILoginSucAction {
type ILogoutAction (line 35) | interface ILogoutAction {
type ILogoutSucAction (line 39) | interface ILogoutSucAction {
type IRegisterAction (line 43) | interface IRegisterAction {
type IRegSucAction (line 48) | interface IRegSucAction {
type IKeepLogin (line 52) | interface IKeepLogin {
type ISetLoadingAction (line 57) | interface ISetLoadingAction {
type UserActionTypes (line 62) | type UserActionTypes =
FILE: src/utils/index.ts
class LocalStorage (line 1) | class LocalStorage {
method get (line 2) | static get(key: string) {
method set (line 5) | static set(key: string, value: string) {
method remove (line 8) | static remove(key: string) {
FILE: src/views/Home/index.tsx
type PropsFromRedux (line 19) | type PropsFromRedux = ConnectedProps<typeof connector>;
FILE: src/views/Todo/index.tsx
type PropsFromRedux (line 37) | type PropsFromRedux = ConnectedProps<typeof connector>;
type ITodoProps (line 39) | interface ITodoProps extends PropsFromRedux {}
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (52K chars).
[
{
"path": ".gitignore",
"chars": 352,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/ser"
},
{
"path": "README.md",
"chars": 871,
"preview": "## 基于 TS + React + AntD + Koa + MongoDB 实现的 TodoList 全栈应用\n\n => {\n const connect = () => {\n mongoose\n .connect"
},
{
"path": "server/src/db/models/todo.ts",
"chars": 138,
"preview": "import { model } from \"mongoose\";\n\nimport { ITodo, TodoSchema } from \"../schemas/todo\";\n\nexport default model<ITodo>(\"To"
},
{
"path": "server/src/db/models/user.ts",
"chars": 138,
"preview": "import { model } from 'mongoose';\n\nimport { UserSchema, IUser } from '../schemas/user';\n\nexport default model<IUser>('Us"
},
{
"path": "server/src/db/schemas/todo.ts",
"chars": 295,
"preview": "import { Document, Schema } from 'mongoose';\n\nexport interface ITodo extends Document {\n content: string;\n status: boo"
},
{
"path": "server/src/db/schemas/user.ts",
"chars": 427,
"preview": "import { Document, Schema } from 'mongoose';\nimport { ITodo } from './todo';\n\nexport interface IUser extends Document {\n"
},
{
"path": "server/src/routes/todo.ts",
"chars": 2745,
"preview": "import { Context } from 'koa';\nimport Router from 'koa-router';\n\nimport TodoService from '../services/todo';\nimport { St"
},
{
"path": "server/src/routes/user.ts",
"chars": 1236,
"preview": "import { Context, Request } from 'koa';\nimport Router from 'koa-router';\n\nimport UserService from '../services/user';\nim"
},
{
"path": "server/src/services/todo.ts",
"chars": 1918,
"preview": "import Todo from '../db/models/todo';\nimport User from '../db/models/user';\n\nexport default class TodoService {\n public"
},
{
"path": "server/src/services/user.ts",
"chars": 862,
"preview": "import User from '../db/models/user';\n\nexport default class UserService {\n public async addUser(usr: string, psd: strin"
},
{
"path": "server/src/utils/enum.ts",
"chars": 179,
"preview": "export enum StatusCode {\n /**\n * 成功\n */\n OK = 200,\n /**\n * 更新成功\n */\n Accepted = 202,\n /**\n * 删除成功\n */\n "
},
{
"path": "server/src/utils/response.ts",
"chars": 431,
"preview": "import { Context } from \"koa\";\nimport { StatusCode } from \"./enum\";\n\ninterface IRes {\n ctx: Context;\n statusCode?: num"
},
{
"path": "server/tsconfig.json",
"chars": 366,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"esModuleInterop\": true,\n \"allowSyntheticDefaultImports\": true"
},
{
"path": "src/App.css",
"chars": 31,
"preview": "@import '~antd/dist/antd.css';\n"
},
{
"path": "src/App.tsx",
"chars": 511,
"preview": "import './App.css';\n\nimport { message } from 'antd';\nimport { BrowserRouter, Route } from 'react-router-dom';\nimport Hom"
},
{
"path": "src/api/request.ts",
"chars": 568,
"preview": "import { message } from 'antd';\nimport axios from 'axios';\nimport Config from 'common/config';\nimport { IRes } from 'com"
},
{
"path": "src/api/todo.ts",
"chars": 874,
"preview": "import request from './request';\n\nclass TodoAPI {\n static PREFIX = '/todos';\n static fetchTodo(userId: string) {\n r"
},
{
"path": "src/api/user.ts",
"chars": 402,
"preview": "import request from './request';\n\nclass UserAPI {\n static PREFIX = '/users';\n static login(username: string, password:"
},
{
"path": "src/common/config/index.ts",
"chars": 82,
"preview": "enum Config {\n API_URI = \"http://localhost:5000/api/\",\n}\n\nexport default Config;\n"
},
{
"path": "src/common/enum/index.ts",
"chars": 58,
"preview": "export enum ModalType {\n Edit = 'EDIT',\n Add = 'ADD',\n}\n"
},
{
"path": "src/common/interface/index.ts",
"chars": 76,
"preview": "export interface IRes {\n data: any;\n error_code: number;\n msg: string;\n}\n"
},
{
"path": "src/components/TodoItem/index.module.scss",
"chars": 723,
"preview": ".item {\n display: flex;\n min-height: 3rem;\n line-height: 3rem;\n padding: 0.5rem 1rem;\n background-color: #fff;\n bo"
},
{
"path": "src/components/TodoItem/index.tsx",
"chars": 1258,
"preview": "import {\n CheckOutlined,\n DeleteOutlined,\n EditOutlined,\n UndoOutlined,\n} from '@ant-design/icons';\nimport { ModalTy"
},
{
"path": "src/components/TodoModal/index.tsx",
"chars": 1570,
"preview": "import { Form, Input, Modal } from 'antd';\nimport { ModalType } from 'common/enum';\nimport { FC, useEffect } from 'react"
},
{
"path": "src/components/UserForm/index.tsx",
"chars": 1801,
"preview": "import { LockOutlined, UserOutlined } from '@ant-design/icons';\nimport { Button, Form, Input } from 'antd';\nimport React"
},
{
"path": "src/index.css",
"chars": 366,
"preview": "body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Can"
},
{
"path": "src/index.tsx",
"chars": 671,
"preview": "import { ConfigProvider } from 'antd';\nimport zhCN from 'antd/lib/locale-provider/zh_CN';\nimport ReactDOM from 'react-do"
},
{
"path": "src/react-app-env.d.ts",
"chars": 40,
"preview": "/// <reference types=\"react-scripts\" />\n"
},
{
"path": "src/reportWebVitals.ts",
"chars": 425,
"preview": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n if (onPerfEntr"
},
{
"path": "src/saga.ts",
"chars": 884,
"preview": "import { takeEvery } from 'redux-saga/effects';\n\nimport {\n addTodo,\n deleteTodo,\n fetchTodo,\n searchTodo,\n updateTo"
},
{
"path": "src/store/index.ts",
"chars": 675,
"preview": "import { applyMiddleware, combineReducers, createStore } from 'redux';\nimport { composeWithDevTools } from 'redux-devtoo"
},
{
"path": "src/store/todo/actions.ts",
"chars": 883,
"preview": "import {\n ADD_TODO,\n CLEAR_TODO,\n DELETE_TODO,\n FETCH_TODO,\n SEARCH_TODO,\n UPDATE_TODO_CONTENT,\n UPDATE_TODO_STAT"
},
{
"path": "src/store/todo/reducers.ts",
"chars": 1046,
"preview": "import {\n ADD_TODO_SUC,\n CLEAR_TODO,\n DELETE_TODO_SUC,\n FETCH_TODO_SUC,\n ITodoState,\n SEARCH_TODO_SUC,\n TodoActio"
},
{
"path": "src/store/todo/saga.ts",
"chars": 2094,
"preview": "import { message } from 'antd';\nimport TodoAPI from 'api/todo';\nimport { IRes } from 'common/interface';\nimport { call, "
},
{
"path": "src/store/todo/types.ts",
"chars": 2471,
"preview": "// Constant\nexport const FETCH_TODO = 'FETCH_TODO';\nexport const FETCH_TODO_SUC = 'FETCH_TODO_SUC';\nexport const ADD_TOD"
},
{
"path": "src/store/user/actions.ts",
"chars": 571,
"preview": "import {\n IAuthState,\n LOGIN,\n REGISTER,\n LOGOUT,\n KEEP_LOGIN,\n IUserState,\n SET_LOADING,\n} from './types';\n\nexpo"
},
{
"path": "src/store/user/reducers.ts",
"chars": 786,
"preview": "import {\n IUserState,\n KEEP_LOGIN,\n LOGIN_SUC,\n LOGOUT_SUC,\n REGISTER_SUC,\n SET_LOADING,\n UserActionTypes,\n} from"
},
{
"path": "src/store/user/saga.ts",
"chars": 1619,
"preview": "import { message } from 'antd';\nimport UserAPI from 'api/user';\nimport { IRes } from 'common/interface';\nimport { call, "
},
{
"path": "src/store/user/types.ts",
"chars": 1325,
"preview": "// Constant\nexport const REGISTER = 'REGISTER';\nexport const REGISTER_SUC = 'REGISTER_SUC';\nexport const LOGIN = 'LOGIN'"
},
{
"path": "src/utils/index.ts",
"chars": 254,
"preview": "export class LocalStorage {\n static get(key: string) {\n return localStorage.getItem(key);\n }\n static set(key: stri"
},
{
"path": "src/views/Home/index.tsx",
"chars": 1007,
"preview": "import { FC, useEffect } from 'react';\nimport { Redirect, RouteComponentProps } from 'react-router-dom';\n\nimport { Local"
},
{
"path": "src/views/Login/index.module.scss",
"chars": 322,
"preview": ".wrapper {\n width: 100vw;\n height: 100vh;\n background: #f6f6f6;\n\n .container {\n position: relative;\n top: 30%;"
},
{
"path": "src/views/Login/index.tsx",
"chars": 671,
"preview": "import UserForm from 'components/UserForm';\nimport { FC, useState } from 'react';\n\nimport styles from './index.module.sc"
},
{
"path": "src/views/Todo/index.module.scss",
"chars": 1064,
"preview": ".wrapper {\n width: 90vw;\n max-width: 600px;\n min-width: 300px;\n display: flex;\n flex-direction: column;\n align-ite"
},
{
"path": "src/views/Todo/index.tsx",
"chars": 4613,
"preview": "import { Button, Empty, Input } from 'antd';\nimport { ModalType } from 'common/enum';\nimport TodoItem from 'components/T"
},
{
"path": "tsconfig.json",
"chars": 557,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n "
}
]
About this extraction
This page contains the full source code of the B2D1/TodoList GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (44.8 KB), approximately 13.9k tokens, and a symbol index with 90 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.