第四期 - 终
第四期 - 终
用户中心笔记第四期 - 终
先修改前端代码,将框架预生成的样式改成自己想要的样式。
尤其注意参数名的修改
1、前后端交互
1.1 前端请求
前端需要向后端发送请求才能获取数据 / 执行操作。
怎么发请求:前端使用 ajax
来请求后端
axios
封装了ajax
request
是 ant design 项目又封装了一次
追踪 request 源码:用到了 umi 的插件、requestConfig 配置文件
1.2 代理的位置
正向代理:替客户端向服务器发送请求,可以解决跨域问题
反向代理:替服务器统一接收请求。
怎么实现代理?
- Nginx 服务器
- Node.js 服务器
举例:
原来请求是:http://localhost:8000/api/user/login
通过反向代理到请求:http://localhost:8080/api/user/login
过程如下:
主要区别:
- 代理的位置:正向代理靠近客户端,反向代理靠近服务器端。
- 客户端的意识:使用正向代理时,客户端意识到代理的存在;而使用反向代理时,客户端通常不知道代理的存在。
- 主要目的:正向代理主要用于客户端的访问控制和匿名性;反向代理主要用于服务器端的负载均衡、安全性和高可用性。
- 配置方式:正向代理需要客户端进行配置;反向代理通常由服务器管理员配置。
2、注册页面
先注册再登录。
复制框架生成的登录页面,将文件名修改为注册页面
添加路由规则
- 在添加完组件以及路由之后,输入
http//localhost:8000/user/register
,发现会被强制路由至登录页面。此时想到Ant Design Pro
框架本身是用来做后台管理系统,初始化框架规定在未登录情况下想操作其它页面,会被强制路由到登录页面。所以需要进行修改。
- 修改前端项目入口文件
src/app.tsx
取消重定向后并在下面的 layout: RunTimeLayoutConfig
中添加如下重定向配置:
onPageChange: () => {
const { location } = history;
// 添加白名单,登录页面和注册页面不需要强制路由
if (location.pathname === "/user/Register") {
return;
}
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
- 修改注册逻辑
const Register: React.FC = () => {
const [type, setType] = useState<string>('account');
const { styles } = useStyles();
const handleSubmit = async (values: API.RegisterParams) => {
// 校验密码
const { userName, password, checkPassword } = values;
if (password !== checkPassword) {
message.error('两次密码不一致');
return;
} else if (userName?.length < 4) {
message.error('账号长度不能小于4')
return;
}
try {
// 注册
const id = await register(values);
if (id > 0) {
const RegisterMessage = '注册成功!';
message.success(RegisterMessage);
/** 此方法会跳转到 redirect 参数所在的位置 **/
if (!history) return;
const { query } = history.location;
history.push({
pathname: '/user/login',
query,
});
return;
} else {
throw new Error(`register error id = ${id}`);
}
} catch (error) {
const RegisterMessage = '注册失败,请重试!';
message.error(RegisterMessage);
}
};
return (
<div className={styles.container}>
<Helmet>
<title>
{'注册'}- {Settings.title}
</title>
</Helmet>
<div style={{ flex: '1', padding: '32px 0', }} >
<LoginForm contentStyle={{ minWidth: 280, maxWidth: '75vw', }}
logo={<img alt="logo" src={SYSTEM_LOGO} />}
title="Byte Lighting"
subTitle={'字节流光 - 一个很酷的学习交流平台'}
submitter={{
searchConfig: {
submitText: '注册',
}
}}
onFinish={async (values) => {
await handleSubmit(values as API.LoginParams);
}}
>
<Tabs activeKey={type} onChange={setType} centered
items={[
{
key: 'account',
label: '账号密码注册',
},
]}
/>
{type === 'account' && (
<>
<ProFormText
name="userName"
fieldProps={{
size: 'large',
prefix: ,
}}
placeholder={'请输入账号'}
rules={[
{
required: true,
message: '账号不能为空!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: ,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码不能为空!',
},
{
min: 8,
type: 'string',
message: '长度不能小于8',
}
]}
/>
<ProFormText.Password
name="checkPassword"
fieldProps={{
size: 'large',
prefix: ,
}}
placeholder={'请确认密码'}
rules={[
{
required: true,
message: '确认密码不能为空!',
},
{
min: 8,
type: 'string',
message: '长度不能小于8',
}
]}
/>
</>
)}
</LoginForm>
</div>
<Footer />
</div>
);
};
修改后将页面中冗余的代码删除。
3、登录页面
添加「注册页面」后,要在登录页面添加「注册链接」,以便从登录页跳转至用户注册页。
<ProFormCheckbox noStyle name="autoLogin">
自动登录
</ProFormCheckbox>
<a
style={{ float: 'initial', }}
href={DOCS_LINK}
target="_blank" rel="noreferrer"
>
忘记密码
</a>
<Link to="/user/register" style={{ float: 'right', }}>没有账号?点击注册</Link>
在「自动登录」旁边加上「注册」,并调整布局,效果如下:
4、获取当前登录用户
点击登录后获取到用户信息后跳转到主页,首先要拿到用户的信息,才能够成功跳转。所以先在后端添加获取当前登录用户信息的接口。
/**
* 获取当前用户
* @param request 请求对象
* @return 用户信息
*/
@GetMapping("/current")
public User getCurrentUser(HttpServletRequest request) {
// 获取登录态
User currentUser = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
if (currentUser == null) {
return null;
}
// 根据id获取到用户信息,去数据库查询
Long userId = currentUser.getUserId();
// TODO 校验用户是否合法,比如封号等
User user = userService.getById(userId);
return userService.getSafetyUser(user);
}
直接获取用户信息的方式并不推荐,还应该对状态异常的用户进行筛选。这里后面再调整。
在 Login/index.tsx
中修改前端登录逻辑,设置登录参数 API.LoginParams
:
const handleSubmit = async (values: API.LoginParams) => {
try {
// 登录
const user = await login({
...values,
type,
});
if (user) {
const loginSuccessMessage = '登录成功!';
message.success(loginSuccessMessage);
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
console.log(user);
// 如果失败去设置用户错误信息
setUserLoginState(user);
} catch (error) {
const loginFailureMessage = '登录失败,请重试!';
console.log(error);
message.error(loginFailureMessage);
}
};
在 typings.d.ts
中添加登录请求参数 API.LoginParams
:
type LoginParams = {
userName?: string;
password?: string;
autoLogin?: boolean;
type?: string;
};
还要在 api.ts
中添加调用的接口地址 /api/user/current
,返回类型为 API.CurrentUser
:
/** 获取当前的用户 GET /api/user/currentUser */
export async function currentUser(options?: { [key: string]: any }) {
return request<API.CurrentUser>('/api/user/current', {
method: 'GET',
...(options || {}),
});
}
再到 typings.d.ts
配置返回的用户信息 CurrentUser
:
declare namespace API {
type CurrentUser = {
userId?: number;
userName?: string;
nickName?: string;
avatar?: string;
gender?: number;
phone?: string;
email?: string;
status?: number;
lastTime?: Date;
userRole?: number;
remark?: string;
};
}
5、后台管理页面
设置登录后自动跳转
修改 src/app.tsx
下的初始化函数 getInitialState()
:
const loginPath = '/user/login';
const NO_NEED_LOGIN_WHITE_LIST = ['/user/register', loginPath];
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
try {
return await queryCurrentUser({
skipErrorHandler: true,
});
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果是登录或注册页面,则执行
if (NO_NEED_LOGIN_WHITE_LIST.includes(history.location.pathname)) {
return {
// @ts-ignore
fetchUserInfo,
settings: defaultSettings,
};
}
const currentUser = await fetchUserInfo();
return {
// @ts-ignore
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
点击登录后成功跳转到后台页面,但是可能右上角的头像和用户名一直加载不出来,大概率是因为后端传的变量名和前端没对上,修改后即可显示。
添加后台管理页面
在 src/pages
下新建用户管理页 Admin/UserManage
,再在 config/routes.ts
中添加路由规则:
{ path: '/admin/user-manage', name: '用户管理', component: './Admin/UserManage' },
ProComponents
在 Ant Design 上进一步封装的前端组件:ProComponents
在用户管理页面下引入组件,选择 ProComponents 中的 高级表格 进行构建。
高级表格的属性
通过 columns
定义表格有哪些列
columns
属性:
dataIndex
:对应返回数据对象的属性title
:表格列名copyable
:是否允许复制ellipsis
:是否允许缩略valueType
:用于声明这一列的类型(dateTime、select等)
修改后的用户管理界面
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { ProTable, TableDropdown } from '@ant-design/pro-components';
import { Image } from 'antd';
import { useRef } from 'react';
import { searchUsers } from '@/services/ant-design-pro/api';
export const waitTimePromise = async (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
export const waitTime = async (time: number = 100) => {
await waitTimePromise(time);
};
// 定义列对应后端字段
const columns: ProColumns<API.CurrentUser>[] = [
{
title: '序号',
dataIndex: 'id',
valueType: 'indexBorder',
width: 48,
},
{
title: '账号',
dataIndex: 'userName',
copyable: true,
},
{
title: '昵称',
dataIndex: 'nickName',
copyable: true,
ellipsis: true,
},
{
title: '头像',
dataIndex: 'avatar',
render: (_, record) => (
<div>
<Image src={record.avatar} width="80px" height="80px" />
</div>
),
copyable: true,
},
{
title: '性别',
dataIndex: 'gender',
valueType: 'select', // 枚举字段
valueEnum: {
0: { text: '男', status: 'Success' },
1: { text: '女', status: 'Error' },
},
},
{
title: '电话',
dataIndex: 'phone',
copyable: true,
},
{
title: '邮箱',
dataIndex: 'email',
copyable: true,
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select', // 枚举字段
valueEnum: {
0: { text: '正常', status: 'Success' },
1: { text: '注销', status: 'Default' },
2: { text: '封号', status: 'Error' },
},
},
{
title: '角色',
dataIndex: 'userRole',
valueType: 'select',
valueEnum: {
0: { text: '普通用户', status: 'Default' },
1: { text: '管理员', status: 'Success' },
},
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.id);
}}
>
编辑
</a>,
<a href={record.url} target="_blank" rel="noopener noreferrer"
key="view">
查看
</a>,
<TableDropdown
key="actionGroup"
onSelect={() => action?.reload()}
menus={[
{ key: 'copy', name: '复制' },
{ key: 'delete', name: '删除' },
]}
/>,
],
},
];
export default () => {
const actionRef = useRef<ActionType>();
return (
<ProTable<API.CurrentUser>
columns={columns}
actionRef={actionRef}
cardBordered
// 获取后端的数据,返回到表格
request={async (params = {}, sort, filter) => {
console.log(sort, filter);
const userList = await searchUsers();
return { data: userList };
}}
editable={{
type: 'multiple',
}}
columnsState={{
persistenceKey: 'pro-table-singe-demos',
persistenceType: 'localStorage',
onChange(value) {
console.log('value: ', value);
},
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
options={{
setting: {
listsHeight: 400,
},
}}
form={{
// 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
created_at: [values.startTime, values.endTime],
};
} return values;
},
}}
pagination={{
pageSize: 5,
onChange: (page) => console.log(page),
}}
dateFormatter="string"
headerTitle="用户列表"
/>
);
};
构建完成后使用 http://localhost:8000/admin/user-manage
访问。
会发现无权访问,对比 routes.ts
中上面的 /user
路径,会发现 /admin
多了权限校验 access: 'canAdmin'
,只有管理员才能够访问用户管理页。而 access
字段设置在 access.ts
文件中,修改权限校验如下:
再次访问即可: