前端初学者的Ant Design Pro V6总结(下)

前端初学者的Ant Design Pro V6总结(下)

前端初学者的Ant Design Pro V6总结(上)
前端初学者的Ant Design Pro V6总结(下)

@umi 请求相关

一个能用的请求配置

Antd Pro的默认的请求配置太复杂了,我写了个简单的,能用,有需要可以做进一步拓展。

import { message } from 'antd';
import { history } from '@umijs/max';
import type { RequestOptions } from '@@/plugin-request/request';
import { RequestConfig } from '@@/plugin-request/request';
import { LOGIN_URL } from '@/***mon/constant';

export const httpCodeDispose = async (code: string | number) => {
  if (code.toString().startsWith('4')) {
    message.error({ content: `请求错误` });
    if (code === 401) {
      message.error({ content: `登录已过期,请重新登录` });
      history.replace({ pathname: LOGIN_URL });
    }
    if (code === 403) {
      message.error({ content: `登录已过期,请重新登录` });
      localStorage.removeItem('UserInfo');
      history.replace({ pathname: LOGIN_URL });
    }
  }
  // 500状态码
  if (code.toString().startsWith('5')) {
    message.error({ content: `服务器错误,请稍后再试` });
  }
};

// 运行时配置
export const errorConfig: RequestConfig = {
  // 统一的请求设定
  timeout: 20000,
  headers: { 'X-Requested-With': 'XMLHttpRequest' },

  // 错误处理: umi@3 的错误处理方案。
  errorConfig: {
    /**
     * 错误接收及处理,主要返回状态码非200,Axios错误的情况
     * @param error 错误类型
     * @param opts 请求参数,请求方法
     */
    errorHandler: async (error: any, opts: any) => {
      if (opts?.skipErrorHandler) throw error;
      // 我们的 errorThrower 抛出的错误。
      if (error.response) {
        // Axios 的错误
        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
        if ((error.message as string).includes('timeout')) {
          message.error('请求错误,请检查网络');
        }
        await httpCodeDispose(error.response.status);
      } else if (error.request) {
        // 请求已经成功发起,但没有收到响应
        // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
        // 而在node.js中是 http.ClientRequest 的实例
        // message.error('无服务器相应,请重试');
      } else {
        // 发送请求时出了点问题
        message.error('请求错误,请重试');
      }
    },
  },

  // 请求拦截器
  requestInterceptors: [
    (config: RequestOptions) => {
      // 拦截请求配置,进行个性化处理。
      const userInfo = JSON.parse(localStorage.getItem('UserInfo') ?? '{}');
      const token = userInfo.token ?? '';
      const headers = {
        ...config.headers,
        'Content-Type': 'application/json',
        Whiteverse: token,
        // Authorization: {
        //   key: 'Whiteverse',
        //   value: `Bearer ${token}`
        // },
      };
      return { ...config, headers };
    },
  ],
  /**
   * 响应拦截器,主要处理服务器返回200,但是实际请求异常的问题
   */
  responseInterceptors: [
    (response: any) => response,
    (error: any) => {
      const code = error.data.code;
      if (!code.toString().startsWith('2')) {
        httpCodeDispose(code);
        return Promise.reject(error);
      }
      return error;
    },
  ],
};

Service层 TS 类型规范

目前团队采用 [name].d.ts 的方式定义公用类型

- src > - types > 
	service.d.ts
	env.d.ts
	module.d.ts

服务层命名 nameplace 要求全部大写

type SortOrder = 'descend' | 'ascend' | null;

/**
 * 通用API
 */
declare namespace API {
  type Response<T> = {
    message: string;
    code: number;
    data: T;
  };

  type QuerySort<T = any> = Record<string | keyof T, SortOrder>;
}

declare namespace ***MON {
  interface Select {
    value: string;
    label: string;
  }
}

/**
 * 分页相关
 */
declare namespace PAGINATE {
  type Data<T> = { total: number; data: T };
  type Query = { current?: number; pageSize?: number };
}

/**
 * 用户服务相关
 */
declare namespace USER {
  /**
   * 用户
   */
  interface User {
    id: string;
    /**
     * 头像
     */
    avatar: string;
    /**
     * 昵称
     */
    nickname: string;
  }

  /**
   * 用户基本信息
   */
  type UserInfo = Omit<User, 'roleIds' | 'updatedAt'>;

  type UsersQuery = PAGINATE.Query & {
    sort?: API.QuerySort;
    nickname?: string;
    mobile?: string;
    roleId?: string;
  };

  /**
   * 创建用户
   */
  type Create = Omit<User, 'id'>;

  /**
   * 登录信息
   */
  interface Login {
    Mobile: string;
    VerificationCode: string;
  }

  /**
   * 管理员登录参数
   */
  interface ALoginParam {
    Mobile: string;
    VerificationCode: string;
  }

  /**
   * 验证码
   */
  interface Captcha {
    base64: string;
    id: string;
  }
}

Service层 函数定义

  1. 为了与普通的函数做区别,方法名全部大写
  2. 使用 PREFIX_URL 请求前缀,方便后期维护

src -> services -> activity -> index.ts

export async function GetActivityList(
  body: ACTIVITY.ActivitiesQuery,
  options?: { [key: string]: any },
) {
  return request<API.Response<PAGINATE.Data<ACTIVITY.Activity[]>>>(`${PREFIX_URL}/activity/list`, {
    method: 'POST',
    data: body,
    ...(options || {}),
  });
}

@umi 请求代理 Proxy

在开发阶段,如果后端服务的端口经常发生变化,可以使用umi 请求代理 替换原有的请求前缀,转发请求。

/**
 * @name 代理的配置
 * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
 * -------------------------------
 * The agent cannot take effect in the production environment
 * so there is no configuration of the production environment
 * For details, please see
 * https://pro.ant.design/docs/deploy
 *
 * @doc https://umijs.org/docs/guides/proxy
 */
export default {
  // 如果需要自定义本地开发服务器  请取消注释按需调整
  dev: {
    '/api-mock/': {
      // 要代理的地址
      target: 'http://127.0.0.1:4523/m1/3280694-0-default',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-mock': '' },
    },
    '/api-sys/': {
      // 要代理的地址
      target: 'http://192.168.50.131:8021',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-sys': '' },
    },
    '/api-user/': {
      // 要代理的地址
      target: 'http://192.168.50.131:8020',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
      pathRewrite: { '^/api-user': '' },
    },
  },

  /**
   * @name 详细的代理配置
   * @doc https://github.***/chimurai/http-proxy-middleware
   */
  test: {
    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
    '/api/': {
      target: 'https://proapi.azurewebsites.***',
      changeOrigin: true,
      pathRewrite: { '^': '' },
    },
  },
  pre: {
    '/api/': {
      target: 'your pre url',
      changeOrigin: true,
      pathRewrite: { '^': '' },
    },
  },
};

@umi/max 简易数据流

useModel 没有类型提示?

还原 tsconfig.json 为默认配置

{
  "extends": "./src/.umi/tsconfig.json"
}

useModel 书写规范

定义Model仓库时,推荐使用匿名默认导出语法

export default () => {}

如果为页面绑定Model,注意页面的层级不要过深,页面组件的名称尽量短

  • 文件名定义
- pages
	- Activity
		- ***ponents
			- ActivityList.tsx
		- models
			- ActivityModels.ts
  • 使用Model
const { getActivityData } = useModel('Activity.ActivityModels', (models) => ({
	getActivityData: models.getActivityData,
}));

带有分页查询的 Model

带有loading,query,分页

可使用Ahooks 的 useRequest 或 自定封装 useRequest

注意Ahooks的 usePagination函数 对Service层的参数有要求

  • service 的第一个参数为 { current: number, pageSize: number }
  • service 返回的数据结构为 { total: number, list: Item[] }
  • 具体看Ahooks文档,不推荐使用或二封分页Hook.
import { useEffect, useState } from 'react';
import { useSetState } from 'ahooks';
import to from 'await-to-js';
import { GetActivityList } from '@/services/activity';

