0、前言

使用umi3和antd pro5从零实现全栈中后台管理系统
image.png

0-1、涉及技术栈

前端: TS 、 React、React Hooks、 umi3、antd-pro5
后端: express、mongodb、jwt
**

0-2、实现的功能

  • 后端用户鉴权
  • 前端权限管理
  • 用户密码加密
  • 封装一套通用弹窗表单组件,实现新建、修改、详情功能
  • 用户登录注册(首次需要后端自己添加一条用户登录信息)
  • image.png
  • 后端通过expressJWT实现接口鉴权与添加白名单
  • 后端日志功能
  • 后端封装方法统一处理返回信息
  • 实现列表的筛选、排序、删除、批量删除
  • 实现新建、修改、查看详情

1、初始化前端项目

umi 官网
coding网址

  1. yarn create umi myapp
  2. npm i
  3. npm run dev

1、设置config下的proxy代理

  1. dev: {
  2. '/api/': {
  3. target: "http://localhost:3000",
  4. changeOrigin: true,
  5. pathRewrite: { '^': '' },
  6. },
  7. },

2、登录

修改src/service/login.ts 接口改为/api/user/login

  1. export async function fakeAccountLogin(params: LoginParamsType) {
  2. return request<API.LoginStateType>('/api/user/login', {
  3. method: 'POST',
  4. data: params,
  5. });
  6. }

存储token pages/user/login/index.tsx
  1. localStorage.setItem('token' , msg.token)

使用token services/user.ts

  1. export async function queryCurrent() {
  2. return request<API.CurrentUser>('/api/currentUser', headers: {
  3. Authorization : 'Bearer ' + `${localStorage.getItem('token')
  4. }`
  5. }
  6. }

每次请求都带上token src/app.tsx

  1. export const request: RequestConfig = {
  2. errorHandler,
  3. headers: {
  4. Authorization : 'Bearer ' + `${localStorage.getItem('token')}`
  5. }
  6. };

退出 RightContent/AvatarDropdown.tsx

  1. localStorage.removeItem('token')

3、pro5参考文档

https://procomponents.ant.design/components/form#proform-1

4、实现一个用户管理

image.png

5、列表页

pages/ListTableList/index.tsx

  1. import { PlusOutlined } from '@ant-design/icons';
  2. import { Button, Divider, message, Avatar } from 'antd';
  3. import React, { useState, useRef } from 'react';
  4. import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
  5. import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
  6. import HandleForm from './components/HandleForm';
  7. import { TableListItem } from './data.d';
  8. import { queryRule, updateRule, addRule, removeRule } from './service';
  9. import moment from 'moment';
  10. /**
  11. * 操作提交
  12. * @param fields
  13. */
  14. const handleSubmit = async (_id?: string, fields?: TableListItem) => {
  15. let title = _id ? '修改' : '新增';
  16. const hide = message.loading(`正在${title}`);
  17. try {
  18. if (_id) {
  19. await updateRule({
  20. _id,
  21. ...fields,
  22. });
  23. } else {
  24. await addRule({ ...fields });
  25. }
  26. hide();
  27. message.success(`${title}成功`);
  28. return true;
  29. } catch (error) {
  30. hide();
  31. message.error(`${title}失败`);
  32. return false;
  33. }
  34. };
  35. /**
  36. * 删除节点
  37. * @param selectedRows
  38. */
  39. const handleRemove = async (selectedRows: string[], _id: string) => {
  40. // console.log(selectedRows,_id,'selectedRows>>>>')
  41. const hide = message.loading('正在删除');
  42. // return
  43. try {
  44. await removeRule({
  45. _id: _id ? _id : selectedRows,
  46. });
  47. hide();
  48. message.success('删除成功');
  49. return true;
  50. } catch (error) {
  51. hide();
  52. message.error('删除失败');
  53. return false;
  54. }
  55. };
  56. const TableList: React.FC<{}> = () => {
  57. const [modalVisible, handleModalVisible] = useState<boolean>(false);
  58. const [currentInfo, handleSaveCurrentInfo] = useState<TableListItem | null>(null);
  59. const [isDetail, setDetail] = useState<boolean>(false);
  60. const actionRef = useRef<ActionType>();
  61. const [selectedRowsState, setSelectedRows] = useState<any>([]);
  62. const columns: ProColumns<TableListItem>[] = [
  63. {
  64. title: '用户名',
  65. dataIndex: 'username',
  66. },
  67. {
  68. title: '密码',
  69. dataIndex: 'password',
  70. hideInDescriptions: true, //详情页不显示
  71. hideInTable: true,
  72. },
  73. {
  74. title: '角色',
  75. dataIndex: 'access',
  76. search: false,
  77. filters: [
  78. { text: '普通用户', value: 'user' },
  79. { text: '管理员', value: 'admin' },
  80. ],
  81. valueEnum: {
  82. user: { text: '普通用户' },
  83. admin: { text: '管理员' },
  84. },
  85. },
  86. {
  87. title: '_id',
  88. dataIndex: '_id',
  89. sorter: true,
  90. hideInForm: true,
  91. search: false,
  92. },
  93. {
  94. title: '头像',
  95. dataIndex: 'avatar',
  96. search: false,
  97. hideInForm: true,
  98. render: (dom, entity) => {
  99. return <Avatar src={entity.avatar} alt="" />;
  100. },
  101. },
  102. {
  103. title: '邮箱',
  104. dataIndex: 'email',
  105. },
  106. {
  107. title: '更新时间',
  108. dataIndex: 'updatedAt',
  109. sorter: true,
  110. hideInForm: true,
  111. search: false,
  112. renderText: (val: string) => {
  113. if (!val) return '';
  114. return moment(val).fromNow(); // 绝对时间转化成相对时间
  115. },
  116. },
  117. {
  118. title: '创建时间',
  119. dataIndex: 'createdAt',
  120. sorter: true,
  121. hideInForm: true,
  122. search: false,
  123. valueType: 'dateTime',
  124. },
  125. {
  126. title: '操作',
  127. dataIndex: 'option',
  128. valueType: 'option',
  129. render: (_, record) => (
  130. <>
  131. <a
  132. href="javascript:;"
  133. onClick={() => {
  134. handleModalVisible(true), handleSaveCurrentInfo(record);
  135. }}
  136. >
  137. 修改
  138. </a>
  139. <Divider type="vertical" />
  140. <a
  141. href="javascript:;"
  142. onClick={() => {
  143. handleModalVisible(true), handleSaveCurrentInfo(record), setDetail(true);
  144. }}
  145. >
  146. 详情
  147. </a>
  148. <Divider type="vertical" />
  149. <a
  150. href="javascript:;"
  151. onClick={async () => {
  152. await handleRemove([], record._id as 'string');
  153. // 刷新
  154. actionRef.current?.reloadAndRest?.();
  155. }}
  156. >
  157. 删除
  158. </a>
  159. </>
  160. ),
  161. },
  162. ];
  163. return (
  164. <PageContainer>
  165. <ProTable<TableListItem>
  166. headerTitle="用户管理"
  167. actionRef={actionRef}
  168. rowKey="_id"
  169. search={{
  170. labelWidth: 120,
  171. }}
  172. toolBarRender={() => [
  173. <Button type="primary" onClick={() => handleModalVisible(true)}>
  174. <PlusOutlined />
  175. 新增
  176. </Button>,
  177. ]}
  178. request={(params, sorter, filter) => queryRule({ ...params, sorter, filter })}
  179. columns={columns}
  180. form={{
  181. submitter: false,
  182. }}
  183. pagination={{ defaultPageSize: 5 }}
  184. rowSelection={{
  185. onChange: (selected, selectedRows) => {
  186. setSelectedRows(selected);
  187. },
  188. }}
  189. />
  190. <HandleForm
  191. onCancel={() => {
  192. handleModalVisible(false), handleSaveCurrentInfo({}), setDetail(false);
  193. }}
  194. modalVisible={modalVisible}
  195. values={currentInfo}
  196. isDetail={isDetail}
  197. onSubmit={async (values) => {
  198. const success = await handleSubmit(currentInfo?._id, values);
  199. if (success) {
  200. handleModalVisible(false);
  201. if (actionRef.current) {
  202. actionRef.current.reload();
  203. }
  204. }
  205. }}
  206. ></HandleForm>
  207. {selectedRowsState?.length > 0 && (
  208. <FooterToolbar
  209. extra={
  210. <div>
  211. 已选择
  212. <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>
  213. </div>
  214. }
  215. >
  216. <Button
  217. onClick={async () => {
  218. await handleRemove(selectedRowsState, '');
  219. setSelectedRows([]);
  220. actionRef.current?.reloadAndRest?.();
  221. }}
  222. >
  223. 批量删除
  224. </Button>
  225. </FooterToolbar>
  226. )}
  227. </PageContainer>
  228. );
  229. };
  230. export default TableList;

pages/ListTableList/data.d.ts

export interface TableListItem {
  _id?: string;
  username?: string;
  password?: string;
  avatar?: string;
  access?: string;
  email?: string;
} 

export interface TableListPagination {
  total: number;
  pageSize: number;
  current: number;
}

export interface TableListData {
  list: TableListItem[];
  pagination: Partial<TableListPagination>;
}

export interface TableListParams {
  _id?: string;
  username?: string;
  password?: string;
  avatar?: string;
  access?: string;
  email?: string;
  pageSize?: number;
  currentPage?: number;
  filter?: { [key: string]: any[] };
  sorter?: { [key: string]: any };
}

pages/ListTableList/service.ts

import { request } from 'umi';
import { TableListParams } from './data.d';

export async function queryRule(params?: TableListParams) {
  return request('/api/user/account', {
    params,
  });
}

export async function removeRule(params: { _id: string|string[] }) {
  return request('/api/user/account', {
    method: 'DELETE',
    data: params
  });
}

export async function addRule(params: TableListParams) {
  return request('/api/user/account', {
    method: 'POST',
    data: {
      ...params
    },
  });
}

export async function updateRule(params: TableListParams) {
  return request(`/api/user/account?_id=${params._id}`, {
    method: 'PUT',
    data: {
      ...params
    },
  });
}

6、弹窗表单组件

pages/ListTableList/components/HandleForm.d.ts
import React from 'react';
import { Modal } from 'antd';
import ProForm, { ProFormText, ProFormRadio } from '@ant-design/pro-form';
import { TableListItem } from '../data';

export interface FormValueType extends Partial<TableListItem> {
  username?: string;
  password?: string;
  type?: string;
  time?: string;
  frequency?: string;
}

export interface CreateFormProps {
  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
  onSubmit: (values?: FormValueType) => Promise<void>;
  modalVisible: boolean;
  values: Partial<TableListItem> | null;
  isDetail?: boolean;
}

const CreateForm: React.FC<CreateFormProps> = ({
  isDetail,
  onCancel,
  modalVisible,
  values,
  onSubmit,
}) => {
  if (values?.password) values.password = '******';
  return (
    <Modal
      destroyOnClose
      title={!values ? '新建用户' : isDetail ? '用户详情' : '更新用户'}
      visible={modalVisible}
      onCancel={() => onCancel()}
      footer={null}
      width={840}
    >
      <ProForm
        initialValues={values as TableListItem}
        onFinish={async (values: Partial<TableListItem>) => {
          !isDetail && onSubmit(values);
        }}
        {...(isDetail && { submitter: false })}
      >
        <ProFormText
          rules={[{ required: true, message: '请输入用户名!' }]}
          disabled={isDetail}
          label="用户名"
          name="username"
        />
        <ProFormText
          rules={[{ required: true, message: '请输入密码!' }]}
          disabled={isDetail}
          label="密码"
          name="password"
        />
        <ProFormText
          rules={[{ required: true, message: '请输入邮箱!' }]}
          disabled={isDetail}
          label="邮箱"
          name="email"
        />
        <ProFormRadio.Group
          name="access"
          disabled={isDetail}
          label="角色"
          rules={[{ required: true, message: '请选择角色!' }]}
          options={[
            {
              label: '管理员',
              value: 'admin',
            },
            {
              label: '用户',
              value: 'user',
            },
          ]}
        />
        <ProFormText
          // rules={[{ required: true, message: '请填写头像!' }]}
          disabled={isDetail}
          label="头像"
          name="avatar"
        />
      </ProForm>
    </Modal>
  );
};

export default CreateForm;

7、登录和用户信息services

pages/services/login.ts

import { request } from 'umi';

export interface LoginParamsType {
  username: string;
  password: string;
  mobile: string;
  captcha: string;
  type: string;
}

export async function fakeAccountLogin(params: LoginParamsType) {
  return request<API.LoginStateType>('/api/user/login', {
    method: 'POST',
    data: params
  });
}

export async function getFakeCaptcha(mobile: string) {
  return request(`/api/login/captcha?mobile=${mobile}`);
}

export async function outLogin() {
  return request('/api/login/outLogin');
}

pages/services/user.ts

import { request } from 'umi';

export async function query() {
  return request<API.CurrentUser[]>('/api/users');
}

export async function queryCurrent() {
  return request<API.CurrentUser>('/api/currentUser', {
    headers: { 
      Authorization :  'Bearer ' + `${localStorage.getItem('token')}`
    }
  });
}

export async function queryNotices(): Promise<any> {
  return request<{ data: API.NoticeIconData[] }>('/api/notices');
}

2、初始化server端项目

参考链接 实现链接数据库的例子˘

0、实现的功能

  • 处理post请求(body-parser)
  • 处理跨域 (cors)
  • 处理cookie (cookie-parser)
  • 打印日志 (morgan)
  • 设置token信息,解析token信息 (jsonwebtoken)
  • 全局验证jwt (express-jwt)
  • 实现登录返回token,全局验证token
  • 实现注册密码加密 (md5)
npm i express mongoose body-parser jsonwebtoken http-status-codes -S

1、入口文件 app.js

var createError = require("http-errors");
let express = require("express");
let bodyParser = require("body-parser");
let app = express();
var cors = require("cors");
var logger = require("morgan");
var cookieParser = require("cookie-parser");
const expressJWT = require("express-jwt");
const config = require("./config");
let { userRouter } = require("./routes/index");

// 处理post请求
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// 打印日志
app.use(logger("dev"));
// 处理跨域
app.use(cors());

// 日志
app.use(logger("dev"));

// 使用cookie
app.use(cookieParser());

// 校验token,获取headers⾥里里的Authorization的token,要写在路由加载之前,静态资源之后
app.use(
  expressJWT({
    secret: config.Secret,
    algorithms: ["HS256"],
    credentialsRequired: true,
  }).unless({
    path: ["/api/user/register", "/api/login", "/api/user/account"], //⽩白名单,除了了这⾥里里写的地址,其他的URL都需要验证
  })
);

app.use("/api", userRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  if (err.name === "UnauthorizedError") {
    // 这个需要根据⾃自⼰己的业务逻辑来处理理
    res.status(401).send({ code: -1, msg: "token验证失败" });
  } else {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get("env") === "development" ? err : {};
    // render the error page
    res.status(err.status || 500);
    res.render("error");
  }
});

app.listen(3000, function () {
  console.log("服务在3000启动了");
});

2、开启mongodb数据库

// 没有/data/db需要创建
cd /usr/local/mongodb/bin
sudo ./mongod  -dbpath /data/db/

3、配置config.js

module.exports={
    dbUrl:'mongodb://localhost:27017/pro5App',
    screct:'pro5',
    EXPIRESD:60*60*24
}

4、路由 routes

routes/index.js

const { userRouter } = require("./user");
module.exports = {
  userRouter,
};

routers/user.js

let express = require("express");
let userRouter = express.Router();
const { UserModel } = require("../model/index");
let jwt = require("jsonwebtoken");
let config = require("../config");
const { SuccessModel, ErrorModel } = require("../utils/resModule");

// 用户注册接口
userRouter.post("/user/register", async function (req, res) {
  await UserModel.create(req.body);
  res.json(new SuccessModel("注册成功"));
});

// 登录接口
userRouter.post("/user/login", async function (req, res) {
  let { username, password } = req.body;
  let query = { username, password };
  try {
    let result = await UserModel.findOne(query);
    let resultJSON = result.toJSON();
    let token = jwt.sign(resultJSON, config.Secret,{expiresIn:config.EXPIRESD});
    res.json(new SuccessModel(token));
  } catch (error) {
    res.json(new ErrorModel("登录失败"));
  }
});

// 查询当前用户信息接口
userRouter.get("/user/currentInfo", async function (req, res) {
  let authorization = req.headers["authorization"];
  let token = authorization.split(" ")[1];
  let result = jwt.verify(token, config.Secret);
  res.json(new SuccessModel(result, "注册成功"));
});

// 查询所有用户信息
userRouter.get("/user/account", async function (req, res) {
  try {
    let result = await UserModel.find();
    res.json(new SuccessModel(result, "查询成功"));
  } catch (error) {
    res.json(new ErrorModel(error));
  }
});

// 删除用户信息
userRouter.delete("/user/account", async function (req, res) {
  let hasRes = await UserModel.findOne(req.body);
  if (hasRes) {
    let { deletedCount } = await UserModel.remove(req.body);
    if (deletedCount) {
      res.json(new SuccessModel("删除成功"));
    }
  } else {
    res.json(new ErrorModel("删除失败"));
  }
});

// 修改用户信息
userRouter.put("/user/account", async function (req, res) {
  let { nModified } = await UserModel.update(
    req.query,
    { $set: req.body },
    { multi: true }
  );
  if (nModified) {
    res.json(new SuccessModel("修改成功"));
  } else {
    res.json(new ErrorModel("修改失败"));
  }
});

module.exports = {
  userRouter,
};

5、模型model

model/index.js

const mongoose = require("mongoose");
const config = require("../config");
const { UserSchema } = require("./user");
// 注册
let connection = mongoose.createConnection(config.dbUrl, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});
// 连接数据库
const UserModel = connection.model("User", UserSchema);
module.exports = {
  UserModel,
};

model/user.js

let mongoose = require("mongoose");
const Schema = mongoose.Schema;
// 定义数据结构
let UserSchema = new Schema({
  username: { type: String },
  email: { type: String },
  password: { type: String },
  avatar: { type: String },
  access: { type: String },
});
module.exports = {
    UserSchema
};

6、工具函数utils

utils/resModule.js

class BaseModel {
    constructor(data,message) {
        if(typeof data === 'string') {
            this.message = data
            data = null
            message = null
        }
        if(data){
            this.data = data
        }
        if(message){
            this.message = message
        }
    }
}

class SuccessModel extends BaseModel {
    constructor(data,message){
        super(data,message)
        this.errno = 0
        this.code = 200
        this.type = "success"
    }
}

class ErrorModel extends BaseModel {
    constructor(data,message,code){
        super(data,message)
        this.errno = -1
        this.type = "error"
        this.code = code
    }
}

module.exports = {
    SuccessModel,
    ErrorModel
}

3、项目地址与参考链接

前端地址:https://rockshang.coding.net/public/react_pro_server/umi3pro5/git
服务端地址:https://rockshang.coding.net/public/react_pro_server/umi3pro5Server/git/files
umi 官网
pro5参考文档:https://procomponents.ant.design/components/form#proform-1