项目总结 通用 CRUD 前端项目 stateful-backend-frontend
前言
学习程序员鱼皮API 开放平台项目开源项目:https://github.com/liyupi/yuapi-backend-public
通过开源项目中给出的前端技术栈,倒推 stateful-backend 的前端实现
前端技术选型
- React 18
- Ant Design Pro 5.x 脚手架
- Ant Design & Procomponents 组件库
- Umi 4 前端框架
- OpenAPI 前端代码生成
项目实现
项目全局配置
页面结构
知识补充
tsx = TypeScript 版本的 jsx
命名规则:一般文件夹全小写,页面文件夹大驼峰
具体结构
- src:源码文件夹
- components:通用组件,但是按页面的目录结构组织
- Footer:采用页面的目录结构进行组织的通用组件
- index.tsx:组件(页面)主体
- Footer:采用页面的目录结构进行组织的通用组件
- pages:页面
- TableList:表格页面
- index.tsx:页面主体
- user
- Forget:忘记密码页面
- index.tsx
- Login:登录页面
- components:页面内部组件
- LoginPageFooter.jsx:组件主体
- index.less:页面样式
- index.tsx:页面主体
- components:页面内部组件
- Register:注册页面
- components:页面内部组件
- RegisterPageFooter.jsx:组件主体
- index.less:页面样式
- index.tsx:页面主体
- components:页面内部组件
- Forget:忘记密码页面
- TableList:表格页面
- components:通用组件,但是按页面的目录结构组织
全局路由配置
/myapp/config/routes.ts
export default [
{
path: '/user',
layout: false,
routes: [
{name: '登录', path: '/user/login', component: './user/Login'},
{name: '注册', path: '/user/register', component: './user/Register'},
{name: '重置密码', path: '/user/forget', component: './user/Forget'},
{component: './404'},
],
},
{path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome'},
{
path: '/admin',
name: '管理页',
icon: 'crown',
access: 'canAdmin',
routes: [
{path: '/admin/sub-page', name: '二级管理页', icon: 'smile', component: './Welcome'},
{component: './404'},
],
},
{name: '查询表格', icon: 'table', path: '/list', component: './TableList'},
{path: '/', redirect: '/welcome'},
{component: './404'},
];
注意:路由内部从上往下扫描,404 页面要写在最下面,否则会出现 404 覆盖正常页面的现象
运行时配置
在构建时是无法使用 dom 的,所以有些配置可能需要运行时来配置,一般而言我们都是在 src/app.tsx
中管理运行时配置。
/myapp/src/app.tsx
//import 部分省略,非关键代码省略
//放行页面配置(无需登录也能跳转到的页面)
const loginPath = '/user/login';
const register = '/user/register'
const forget = '/user/forget'
const paths = [loginPath, register, forget]
//获取页面初始状态
// 参数:无
// 返回值:Promise 对象,包含一个对象,对象的属性有:
// settings: 可选的 Partial 类型,表示布局设置的部分内容
// currentUser: 可选的 API.CurrentUser 类型,表示当前用户信息
// loading: 可选的 boolean 类型,表示加载状态
// fetchUserInfo: 可选的函数,返回一个 Promise 对象,用于获取用户信息
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
// 声明用于获取用户信息的函数
const fetchUserInfo = async () => {
try {
//使用 Ant Design Pro 提供的用户态机制,但是利用自定义的后端接口获取用户信息
const msg = await getLoginUserUsingGET();
//后端正常响应
if (msg.code === 200) {
//使用 Ant Design Pro 提供的用户态机制 → 需要将返回值封装成 API.CurrentUser 类型的对象
const res: API.CurrentUser = {
name: msg.data?.userAccount,
userid: msg.data?.id?.toString(),
access: msg.data?.userRole,
};
//判断通过后端获取到的用户态信息是否为空,若存在空属性,则返回 undefined
if (res.name === undefined
|| res.userid === undefined
|| res.access === undefined) {
return undefined
}
//若不为空,则返回封装成 API.CurrentUser 类型的从后端获取的用户态信息
return res;
} else {
//后端响应异常,直接返回 undefined
return undefined;
}
} catch (error) { //出现其他异常,跳转回登录页面
history.push(loginPath);
}
return undefined;
};
// 如果不是直接放行的页面,先尝试获取用户信息
if (!paths.includes(history.location.pathname)) {
//获取用户信息
const currentUser = await fetchUserInfo();
//返回 Promise 对象(带用户信息)
return {
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
//如果是放行的页面,无需获取用户信息,直接返回 Promise 对象(不带用户信息)
return {
fetchUserInfo,
settings: defaultSettings,
};
}
//运行时布局配置(无需额外配置)
export const layout: RunTimeLayoutConfig = ({initialState, setInitialState}) => {
return {
//右边栏渲染器
rightContentRender: () => <RightContent/>,
disableContentMargin: false,
waterMarkProps: {
content: initialState?.currentUser?.name,
},
//页脚渲染器
footerRender: () => <Footer/>,
onPageChange: () => {
const {location} = history;
// 如果没有用户态信息,重定向到 login
// 如果不是放行页面,重定向到 login
// 即如果有用户态信息或是放行页面,则不重定向(逆否命题)
if (!initialState?.currentUser && !paths.includes(location.pathname)) {
console.log(initialState?.currentUser);
history.push(loginPath);
}
},
//如果是 dev 环境,显示以下内容
links: isDev
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined/>
<span>OpenAPI 文档</span>
</Link>,
<Link to="/~docs" key="docs">
<BookOutlined/>
<span>业务组件文档</span>
</Link>,
]
: [],
menuHeaderRender: undefined,
//子渲染器(无需额外配置)
childrenRender: (children, props) => {
return (
<>
{children}
{!props.location?.pathname?.includes('/login') && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},
...initialState?.settings,
};
};
知识补充:常规函数声明 vs 箭头函数声明
//常规函数声明
function greet(name: string): string {
return `Hello, ${name}!`;
}
//箭头函数声明
const greet = (name: string): string => {
return `Hello, ${name}!`;
};
- 相同点:
- 都可以用来定义函数。
- 都可以接收参数,并返回结果。
- 不同点:
- 常规函数声明使用 function 关键字,箭头函数声明使用箭头(=>)符号。
- 箭头函数声明更加简洁,可以在一行内定义函数体,而常规函数声明需要使用大括号包裹函数体。
- 箭头函数声明中的 this 指向的是定义时的上下文,而常规函数声明中的 this 指向的是调用时的上下文。
登录页面实现
- 静态页面
- 登录表单
- 输入框
- 提示信息
- 登录表单
- 接口对接
- 用户信息获取
- 用户登录
代码汇总
省略 import 部分的代码,反复使用的自定义组件只说明一次
页面主体
//函数组件(不带参数):登录
const Login: React.FC = () => {
//通过 useModel 处理初始状态
const {initialState, setInitialState} = useModel('@@initialState');
//获取用户信息
//1.尝试从初始状态中获取
//2.初始状态中不存在用户信息,则尝试从接口中获取,并将信息保存到初始状态中
const fetchUserInfo = async () => {
//尝试从初始状态中获取用户信息
const userInfo = initialState?.currentUser;
//初始状态中的用户信息不存在,则通过后端接口获取信息
if (!userInfo) {
const msg = await getLoginUserUsingGET();
if (msg.code === 200) {
const res: API.CurrentUser = {
name: msg.data?.userAccount,
userid: msg.data?.id?.toString(),
access: msg.data?.userRole
}
console.log(res)
//设置初始状态,保存获取的用户信息
await setInitialState((s) => ({
...s,
currentUser: res,
}));
//返回用户信息
return userInfo;
}
}
return userInfo;
};
//注册函数,对接自动生成的接口函数
const handleSubmit = async (values: API.loginUserParams) => {
try {
console.log("login:userLoginUsingPOST")
const res = await userLoginUsingPOST({
userAccount: values.username,
userPassword: values.password
});
console.log(res)
//根据返回值判断是否登录成功
if (res.code === 200) {
//登录成功,则提示成功信息
message.success("登录成功");
//获取已登录的用户信息
await fetchUserInfo();
//页面跳转
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
const {redirect} = query as {
redirect: string;
};
history.push(redirect || '/');
return;
} else {
//登录失败,
message.error(res.message + " : " + res.description)
}
} catch (error) {
message.error("内部错误,请联系管理员!");
}
}
//页面元素描述,等效于 React 类组件中的 render()
return (
//只允许一个根组件
<div className={styles.container}>
<div className={styles.content}>
{/* 登录表单 */}
<LoginForm
logo={<img alt="logo" src="/logo.svg"/>}
title="登录页面"
subTitle={'本项目为基于 Ant Design Pro 的通用中后台系统模板'}
initialValues={{
autoLogin: true,
}}
onFinish={async (values) => {
await handleSubmit(values as API.loginUserParams);
}}
>
<Tabs>
<Tabs.TabPane tab={'账户密码登录'}/>
</Tabs>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入用户名'}
rules={[
{
required: true,
message: '用户名是必填项!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码是必填项!',
},
]}
/>
<LoginPageFooter/>
<div style={{height: "3vh"}}/>
</LoginForm>
</div>
<Footer/>
</div>
);
};
export default Login;
自定义组件
const LoginPageFooter = () => {
const history = useHistory();
const handleRegisterClick = () => {
// 处理点击“没有账号?”时的跳转逻辑
// 跳转到注册页面
history.push('/user/register');
};
const handleForgotPasswordClick = () => {
// 处理点击“忘记密码?”时的跳转逻辑
// 跳转到找回密码页面
history.push('/user/forget');
};
return (
<div>
<div style={{float: 'left'}}>
没有账号?
<a onClick={handleRegisterClick}>注册</a>
</div>
<div style={{float: 'right'}}>
<a onClick={handleForgotPasswordClick}>忘记密码?</a>
</div>
</div>
);
};
export default LoginPageFooter;
const Footer: React.FC = () => {
const defaultMessage = '蚂蚁集团体验技术部出品';
const currentYear = new Date().getFullYear();
return (
<DefaultFooter
copyright={`${currentYear} ${defaultMessage}`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
};
export default Footer;
注册页面实现
- 静态页面
- 通用表单
- 输入框
- 提示信息
- 通用表单
- 接口对接
- 注册功能
代码汇总
页面本体
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
const handleSubmit = async (values: any) => {
console.log(values);
if (values.password != values.checkPassword) {
message.error("两次输入的密码不一致");
return;
}
const req: API.UserRegisterRequest = {
userAccount: values.username,
userPassword: values.password,
checkPassword: values.checkPassword
}
const msg = await register(req);
console.log("register");
console.log(msg);
if (msg.code === 200) {
message.success("注册成功")
history.push("/user/login")
} else {
message.error(msg.message + ":" + msg.description);
}
}
const Register: React.FC = () => {
//页面元素描述,等效于 React 类组件中的 render()
return (
<>
<div className={styles.container}>
<div className={styles.content}>
<div
style={{
margin: 24,
}}
>
<ProForm
onFinish={async (values: any) => {
//TODO 控制冷却时间
await waitTime(200);
await handleSubmit(values);
}}
>
<ProFormText
// width="md"
name="username"
label="用户名"
tooltip="长度限制为 4 到 20 个字符"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入用户名'}
rules={[
{
min: 4,
max: 20,
required: true,
message: '请输入 4 到 20 个字符的用户名!',
},
]}
/>
<ProFormText.Password
label="用户密码"
name="password"
tooltip="长度至少为 8 个字符"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
// placeholder={'密码: ant.design'}
placeholder={'请输入密码'}
rules={[
{
min: 8,
required: true,
message: '至少输入 8 位密码!',
},
]}
/>
<ProFormText.Password
name="checkPassword"
label="密码检验"
tooltip="长度至少为 8 个字符"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请再次输入密码'}
rules={[
{
min: 8,
required: true,
message: '至少输入 8 位密码!',
},
]}
/>
<RegisterPageFooter/>
</ProForm>
<Footer/>
</div>
</div>
</div>
</>
);
};
export default Register;
自定义组件
const RegisterPageFooter = () => {
const history = useHistory();
const handleLoginClick = () => {
// 处理点击“没有账号?”时的跳转逻辑
// 跳转到注册页面
history.push('/user/login');
};
return (
<div>
<div style={{float: 'right'}}>
已有账号?
<a onClick={handleLoginClick}>登录</a>
</div>
</div>
);
};
export default RegisterPageFooter;
表单页面实现
- 静态页面
- 高级表格(ProTable)
- 底部工具栏(FooterToolBar)
- 弹窗表单(ModalForm)
- 抽屉(Drawer)
- 接口对接
- Sample 管理
- 增
- 删
- 改
- 查
- Sample 管理
代码汇总
页面本体 · CRUD 接口对接函数
//引入接口方法,并重命名
import {
addSampleUsingPOST as add,
updateSampleUsingPOST as update,
deleteSampleUsingPOST as remove,
listUsingGET as list
} from "@/services/stateful-backend/sampleController";
//增
const handleAdd = async (fields: API.SampleAddRequest) => {
console.log("handleAdd")
console.log(fields)
const hide = message.loading('正在添加');
try {
await add({
...fields,
});
hide();
message.success('新建样例成功');
return true;
} catch (error) {
hide();
message.error('新建样例失败,请重试');
return false;
}
};
//删
const handleRemove = async (selectedRows: API.IdRequest[]) => {
console.log("handleRemove")
console.log(selectedRows)
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
for (const row of selectedRows) {
await remove(row);
}
hide();
message.success('删除成功');
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
return false;
}
};
//改
const handleUpdate = async (fields: API.SampleUpdateRequest) => {
console.log("handleUpdate")
console.log(fields)
const hide = message.loading('更新中');
try {
await update(fields);
hide();
message.success('更新成功');
return true;
} catch (error) {
hide();
message.error('更新失败,请重试');
return false;
}
};
//查
const getList = async () => {
console.log("getList")
const msg = await list({});
return {
data: msg.data,
total: msg.data?.length,
success: true
};
}
页面本体 · 渲染器
const TableList: React.FC = () => {
//add 表单弹窗状态(显示 or 不显示)控制
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
//update 表单弹窗状态(显示 or 不显示)控制
const [updateModalVisible, handleModalVisibleForUpdate] = useState<boolean>(false);
//detail 抽屉状态(显示 or 不显示)控制
const [showDetail, setShowDetail] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
//行选取状态控制
const [currentRow, setCurrentRow] = useState<API.SampleVO>();
const [selectedRowsState, setSelectedRows] = useState<API.SampleVO[]>([]);
//例属性配置
const columns: ProColumns<API.SampleVO>[] = [...]
// 渲染器配置
return (
<PageContainer>
<ProTable>完整表单</ProTable>
<FooterToolBar>底部工具栏</FooterToolBar>
<ModalForm>add 表单</ModalForm>
<ModalForm>update 表单</ModalForm>
<Drawer>抽屉</Drawer>
</PageContainer>);
};
export default TableList;
页面本体 · ProTable 表单
//列属性配置
const columns: ProColumns<API.SampleVO>[] = [
{
title: 'Sample',
dataIndex: 'id',
tip: 'The id is the unique key',
search: false,
render: (dom, entity) => {
return (
<a
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
);
},
},
{
title: '文本',
dataIndex: 'sampleTest',
//自动缩略
ellipsis: true,
search: false,
render: (dom) => {
return (
<>{dom}</>
);
},
},
{
title: '状态',
dataIndex: 'sampleStatus',
search: false,
render: (dom) => [
<>{dom}</>
],
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
key="config"
onClick={() => {
//设置当前行为选中的记录
setCurrentRow(record);
//打开弹窗
handleModalVisibleForUpdate(true);
}}
>
修改 Sample
</a>,
],
},
];
{/* 查询结果表格 */}
<ProTable
<API.SampleVO, API.PageParams>
//基础配置
headerTitle={'查询表格'}
actionRef={actionRef}
rowKey="id"
search={{labelWidth: 120,}}
//工具栏渲染器
toolBarRender={() => [
<Button
type="primary"
key="primary"
onClick={() => {
handleModalVisible(true);
}}
>
<PlusOutlined/> 新建
</Button>,
]}
//数据请求
//使用前端自带的分页机制,此处获取所有数据即可
request={async () => {
const res = await getList();
return {data: res.data, success: res.success, total: res.total}
}}
//列属性配置
columns={columns}
//行选择器
rowSelection={{
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
/>
页面本体 · 批量删除工具栏
{/*批量删除工具栏*/}
{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>
)}
页面主体 · add 表单(ModalForm)
{/* add 表单 */}
<ModalForm
//基础配置
title={'新建 Sample'}
width="400px"
//状态监听(是否显示)
visible={createModalVisible}
onVisibleChange={handleModalVisible}
//确定按钮事件绑定
onFinish={async (value) => {
//TODO:更改返回值类型
const success = await handleAdd(value as API.SampleAddRequest);
if (success) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}
}
>
<ProFormText
placeholder='请输入 id'
rules={[
{
required: true,
message: 'id 为必填项',
},
]}
width="md"
name="id"
/>
<ProFormText
placeholder='请输入样例文本'
rules={[
{
required: true,
message: '样例文本为必填项',
},
]}
width="md"
name="sampleTest"
/>
<ProFormText
placeholder='请输入样例状态'
rules={[
{
required: false,
},
]}
width="md"
name="sampleStatus"
/>
</ModalForm>
页面主体 · update 表单(ModalForm)
{/* update 表单 */}
<ModalForm
//基础配置
title={'更新 Sample'}
width="400px"
modalProps={{destroyOnClose: true}} //是否关闭时清除输入的内容
//状态监听(是否显示)
visible={updateModalVisible}
onVisibleChange={handleModalVisibleForUpdate}
//确定按钮事件绑定
onFinish={async (value) => {
console.log(value)
//TODO:更改返回值类型
const success = await handleUpdate(value as API.SampleUpdateRequest);
if (success) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
//返回 true 则操作完成后关闭弹窗
return true
}
}
>
<ProFormText
initialValue={currentRow?.id}
width="md"
name="id"
disabled
/>
<ProFormText
placeholder='请输入样例文本'
rules={[
{
required: true,
message: '样例文本为必填项',
},
]}
width="md"
name="sampleTest"
/>
<ProFormText
placeholder='请输入状态'
rules={[
{
required: true,
message: '状态为必填项',
},
]}
width="md"
name="sampleStatus"
/>
</ModalForm>
页面本体 · 抽屉
{/*抽屉*/}
<Drawer
width={600}
visible={showDetail}
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);
}}
closable={false}
>
<ProDescriptions
//TODO 更改返回值类型
<API.SampleVO>
column={1}
bordered={true}
size={"default"}
title="样例"
dataSource={
{
id: currentRow?.id,
sampleTest: currentRow?.sampleTest,
sampleStatus: currentRow?.sampleStatus,
}}
columns={[
{
title: 'id',
key: 'id',
dataIndex: 'id',
},
{
title: '文本',
copyable: true,
dataIndex: 'sampleTest',
//自动缩略
ellipsis: false
},
{
title: '状态',
dataIndex: 'sampleStatus',
},
]}
/>
</Drawer>
项目总结
项目开发流程
回顾 Web 开发分工
- 前端开发 = 用户交互 + 数据处理 + 接口对接
- 后端开发 = 业务逻辑 + 数据存取 + 接口封装
个人开发流程(实践总结)
- 预备工作:需求分析 + 系统设计
- 后端开发(业务逻辑 + 数据存取 + 接口封装)
- 后端通过
Knife4j
生成遵守规范(此处为 OpenAPI 规范:OAS)的 接口管理文档 - 前端通过
umi openapi
,利用后端生成的接口管理文档,生成前端 接口方法 和对应的 对象类型 - 前端开发(静态页面)
- 前端对接生成的 接口方法
团队开发流程(理论设计)
- 预备工作:需求分析 + 系统设计
- 前后端并行开发
- 前端实现静态页面(用户交互 + 数据处理)
- 后端实现业务逻辑、数据存取、接口封装
- 后端通过
Knife4j
生成遵守规范(此处为 OpenAPI 规范:OAS)的 接口管理文档 - 前端通过
umi openapi
,利用后端生成的接口管理文档,生成前端 接口方法 和对应的 对象类型 - 前端对接生成的 接口方法
ProComponents 常用组件用法汇总
表格组件(ProTable)
//列属性配置
const columns: ProColumns<API.SampleVO>[] = [
{
title: 'Sample',
dataIndex: 'id',
tip: 'The id is the unique key',
search: false,
render: (dom, entity) => {
return (
<a
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
);
},
},
{
title: '文本',
dataIndex: 'sampleTest',
//自动缩略
ellipsis: true,
search: false,
render: (dom) => {
return (
<>{dom}</>
);
},
},
{
title: '状态',
dataIndex: 'sampleStatus',
search: false,
render: (dom) => [
<>{dom}</>
],
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
key="config"
onClick={() => {
//设置当前行为选中的记录
setCurrentRow(record);
//打开弹窗
handleModalVisibleForUpdate(true);
}}
>
修改 Sample
</a>,
],
},
];
{/* 查询结果表格 */}
<ProTable
<API.SampleVO, API.PageParams>
//基础配置
headerTitle={'查询表格'}
actionRef={actionRef}
rowKey="id"
search={{labelWidth: 120,}}
//工具栏渲染器
toolBarRender={() => [
<Button
type="primary"
key="primary"
onClick={() => {
handleModalVisible(true);
}}
>
<PlusOutlined/> 新建
</Button>,
]}
//数据请求
//使用前端自带的分页机制,此处获取所有数据即可
request={async () => {
const res = await getList();
return {data: res.data, success: res.success, total: res.total}
}}
//列属性配置
columns={columns}
//行选择器
rowSelection={{
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
/>
文本输入框(ProFormText)
<ProFormText
// width="md" //宽度调整
// disabled //是否禁用
name="username"
label="用户名"
tooltip="长度限制为 4 到 20 个字符"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入用户名'}
rules={[
{
min: 4,
max: 20,
required: true,
message: '请输入 4 到 20 个字符的用户名!',
},
]}
/>
<ProFormText.Password
label="用户密码"
name="password"
tooltip="长度至少为 8 个字符"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
// placeholder={'密码: ant.design'}
placeholder={'请输入密码'}
rules={[
{
min: 8,
required: true,
message: '至少输入 8 位密码!',
},
]}
/>
常规表单组件(ProForm)
<ProForm
onFinish={async (values: any) => {
await waitTime(200);
await handleSubmit(values);
}}
>
{/*内部标签省略,一般就是放 <ProFormText/> */}
</ProForm>
弹窗表单组件(ModalForm)
//add 表单弹窗状态(显示 or 不显示)控制
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
{/* add 表单 */}
<ModalForm
//基础配置
title={'新建 Sample'}
width="400px"
//状态监听(是否显示)
visible={createModalVisible}
onVisibleChange={handleModalVisible}
//确定按钮事件绑定
onFinish={async (value) => {
//...
}}
>
{/*内部标签省略,一般就是放 <ProFormText/> */}
</ModalForm>
抽屉组件(Drawer)
//detail 抽屉状态(显示 or 不显示)控制
const [showDetail, setShowDetail] = useState<boolean>(false);
{/*抽屉*/}
<Drawer
//宽度
width={600}
//状态控制
visible={showDetail}
//关闭时执行的回调函数
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);
}}
//是否显示关闭按钮
closable={false}
>
{/*内部标签省略,一般就是放 <ProDescriptions/> */}
</Drawer>
<ProDescriptions
//类型声明
<API.SampleVO>
{/*每行显示的属性个数(列数)*/}
column={1}
{/*是否显示边框*/}
bordered={true}
{/*标题*/}
title="样例"
{/*数据源*/}
dataSource={
{
id: currentRow?.id,
sampleTest: currentRow?.sampleTest,
sampleStatus: currentRow?.sampleStatus,
}}
{/*类属性配置*/}
columns={[
{
title: 'id',
key: 'id',
dataIndex: 'id',
},
{
title: '文本',
copyable: true,
dataIndex: 'sampleTest',
//自动缩略,设置为 false 时,能全部显示
ellipsis: false
},
{
title: '状态',
dataIndex: 'sampleStatus',
},
]}
/>
Ant Design Pro 知识补充
简介
开箱即用的中台前端/设计解决方案
- 前端 = 组件交互 + 数据处理 + 接口对接
- 开箱即用:封装程度高
- 中台:数据多,重 CRUD,轻用户交互
感受:Ant Design Pro 的引入,使得前端规范化(强类型 + 组件化) + 前后端对齐
- 前端规范化
- 强类型 By TypeScript 的引入
- 组件化 By React Style 的引入
- 前后端对齐 By services 的引入
项目元素
-
页面
- 组织
- 大驼峰文件夹
- components:内部组件文件夹
- index.less:页面样式
- index.tsx:页面主体
- 大驼峰文件夹
- 使用:绑定路由,作为页面整体使用
- 组织
-
组件
- 组织:大驼峰.tsx
- 使用:通过标签,作为页面部分使用
-
类型
- 组织:typings.d.ts
- 使用:按
<namespace-name>.<type-name>
的形式使用
-
接口
-
组织:小驼峰.ts
-
使用:
-
预备工作:配置并生成接口方法
- 配置
config/config.ts
defineConfig.openAPI.schemaPath
defineConfig.openAPI.projectName
- 通过
umi openapi
生成接口方法
- 配置
-
import 接口文件内部 export 的方法
知识补充:
import {request} from 'umi'
在UMI中的
request
函数实际上是对axios
的封装,提供了更加简洁、易用的API,使得我们可以更方便地发送HTTP请求并处理响应。axios
是一个基于Promise的HTTP客户端库,它封装了XMLHttpRequest
和fetch
等底层API,提供了更简单、更强大的接口来发送HTTP请求和处理响应。通过
axios
,我们可以方便地发送各种类型的HTTP请求,包括GET、POST、PUT、DELETE等,并且可以设置请求头、处理请求参数、设置超时时间、处理响应等。 -
通过该方法请求后端响应
知识补充:async await 机制使用
async/await 是 JavaScript 中的一种异步编程模式,它基于 JavaScript 的 Promise 对象,并提供了一种更简洁、更直观的语法来处理异步操作。
- 在 async 函数内部,通过在函数前面加上
async
关键字来声明一个异步函数。 - 在异步函数中,可以使用
await
关键字来等待一个 Promise 对象的状态变为fulfilled
或rejected
,然后获取其结果。
使用 async/await 的语法,可以实现顺序执行异步操作的效果,让代码看起来更像是同步的,更易于理解。
- 在 async 函数内部,通过在函数前面加上
-
-
TypeScript 知识点汇总
简介
TypeScript 🆚 JavaScript 类似于 C++ 🆚 C
TypeScript | JavaScript |
---|---|
TypeScript 是 JavaScript 的超集 | JavaScript 是一种编程语言 |
强类型,支持静态类型检查 | 弱类型,动态类型检查 |
可以编译成纯 JavaScript 代码 | 直接执行在浏览器或服务器上 |
支持面向对象、类、接口等概念 | 支持面向对象、函数式编程等概念 |
提供更好的工具和编辑器支持 | 工具和编辑器支持相对较弱 |
拥有更多的语言特性和扩展 | 相对较少的语言特性和扩展 |
能够提高代码的可读性和可维护性 | 更加灵活但可能导致代码质量下降 |
TypeScript 编译器将 TypeScript 代码转换为相应的 ECMAScript(JavaScript)版本的代码,然后在运行时执行这些代码。这是因为浏览器和服务器环境只能理解和执行 JavaScript 代码,不能直接执行 TypeScript 代码,所以需要将 TypeScript 代码编译为 JavaScript 代码才能运行。
类型介绍
TypeScript 的基础数据类型包括:
- 布尔类型(boolean)
- 数字类型(number)
- 字符串类型(string)
- 数组类型(array)
- 元组类型(tuple)
- 枚举类型(enum)
- 任意类型(any)
- 空类型(void)
- null 和 undefined
通过 namespace
可以组成复合数据类型。在 TypeScript 中,namespace
可以用来创建具有层级结构的命名空间,用来组织和管理类型或代码。可以通过在命名空间中声明变量、函数、类等,并通过命名空间来调用它们。命名空间可以嵌套,以形成复杂的数据结构。在使用命名空间内的变量、函数、类时,需要使用命名空间作为前缀来访问。
通过 namespace 创建等效类
//namespace 声明
namespace MyNamespace {
export interface Person {
name: string;
age: number;
}
export function sayHello(name: string) {
console.log(`Hello, ${name}!`);
}
}
//namespace 使用
const person: MyNamespace.Person = { name: "Alice", age: 25 };
MyNamespace.sayHello(person.name);
通过 namespace 声明复合数据类型(等效结构体)
declare namespace MyNamespace {
type Person = {
name: string;
age: number;
};
}
在使用 declare namespace
声明的命名空间中,类型不会被实际生成为 JavaScript 代码,而只是用于编译器进行类型检查。
类型使用
当在 TypeScript 中声明变量时,我们可以使用类型注解来指定变量的类型。下面是一个示例代码,在 TypeScript 中按类型声明变量的几种方式:
// 使用冒号加上类型注解的方式
let num1: number = 10;
let str1: string = 'Hello';
let bool1: boolean = true;
// 使用 as 关键字的方式
let num2 = 20 as number;
let str2 = 'World' as string;
let bool2 = false as boolean;
// 使用 <> 操作符的方式
let num3 = <number>30;
let str3 = <string>'Hi';
let bool3 = <boolean>false;
以上代码展示了三种不同的方式来按类型声明变量,使用冒号加上类型注解的方式是最常见且推荐的方式。另外,注意在 TypeScript 中,也可以通过类型推断的方式,省略类型注解,让编译器根据赋值来推断变量的类型。
空值检查
作用类似于 Java 中的 Optional
-
不保证非空
-
条件判断
if (res.data) { // 对 res.data 进行操作 const data: DataType = res.data; } else { // 处理 'res.data' 为 undefined 的情况 let res.data = defaultValue; }
-
条件式
res.value ? res.value : defaultValue
// 等效的条件式写法 const data: DataType = res.data ? res.data : defaultValue;
-
-
保证非空
-
非空断言操作符
res.data!
const data: DataType = res.data!;
-
Ant Design Pro 机制整理
可复用 CRUD 机制
机制实现
通过 umi openapi
自动生成接口文档机制,实现接口方法和对象类型的自动生成(后端接口对齐)
对以下组件进行改造,实现通用的信息录入逻辑
- ProTable
- ModalForm
- Drawer
机制载入
复制 TableList 页面
机制使用
- 用
umi openapi
生成接口方法 - TableList 页面配置
- 修改引入的方法
- 修改页面元素类型
- 配置表格列属性
- 更改列表元素类型 改第一个就行
- 更改 handteAdd 方法的参数类型
- 更改 handleUpdate 方法的参数类型
- 配置 <Draw> 中的 <ProDescriptions>
- 修改页面文本
Ant Design Pro 解决方案整理
接口方法生成解决方案
解决方案原理
前提条件是建立规范
处理流程:
接口文件即中间代码,接口方法即最终代码
- 静态分析:
umi openapi
会使用 AST(抽象语法树)去分析项目中的接口文件,提取接口信息,包括请求 URL、请求方式、参数等。 - 代码生成:根据接口信息,
umi openapi
会自动生成对应的 API 接口方法的代码,这些方法会被写入到指定的目标文件中,一般是一个单独的文件用于存放所有的 API 接口方法。
解决方案配置
export default defineConfig({
...
openAPI: [
{
requestLibPath: "import { request } from 'umi'",
// 配置接口文档的请求路径
schemaPath:'http://localhost:8080/v3/api-docs',
// 配置生成代码的文件夹名称
projectName: 'stateful-backend',
},
],
...
});
解决方案使用
运行 package.json 中的以下命令
{
...
"scripts": {
...
"openapi": "umi openapi",
...
}
...
}
分页解决方案
解决方案原理
原理不明
解决方案配置
无需额外配置
解决方案使用
<ProTable
...
//数据请求
//使用前端自带的分页机制,此处获取所有数据即可
request={async () => {
const res = await getList();
return {data: res.data, success: res.success, total: res.total}
}}
...
/>
Mock 解决方案
解决方案原理
原理不明
解决方案配置
无需额外配置
解决方案使用
-
mock 模拟:通过编写一个 mock.ts 文件作为前端访问的假数据
- mock 数据生成
- mock 数据获取
- mock 数据处理
-
真实后端接入:通过控制 baseUrl 实现后端切换(真实 or mock),接口请求方法是完全相同的
//真实后端接入 const baseUrl:string = "http://localhost:8080"; //接入 mock const baseUrl:string = "";
/** 获取用户信息列表 GET /api/v1/user/getLists */ export async function users( params: { //参数省略... }, options?: { [key: string]: any },) { return request<API.userList>(baseUrl + '/api/v1/user/getLists', { method: 'GET', params: { ...params, }, ...(options || {}), }); }
出现以下内容,证明没设置 mock (即没给出对应的 ts 文件)又访问 mock
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="theme-color" content="#1890ff" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="keywords"
content="antd,umi,umijs,ant design,Scaffolding, layout, Ant Design, project, Pro, admin, console, homepage, out-of-the-box, middle and back office, solution, component library"
/>
<meta
name="description"
content="
An out-of-box UI solution for enterprise applications as a React boilerplate."
/>
<meta
name="description"
content="
Out-of-the-box mid-stage front-end/design solution."
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>Ant Design Pro</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<script>
window.routerBase = "/";
</script>
<script src="/@@/devScripts.js"></script>
<script>
//! umi version: 3.5.41
</script>
<script>
!(function () {
var e =
navigator.cookieEnabled && void 0 !== window.localStorage
? localStorage.getItem("dumi:prefers-color")
: "auto",
o = window.matchMedia("(prefers-color-scheme: dark)").matches,
t = ["light", "dark", "auto"];
document.documentElement.setAttribute(
"data-prefers-color",
e === t[2] ? (o ? t[1] : t[0]) : t.indexOf(e) > -1 ? e : t[0]
);
})();
</script>
</head>
<body>
<noscript>
<div class="noscript-container">
Hi there! Please
<div class="noscript-enableJS">
<a
href="https://www.enablejavascript.io/en"
target="_blank"
rel="noopener noreferrer"
>
<b>enable Javascript</b>
</a>
</div>
in your browser to use Ant Design, Out-of-the-box mid-stage front/design
solution!
</div>
</noscript>
<div id="root">
<style>
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
}
#root {
background-repeat: no-repeat;
background-size: 100% auto;
}
.noscript-container {
display: flex;
align-content: center;
justify-content: center;
margin-top: 90px;
font-size: 20px;
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
}
.noscript-enableJS {
padding-right: 3px;
padding-left: 3px;
}
.page-loading-warp {
display: flex;
align-items: center;
justify-content: center;
padding: 98px;
}
.ant-spin {
position: absolute;
display: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
color: #1890ff;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
text-align: center;
list-style: none;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-webkit-font-feature-settings: "tnum";
font-feature-settings: "tnum";
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
font-size: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antspinmove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antrotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
width: 32px;
height: 32px;
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 420px;
"
>
<img src="/pro_icon.svg" alt="logo" width="256" />
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin"
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
></span>
</div>
</div>
<div
style="display: flex; align-items: center; justify-content: center"
>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="32"
style="margin-right: 8px"
/>
Ant Design
</div>
</div>
</div>
<script src="/umi.js"></script>
</body>
</html>
样例 1: 列表数据获取
//import 部分省略
//mock 数据生成
const genList = (current: number, pageSize: number) => {
const tableListDataSource: API.RuleListItem[] = [];
for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: moment().format('YYYY-MM-DD'),
createdAt: moment().format('YYYY-MM-DD'),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};
//mock 数据获取
let tableListDataSource = genList(1, 100);
//mock 数据处理
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = parse(realUrl, true).query as unknown as API.PageParams &
API.RuleListItem & {
sorter: any;
filter: any;
};
let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
if (params.sorter) {
const sorter = JSON.parse(params.sorter);
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as {
[key: string]: string[];
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.current}`, 10) || 1,
};
return res.json(result);
}
// mock 方法暴露
export default {
'GET /api/rule': getRule,
};
样例 2: 登录接口功能
import {Request, Response} from 'express';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
export default {
//登录
'POST /user/login': async (req: Request, res: Response) => {
const {
userAccount,
userPassword
} = req.body;
await waitTime(200);
if (userPassword === 'ant.design' && userAccount == 'user') {
res.send({
code: 200,
data: {
"createTime": "2022-01-01",
"id": 1,
"isDelete": 0,
"updateTime": "2022-01-02",
"userAccount": "user",
"userPassword": "",
"userRole": "user"
},
description: '',
message: 'ok',
})
return
}
if (userPassword === 'ant.design' && userAccount == 'admin') {
res.send({
code: 200,
data: {
"createTime": "2022-01-01",
"id": 2,
"isDelete": 0,
"updateTime": "2022-01-02",
"userAccount": "admin",
"userPassword": "",
"userRole": "admin"
},
description: '',
message: 'ok',
})
return
}
res.send({
code: 40000,
data: {},
description: '用户不存在或密码错误',
message: '请求参数错误',
})
},
//获取已登录用户信息
'GET /user/get/login': (req: Request, res: Response) => {
res.send({
code: 200,
data: {
"createTime": "2022-01-01",
"id": 1,
"isDelete": 0,
"updateTime": "2022-01-02",
"userAccount": "testUser",
"userPassword": "",
"userRole": "admin"
},
description: '',
message: 'ok',
})
return
},
}
用户态存储 → 页面访问逻辑控制 解决方案
解决方案原理
Ant Design Pro 内置了一套通过 useModel
控制页面初始状态来实现用户态记录的解决方案
通过用户态记录,可以实现对于页面访问逻辑的控制
解决方案配置
接口方法配置
/** getLoginUser GET /user/get/login */
export async function getLoginUserUsingGET(options?: { [key: string]: any }) {
return request<API.BaseResponseUserVO>(local + '/user/get/login', {
//TODO 携带 cookie
credentials:'include',
method: 'GET',
...(options || {}),
});
}
/** userLogin POST /user/login */
export async function userLoginUsingPOST(
body: API.UserLoginRequest,
options?: { [key: string]: any },
) {
return request<API.BaseResponseUser>(local + '/user/login', {
//TODO 携带 cookie
credentials:'include',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
后端拦截器配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 覆盖所有请求
registry.addMapping("/**")
// 允许发送 Cookie
.allowCredentials(true)
// 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
放行页面配置
//import 省略
//放行页面配置(无需登录也能跳转到的页面)
const loginPath = '/user/login';
const register = '/user/register'
const forget = '/user/forget'
const paths = [loginPath, register, forget]
export async function getInitialState(): Promise<{
//页面配置
settings?: Partial<LayoutSettings>;
//当前用户信息
currentUser?: API.CurrentUser;
loading?: boolean;
//获取用户信息
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
//获取用户信息
const fetchUserInfo = async () => {
console.log("app:fetchUserInfo")
try {
// 更改为后端获取用户信息的方法
// 为了复用 Ant Design Pro CurrentUser 的机制,此处要进行数据类型转换
// 将后台获取到的信息包装成 API.CurrentUser 类型
const msg = await getLoginUserUsingGET();
//后端正常响应
if (msg.code === 200) {
const res: API.CurrentUser = {
name: msg.data?.userAccount,
userid: msg.data?.id?.toString(),
access: msg.data?.userRole,
};
//判断通过后端获取到的用户态是否为空,若为空,则返回 undefined
if (res.name === undefined
|| res.userid === undefined
|| res.access === undefined) {
return undefined
}
//若不为空,则返回用户态信息
return res;
} else {
//后端响应异常,直接返回 undefined
return undefined;
}
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果不是直接放行的页面,先尝试获取用户信息
if (!paths.includes(history.location.pathname)) {
//获取用户信息
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
//如果是放行的页面,无需获取用户信息
return {
fetchUserInfo,
settings: defaultSettings,
};
}
解决方案使用
用户态存取
//函数组件(不带参数):登录
const Login: React.FC = () => {
//通过 useModel 处理初始状态
const {initialState, setInitialState} = useModel('@@initialState');
//获取用户信息
//1.尝试从初始状态中获取
//2.初始状态中不存在用户信息,则尝试从接口中获取,并将信息保存到初始状态中
const fetchUserInfo = async () => {
//尝试从初始状态中获取用户信息
const userInfo = initialState?.currentUser;
console.log("login:fetchUserInfo")
console.log(userInfo)
//初始状态中的用户信息不存在,则通过后端接口获取信息
if (!userInfo) {
console.log("login:getLoginUserUsingGET")
const msg = await getLoginUserUsingGET();
console.log(msg)
if (msg.code === 200) {
const res: API.CurrentUser = {
name: msg.data?.userAccount,
userid: msg.data?.id?.toString(),
access: msg.data?.userRole
}
console.log(res)
//设置初始状态,保存获取的用户信息
await setInitialState((s) => ({
...s,
currentUser: res,
}));
//返回用户信息
return userInfo;
}
}
return userInfo;
};
//登录函数,对接自动生成的接口函数
const handleSubmit = async (values: API.loginUserParams) => {
try {
console.log("login:userLoginUsingPOST")
const res = await userLoginUsingPOST({
userAccount: values.username,
userPassword: values.password
});
console.log(res)
//根据返回值判断是否登录成功
if (res.code === 200) {
//登录成功,则提示成功信息
message.success("登录成功");
//获取已登录的用户信息
await fetchUserInfo();
//页面跳转
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
const {redirect} = query as {
redirect: string;
};
history.push(redirect || '/');
return;
} else {
//登录失败,
message.error(res.message + " : " + res.description)
}
} catch (error) {
message.error("内部错误,请联系管理员!");
}
}
//页面元素描述,等效于 React 类组件中的 render()
return (
//页面代码省略
)
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示