export default () => {
  const initialParam = { current: 1, pageSize: 20 };
  const [query, queryChange] = useSetState<ACTIVITY.ActivitiesQuery>(initialParam);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<Error | null>();

  const [activityData, setActivityData] = useState<ACTIVITY.Activity[]>();
  const [total, setTotal] = useState<number>(0);
  const getActivityData = async (_param: ACTIVITY.ActivitiesQuery) => {
    // 请求前
    if (loading) await Promise.reject();

    // 请求中
    setLoading(true);
    const [err, res] = await to(GetActivityList(_param));
    setLoading(false);

    // 请求结束
    if (!err && res.code === 200) {
      setActivityData(res.data.data);
      setTotal(res.data.total);
      return res.data;
    } else {
      setError(err);
      return await Promise.reject();
    }
  };

  useEffect(() => {
    if (!activityData) getActivityData(query);
  }, []);

  return {
    // 状态
    loading,
    setLoading,
    error,
    setError,
    query,
    queryChange,
    total,
    setTotal,
    activityData,
    setActivityData,
    // 方法
    getActivityData,
  };
};

ProForm 复杂表单

当外部数据发生变化,ProForm不更新?

解决方案一:

// 监测外部值的变化,更新表单内的数据
useEffect(() => formRef.current && formRef.current.setFieldsValue(selectedNode), [selectedNode]);

解决方案二:

<ProForm<SysRole.Role>
	request={async (params) => {
		formRef.current?.resetFields();
		const res = await GetRole({id: params.id});
		return res.data
	}}
>
// ...	
</ProForm>

ProForm onFinish中请求错误,提交按钮一直Loading

onFinish 方法需要返回一个Promise.resolve(boolean),reject时,会一直loading

一个综合案例

const handleAddActivity = async (fields: ACTIVITY.Create) => {
	const hide = message.loading('正在创建活动');
	try {
		const response = await CreateActivity({ ...fields });
		hide();
		message.su***ess('活动创建成功!');
		return response;
	} catch (error) {
		hide();
		message.error('添加失败,请重试!');
		return Promise.reject(false);
	}
};

<StepsForm.StepForm<ACTIVITY.Create>
  title={"创建活动"}
  stepProps={{
    description: "请输入活动信息",
  }}
  onFinish={async (formData: ACTIVITY.Create & { ActivityTime?: string[] }) => {
    try {
      const requestBody = { ...formData };
      requestBody.StartTime = formData.ActivityTime![0];
      requestBody.EndTime = formData.ActivityTime![1]!;
      delete requestBody["ActivityTime"];
      const response = await handleAddActivity(requestBody);
      const ActivityId = response.data;
      uploadFormsRef.current?.setFieldValue("ActivityId", ActivityId);
      return Promise.resolve(true);
    } catch (e) {
      return Promise.resolve(true);
    }
  }}
/>

更加优雅的办法是给onFinish 提交的数据添加一个convertValues

const convertValues = useMemo((values: FormColumn) => {
    return { ...values };
}, []);

注意:

ProForm中的transform和convertValue属性,仅能操作本字段内容,这个特性在某种情况下会出现一些问题

例如:

<ProFormDateTimeRangePicker
    name="ActivityTime"
    label="投放时间"
    width={'lg'}
    rules={[{required: true, message: '请选择活动投放时间!'}]}
    dataFormat={FORMAT_DATE_TIME_***}
/>

时间范围组件返回的数据格式是

ActivityTime: string[] // 如果不给dataFormat,就是 Dayjs[]

如果后端接口的数据格式是

{startTime: string, endTime: string}

这个时候如果使用convertValue无法解决业务问题,需要在onFinish或onSubmit中进行数据转化。

EditorTable 可编辑表格

提交按钮一直Loading?

如果onSave时网络请求错误或者发生异常,返回Promise.reject,onSave就不会生效。

if (!activityIdField) {
	const errorContent = '请先创建活动';
	message.error(errorContent);
	return Promise.reject(errorContent);
}

return handleSaveRow(record);

columns 自定义表单、自定义渲染

 const columns: ProColumns<DataSourceType>[] = [
    {
        title: '模型文件',
        dataIndex: '_File',
        width: 150,
        render: (_, entity) => {
          return (
            <Button
              type={'link'}
              onClick={() => {
                downloadFile(entity._File!.originFileObj!);
              }}
            >
              {entity._File?.name}
            </Button>
          );
        },
        formItemProps: {
          valuePropName: 'file',
          trigger: 'fileChange',
          rules: [{ required: true, message: '此项是必填项.' }],
        },
        renderFormItem: () => <ModelUploadButton />,
    }   
]

formItemProps 它本质就是<Form.Item>,基本照着Form.Item那边去配置就行。

form / formRef 的 setFieldValue / getFieldsValue 无效?

原因一:

由于EditorTable的 Form实际上是新增的一行,是动态的,formRef 更新不及时可能导致formRef.current 为 undefined。

原因二:

普通的form组件内部的数据模型形如这样:

{
    "homePath": "/",
    "status": true,
    "sort": 1
}

但是editorForm在编辑时内部的数据模型是这样的:

{
    "229121": {
        "ModelLoadName": "11",
        "ModelShowName": "222",
        "ModelNo": "333",
        "MobileOS": "android",
        "_Position": [
            {
                "position": [
                    123.42932734052755,
                    41.79745486673118
                ]
            }
        ],
    }
}

它在外面包了一层,因此设置列的时候需要这么写

renderFormItem: (schema, config, form, action) => {
    const fieldsValue = form.getFieldsValue()
    const key = Object.keys(fieldsValue)[0];
    const fields = fieldsValue[key];
    const fieldName = schema.dataIndex! as keyof typeof fields // you want setting field
    fields[fieldName] = 'you want setting value';
    formRef?.current?.setFieldValue(key, fields);
    return <***ponent />
},

Upload / ProUploader 文件上传

ImgCrop 实现图片裁切

实现功能:

  • 文件格式限制
  • 文件上传尺寸限制
  • 文件缩放大小限制

工具函数

function getImageFileAsync(file: File): Promise<{
  width: number;
  height: number;
  aspectRatio: number;
  image: HTMLImageElement;
}> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    const img = new Image();

    reader.onload = () => {
      img.src = reader.result as string;
    };

    img.onload = () => {
      const width = img.width;
      const height = img.height;
      const aspectRatio = width / height;
      resolve({
        width,
        height,
        aspectRatio,
        image: img,
      });
    };

    img.onerror = () => {
      reject(new Error('图片加载失败'));
    };

    reader.onerror = () => {
      reject(new Error('文件读取错误'));
    };
    // 读取文件内容
    reader.readAsDataURL(file);
  });
}

组件

import { FC, ReactNode, useRef, useState } from 'react';
import { message, Modal, Upload, UploadFile, UploadProps } from 'antd';
import ImgCrop, { ImgCropProps } from 'antd-img-crop';
import { RcFile } from 'antd/es/upload';
import { getBase64, getImageFileAsync } from '@/utils/***mon';

const fileTypes = ['image/jpg', 'image/jpeg', 'image/png'];

interface PictureUploadProps {
  // 上传最大数量
  maxCount?: number;
  // 文件更新
  filesChange?: (files: UploadFile[]) => void;
  // 图片最小大小,宽,高
  minImageSize?: number[];
  // 图片裁切组件配置
  imgCropProps?: Omit<ImgCropProps, 'children'>;
  // 上传提示内容文本
  children?: ReactNode | ReactNode[];
}

const PictureUpload: FC<PictureUploadProps> = ({
  maxCount,
  filesChange,
  minImageSize,
  imgCropProps,
  children,
}) => {
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState('');
  const [previewTitle, setPreviewTitle] = useState('');
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const [maxZoom, setMaxZoom] = useState(2);
  const isCropRef = useRef<boolean>(false);

  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    setFileList(newFileList);
    if (filesChange) filesChange(fileList);
  };

  const handleCancel = () => setPreviewOpen(false);

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as RcFile);
    }

    setPreviewImage(file.url || (file.preview as string));
    setPreviewOpen(true);
    setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1));
  };

  return (
    <>
      <ImgCrop
        quality={1}
        zoomSlider={true}
        minZoom={1}
        maxZoom={maxZoom}
        aspect={minImageSize && minImageSize[0] / minImageSize[1]}
        beforeCrop={async (file) => {
          isCropRef.current = false;
          // 判断文件类型
          const typeMatch = fileTypes.some((type) => type === file.type);
          if (!typeMatch) {
            await message.error(
              '图片格式仅支持' +
                fileTypes.reduce(
                  (prev, cur, index, array) => prev + cur + (index === array.length - 1 ? '' : ','),
                  '',
                ),
            );
            return false;
          }
          // 判断图片大小限制
          if (minImageSize) {
            const { width: imageWidth, height: imageHeight } = await getImageFileAsync(file);
            if (imageWidth < minImageSize[0]) {
              await message.error(
                `当前图片宽度为${imageWidth}像素,请上传不小于${minImageSize[0]}像素的图片.`,
              );
              return false;
            }
            if (imageHeight < minImageSize[1]) {
              await message.error(
                `当前图片高度为${imageHeight}像素,请上传不小于${minImageSize[1]}像素的图片.`,
              );
              return false;
            }
            // 计算最大缩放比例
            const widthMaxZoom = Number((imageWidth / minImageSize[0]).toFixed(1));
            const heightMaxZoom = Number((imageHeight / minImageSize[1]).toFixed(1));
            setMaxZoom(Math.min(widthMaxZoom, heightMaxZoom));
          }
          isCropRef.current = true;
          return true;
        }}
        {...imgCropProps}
      >
        <Upload
          action="/"
          listType="picture-card"
          fileList={fileList}
          onPreview={handlePreview}
          onChange={(files) => {
            handleChange(files);
            console.log(files);
          }}
          maxCount={maxCount}
          a***ept={'.jpg, .jpeg, .png'}
          beforeUpload={async (file) => {
            if (!isCropRef.current) return Upload.LIST_IGNORE;
            return file;
          }}
        >
          {maxCount ? fileList.length < maxCount && children : children}
        </Upload>
      </ImgCrop>
      <Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handleCancel}>
        <img alt="example" style={{ width: '100%' }} src={previewImage} />
      </Modal>
    </>
  );
};

export default PictureUpload;

ImgCrop 组件注意事项

  • 拦截裁切事件

    • ImgCrop 组件 的 beforeCrop 返回 false 后不再弹出模态框,但是文件会继续走 Upload 的 beforeUpload 流程,如果想要拦截上传事件,需要在beforeUpload 中返回 Upload.LIST_IGNORE
    • 判断是否拦截的状态变量需要用 useRef ,useState测试无效。
  • Upload组件 配合 ImgCrop组件时,一定要在 beforeUpload 中返回 事件回调中的 file,否则裁切无效。

  • 如果不想做像素压缩,设置quality={1}

StepsForm 分布表单

如何在 StepsForm 中 更新子表单?

通过StepsForm的 formMapRef 属性,它可以拿到子StepForm的全部ref。

const stepFormMapRef = useRef<Array<MutableRefObject<ProFormInstance>>>([]);
return <StepsForm formMapRef={stepFormMapRef} />

打印 ref.current

[
    {
        "current": {
            // getFieldError: f(name)
        }
    },
    {
        "current": {
            // getFieldError: f(name)
        }
    },
    {
        "current": {
            // getFieldError: f(name)
        }
    }
]

如何手动控制 步骤 前进、后退?

灵活使用 current、onCurrentChange、submitter属性

const [currentStep, setCurrentStep] = useState<number>(0);

