0


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

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

文章目录

@umi 请求相关

一个能用的请求配置

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

import{ message }from'antd';import{ history }from'@umijs/max';importtype{ RequestOptions }from'@@/plugin-request/request';import{ RequestConfig }from'@@/plugin-request/request';import{LOGIN_URL}from'@/common/constant';exportconsthttpCodeDispose=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:`服务器错误,请稍后再试`});}};// 运行时配置exportconst 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 asstring).includes('timeout')){
          message.error('请求错误,请检查网络');}awaithttpCodeDispose(error.response.status);}elseif(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);returnPromise.reject(error);}return error;},],};

Service层 TS 类型规范

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

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

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

typeSortOrder='descend'|'ascend'|null;/**
 * 通用API
 */declarenamespaceAPI{typeResponse<T>={
    message:string;
    code:number;
    data:T;};typeQuerySort<T=any>= Record<string|keyofT, SortOrder>;}declarenamespaceCOMMON{interfaceSelect{
    value:string;
    label:string;}}/**
 * 分页相关
 */declarenamespacePAGINATE{typeData<T>={ total:number; data:T};typeQuery={ current?:number; pageSize?:number};}/**
 * 用户服务相关
 */declarenamespaceUSER{/**
   * 用户
   */interfaceUser{
    id:string;/**
     * 头像
     */
    avatar:string;/**
     * 昵称
     */
    nickname:string;}/**
   * 用户基本信息
   */typeUserInfo= Omit<User,'roleIds'|'updatedAt'>;typeUsersQuery=PAGINATE.Query &{
    sort?:API.QuerySort;
    nickname?:string;
    mobile?:string;
    roleId?:string;};/**
   * 创建用户
   */typeCreate= Omit<User,'id'>;/**
   * 登录信息
   */interfaceLogin{
    Mobile:string;
    VerificationCode:string;}/**
   * 管理员登录参数
   */interfaceALoginParam{
    Mobile:string;
    VerificationCode:string;}/**
   * 验证码
   */interfaceCaptcha{
    base64:string;
    id:string;}}

Service层 函数定义

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

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

