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

清紫悠悠 2024-07-01 14:33:02 阅读 83

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

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

文章目录

@umi 请求相关一个能用的请求配置Service层 TS 类型规范Service层 函数定义@umi 请求代理 Proxy

@umi/max 简易数据流useModel 没有类型提示?useModel 书写规范

ProForm 复杂表单当外部数据发生变化,ProForm不更新?ProForm onFinish中请求错误,提交按钮一直Loading

EditorTable 可编辑表格提交按钮一直Loading?columns 自定义表单、自定义渲染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' },

// 错误处理: 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 COMMON {

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层 函数定义

为了与普通的函数做区别,方法名全部大写使用 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.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';

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.success('活动创建成功!');

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_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);

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 <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?: 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}

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>

</>

);

};

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: '/',

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) => {

// 中间件支持 cors

api.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 (

<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;

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。