0、前言
使用umi3和antd pro5从零实现全栈中后台管理系统
0-1、涉及技术栈
前端: TS 、 React、React Hooks、 umi3、antd-pro5 后端: express、mongodb、jwt **
0-2、实现的功能
- 后端用户鉴权
- 前端权限管理
- 用户密码加密
- 封装一套通用弹窗表单组件,实现新建、修改、详情功能
- 用户登录注册(首次需要后端自己添加一条用户登录信息)
- 后端通过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、实现一个用户管理
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/…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!