exportasyncfunctionGetActivityList(
  body:ACTIVITY.ActivitiesQuery,
  options?:{[key:string]:any},){returnrequest<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
 */exportdefault{// 如果需要自定义本地开发服务器  请取消注释按需调整
  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.com/chimurai/http-proxy-middleware
   */
  test:{// localhost:8000/api/** -> https://preview.pro.ant.design/api/**'/api/':{
      target:'https://proapi.azurewebsites.net',
      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
        - components
            - 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';exportdefault()=>{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);constgetActivityData=async(_param:ACTIVITY.ActivitiesQuery)=>{// 请求前if(loading)awaitPromise.reject();// 请求中setLoading(true);const[err, res]=awaitto(GetActivityList(_param));setLoading(false);// 请求结束if(!err && res.code ===200){setActivityData(res.data.data);setTotal(res.data.total);return res.data;}else{setError(err);returnawaitPromise.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

一个综合案例

consthandleAddActivity=async(fields:ACTIVITY.Create)=>{const hide = message.loading('正在创建活动');try{const response =awaitCreateActivity({...fields });hide();
        message.success('活动创建成功!');return response;}catch(error){hide();
        message.error('添加失败,请重试!');returnPromise.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 =awaithandleAddActivity(requestBody);const ActivityId = response.data;
      uploadFormsRef.current?.setFieldValue("ActivityId", ActivityId);returnPromise.resolve(true);}catch(e){returnPromise.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_CN}/>

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

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

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

{startTime:string, endTime:string}

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

EditorTable 可编辑表格

提交按钮一直Loading?

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

if(!activityIdField){const errorContent ='请先创建活动';
    message.error(errorContent);returnPromise.reject(errorContent);}returnhandleSaveRow(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!askeyoftypeof fields // you want setting field
    fields[fieldName]='you want setting value';
    formRef?.current?.setFieldValue(key, fields);return<Component />},

Upload / ProUploader 文件上传

ImgCrop 实现图片裁切

实现功能:

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

工具函数

functiongetImageFileAsync(file: File):Promise<{
  width:number;
  height:number;
  aspectRatio:number;
  image: HTMLImageElement;}>{returnnewPromise((resolve, reject)=>{const reader =newFileReader();const img =newImage();

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

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

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

    reader.onerror=()=>{reject(newError('文件读取错误'));};// 读取文件内容
    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/common';const fileTypes =['image/jpg','image/jpeg','image/png'];interfacePictureUploadProps{// 上传最大数量
  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);};consthandleCancel=()=>setPreviewOpen(false);consthandlePreview=async(file: UploadFile)=>{if(!file.url &&!file.preview){
      file.preview =awaitgetBase64(file.originFileObj as RcFile);}setPreviewImage(file.url ||(file.preview asstring));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?'':','),'',),);returnfalse;}// 判断图片大小限制if(minImageSize){const{ width: imageWidth, height: imageHeight }=awaitgetImageFileAsync(file);if(imageWidth < minImageSize[0]){await message.error(`当前图片宽度为${imageWidth}像素,请上传不小于${minImageSize[0]}像素的图片.`,);returnfalse;}if(imageHeight < minImageSize[1]){await message.error(`当前图片高度为${imageHeight}像素,请上传不小于${minImageSize[1]}像素的图片.`,);returnfalse;}// 计算最大缩放比例const widthMaxZoom =Number((imageWidth / minImageSize[0]).toFixed(1));const heightMaxZoom =Number((imageHeight / minImageSize[1]).toFixed(1));setMaxZoom(Math.min(widthMaxZoom, heightMaxZoom));}
          isCropRef.current =true;returntrue;}}{...imgCropProps}><Upload
          action="/"
          listType="picture-card"
          fileList={fileList}
          onPreview={handlePreview}
          onChange={(files)=>{handleChange(files);console.log(files);}}
          maxCount={maxCount}
          accept={'.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></>);};exportdefault 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){case0:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
                    下一步
                  </Button>);}case1:{return(<Button type="primary" onClick={()=> props.onSubmit?.()}>
                    下一步
                  </Button>);}case2:{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';exportdefaultdefineConfig({
  title:'xxxxxx',
  routes:[{
      path:'/',
      component:'index',},{ path:'/scene-obj', component:'OBJScene'},{ path:'/*', redirect:'/'},],
  npmClient:'pnpm',
  proxy:{'/api':{
      target:'http://jsonplaceholder.typicode.com/',
      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,});constREG=/\.wasm$/;

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

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

三、跨域配置

importtype{ IApi }from'umi';exportdefault(api: IApi)=>{// 中间件支持 cors
  api.addMiddlewares(()=>{returnfunctioncors(req, res, next){
      res.setHeader('Access-Control-Allow-Origin','*');
      res.setHeader('Access-Control-Allow-Headers','*');next();};});
  api.onBeforeMiddleware(({ app })=>{
    app.request.headers['access-control-allow-origin']='*';
    app.request.headers['access-control-allow-headers']='*';
    app.request.headers['access-control-allow-credentials']='*';
    app.request.originalUrl ='*';});};

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

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

父应用配置(@umi/max)

config.ts

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

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

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

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

index.tsx

import{ PageContainer }from'@ant-design/pro-components';import{ memo }from'react';import{ MicroAppWithMemoHistory }from'@umijs/max';import'./index.less';constRole=()=>{return(<PageContainer><MicroAppWithMemoHistory
        name="r3f-viewer"
        url="/umi-r3f-view"
        autoSetLoading={true}
        className={'microApp'}/></PageContainer>);};exportdefaultmemo(Role);

index.less

.microApp,
#root{min-height: 800px !important;height: 800px !important;max-height: 800px !important;width: 100% !important;}

本文转载自: https://blog.csdn.net/qq_36833171/article/details/134790884
版权归原作者 流水吾情 所有, 如有侵权,请联系我们删除。

“前端初学者的Ant Design Pro V6总结(下)”的评论:

还没有评论