需求背景
当前用户需要通过账号权限申请开通后,才能访问相关服务。目前的状况是其他团队都需要通过 sre 以及 self-service 来进行申请,再被 approve,流程较长。
需求目的
用户可以跳过部分人工流程,直接通过平台申请账号和权限,以此访问相关的功能服务。
代码细节
踩坑
将searchParams存储为state
- 在后台系统中,用户可能会在不同的地方,比如子组件modal框中新增一个record,那么便需要重新向服务端发送请求,获取新的数据,所以可以将searchParams作为一个state进行存储,通过useEffect对searchParams状态进行监听,同时将setSearchParams的方法传递给子组件。每当新建record完成后,调用setSearchParams的方法触发searchParams的变化,就重新发送请求。【我觉得找准真正触发状态变化的点很重要】
- 自己以前没有将searchParams作为state进行存储,每次更新都重新调用方法,这样会比较麻烦,并且有时候这个方法中可能存在一些父组件中定义的状态,处理起来比较麻烦。
useContext 进行传递
将新建或者更新的modal封装成一个组件,并在父组件中进行引用。经常存在一种情况,在新建modal中创建了一个新的record后,需要重新调用请求接口获取新的数据,但是新建modal是一个子组件,而searchParams是存在父组件中的状态,如果想在子组件中调用父组件的方法,最简单的是通过props进行传递。因为有多个组件,为了避免麻烦,目前我使用context进行传递。【但是创建一个context,尽量放在一个单独的文件中,然后在组件中进行引入。一开始直接在组件中创建context,有时候会报initial的错误,目前原因还没有找到】
// 创建单独的context文件
import { createContext } from "react";
export const setApprovalContext = createContext<any>(null);
export const setApplicationContext = createContext<any>(null); // todo
// 引入context
import { setApplicationContext } from "./context";
const handleSetSearchParams = (data?: SearchParamsType) => {
setSearchParams({
...searchParams,
...data,
});
};
return (
<setApplicationContext.Provider value={handleSetSearchParams}>
<ApplyAccountModal
visible={isShowApplyModal}
showApplyModal={showApplyModal}
/>
</setApplicationContext.Provider>
);
};
export default MyApplication;
自定义请求hooks
写代码的时候,自己会下意识的看哪些代码的逻辑可以封装,但是越想多,越绕进去了,踩了好久的坑【自定义hooks是封装逻辑方法,但是不能共享数据】
useEffect 监听 state 变化
需求背景:用户进入页面后,首先显示loading状态,等判断用户是否授权后,再展示相应的授权页面或者为授权页面。其实这个需求真的是很简单,我也不知道咋搞的,想复杂了,也是醉了。请求完admin数据后,直接setAuthorized和setGlobalLoading即可,因为setState会批量更新。
useEffect(() => {
getAdministrators().then((res) => {
const admins = res.data.admins;
setAuthorized(
admins.some(
(item: string) => item === idTokenParsed?.preferred_username
)
);
setGlobalLoading(false);
});
}, []);
我一开始想成了这样,只有当authorized发生变化了,才会改变globalLoading的状态,所以写成了下面的形式
useEffect(() => {
setGlobalLoading(false);
}, [authorized]);
但是上述代码有个坑,一开始authorized的值是false,如果对用户信息进行判断后,是未授权的,那么setAuthorized(false),因为authorized是基本类型,判断前后两者都是false,所以不会执行回调函数,就导致未授权用户会一直处于loading状态,不会出现403页面。想到的一个解决办法是,将authorized从布尔类型转换成一个对象类型,类似authorized:{value:false}。但其实真正原因本质是:
- useEffect监听的state有点问题,一般来说,通过useEffect对一个状态进行监听,必然会在回调函数中使用到这个状态,而我并没有使用到,这时候应该转换一下思路,可能这里并不需要useEffect对authorized进行监听
- 无非是setAuthorized后进行setGlobalLoading,那么直接在then中同时执行一下,因为react的批量更新,所以页面真正渲染的时候,authorized和globalLoading都已经是最新的值了。【一开始忘了setState的批量更新,想着setState是异步的,可能authorized还没有变化,globalLoading已经变化了,这样可能会出现授权用户一开始出现403页面,然后才出现授权页面,ORZ】
myApplication
页面基本UI如下图所示,table根据不同的state显示不同的样式,所以需要将相关的列封装成一个组件,根据传进来的state来进行相应的显示
State组件
因为后端返回的application有四种状态,可以使用一个对象进行映射,但是在ts中可以通过枚举进行映射
// 对象映射
const application = {
Applying = "applying",
Pass = "pass",
Refuse = "refuse",
Revoke = "revoke",
}
// 使用枚举
export enum ApplicationState {
Applying = "applying",
Pass = "pass",
Refuse = "refuse",
Revoke = "revoke",
}
不同的状态显示不同的颜色,不同的状态显示不同的显示文字。因此可以将状态与颜色做一个映射对应,将状态与显示文字做映射。整体代码如下:
import { Badge } from "antd";
import { ApplicationState } from "../../pages/openPlatform/types";
export const stateEnum = {
Applying: "applying",
Pass: "pass",
Refuse: "refuse",
Revoke: "revoke",
};
const APPLICATION_STATE_DISPLAY_COLOR = {
[ApplicationState.Applying]: "blue",
[ApplicationState.Pass]: "green",
[ApplicationState.Refuse]: "red",
[ApplicationState.Revoke]: "grey",
};
const APPLICATION_STATE_DISPLAY_NAME = {
[ApplicationState.Applying]: "Applying",
[ApplicationState.Pass]: "Passed",
[ApplicationState.Revoke]: "Revoked",
[ApplicationState.Refuse]: "Refused",
};
interface ApplicationStatusProps {
status: string;
}
export const ApplicationStatus = (props: ApplicationStatusProps) => {
const { status } = props;
return (
<Badge
color={APPLICATION_STATE_DISPLAY_COLOR[status]}
text={APPLICATION_STATE_DISPLAY_NAME[status]}
/>
);
};
Details组件
当申请通过,显示相应的id和key;申请失败,显示原因;申请中,不显示。【逻辑不难,但是我觉得这儿用Partial有点不太合适,一时想不到更好的方法】
interface ShowDetailsProps {
record: Partial<ApplicationDataType>;
}
export const ShowDetails = (props: ShowDetailsProps) => {
const { state, detail, app_id, id } = props.record;
const [appKey, setAppKey] = useState<string>("");
const [open, setOpen] = useState<boolean>(false);
const handleOpenChange = () => {
setOpen(!open);
};
useEffect(() => {
if (open && id) {
getApplicationDetail(id)
.then((res) => {
setAppKey(res.data.application.app_key);
})
.catch((err: any) => {
message.error(err);
});
}
}, [open]);
return (
<>
{state === ApplicationState.Pass && (
<Row align="middle">
<Col span={12}>APPID: {app_id}</Col>
<Col span={8}>
<Popover
content={
<p style={{ display: "flex" }}>
<span>APPKEY: </span>
<Typography.Paragraph copyable>{appKey}</Typography.Paragraph>
</p>
}
trigger="click"
onOpenChange={handleOpenChange}
overlayStyle={{ minWidth: 400 }}
open={open}
>
<Button type="link">More details</Button>
</Popover>
</Col>
</Row>
)}
{state === ApplicationState.Refuse && (
<span>{detail ? detail : "--"}</span>
)}
{(state === ApplicationState.Applying ||
state === ApplicationState.Revoke) && <span>--</span>}
</>
);
};
myApproval
主要是根据不同的状态,显示不同的table。因为有三种不同的显示方式,所以需要用一个state去存储column类型。当点击左上角的tab栏时,根据key值setColumn,同时更改searchParams发送请求获取最新的数据
columns定义
有些不应该用any定义的,我暂时也想不到好的方法
import type { ColumnsType } from "antd/es/table";
import { ApplicationStatus } from "../../components/openPlatform/applicationStatus";
import { ApprovalOperation } from "../../components/openPlatform/approvalOperation";
import { CancelButton } from "../../components/openPlatform/cancelButton";
import { ShowDetails } from "../../components/openPlatform/showDetails";
export interface ApplicationDataType {
app_id: string;
applicant: string;
application_time: string;
approver: string;
detail: string;
id: number;
state: string;
team: string;
}
type ApplicationTableType = Omit<
ApplicationDataType,
"app_id" | "applicant" | "team"
> & {
operation: string;
key: string;
};
export interface ApprovalTableType {
app_id?: string;
applicant: string;
team: string;
application_time: string;
operation?: (value: any, record: any) => JSX.Element;
detail?: string;
approval?: string;
id?: number;
state?: string;
}
export interface ApprovalDataType {
id: number;
applicant: string;
team: string;
state: string;
detail: string;
application_time: string;
approval: string;
app_id: string;
updated_time: string;
}
export const applicationColumns: ColumnsType<ApplicationTableType> = [
{
title: "Application Time",
dataIndex: "application_time",
width: 100,
key: "application_time",
},
{
title: "State",
dataIndex: "state",
key: "state",
width: 80,
render: (value: string) => {
return <ApplicationStatus status={value} />;
},
},
{
title: "Approver",
dataIndex: "approver",
key: "approver",
width: 80,
render: (value: string) => {
return <span>{value ? value : "--"}</span>;
},
},
{
title: "Details",
dataIndex: "detail",
width: 120,
key: "detail",
render: (_: string, record: Partial<ApplicationDataType>) => {
return <ShowDetails record={record} />;
},
},
{
title: "Operation",
key: "operation",
width: 40,
dataIndex: "Operation",
render: (_: string, record: any) => {
return <CancelButton status={record?.state} id={record.id} />;
},
},
];
const commonColumns: ColumnsType<ApprovalTableType> = [
{
title: "Applicant",
dataIndex: "applicant",
width: 120,
key: "applicant",
},
{
title: "Team",
dataIndex: "team",
key: "team",
width: 180,
},
{
title: "Application Time",
dataIndex: "application_time",
key: "application_time",
width: 200,
},
];
export const applyingColumns = [
...commonColumns,
{
title: "Operation",
dataIndex: "operation",
width: 200,
key: "operation",
render: (_: string, record: ApprovalDataType) => {
return <ApprovalOperation record={record} />;
},
},
];
export const passedColumns = [
...commonColumns,
{
title: "Passed Time",
dataIndex: "updated_time",
key: "updated_time",
width: 200,
},
{
title: "Approver",
dataIndex: "approver",
width: 120,
key: "approver",
},
{
title: "Details",
dataIndex: "detail",
width: 200,
key: "detail",
render: (_: string, record: ApprovalDataType) => {
return <span>APPID: {record.app_id}</span>;
},
},
];
export const refusedColumns = [
...commonColumns,
{
title: "Refused Time",
dataIndex: "updated_time",
key: "updated_time",
width: 200,
},
{
title: "Approver",
dataIndex: "approver",
width: 120,
key: "approver",
},
{
title: "Details",
dataIndex: "detail",
width: 200,
key: "detail",
render: (value: string) => {
if (value === "") {
return "--";
} else {
return value;
}
},
},
];
搜索组件
- 以前实习的时候,因为搜索字段比较多,是和后端约定好,给每个字段一个默认值,如果前端传递是默认值,则表明query参数中并没有这个字段。现在通过接口进行测试,只有当用户搜索某个参数,才在query参数中加上这个字段,没有搜索,则不添加。所以,我需要在请求函数中对传进来的请求字段做一个判断。
- 有一个需要注意的地方,antd中的search框在用户没有任何输入的情况下,传递的是undefined值,当用户清除后,传递的是空字符串。
- 这边用到了拷贝,一开始我是直接对传进来的data进行delete操作,虽然对功能实现上没有造成影响,但是当时对一个逻辑小细节纠结了比较久(就是自己觉得不应该少属性,但是它却少了),后来想想要是后期功能复杂了,对data进行溯源,可能都不知道是哪一步发生了错误,还是遵循immutable思想
const getApprovalData = async (data: SearchParamsType) => {
setLoading(true);
setTableData([]);
const searchData = _.cloneDeep(data);
if (searchData.applicant === undefined || searchData.applicant === "") {
delete searchData.applicant;
}
if (searchData.team_name === undefined || searchData.team_name === "") {
delete searchData.team_name;
}
try {
const res = await getApprovals(searchData);
setTableData(res.data.records);
setTotalCount(res.data.total);
setLoading(false);
} catch (err: any) {
message.error(err);
setLoading(false);
}
};
分页组件
当用户手动点击相应的页面,会改变page;当用户新建一个record的时候,应该返回到table的第一页;当用户删除某一个record的时候,应该还是保留在当前页。所以每次改变的时候,需要手动setPage一下,因此需要将page也作为一个state进行存储
pagination={{
defaultCurrent: 1,
current: page,
defaultPageSize: searchParams.page_size,
showQuickJumper: true,
showSizeChanger: false,
total: totalCount,
onChange: (page, pageSize) => {
setPage(page);
setSearchParams({
...searchParams,
page_no: page,
page_size: pageSize,
state: tabKeyStatusMap[currentTabKey],
});
},
showTotal: () => (
<p>
{searchParams.page_size} records per page, {totalCount} records
in total
</p>
),
}}