return (
	<StepsForm 
		current={currentStep}
		onCurrentChange={setCurrentStep}
		submitter={{
          render: (props) => {
            switch (props.step) {
              case 0: {
                return (
                  <Button type="primary" onClick={() => props.onSubmit?.()}>
                    下一步
                  </Button>
                );
              }
              case 1: {
                return (
                  <Button type="primary" onClick={() => props.onSubmit?.()}>
                    下一步
                  </Button>
                );
              }
              case 2: {
                return (
                  <Button
                    type="primary"
                    onClick={() => {
                      setCurrentStep(0);
                      onCancel();
                    }}
                  >
                    完成
                  </Button>
                );
              }
            }
          },
        }}
        stepsProps={{ direction: 'horizontal', style: { padding: '0 50px' } }}
	>
		{ // StepForm }
	</StepsForm>
)

微前端 Qiankun

文档:https://umijs.org/docs/max/micro-frontend

子应用配置(@umi)

一、使用umi创建React App

二、配置umi

这里有一些WASM的配置,不想要可以去掉

import { defineConfig } from 'umi';

export default defineConfig({
  title: 'xxxxxx',
  routes: [
    {
      path: '/',
      ***ponent: 'index',
    },
    { path: '/scene-obj', ***ponent: 'OBJScene' },
    { path: '/*', redirect: '/' },
  ],
  npmClient: 'pnpm',
  proxy: {
    '/api': {
      target: 'http://jsonplaceholder.typicode.***/',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  },
  plugins: [
    '@umijs/plugins/dist/model',
    '@umijs/plugins/dist/qiankun',
    '@umijs/plugins/dist/request',
  ],
  model: {},
  qiankun: {
    slave: {},
  },
  request: {
    dataField: 'data',
  },
  mfsu: {
    mfName: 'umiR3f', // 默认的会冲突,所以需要随便取个名字避免冲突
  },
  chainWebpack(config) {
    config.set('experiments', {
      ...config.get('experiments'),
      asyncWebAssembly: true,
    });

    const REG = /\.wasm$/;

    config.module.rule('asset').exclude.add(REG).end();

    config.module
      .rule('wasm')
      .test(REG)
      .exclude.add(/node_modules/)
      .end()
      .type('webassembly/async')
      .end();
  },
});

三、跨域配置

import type { IApi } from 'umi';

export default (api: IApi) => {
  // 中间件支持 cors
  api.addMiddlewares(() => {
    return function cors(req, res, next) {
      res.setHeader('A***ess-Control-Allow-Origin', '*');
      res.setHeader('A***ess-Control-Allow-Headers', '*');
      next();
    };
  });
  api.onBeforeMiddleware(({ app }) => {
    app.request.headers['a***ess-control-allow-origin'] = '*';
    app.request.headers['a***ess-control-allow-headers'] = '*';
    app.request.headers['a***ess-control-allow-credentials'] = '*';
    app.request.originalUrl = '*';
  });
};

四、修改app.ts,子应用配置生命周期钩子.

export const qiankun = {
  // 应用加载之前
  async bootstrap(props: any) {
    console.log('app1 bootstrap', props);
  },
  // 应用 render 之前触发
  async mount(props: any) {
    console.log('app1 mount', props);
  },
  // 应用卸载之后触发
  async unmount(props: any) {
    console.log('app1 unmount', props);
  },
};

父应用配置(@umi/max)

config.ts

export default defineConfig({
  qiankun: {
    master: {
      apps: [
        {
          name: 'r3f-viewer', // 子应用的名称
          entry: 'http://localhost:5174', // your microApp address
        },
      ],
    },
  },
})

使用路由的方式引入子应用

export default [
  {
    name: 'slave',
    path: '/slave/*',
    microApp: 'slave',
    microAppProps: {
      autoSetLoading: true,
      autoCaptureError: true,
      className: 'MicroApp',
      wrapperClassName: 'MicroAppWrapper'
    },
  },
]

使用组件的方式引入子应用

index.tsx

import { PageContainer } from '@ant-design/pro-***ponents';
import { memo } from 'react';
import { MicroAppWithMemoHistory } from '@umijs/max';
import './index.less';

const Role = () => {
  return (
    <PageContainer>
      <MicroAppWithMemoHistory
        name="r3f-viewer"
        url="/umi-r3f-view"
        autoSetLoading={true}
        className={'microApp'}
      />
    </PageContainer>
  );
};

export default memo(Role);

index.less

.microApp,
#root {
  min-height: 800px !important;
  height: 800px !important;
  max-height: 800px !important;
  width: 100% !important;
}
转载请说明出处内容投诉
CSS教程_站长资源网 » 前端初学者的Ant Design Pro V6总结(下)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买