网站性能策划,长沙网页建站,课程分销平台,ui设计培训多长时间前端初学者的Ant Design Pro V6总结#xff08;下#xff09; 文章目录 前端初学者的Ant Design Pro V6总结#xff08;下#xff09;umi 请求相关一个能用的请求配置Service层 TS 类型规范Service层 函数定义umi 请求代理 Proxy umi/max 简易数据流useModel 没有类型提示下 文章目录 前端初学者的Ant Design Pro V6总结下umi 请求相关一个能用的请求配置Service层 TS 类型规范Service层 函数定义umi 请求代理 Proxy umi/max 简易数据流useModel 没有类型提示useModel 书写规范 ProForm 复杂表单当外部数据发生变化ProForm不更新ProForm onFinish中请求错误提交按钮一直Loading EditorTable 可编辑表格提交按钮一直Loadingcolumns 自定义表单、自定义渲染form / formRef 的 setFieldValue / getFieldsValue 无效 Upload / ProUploader 文件上传ImgCrop 实现图片裁切ImgCrop 组件注意事项 StepsForm 分布表单如何在 StepsForm 中 更新子表单如何手动控制 步骤 前进、后退 微前端 Qiankun子应用配置umi父应用配置umi/max 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 /common/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 },// 错误处理 umi3 的错误处理方案。errorConfig: {/*** 错误接收及处理主要返回状态码非200Axios错误的情况* 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.tsenv.d.tsmodule.d.ts服务层命名 nameplace 要求全部大写
type SortOrder descend | ascend | null;/*** 通用API*/
declare namespace API {type ResponseT {message: string;code: number;data: T;};type QuerySortT any Recordstring | keyof T, SortOrder;
}declare namespace COMMON {interface Select {value: string;label: string;}
}/*** 分页相关*/
declare namespace PAGINATE {type DataT { total: number; data: T };type Query { current?: number; pageSize?: number };
}/*** 用户服务相关*/
declare namespace USER {/*** 用户*/interface User {id: string;/*** 头像*/avatar: string;/*** 昵称*/nickname: string;}/*** 用户基本信息*/type UserInfo OmitUser, roleIds | updatedAt;type UsersQuery PAGINATE.Query {sort?: API.QuerySort;nickname?: string;mobile?: string;roleId?: string;};/*** 创建用户*/type Create OmitUser, id;/*** 登录信息*/interface Login {Mobile: string;VerificationCode: string;}/*** 管理员登录参数*/interface ALoginParam {Mobile: string;VerificationCode: string;}/*** 验证码*/interface Captcha {base64: string;id: string;}
}Service层 函数定义
为了与普通的函数做区别方法名全部大写使用 PREFIX_URL 请求前缀方便后期维护
src - services - activity - index.ts
export async function GetActivityList(body: ACTIVITY.ActivitiesQuery,options?: { [key: string]: any },
) {return requestAPI.ResponsePAGINATE.DataACTIVITY.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 的功能可能需要这个比如 cookiechangeOrigin: true,pathRewrite: { ^/api-mock: },},/api-sys/: {// 要代理的地址target: http://192.168.50.131:8021,// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个比如 cookiechangeOrigin: true,pathRewrite: { ^/api-sys: },},/api-user/: {// 要代理的地址target: http://192.168.50.131:8020,// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个比如 cookiechangeOrigin: 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
带有loadingquery分页
可使用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] useSetStateACTIVITY.ActivitiesQuery(initialParam);const [loading, setLoading] useStateboolean(false);const [error, setError] useStateError | null();const [activityData, setActivityData] useStateACTIVITY.Activity[]();const [total, setTotal] useStatenumber(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]);解决方案二
ProFormSysRole.Rolerequest{async (params) {formRef.current?.resetFields();const res await GetRole({id: params.id});return res.data}}// ...
/ProFormProForm 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.success(活动创建成功);return response;} catch (error) {hide();message.error(添加失败请重试);return Promise.reject(false);}
};StepsForm.StepFormACTIVITY.Createtitle{创建活动}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属性仅能操作本字段内容这个特性在某种情况下会出现一些问题
例如
ProFormDateTimeRangePickernameActivityTimelabel投放时间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.rejectonSave就不会生效。
if (!activityIdField) {const errorContent 请先创建活动;message.error(errorContent);return Promise.reject(errorContent);
}return handleSaveRow(record);columns 自定义表单、自定义渲染 const columns: ProColumnsDataSourceType[] [{title: 模型文件,dataIndex: _File,width: 150,render: (_, entity) {return (Buttontype{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 fieldfields[fieldName] you want setting value;formRef?.current?.setFieldValue(key, fields);return Component /
},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/common;const fileTypes [image/jpg, image/jpeg, image/png];interface PictureUploadProps {// 上传最大数量maxCount?: number;// 文件更新filesChange?: (files: UploadFile[]) void;// 图片最小大小宽,高minImageSize?: number[];// 图片裁切组件配置imgCropProps?: OmitImgCropProps, children;// 上传提示内容文本children?: ReactNode | ReactNode[];
}const PictureUpload: FCPictureUploadProps ({maxCount,filesChange,minImageSize,imgCropProps,children,
}) {const [previewOpen, setPreviewOpen] useState(false);const [previewImage, setPreviewImage] useState();const [previewTitle, setPreviewTitle] useState();const [fileList, setFileList] useStateUploadFile[]([]);const [maxZoom, setMaxZoom] useState(2);const isCropRef useRefboolean(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 (ImgCropquality{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}Uploadaction/listTypepicture-cardfileList{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/ImgCropModal open{previewOpen} title{previewTitle} footer{null} onCancel{handleCancel}img altexample 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 useRefArrayMutableRefObjectProFormInstance([]);
return StepsForm formMapRef{stepFormMapRef} /打印 ref.current
[{current: {// getFieldError: f(name)}},{current: {// getFieldError: f(name)}},{current: {// getFieldError: f(name)}}
]如何手动控制 步骤 前进、后退
灵活使用 current、onCurrentChange、submitter属性
const [currentStep, setCurrentStep] useStatenumber(0);return (StepsForm current{currentStep}onCurrentChange{setCurrentStep}submitter{{render: (props) {switch (props.step) {case 0: {return (Button typeprimary onClick{() props.onSubmit?.()}下一步/Button);}case 1: {return (Button typeprimary onClick{() props.onSubmit?.()}下一步/Button);}case 2: {return (ButtontypeprimaryonClick{() {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: /,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,});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) {// 中间件支持 corsapi.addMiddlewares(() {return function cors(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子应用配置生命周期钩子.
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-components;
import { memo } from react;
import { MicroAppWithMemoHistory } from umijs/max;
import ./index.less;const Role () {return (PageContainerMicroAppWithMemoHistorynamer3f-viewerurl/umi-r3f-viewautoSetLoading{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;
}