Open-Platform

107 阅读8分钟

需求背景

当前用户需要通过账号权限申请开通后,才能访问相关服务。目前的状况是其他团队都需要通过 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来进行相应的显示

截屏2023-01-10 上午10.19.31.png

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: &nbsp;</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发送请求获取最新的数据

截屏2023-01-10 下午3.39.55.png

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>
            ),
          }}