一、实现组件继承属性类型
- 实现类型 ComponentType 以及函数createComponent的类型定义(无需实现功能)
- 使得函数createComponent能够创建一个React组件,支持设置三个属性值:props属性,emits事件以及inherit继承组件,具体要求看使用代码;
- 先做的简单一点,组件所有属性都是可选属性(props,emits以及继承的属性都是可选的)。
- 提示:先完整看一遍题目再开始实现功能;
function createComponent<Option extends ComponentOption>(option: Option): { (props: ComponentType<Option>): any } {return {} as any}
// 基于button标签封装的组件,覆盖title属性以及onClick事件类型
const Button = createComponent({
inherit: "button", // 继承button标签所有属性以及事件
props: {
// 基础类型的属性
label: String,
width: Number,
loading: Boolean,
block: [Boolean, Number], // 联合属性类型:block: boolean|number
title: Number, // 覆盖button的属性类型 title:string -> title:number
},
emits: {
'show-change': (len: number) => {}, // 自定义的事件类型
click: (name: string) => {}, // 覆盖button的click事件类型
},
})
console.log(
/*
* 要求:
* 1. 属性类型为 {label?:string, width?:number, loading?: boolean, block?:boolean|number, title?:number}
* 2. 事件类型为:{onShowChange?:(len:number)=>void, onClick?:(name:string)=>void}
* 3. 能够继承button的所有属性以及事件
*/
<Button
label={""}
width={100}
title={111}
onShowChange={len => {
console.log(len.toFixed(0)) // 不允许有隐式的any类型,这里即使没有定义len的类型,len也应该能够自动推断出来为number类型
}}
onClick={e => {
console.log(e.charAt(0))
}}
/>
)
// 基于Button组件封装的组件,覆盖label属性以及show-change,click事件类型
const ProButton = createComponent({
inherit: Button, // 继承Button所有属性以及事件
props: {
// 基础类型数据推断
proLabel: String,
label: [String, Number], // 覆盖Button的label属性类型:label:string -> label:string|number
},
emits: {
'show-change': (el: HTMLButtonElement) => {},// 覆盖的事件类型
click: (el: HTMLButtonElement) => {}, // 覆盖的事件类型
'make-pro': () => {}, // 自定义事件类型
},
})
console.log(
/*
* 要求:
* 1. 属性类型为 {proLabel?:string, label?:string|number}
* 2. 事件类型为:{onShowChange?:(el: HTMLButtonElement)=>void, onClick?:(el: HTMLButtonElement)=>void, onMakePro?:()=>void}
* 3. 继承Button组件所有的属性以及事件
*/
<ProButton
label={111}
onShowChange={e => {
console.log(e.offsetWidth) // 不允许有隐式的any类型,这里即使没有定义len的类型,len也应该能够自动推断出来为number类型
}}
onClick={e => {
console.log(e.offsetWidth)
}}
onMakePro={() => {}}
/>
)
提示,如何得到button标签的属性类型
- 在文件:node_modules/@types/react/index.d.ts 中寻找 JSX.IntrinsicElements
- 比如div标签的属性类型为 JSX.IntrinsicElements[“div”]
const MyDiv = (props: JSX.IntrinsicElements["div"]) => null
console.log(<>
<div contentEditable={true} aria-label="div text"/>
<MyDiv contentEditable={true} aria-label="div text"/>
</>)
实现:
//横杠命名转化为驼峰命名
type CapitalizeString<T> = T extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}` : T;
type CamelCase<T extends string, S extends string = ''> = T extends `${infer L}-${infer R}` ? CamelCase<R, `${S}${CapitalizeString<L>}`> : `${S}${CapitalizeString<T>}`
interface SimpleConstruct { new(): any }; //表示class类型约束。例如:String Number ...
//如果 T 是函数,返回函数的返回值;如果是类,返回类的实例本身;否则直接返回 T。此处是为了解决StringConstructor的问题
type InferInstance<T> = T extends () => infer R ? R : (T extends new (...args: any[]) => infer R ? R : T);
// type _string = StringConstructor extends new (...args: any[]) => infer R ? R: never; //String
type ComponentOption = {
inherit?: keyof JSX.IntrinsicElements | ((props: any) => any);
props?: Record<string, SimpleConstruct | SimpleConstruct[]>;
emits?: Record<string, (...args: any[]) => any>;
}
//inherit可能继承button等原生标签,也可能是自定义封装的组件Button
type ExtractInheritType<T> = T extends (props: infer R) => any ? R : (T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : {});
type ExtractPropType<T> = { [k in keyof T]?: T[k] extends any[] ? InferInstance<T[k][number]> : InferInstance<T[k]> }; //将类型数组转成联合类型
//将绑定的事件处理成标准格式
type ExtractEmitType<T> = {
[k in keyof T as `on${k extends string ? CamelCase<k> : ''}`]?: T[k] extends (...args: infer A) => any ? (...args: A) => void : T[k]
};
type MergeTypes<Inherit, Props, Emits> = Props & Emits & Omit<Inherit, keyof Props | keyof Emits>; //合并
type ComponentType<Option> = Option extends { inherit?: infer Inherit, props?: infer Props, emits?: infer Emits } ?
MergeTypes<ExtractInheritType<Inherit>, ExtractPropType<Props>, ExtractEmitType<Emits>> : never;
function createComponent<Option extends ComponentOption>(option: Option): { (props: ComponentType<Option>): any } { return {} as any };
二、(React)实现Hook函数useAsyncMethods
- React同学专属题目,Vue同学请看第三题;
- 使用hook函数以及hook组件实现,如果可以的话尽量不要使用class组件;
- useAsyncMethods函数是一个hook函数,接收两个参数:(methods:Record
,alone?:boolean) - SimpleMethod类型
interface SimpleMethod {(...args: any[]): any}
- 返回值类型为一个对象,这个对象的类型与参数methods一致,不过会多出来一个属性 loading;loading是一个对象,对象的key类型为methods中的key,除此之外还多了一个key,叫做global,loading对象所有属性值类型都是布尔值;这些属性的作用如下所示:
- 比如
const methods = useAsyncMethods({fun1:(val:string)=>{},fun2:(val:number)=>{}})
methods.fun1
与定义的时候的类型一致,只不过返回值一定是Promise的包装类型,不管原始的fun1是否为异步函数;methods.fun2
也是一样,与定义的时候的类型一致;methods.loading.fun1
可以用来判断 fun1是否执行完毕,同理methods.loading.fun2
也是;methods.loading.global
任意一个函数没有执行完,这个值就是true,所有函数执行完毕之后,这个值就是false;
- 比如
- 当
methods.fun1
没有执行完毕时,再次调用该函数无效,也就是在没有结束之前不会执行定义的时候的fun1函数; - 当设置了alone参数为true的时候,只有当所有函数执行完毕之后才能执行下一个函数;也就是说,alone为false的时候,函数执行只跟自己是互斥的,fun1执行完之后才能再次执行fun1; 与fun2无关;当设置了alone为true的时候,所有函数都是互斥的,fun1执行完之后才能执行fun1,fun2;
示例效果页面
- http://martsforever-demo.gitee.io/template-plain-react-micro-base/
- 子应用 -> React子应用 -> 测试 createAsyncMethods 按钮
- 目前有四个按钮,每个按钮对应一个异步函数执行;
- 每个异步函数都会有一个state,是个数字类型的count,异步函数执行完之后count会加一;
用来测试的示例代码
import React, {useState} from "react";
import {randomDelay, useAsyncMethods} from "@/pages/message/useAsyncMethods";
import {Button, Spin} from "antd";
const Demo1 = () => {
const [method1, setMethod1] = useState(0)
const [method2, setMethod2] = useState(0)
const [method3, setMethod3] = useState(0)
const [togetherMethod2and3, setTogetherMethod2and3] = useState(0)
const methods = useAsyncMethods({
method1: async (id: string) => {
console.log('任务一开始')
await randomDelay(1000, 3000)
console.log('任务一结束')
setMethod1(val => val + 1)
},
method2: async (start: number, end: number) => {
console.log('任务二开始')
await randomDelay(1000, 2000)
console.log('任务二结束')
setMethod2(val => val + 1)
return start + end
},
method3: async (result: any) => {
console.log('任务三开始', {result})
await randomDelay(2000, 3000)
console.log('任务三结束')
setMethod3(val => val + 1)
},
togetherMethod2and3: async () => {
console.log('任务四开始')
// const ret = await methods.method2() // 错误,缺少必须参数start以及end
const ret = await methods.method2(2, 3)
// await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
await methods.method3(ret.toFixed(2))
console.log('任务四结束')
setTogetherMethod2and3(val => val + 1)
},
})
return <>
<div style={{backgroundColor: 'white', padding: '20px'}}>
<h1>测试createAsyncMethods</h1>
<h3>允许多个不同的异步同时执行,但是同一个异步函数不能同时执行多个,必须在函数执行完毕之后,才能开始再次执行该异步函数</h3>
<Button.Group>
<Button onClick={() => methods.method1('__')}>
<span>一号异步任务({method1})</span>
{!!methods.loading.method1 && <Spin/>}
</Button>
<Button onClick={() => methods.method2(0, 1)}>
<span>二号异步任务({method2})</span>
{!!methods.loading.method2 && <Spin/>}
</Button>
<Button onClick={() => methods.method3('?')}>
<span>三号异步任务({method3})</span>
{!!methods.loading.method3 && <Spin/>}
</Button>
<Button onClick={() => methods.togetherMethod2and3()}>
<span>四号异步任务({togetherMethod2and3})</span>
{!!methods.loading.togetherMethod2and3 && <Spin/>}
</Button>
</Button.Group>
</div>
</>
}
const Demo2 = () => {
const [method1, setMethod1] = useState(0)
const [method2, setMethod2] = useState(0)
const [method3, setMethod3] = useState(0)
const [togetherMethod2and3, setTogetherMethod2and3] = useState(0)
const methods = useAsyncMethods((() => {
const m = {
method1: async (id: string) => {
console.log('任务一开始')
await randomDelay(1000, 3000)
console.log('任务一结束')
setMethod1(val => val + 1)
},
method2: async (start: number, end: number) => {
console.log('任务二开始')
await randomDelay(1000, 2000)
console.log('任务二结束')
setMethod2(val => val + 1)
return start + end
},
method3: async (result: any) => {
console.log('任务三开始', {result})
await randomDelay(2000, 3000)
console.log('任务三结束')
setMethod3(val => val + 1)
},
togetherMethod2and3: async () => {
console.log('任务四开始')
// const ret = await methods.method2() // 错误,缺少必须参数start以及end
const ret = await m.method2(2, 3)
// await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
await m.method3(ret.toFixed(2))
console.log('任务四结束')
setTogetherMethod2and3(val => val + 1)
},
}
return m
})(), true)
return <>
<div style={{backgroundColor: 'white', padding: '20px'}}>
<h3>无论是否为同一个异步函数,同一时刻仅能够有一个异步函数在执行</h3>
<Button.Group>
<Button onClick={() => methods.method1('__')}>
<span>一号异步任务({method1})</span>
{!!methods.loading.method1 && <Spin/>}
</Button>
<Button onClick={() => methods.method2(0, 1)}>
<span>二号异步任务({method2})</span>
{!!methods.loading.method2 && <Spin/>}
</Button>
<Button onClick={() => methods.method3('?')}>
<span>三号异步任务({method3})</span>
{!!methods.loading.method3 && <Spin/>}
</Button>
<Button onClick={() => methods.togetherMethod2and3()}>
<span>四号异步任务({togetherMethod2and3})</span>
{!!methods.loading.togetherMethod2and3 && <Spin/>}
</Button>
</Button.Group>
</div>
</>
}
export default () => {
return <>
<Demo1/>
<Demo2/>
</>
}
问题
Demo2中的useAsyncMethods为什么要这样创建;
Demo2设置了全局一次只能有一个异步任务执行,为了避免在执行任务四时,其它异步任务无法调用的情况。所以需要调用原生的方法。
实现:
import React, { useState } from "react";
//randomDelay的实现
const delay = (t = 0) => new Promise(resolve => setTimeout(resolve, t));
export const randomDelay = async (start: number, end: number) => await delay(Math.random() * (end - start) + start);
interface SimpleMethod { (...args: any[]): any };
const getAsyncState = <T>(setter: (getter: (val: T) => any) => any): Promise<T> => {
return new Promise<T>(resolve => setter(val => {
resolve(val);
return val;
}))
}
export function useAsyncMethods<Methods extends Record<string, SimpleMethod>>(methods: Methods, alone?: boolean) {
const methodNames = Object.keys(methods) as (keyof Methods)[];
const newMethods = {} as Record<keyof Methods, SimpleMethod>;
const loadingInitialState = {} as Record<keyof Methods, boolean>;
methodNames.forEach(methodName => {
const method = methods[methodName];
loadingInitialState[methodName] = false;
newMethods[methodName] = async (...args: any[]) => {
const loading = await getAsyncState(setLoading);
if (alone) {
if (loading.global) { return }
} else {
if (loading[methodName]) { return }
}
setLoading(loading => {
return {
...loading,
[methodName]: true,
global: true
}
});
try {
return await method(...args);
} finally {
setLoading(loading => {
return {
...loading,
[methodName]: false,
global: methodNames.findIndex(name => name !== methodName && loading[name]) > -1
}
})
}
}
});
const [loading, setLoading] = useState({
...loadingInitialState,
global: false,
})
return {
...newMethods,
loading
}
}
三、(Vue3.0)实现Composition函数createAsyncMethods
- Vue3.0同学专属题目,React同学请看第二题
- 使用reactive api实现
- createAsyncMethods函数是一个普通函数,接收两个参数:(methods:Record
,alone?:boolean) - SimpleMethod类型
interface SimpleMethod {(...args: any[]): any}
- 返回值类型为一个对象,这个对象的类型与参数methods一致,不过会多出来一个属性 loading;loading是一个对象,对象的key类型为methods中的key,除此之外还多了一个key,叫做global,loading对象所有属性值类型都是布尔值;这些属性的作用如下所示:
- 比如
const methods = createAsyncMethods({fun1:(val:string)=>{},fun2:(val:number)=>{}})
methods.fun1
与定义的时候的类型一致,只不过返回值一定是Promise的包装类型,不管原始的fun1是否为异步函数;methods.fun2
也是一样,与定义的时候的类型一致;methods.loading.fun1
可以用来判断 fun1是否执行完毕,同理methods.loading.fun2
也是;methods.loading.global
任意一个函数没有执行完,这个值就是true,所有函数执行完毕之后,这个值就是false;
- 比如
- 当
methods.fun1
没有执行完毕时,再次调用该函数无效,也就是在没有结束之前不会执行定义的时候的fun1函数; - 当设置了alone参数为true的时候,只有当所有函数执行完毕之后才能执行下一个函数;也就是说,alone为false的时候,函数执行只跟自己是互斥的,fun1执行完之后才能再次执行fun1; 与fun2无关;当设置了alone为true的时候,所有函数都是互斥的,fun1执行完之后才能执行fun1,fun2;
示例效果页面
- http://martsforever-demo.gitee.io/template-plain-react-micro-base
- 子应用 -> Vue子应用 -> 测试 createAsyncMethods 按钮
- 目前有四个按钮,每个按钮对应一个异步函数执行;
- 每个异步函数都会有一个state,是个数字类型的count,异步函数执行完之后count会加一;
用来测试的示例代码
<template>
<div style="background-color: white;padding: 20px 10px">
<h1>测试createAsyncMethods</h1>
<h3>允许多个不同的异步同时执行,但是同一个异步函数不能同时执行多个,必须在函数执行完毕之后,才能开始再次执行该异步函数</h3>
<el-button @click="methods.method1">
<span>一号异步任务({{ state.method1 }})</span>
<el-icon class="is-loading" v-if="methods.loading.method1">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods.method2">
<span>二号异步任务({{ state.method2 }})</span>
<el-icon class="is-loading" v-if="methods.loading.method2">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods.method3">
<span>三号异步任务({{ state.method3 }})</span>
<el-icon class="is-loading" v-if="methods.loading.method3">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods.togetherMethod2and3">
<span>四号异步任务({{ state.togetherMethod2and3 }})</span>
<el-icon class="is-loading" v-if="methods.loading.togetherMethod2and3">
<Loading/>
</el-icon>
</el-button>
<h3>无论是否为同一个异步函数,同一时刻仅能够有一个异步函数在执行</h3>
<el-button @click="methods2.method1">
<span>一号异步任务({{ state2.method1 }})</span>
<el-icon class="is-loading" v-if="methods2.loading.method1">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods2.method2">
<span>二号异步任务({{ state2.method2 }})</span>
<el-icon class="is-loading" v-if="methods2.loading.method2">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods2.method3">
<span>三号异步任务({{ state2.method3 }})</span>
<el-icon class="is-loading" v-if="methods2.loading.method3">
<Loading/>
</el-icon>
</el-button>
<el-button @click="methods2.togetherMethod2and3">
<span>四号异步任务({{ state2.togetherMethod2and3 }})</span>
<el-icon class="is-loading" v-if="methods2.loading.togetherMethod2and3">
<Loading/>
</el-icon>
</el-button>
</div>
</template>
<script lang="ts">
import {createAsyncMethods, randomDelay} from "@/pages/message/createAsyncMethods";
import {Loading} from '@element-plus/icons'
import {defineComponent, reactive} from 'vue'
export default defineComponent({
components: {Loading},
setup() {
const state = reactive({
method1: 0,
method2: 0,
method3: 0,
togetherMethod2and3: 0,
})
const methods = createAsyncMethods({
method1: async (id: string) => {
console.log('任务一开始')
await randomDelay(1000, 3000)
console.log('任务一结束')
state.method1++
},
method2: async (start: number, end: number) => {
console.log('任务二开始')
await randomDelay(1000, 2000)
console.log('任务二结束')
state.method2++
return start + end
},
method3: async (result: any) => {
console.log('任务三开始', {result})
await randomDelay(2000, 3000)
console.log('任务三结束')
state.method3++
},
togetherMethod2and3: async () => {
console.log('任务四开始')
// const ret = await methods.method2() // 错误,缺少必须参数start以及end
const ret = await methods.method2(2, 3)
// await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
await methods.method3(ret.toFixed(2))
console.log('任务四结束')
state.togetherMethod2and3++
},
})
const state2 = reactive({
method1: 0,
method2: 0,
method3: 0,
togetherMethod2and3: 0,
})
const methods2 = createAsyncMethods((() => {
const m = {
method1: async (id: string) => {
console.log('任务一开始')
await randomDelay(1000, 3000)
console.log('任务一结束')
state2.method1++
},
method2: async (start: number, end: number) => {
console.log('任务二开始')
await randomDelay(1000, 2000)
console.log('任务二结束')
state2.method2++
return start + end
},
method3: async (result: any) => {
console.log('任务三开始', {result})
await randomDelay(2000, 3000)
console.log('任务三结束')
state2.method3++
},
togetherMethod2and3: async () => {
console.log('任务四开始')
const ret = await m.method2(2, 3)
await m.method3(ret.toFixed(2))
console.log('任务四结束')
state2.togetherMethod2and3++
},
}
return m
})(), true)
return {
state,
methods,
state2,
methods2,
}
},
})
</script>
问题
methods2中的createAsyncMethods为什么要这样创建;
四、(React)实现使用弹框选择数据服务:pick函数
- React同学专属题目,Vue同学请看第五题
- pick函数是一个异步函数,参数是一个对象;返回值的类型,依据参数类型而定;参数类型如下所示
interface iUsePickOption<Data> {
data: Data[],
render: (row: Data, index: number) => any
}
interface iUsePickOptionMultiple<Data> {
data: Data[],
render: (row: Data, index: number) => any
multiple: true
}
- (单选)当参数符合
iUsePickOption
时,返回值为选项data数组中元素的类型; - (多选)当参数符合
iUsePickOptionMultiple
是,返回值类型等同于选项data,也是个数组; - 使用弹框渲染这个data数据,点击弹框取消按钮或者遮罩时,pick异步任务终止(Promise.reject);
- 用户没有选择数据点击弹框确定按钮的时候,如果没有选中任何一条数据,提示选择数据,但是不得关闭弹框;
- 用户选中数据,并且点击确定之后,异步任务执行完毕,返回用户选中数据;
- pick函数还能够接收一个泛型,当传入这个泛型的时候,选项中的data的类型将忽略,返回值以这个泛型类型为主,如下列示例代码中的第三个示例为例;
const pickWithCustomType = await pick<Student>(...)
得到的返回值类型为Student
,如果是多选,则返回值为Student[]
提示:函数pick的类型为一个重载函数
测试代码如下所示:
import {Button, Modal} from "antd"
import {pick} from "@/pages/demo-pick/pick";
import studentJsonData from './student.json'
export default () => {
interface Staff {
name: string,
age: number,
avatar: string,
}
const staffData: Staff[] = [
{
"name": "张三",
"age": 20,
"avatar": "http://abc.com/zhangsan"
},
{
"name": "李四",
"age": 21,
"avatar": "http://abc.com/lisi"
},
{
"name": "王五",
"age": 22,
"avatar": "http://abc.com/wangwu"
}
]
/*---------------------------------------单选-------------------------------------------*/
const pick1 = async () => {
// pickPerson自动推导类型为 Staff
const pickStaff = await pick({
data: staffData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.age, row.avatar].join(',')
})
Modal.info({maskClosable: true, content: [pickStaff.name, pickStaff.age, pickStaff.avatar].join(',')})
}
/*---------------------------------------多选-------------------------------------------*/
const pick2 = async () => {
// pickPersonList自动推导类型为 Staff[]
const pickStaffList = await pick({
data: staffData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.age, row.avatar].join(','),
multiple: true,
})
Modal.info({
maskClosable: true,
content:
pickStaffList.map(staff => [staff.name, staff.age, staff.avatar].join(',')).join('\n')
})
}
/*---------------------------------------多选,手动传递类型-------------------------------------------*/
interface Student {
name: string,
code: string,
grade: number
}
const pick3 = async () => {
const pickWithCustomType = await pick<Student>({
// 无法确定data的类型
data: studentJsonData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.grade, row.code].join(','),
multiple: true,
})
// pickWithCustomType推导类型为 Student[]
Modal.info({
maskClosable: true,
content:
pickWithCustomType.map(student => [student.name, student.grade, student.code].join(',')).join('\n')
})
}
return (
<div style={{backgroundColor: 'white', padding: '20px 10px'}}>
<h1>TestPick</h1>
<Button.Group>
<Button onClick={pick1}>选中单条数据</Button>
<Button onClick={pick2}>选中多条数据</Button>
<Button onClick={pick3}>选中多条数据(手动传递类型)</Button>
</Button.Group>
</div>
)
}
student.json
[
{
"name": "张三",
"grade": 4,
"code": "01001"
},
{
"name": "李四",
"grade": 5,
"code": "01002"
},
{
"name": "王五",
"grade": 6,
"code": "01003"
}
]
实现:
import {Modal} from 'antd'
import {useState} from "react";
type Deferred<T> = {
promise: Promise<T>;
resolve: (value?: T) => void;
reject: (reason?: any) => void;
};
const defer = function <T>() {
let dfd = {} as Deferred<T>;
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd
}
type SimpleObject = Record<string, any>
type NotUnknown<T, K> = unknown extends T ? K : T
interface iUsePickOption<Data> {
data: Data[],
render: (row: Data, index: number) => any
}
interface iUsePickOptionMultiple<Data> extends iUsePickOption<Data> {
multiple: true
}
/*函数类型: 单选*/
export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOption<NotUnknown<T, ListT>>): Promise<NotUnknown<T, ListT>>
/*函数类型: 多选*/
export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOptionMultiple<NotUnknown<T, ListT>>): Promise<NotUnknown<T, ListT>[]>
/*函数实现*/
export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOption<NotUnknown<T, ListT>> | iUsePickOptionMultiple<NotUnknown<T, ListT>>) {
const dfd = defer<NotUnknown<T, ListT> | NotUnknown<T, ListT>[]>()
// 用来在beforeClose的时候判断是否已经选中任何内容
const state = {
checked: null as null | NotUnknown<T, ListT>,
checkedList: [] as NotUnknown<T, ListT>[],
}
const Message = () => {
const [checked, setChecked] = useState(null as null | NotUnknown<T, ListT>)
const [checkedList, setCheckedList] = useState([] as NotUnknown<T, ListT>[])
/*获取元素位置索引*/
const getIndex = (row: any) => checkedList.indexOf(row)
/*元素是否已经选中*/
const isChecked = (row: any) => 'multiple' in option && option.multiple ? getIndex(row) > -1 : checked == row
const onClick = (row: NotUnknown<T, ListT>) => {
if ('multiple' in option && option.multiple) {
if (isChecked(row)) {
checkedList.splice(getIndex(row), 1)
} else {
checkedList.push(row as any)
}
state.checkedList = checkedList
setCheckedList([...checkedList])
} else {
state.checked = row
setChecked(row)
}
}
return (
<ul>
{option.data.map((row, index) => (
<li
style={{
padding: '10px 16px',
backgroundColor: isChecked(row) ? 'aliceblue' : ''
}}
onClick={() => onClick(row as any)}
key={index}
>
{option.render(row as any, index)}
</li>
))}
</ul>
)
}
const modal = Modal.info({
title: '选择',
okText: '确定',
cancelText: '取消',
closable: true,
maskClosable: true,
content: <Message/>,
onCancel: () => dfd.reject('cancel'),
okButtonProps: {
onClick: () => {
if ('multiple' in option && option.multiple) {
if (state.checkedList.length === 0) {
return Modal.info({content: '请至少选中一条数据', maskClosable: true})
} else {
dfd.resolve(state.checkedList)
}
} else {
if (!state.checked) {
return Modal.info({content: '请选中一条数据', maskClosable: true})
} else {
dfd.resolve(state.checked)
}
}
return modal.update({visible: false})
},
},
})
return dfd.promise
}
五、(Vue3.0)实现使用弹框选择数据服务:pick函数
- Vue同学专属题目,React同学请看第四题
- pick函数是一个异步函数,参数是一个对象;返回值的类型,依据参数类型而定;参数类型如下所示
interface iUsePickOption<Data> {
data: Data[],
render: (row: Data, index: number) => any
}
interface iUsePickOptionMultiple<Data> {
data: Data[],
render: (row: Data, index: number) => any
multiple: true
}
- (单选)当参数符合
iUsePickOption
时,返回值为选项data数组中元素的类型; - (多选)当参数符合
iUsePickOptionMultiple
是,返回值类型等同于选项data,也是个数组; - 使用弹框渲染这个data数据,点击弹框取消按钮或者遮罩时,pick异步任务终止(Promise.reject);
- 用户没有选择数据点击弹框确定按钮的时候,如果没有选中任何一条数据,提示选择数据,但是不得关闭弹框;
- 用户选中数据,并且点击确定之后,异步任务执行完毕,返回用户选中数据;
- pick函数还能够接收一个泛型,当传入这个泛型的时候,选项中的data的类型将忽略,返回值以这个泛型类型为主,如下列示例代码中的第三个示例为例;
const pickWithCustomType = await pick<Student>(...)
得到的返回值类型为Student
,如果是多选,则返回值为Student[]
提示:函数pick的类型为一个重载函数
测试代码如下所示:
<template>
<div style="background-color: white;padding: 20px 10px">
<h1>TestPick</h1>
<el-button-group>
<el-button @click="pick1">选中单条数据</el-button>
<el-button @click="pick2">选中多条数据</el-button>
<el-button @click="pick3">选中多条数据(手动传递类型)</el-button>
</el-button-group>
</div>
</template>
<script lang="ts">
import {pick} from "./pick";
import studentJsonData from './student.json'
import {ElMessageBox} from 'element-plus'
export default {
setup() {
interface Staff {
name: string,
age: number,
avatar: string,
}
const staffData: Staff[] = [
{
"name": "张三",
"age": 20,
"avatar": "http://abc.com/zhangsan"
},
{
"name": "李四",
"age": 21,
"avatar": "http://abc.com/lisi"
},
{
"name": "王五",
"age": 22,
"avatar": "http://abc.com/wangwu"
}
]
/*---------------------------------------单选-------------------------------------------*/
const pick1 = async () => {
// pickPerson自动推导类型为 Staff
const pickStaff = await pick({
data: staffData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.age, row.avatar].join(',')
})
ElMessageBox({message: [pickStaff.name, pickStaff.age, pickStaff.avatar].join(',')})
}
/*---------------------------------------多选-------------------------------------------*/
const pick2 = async () => {
// pickPersonList自动推导类型为 Staff[]
const pickStaffList = await pick({
data: staffData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.age, row.avatar].join(','),
multiple: true,
})
ElMessageBox({
message:
pickStaffList.map(staff => [staff.name, staff.age, staff.avatar].join(',')).join('\n')
})
}
/*---------------------------------------多选,手动传递类型-------------------------------------------*/
interface Student {
name: string,
code: string,
grade: number
}
const pick3 = async () => {
const pickWithCustomType = await pick<Student>({
// 无法确定data的类型
data: studentJsonData,
// render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
render: (row) => [row.name, row.grade, row.code].join(','),
multiple: true,
})
// pickWithCustomType推导类型为 Student[]
ElMessageBox({
message:
pickWithCustomType.map(student => [student.name, student.grade, student.code].join(',')).join('\n')
})
}
return {
pick1,
pick2,
pick3,
}
},
}
</script>
student.json
[
{
"name": "张三",
"grade": 4,
"code": "01001"
},
{
"name": "李四",
"grade": 5,
"code": "01002"
},
{
"name": "王五",
"grade": 6,
"code": "01003"
}
]