├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── logo.svg ├── next.svg ├── thirteen.svg └── vercel.svg ├── screenshot ├── 1.png ├── 2.png └── 3.gif ├── src ├── api │ ├── book.ts │ ├── borrow.ts │ ├── category.ts │ ├── index.ts │ └── user.ts ├── components │ ├── AuthHoc │ │ └── index.tsx │ ├── BookForm │ │ ├── index.module.css │ │ └── index.tsx │ ├── BorrowForm │ │ ├── index.module.css │ │ └── index.tsx │ ├── Content │ │ ├── index.module.css │ │ └── index.tsx │ ├── Layout │ │ ├── index.module.css │ │ └── index.tsx │ ├── UserForm │ │ ├── index.module.css │ │ └── index.tsx │ └── index.ts ├── constants │ └── index.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── book │ │ ├── add │ │ │ └── index.tsx │ │ ├── edit │ │ │ └── [id] │ │ │ │ └── index.tsx │ │ ├── index.module.css │ │ └── index.tsx │ ├── borrow │ │ ├── add │ │ │ └── index.tsx │ │ ├── edit │ │ │ └── [id] │ │ │ │ └── index.tsx │ │ ├── index.module.css │ │ └── index.tsx │ ├── category │ │ ├── index.module.css │ │ └── index.tsx │ ├── login │ │ ├── index.module.css │ │ └── index.tsx │ └── user │ │ ├── add │ │ └── index.tsx │ │ ├── edit │ │ └── [id] │ │ │ └── index.tsx │ │ ├── index.module.css │ │ └── index.tsx ├── styles │ └── globals.css ├── types │ ├── book.d.ts │ ├── borrow.d.ts │ ├── category.d.ts │ ├── index.d.ts │ └── user.ts └── utils │ ├── hoos.ts │ └── request.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "importOrder": [ 3 | "^components/(.*)$", 4 | "^[./]" 5 | ], 6 | "importOrderSeparation": true, 7 | "importOrderSortSpecifiers": true 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sanmu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 三木图书管理系统 2 | 3 | # 简介 4 | 5 | book-admin 系列是包含了完整的前端代码和后端代码。目前完成的技术栈是 前端有 react,后端有 express+mongodb。 6 | 7 | 目前这个仓库是前端部分,基于 **react、nextjs、antd** 技术栈实现的。 8 | 9 | **通过这个项目你可以学习到:** 10 | 11 | 1. 如何使用 nextjs 开发一个真实项目 12 | 2. react 的基本语法以及 react hook 的使用 13 | 3. react 项目是如何请求数据 14 | 4. axios 的封装 15 | 5. react 的组件如何封装 16 | 17 | 如果你只关心学习 react 相关的知识,则下载这个仓库运行即可,它通过 apifox 实现了完整的 mock 数据,访问 [mock 服务](https://www.apifox.cn/apidoc/shared-e625f799-2c80-483c-beb7-eb6c9ef4a552)。你只需要启动项目,无需其他操作,即可使用完整的前端功能。 18 | 19 | 若你不想用我搭建好的 mock 数据,可以联系我要完整的 mock 数据导出包,导入到自己的 apifox 中使用。 20 | 21 | ### 搭配后端启动 22 | 23 | 若你想学习完整的前端后端的知识,可以访问 [book-admin-express](https://github.com/calmound/book-admin-express)。它是对应的服务端代码,基于 **express、mongoodb** 技术栈实现的。关于后端项目如何启动,详情查看服务端的仓库介绍。 24 | 25 | ### 规划 26 | 27 | book-admin 系统前端实现了 **react** 版本,后端实现了 **express** 和 **mongodb** 版本。未来前端还会实现 **vue**,后端实现 **nestjs** 和 **mysql**。每个技术栈都独占一个仓库,方便感兴趣的同学自由搭配前后端项目的技术栈。 28 | 29 | **如果对你有一些帮助,欢迎 star**! 30 | 31 | # 功能介绍 32 | 33 | ## 功能流程图 34 | 35 | ![](https://raw.githubusercontent.com/calmound/book-admin-react/master/screenshot/1.png) 36 | 37 | ### 系统演示 38 | 39 | ![](https://raw.githubusercontent.com/calmound/book-admin-react/master/screenshot/3.gif) 40 | 41 | # 启动 42 | 43 | 1. 下载代码,终端进入该项目目录下 44 | 2. 下载依赖包,执行 45 | 46 | ```shell 47 | npm install 48 | ``` 49 | 50 | 3. 若连接启动 mock 服务,打开根目录下的 next.config.js 文件,确认以下代码不在注释中 51 | 52 | ``` 53 | destination: `https://mock.apifox.cn/m1/2398938-0-default/api/:path*`, 54 | ``` 55 | 56 | 4. 若期望连接 nodejs 的本地服务,打开根目录下的 next.config.js 文件,确认以下代码不在注释中 57 | 58 | ```javascript 59 | destination: `http://localhost:3001/api/:path*`, 60 | ``` 61 | 62 | 5. 运行项目 63 | 64 | ```shell 65 | npm run dev 66 | ``` 67 | 68 | 6. 访问 localhost:3000/login 69 | 7. 看到如下页面,表明启动成功 70 | ![](https://raw.githubusercontent.com/calmound/book-admin-react/master/screenshot/2.png) 71 | 8. 账号,管理员(账号:admin,密码:admin),用户(账号:user,密码:user) 72 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | async rewrites() { 5 | return [ 6 | { 7 | source: `/api/:path*`, 8 | // 启动mock服务,执行这个代码 9 | destination: `http://localhost:3001/api/:path*`, 10 | // 连接本地的nodejs服务,执行这个代码 11 | // destination: `https://mock.apifox.cn/m1/2398938-0-default/api/:path*`, 12 | }, 13 | ] 14 | } 15 | } 16 | 17 | module.exports = nextConfig 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "18.14.5", 13 | "@types/react": "18.0.28", 14 | "@types/react-dom": "18.0.11", 15 | "antd": "5.2.3", 16 | "axios": "^1.3.4", 17 | "eslint": "8.35.0", 18 | "eslint-config-next": "13.2.3", 19 | "next": "13.2.3", 20 | "nextjs-progressbar": "^0.0.16", 21 | "qs": "^6.11.0", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "typescript": "4.9.5" 25 | }, 26 | "devDependencies": { 27 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 28 | "@types/qs": "^6.9.7", 29 | "eslint-config-prettier": "^8.6.0", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "prettier": "^2.8.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calmound/book-admin-react/4d23c1427976179d053e3485df7e66993f9f6bc1/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calmound/book-admin-react/4d23c1427976179d053e3485df7e66993f9f6bc1/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calmound/book-admin-react/4d23c1427976179d053e3485df7e66993f9f6bc1/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calmound/book-admin-react/4d23c1427976179d053e3485df7e66993f9f6bc1/screenshot/3.gif -------------------------------------------------------------------------------- /src/api/book.ts: -------------------------------------------------------------------------------- 1 | import { BookType } from "@/types"; 2 | import request from "@/utils/request"; 3 | import qs from "qs"; 4 | 5 | export const getBookList = ( 6 | params: Partial> & { 7 | current?: number; 8 | pageSize?: number; 9 | all?: boolean; 10 | } 11 | ) => { 12 | return request.get(`/api/books?${qs.stringify(params)}`); 13 | }; 14 | 15 | export const bookUpdate = (id: string, params: BookType) => { 16 | return request.put(`/api/books/${id}`, params); 17 | }; 18 | 19 | export const bookAdd = (params: BookType) => { 20 | return request.post("/api/books", params); 21 | }; 22 | 23 | export const getBookDetail = (id: string) => { 24 | return request.get(`/api/books/${id}`); 25 | }; 26 | 27 | export const bookDelete = (id: string) => { 28 | return request.delete(`/api/books/${id}`); 29 | }; 30 | -------------------------------------------------------------------------------- /src/api/borrow.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import qs from "qs"; 3 | 4 | import { BorrowQueryType, BorrowType } from "./../types/borrow.d"; 5 | 6 | export const getBorrowList = (params: BorrowQueryType) => { 7 | return request.get(`/api/borrows?${qs.stringify(params)}`); 8 | }; 9 | 10 | export const getBorrowDetail = (id: string) => { 11 | return request.get(`/api/borrows/${id}`); 12 | }; 13 | 14 | export const borrowAdd = (params: BorrowType) => { 15 | return request.post(`/api/borrows`, params); 16 | }; 17 | 18 | export const borrowDelete = (id: string) => { 19 | return request.delete(`/api/borrows/${id}`); 20 | }; 21 | export const borrowUpdate = (id: string, params: BorrowType) => { 22 | return request.post(`/api/borrows/${id}`, params); 23 | }; 24 | 25 | export const borrowBack = (id: string) => { 26 | return request.put(`/api/borrows/back/${id}`); 27 | }; 28 | -------------------------------------------------------------------------------- /src/api/category.ts: -------------------------------------------------------------------------------- 1 | import { CategoryType } from "@/types"; 2 | import request from "@/utils/request"; 3 | import qs from "qs"; 4 | 5 | export const getCategoryList = (params?: { level?: number; all?: boolean }) => { 6 | return request.get(`/api/categories?${qs.stringify(params)}`); 7 | }; 8 | 9 | export const categoryAdd = (params: CategoryType) => { 10 | return request.post("/api/categories", params); 11 | }; 12 | 13 | export const categoryUpdate = (id: string, params: CategoryType) => { 14 | return request.put(`/api/categories/${id}`, params); 15 | }; 16 | 17 | export const categoryDelete = (id: string) => { 18 | return request.delete(`/api/categories/${id}`); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './book'; 2 | export * from './category'; 3 | export * from './borrow'; 4 | export * from './user'; 5 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { UserQueryType, UserType } from "@/types"; 2 | import request from "@/utils/request"; 3 | import qs from "qs"; 4 | 5 | export const setLogout = async () => { 6 | await request.get(`/api/logout`); 7 | }; 8 | 9 | export const getUserList = (params?: UserQueryType) => { 10 | return request.get(`/api/users?${qs.stringify(params)}`); 11 | }; 12 | 13 | export const getUserDetail = (id: string) => { 14 | return request.get(`/api/users/${id}`); 15 | }; 16 | 17 | export const userDelete = (id: string) => { 18 | return request.delete(`/api/users/${id}`); 19 | }; 20 | 21 | export const userAdd = (params: UserType) => { 22 | return request.post("/api/users", params); 23 | }; 24 | 25 | export const userUpdate = (id: string, params: UserType) => { 26 | return request.put(`/api/users/${id}`, params); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/AuthHoc/index.tsx: -------------------------------------------------------------------------------- 1 | import { USER_ROLE } from "@/constants"; 2 | import { useCurrentUser } from "@/utils/hoos"; 3 | import { PropsWithChildren, cloneElement } from "react"; 4 | 5 | const AuthHoc: React.FC = ({ children }) => { 6 | const user = useCurrentUser(); 7 | return user?.role === USER_ROLE.ADMIN ? <>{children} : null; 8 | }; 9 | export default AuthHoc; 10 | -------------------------------------------------------------------------------- /src/components/BookForm/index.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding: 32px; 3 | overflow: auto; 4 | height: 100%; 5 | } 6 | 7 | .coverWrap { 8 | width: 100%; 9 | } 10 | 11 | .textarea { 12 | height: 150px !important; 13 | } 14 | 15 | .btn { 16 | margin: 0 auto; 17 | display: block; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/BookForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCategoryList } from "@/api"; 2 | import { bookAdd, bookUpdate } from "@/api/book"; 3 | import { BookFormType, BookType, CategoryType } from "@/types"; 4 | import { 5 | Button, 6 | DatePicker, 7 | Form, 8 | Image, 9 | Input, 10 | InputNumber, 11 | Select, 12 | message, 13 | } from "antd"; 14 | import dayjs from "dayjs"; 15 | import "dayjs/locale/zh-cn"; 16 | import { useRouter } from "next/router"; 17 | import { useEffect, useMemo, useState } from "react"; 18 | 19 | import Content from "../Content"; 20 | import styles from "./index.module.css"; 21 | 22 | const Option = Select.Option; 23 | const { TextArea } = Input; 24 | 25 | const BookForm: React.FC = ({ title, editData }) => { 26 | const router = useRouter(); 27 | const [form] = Form.useForm(); 28 | const [categoryList, setCategoryList] = useState([]); 29 | const [preview, setPreview] = useState(); 30 | const [cover, setCover] = useState(); 31 | 32 | useEffect(() => { 33 | getCategoryList({ 34 | all: true, 35 | }).then((res) => { 36 | setCategoryList(res.data); 37 | }); 38 | }, []); 39 | 40 | useEffect(() => { 41 | if (editData) { 42 | const data = { 43 | ...editData, 44 | category: editData.category 45 | ? (editData.category as unknown as CategoryType)._id 46 | : undefined, 47 | publishAt: editData.publishAt ? dayjs(editData.publishAt) : undefined, 48 | }; 49 | setCover(editData.cover); 50 | form.setFieldsValue(data); 51 | } 52 | }, [editData, form]); 53 | 54 | const handleFinish = async (values: BookType) => { 55 | console.log( 56 | "%c [ values ]-53", 57 | "font-size:13px; background:pink; color:#bf2c9f;", 58 | values 59 | ); 60 | // 编辑 61 | if (values.publishAt) { 62 | values.publishAt = dayjs(values.publishAt).valueOf(); 63 | } 64 | if (editData?._id) { 65 | await bookUpdate(editData._id, values); 66 | message.success("编辑成功"); 67 | } else { 68 | await bookAdd(values); 69 | message.success("创建成功"); 70 | } 71 | router.push("/book"); 72 | }; 73 | 74 | const handlePreview = () => { 75 | setPreview(form.getFieldValue("cover")); 76 | }; 77 | 78 | { 79 | categoryList?.map((category) => ( 80 | 83 | )); 84 | } 85 | 86 | return ( 87 | <> 88 | 89 |
99 | 109 | 110 | 111 | 121 | 122 | 123 | 133 | 140 | 141 | 142 | 143 | { 147 | setCover(e.target.value); 148 | form.setFieldValue("cover", e.target.value); 149 | }} 150 | /> 151 | 154 | 155 | 156 | {preview && ( 157 | 158 | 封面 159 | 160 | )} 161 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |