最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Umi3与Antd-Pro5中后台全栈项目实战

    正文概述 掘金(重阳微噪)   2020-12-09   803

    0、前言

    使用umi3和antd pro5从零实现全栈中后台管理系统 Umi3与Antd-Pro5中后台全栈项目实战

    0-1、涉及技术栈

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

    0-2、实现的功能

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

    1、初始化前端项目

    umi 官网 coding网址

    yarn create umi myapp
    npm i 
    npm run dev
    

    1、设置config下的proxy代理

      dev: {
        '/api/': {
          target: "http://localhost:3000",
          changeOrigin: true,
          pathRewrite: { '^': '' },
        },
      },
    

    2、登录

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

    export async function fakeAccountLogin(params: LoginParamsType) {
      return request<API.LoginStateType>('/api/user/login', {
        method: 'POST',
        data: params,
      });
    }
    
    存储token pages/user/login/index.tsx
    localStorage.setItem('token' , msg.token)
    

    使用token services/user.ts

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

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

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

    退出 RightContent/AvatarDropdown.tsx

     localStorage.removeItem('token')
    

    3、pro5参考文档

    procomponents.ant.design/components/…

    4、实现一个用户管理

    Umi3与Antd-Pro5中后台全栈项目实战

    5、列表页

    pages/ListTableList/index.tsx

    import { PlusOutlined } from '@ant-design/icons';
    import { Button, Divider, message, Avatar } from 'antd';
    import React, { useState, useRef } from 'react';
    import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
    import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
    import HandleForm from './components/HandleForm';
    import { TableListItem } from './data.d';
    import { queryRule, updateRule, addRule, removeRule } from './service';
    import moment from 'moment';
    /**
     * 操作提交
     * @param fields
     */
    const handleSubmit = async (_id?: string, fields?: TableListItem) => {
    
      let title = _id ? '修改' : '新增';
      const hide = message.loading(`正在${title}`);
      try {
        if (_id) {
          await updateRule({
            _id,
            ...fields,
          });
        } else {
          await addRule({ ...fields });
        }
        hide();
        message.success(`${title}成功`);
        return true;
      } catch (error) {
        hide();
        message.error(`${title}失败`);
        return false;
      }
    };
    
    /**
     *  删除节点
     * @param selectedRows
     */
    const handleRemove = async (selectedRows: string[], _id: string) => {
      // console.log(selectedRows,_id,'selectedRows>>>>')
      const hide = message.loading('正在删除');
      // return
      try {
        await removeRule({
          _id: _id ? _id : selectedRows,
        });
        hide();
        message.success('删除成功');
        return true;
      } catch (error) {
        hide();
        message.error('删除失败');
        return false;
      }
    };
    
    const TableList: React.FC<{}> = () => {
      const [modalVisible, handleModalVisible] = useState<boolean>(false);
      const [currentInfo, handleSaveCurrentInfo] = useState<TableListItem | null>(null);
      const [isDetail, setDetail] = useState<boolean>(false);
      const actionRef = useRef<ActionType>();
      const [selectedRowsState, setSelectedRows] = useState<any>([]);
      const columns: ProColumns<TableListItem>[] = [
        {
          title: '用户名',
          dataIndex: 'username',
        },
        {
          title: '密码',
          dataIndex: 'password',
          hideInDescriptions: true, //详情页不显示
          hideInTable: true,
        },
        {
          title: '角色',
          dataIndex: 'access',
          search: false,
          filters: [
            { text: '普通用户', value: 'user' },
            { text: '管理员', value: 'admin' },
          ],
          valueEnum: {
            user: { text: '普通用户' },
            admin: { text: '管理员' },
          },
        },
        {
          title: '_id',
          dataIndex: '_id',
          sorter: true,
          hideInForm: true,
          search: false,
        },
        {
          title: '头像',
          dataIndex: 'avatar',
          search: false,
          hideInForm: true,
          render: (dom, entity) => {
            return <Avatar src={entity.avatar}  />;
          },
        },
        {
          title: '邮箱',
          dataIndex: 'email',
        },
        {
          title: '更新时间',
          dataIndex: 'updatedAt',
          sorter: true,
          hideInForm: true,
          search: false,
          renderText: (val: string) => {
            if (!val) return '';
            return moment(val).fromNow(); // 绝对时间转化成相对时间
          },
        },
        {
          title: '创建时间',
          dataIndex: 'createdAt',
          sorter: true,
          hideInForm: true,
          search: false,
          valueType: 'dateTime',
        },
        {
          title: '操作',
          dataIndex: 'option',
          valueType: 'option',
          render: (_, record) => (
            <>
              <a
                href="javascript:;"
                onClick={() => {
                  handleModalVisible(true), handleSaveCurrentInfo(record);
                }}
              >
                修改
              </a>
              <Divider type="vertical" />
              <a
                href="javascript:;"
                onClick={() => {
                  handleModalVisible(true), handleSaveCurrentInfo(record), setDetail(true);
                }}
              >
                详情
              </a>
              <Divider type="vertical" />
              <a
                href="javascript:;"
                onClick={async () => {
                  await handleRemove([], record._id as 'string');
                  // 刷新
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                删除
              </a>
            </>
          ),
        },
      ];
    
      return (
        <PageContainer>
          <ProTable<TableListItem>
            headerTitle="用户管理"
            actionRef={actionRef}
            rowKey="_id"
            search={{
              labelWidth: 120,
            }}
            toolBarRender={() => [
              <Button type="primary" onClick={() => handleModalVisible(true)}>
                <PlusOutlined />
                新增
              </Button>,
            ]}
            request={(params, sorter, filter) => queryRule({ ...params, sorter, filter })}
            columns={columns}
            form={{
              submitter: false,
            }}
            pagination={{ defaultPageSize: 5 }}
            rowSelection={{
              onChange: (selected, selectedRows) => {
                setSelectedRows(selected);
              },
            }}
          />
          <HandleForm
            onCancel={() => {
              handleModalVisible(false), handleSaveCurrentInfo({}), setDetail(false);
            }}
            modalVisible={modalVisible}
            values={currentInfo}
            isDetail={isDetail}
            onSubmit={async (values) => {
              const success = await handleSubmit(currentInfo?._id, values);
              if (success) {
                handleModalVisible(false);
                if (actionRef.current) {
                  actionRef.current.reload();
                }
              }
            }}
          ></HandleForm>
          {selectedRowsState?.length > 0 && (
            <FooterToolbar
              extra={
                <div>
                  已选择
                  <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a> 项
                </div>
              }
            >
              <Button
                onClick={async () => {
                  await handleRemove(selectedRowsState, '');
                  setSelectedRows([]);
                  actionRef.current?.reloadAndRest?.();
                }}
              >
                批量删除
              </Button>
            </FooterToolbar>
          )}
        </PageContainer>
      );
    };
    
    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、项目地址与参考链接

    前端地址:rockshang.coding.net/public/reac… 服务端地址:rockshang.coding.net/public/reac… umi 官网 pro5参考文档:procomponents.ant.design/components/…


    起源地下载网 » Umi3与Antd-Pro5中后台全栈项目实战

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元