一、实现组件继承属性类型

  • 实现类型 ComponentType 以及函数createComponent的类型定义(无需实现功能)
  • 使得函数createComponent能够创建一个React组件,支持设置三个属性值:props属性,emits事件以及inherit继承组件,具体要求看使用代码;
  • 先做的简单一点,组件所有属性都是可选属性(props,emits以及继承的属性都是可选的)。
  • 提示:先完整看一遍题目再开始实现功能;
  1. function createComponent<Option extends ComponentOption>(option: Option): { (props: ComponentType<Option>): any } {return {} as any}
  2. // 基于button标签封装的组件,覆盖title属性以及onClick事件类型
  3. const Button = createComponent({
  4. inherit: "button", // 继承button标签所有属性以及事件
  5. props: {
  6. // 基础类型的属性
  7. label: String,
  8. width: Number,
  9. loading: Boolean,
  10. block: [Boolean, Number], // 联合属性类型:block: boolean|number
  11. title: Number, // 覆盖button的属性类型 title:string -> title:number
  12. },
  13. emits: {
  14. 'show-change': (len: number) => {}, // 自定义的事件类型
  15. click: (name: string) => {}, // 覆盖button的click事件类型
  16. },
  17. })
  18. console.log(
  19. /*
  20. * 要求:
  21. * 1. 属性类型为 {label?:string, width?:number, loading?: boolean, block?:boolean|number, title?:number}
  22. * 2. 事件类型为:{onShowChange?:(len:number)=>void, onClick?:(name:string)=>void}
  23. * 3. 能够继承button的所有属性以及事件
  24. */
  25. <Button
  26. label={""}
  27. width={100}
  28. title={111}
  29. onShowChange={len => {
  30. console.log(len.toFixed(0)) // 不允许有隐式的any类型,这里即使没有定义len的类型,len也应该能够自动推断出来为number类型
  31. }}
  32. onClick={e => {
  33. console.log(e.charAt(0))
  34. }}
  35. />
  36. )
  37. // 基于Button组件封装的组件,覆盖label属性以及show-change,click事件类型
  38. const ProButton = createComponent({
  39. inherit: Button, // 继承Button所有属性以及事件
  40. props: {
  41. // 基础类型数据推断
  42. proLabel: String,
  43. label: [String, Number], // 覆盖Button的label属性类型:label:string -> label:string|number
  44. },
  45. emits: {
  46. 'show-change': (el: HTMLButtonElement) => {},// 覆盖的事件类型
  47. click: (el: HTMLButtonElement) => {}, // 覆盖的事件类型
  48. 'make-pro': () => {}, // 自定义事件类型
  49. },
  50. })
  51. console.log(
  52. /*
  53. * 要求:
  54. * 1. 属性类型为 {proLabel?:string, label?:string|number}
  55. * 2. 事件类型为:{onShowChange?:(el: HTMLButtonElement)=>void, onClick?:(el: HTMLButtonElement)=>void, onMakePro?:()=>void}
  56. * 3. 继承Button组件所有的属性以及事件
  57. */
  58. <ProButton
  59. label={111}
  60. onShowChange={e => {
  61. console.log(e.offsetWidth) // 不允许有隐式的any类型,这里即使没有定义len的类型,len也应该能够自动推断出来为number类型
  62. }}
  63. onClick={e => {
  64. console.log(e.offsetWidth)
  65. }}
  66. onMakePro={() => {}}
  67. />
  68. )

提示,如何得到button标签的属性类型

  • 在文件:node_modules/@types/react/index.d.ts 中寻找 JSX.IntrinsicElements
  • 比如div标签的属性类型为 JSX.IntrinsicElements[“div”]
  1. const MyDiv = (props: JSX.IntrinsicElements["div"]) => null
  2. console.log(<>
  3. <div contentEditable={true} aria-label="div text"/>
  4. <MyDiv contentEditable={true} aria-label="div text"/>
  5. </>)

实现:

  1. //横杠命名转化为驼峰命名
  2. type CapitalizeString<T> = T extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}` : T;
  3. type CamelCase<T extends string, S extends string = ''> = T extends `${infer L}-${infer R}` ? CamelCase<R, `${S}${CapitalizeString<L>}`> : `${S}${CapitalizeString<T>}`
  4. interface SimpleConstruct { new(): any }; //表示class类型约束。例如:String Number ...
  5. //如果 T 是函数,返回函数的返回值;如果是类,返回类的实例本身;否则直接返回 T。此处是为了解决StringConstructor的问题
  6. type InferInstance<T> = T extends () => infer R ? R : (T extends new (...args: any[]) => infer R ? R : T);
  7. // type _string = StringConstructor extends new (...args: any[]) => infer R ? R: never; //String
  8. type ComponentOption = {
  9. inherit?: keyof JSX.IntrinsicElements | ((props: any) => any);
  10. props?: Record<string, SimpleConstruct | SimpleConstruct[]>;
  11. emits?: Record<string, (...args: any[]) => any>;
  12. }
  13. //inherit可能继承button等原生标签,也可能是自定义封装的组件Button
  14. type ExtractInheritType<T> = T extends (props: infer R) => any ? R : (T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : {});
  15. type ExtractPropType<T> = { [k in keyof T]?: T[k] extends any[] ? InferInstance<T[k][number]> : InferInstance<T[k]> }; //将类型数组转成联合类型
  16. //将绑定的事件处理成标准格式
  17. type ExtractEmitType<T> = {
  18. [k in keyof T as `on${k extends string ? CamelCase<k> : ''}`]?: T[k] extends (...args: infer A) => any ? (...args: A) => void : T[k]
  19. };
  20. type MergeTypes<Inherit, Props, Emits> = Props & Emits & Omit<Inherit, keyof Props | keyof Emits>; //合并
  21. type ComponentType<Option> = Option extends { inherit?: infer Inherit, props?: infer Props, emits?: infer Emits } ?
  22. MergeTypes<ExtractInheritType<Inherit>, ExtractPropType<Props>, ExtractEmitType<Emits>> : never;
  23. 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;

示例效果页面

用来测试的示例代码

  1. import React, {useState} from "react";
  2. import {randomDelay, useAsyncMethods} from "@/pages/message/useAsyncMethods";
  3. import {Button, Spin} from "antd";
  4. const Demo1 = () => {
  5. const [method1, setMethod1] = useState(0)
  6. const [method2, setMethod2] = useState(0)
  7. const [method3, setMethod3] = useState(0)
  8. const [togetherMethod2and3, setTogetherMethod2and3] = useState(0)
  9. const methods = useAsyncMethods({
  10. method1: async (id: string) => {
  11. console.log('任务一开始')
  12. await randomDelay(1000, 3000)
  13. console.log('任务一结束')
  14. setMethod1(val => val + 1)
  15. },
  16. method2: async (start: number, end: number) => {
  17. console.log('任务二开始')
  18. await randomDelay(1000, 2000)
  19. console.log('任务二结束')
  20. setMethod2(val => val + 1)
  21. return start + end
  22. },
  23. method3: async (result: any) => {
  24. console.log('任务三开始', {result})
  25. await randomDelay(2000, 3000)
  26. console.log('任务三结束')
  27. setMethod3(val => val + 1)
  28. },
  29. togetherMethod2and3: async () => {
  30. console.log('任务四开始')
  31. // const ret = await methods.method2() // 错误,缺少必须参数start以及end
  32. const ret = await methods.method2(2, 3)
  33. // await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
  34. await methods.method3(ret.toFixed(2))
  35. console.log('任务四结束')
  36. setTogetherMethod2and3(val => val + 1)
  37. },
  38. })
  39. return <>
  40. <div style={{backgroundColor: 'white', padding: '20px'}}>
  41. <h1>测试createAsyncMethods</h1>
  42. <h3>允许多个不同的异步同时执行,但是同一个异步函数不能同时执行多个,必须在函数执行完毕之后,才能开始再次执行该异步函数</h3>
  43. <Button.Group>
  44. <Button onClick={() => methods.method1('__')}>
  45. <span>一号异步任务({method1})</span>
  46. {!!methods.loading.method1 && <Spin/>}
  47. </Button>
  48. <Button onClick={() => methods.method2(0, 1)}>
  49. <span>二号异步任务({method2})</span>
  50. {!!methods.loading.method2 && <Spin/>}
  51. </Button>
  52. <Button onClick={() => methods.method3('?')}>
  53. <span>三号异步任务({method3})</span>
  54. {!!methods.loading.method3 && <Spin/>}
  55. </Button>
  56. <Button onClick={() => methods.togetherMethod2and3()}>
  57. <span>四号异步任务({togetherMethod2and3})</span>
  58. {!!methods.loading.togetherMethod2and3 && <Spin/>}
  59. </Button>
  60. </Button.Group>
  61. </div>
  62. </>
  63. }
  64. const Demo2 = () => {
  65. const [method1, setMethod1] = useState(0)
  66. const [method2, setMethod2] = useState(0)
  67. const [method3, setMethod3] = useState(0)
  68. const [togetherMethod2and3, setTogetherMethod2and3] = useState(0)
  69. const methods = useAsyncMethods((() => {
  70. const m = {
  71. method1: async (id: string) => {
  72. console.log('任务一开始')
  73. await randomDelay(1000, 3000)
  74. console.log('任务一结束')
  75. setMethod1(val => val + 1)
  76. },
  77. method2: async (start: number, end: number) => {
  78. console.log('任务二开始')
  79. await randomDelay(1000, 2000)
  80. console.log('任务二结束')
  81. setMethod2(val => val + 1)
  82. return start + end
  83. },
  84. method3: async (result: any) => {
  85. console.log('任务三开始', {result})
  86. await randomDelay(2000, 3000)
  87. console.log('任务三结束')
  88. setMethod3(val => val + 1)
  89. },
  90. togetherMethod2and3: async () => {
  91. console.log('任务四开始')
  92. // const ret = await methods.method2() // 错误,缺少必须参数start以及end
  93. const ret = await m.method2(2, 3)
  94. // await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
  95. await m.method3(ret.toFixed(2))
  96. console.log('任务四结束')
  97. setTogetherMethod2and3(val => val + 1)
  98. },
  99. }
  100. return m
  101. })(), true)
  102. return <>
  103. <div style={{backgroundColor: 'white', padding: '20px'}}>
  104. <h3>无论是否为同一个异步函数,同一时刻仅能够有一个异步函数在执行</h3>
  105. <Button.Group>
  106. <Button onClick={() => methods.method1('__')}>
  107. <span>一号异步任务({method1})</span>
  108. {!!methods.loading.method1 && <Spin/>}
  109. </Button>
  110. <Button onClick={() => methods.method2(0, 1)}>
  111. <span>二号异步任务({method2})</span>
  112. {!!methods.loading.method2 && <Spin/>}
  113. </Button>
  114. <Button onClick={() => methods.method3('?')}>
  115. <span>三号异步任务({method3})</span>
  116. {!!methods.loading.method3 && <Spin/>}
  117. </Button>
  118. <Button onClick={() => methods.togetherMethod2and3()}>
  119. <span>四号异步任务({togetherMethod2and3})</span>
  120. {!!methods.loading.togetherMethod2and3 && <Spin/>}
  121. </Button>
  122. </Button.Group>
  123. </div>
  124. </>
  125. }
  126. export default () => {
  127. return <>
  128. <Demo1/>
  129. <Demo2/>
  130. </>
  131. }

问题

Demo2中的useAsyncMethods为什么要这样创建;

Demo2设置了全局一次只能有一个异步任务执行,为了避免在执行任务四时,其它异步任务无法调用的情况。所以需要调用原生的方法。

实现:

  1. import React, { useState } from "react";
  2. //randomDelay的实现
  3. const delay = (t = 0) => new Promise(resolve => setTimeout(resolve, t));
  4. export const randomDelay = async (start: number, end: number) => await delay(Math.random() * (end - start) + start);
  5. interface SimpleMethod { (...args: any[]): any };
  6. const getAsyncState = <T>(setter: (getter: (val: T) => any) => any): Promise<T> => {
  7. return new Promise<T>(resolve => setter(val => {
  8. resolve(val);
  9. return val;
  10. }))
  11. }
  12. export function useAsyncMethods<Methods extends Record<string, SimpleMethod>>(methods: Methods, alone?: boolean) {
  13. const methodNames = Object.keys(methods) as (keyof Methods)[];
  14. const newMethods = {} as Record<keyof Methods, SimpleMethod>;
  15. const loadingInitialState = {} as Record<keyof Methods, boolean>;
  16. methodNames.forEach(methodName => {
  17. const method = methods[methodName];
  18. loadingInitialState[methodName] = false;
  19. newMethods[methodName] = async (...args: any[]) => {
  20. const loading = await getAsyncState(setLoading);
  21. if (alone) {
  22. if (loading.global) { return }
  23. } else {
  24. if (loading[methodName]) { return }
  25. }
  26. setLoading(loading => {
  27. return {
  28. ...loading,
  29. [methodName]: true,
  30. global: true
  31. }
  32. });
  33. try {
  34. return await method(...args);
  35. } finally {
  36. setLoading(loading => {
  37. return {
  38. ...loading,
  39. [methodName]: false,
  40. global: methodNames.findIndex(name => name !== methodName && loading[name]) > -1
  41. }
  42. })
  43. }
  44. }
  45. });
  46. const [loading, setLoading] = useState({
  47. ...loadingInitialState,
  48. global: false,
  49. })
  50. return {
  51. ...newMethods,
  52. loading
  53. }
  54. }

三、(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;

示例效果页面

用来测试的示例代码

  1. <template>
  2. <div style="background-color: white;padding: 20px 10px">
  3. <h1>测试createAsyncMethods</h1>
  4. <h3>允许多个不同的异步同时执行,但是同一个异步函数不能同时执行多个,必须在函数执行完毕之后,才能开始再次执行该异步函数</h3>
  5. <el-button @click="methods.method1">
  6. <span>一号异步任务({{ state.method1 }})</span>
  7. <el-icon class="is-loading" v-if="methods.loading.method1">
  8. <Loading/>
  9. </el-icon>
  10. </el-button>
  11. <el-button @click="methods.method2">
  12. <span>二号异步任务({{ state.method2 }})</span>
  13. <el-icon class="is-loading" v-if="methods.loading.method2">
  14. <Loading/>
  15. </el-icon>
  16. </el-button>
  17. <el-button @click="methods.method3">
  18. <span>三号异步任务({{ state.method3 }})</span>
  19. <el-icon class="is-loading" v-if="methods.loading.method3">
  20. <Loading/>
  21. </el-icon>
  22. </el-button>
  23. <el-button @click="methods.togetherMethod2and3">
  24. <span>四号异步任务({{ state.togetherMethod2and3 }})</span>
  25. <el-icon class="is-loading" v-if="methods.loading.togetherMethod2and3">
  26. <Loading/>
  27. </el-icon>
  28. </el-button>
  29. <h3>无论是否为同一个异步函数,同一时刻仅能够有一个异步函数在执行</h3>
  30. <el-button @click="methods2.method1">
  31. <span>一号异步任务({{ state2.method1 }})</span>
  32. <el-icon class="is-loading" v-if="methods2.loading.method1">
  33. <Loading/>
  34. </el-icon>
  35. </el-button>
  36. <el-button @click="methods2.method2">
  37. <span>二号异步任务({{ state2.method2 }})</span>
  38. <el-icon class="is-loading" v-if="methods2.loading.method2">
  39. <Loading/>
  40. </el-icon>
  41. </el-button>
  42. <el-button @click="methods2.method3">
  43. <span>三号异步任务({{ state2.method3 }})</span>
  44. <el-icon class="is-loading" v-if="methods2.loading.method3">
  45. <Loading/>
  46. </el-icon>
  47. </el-button>
  48. <el-button @click="methods2.togetherMethod2and3">
  49. <span>四号异步任务({{ state2.togetherMethod2and3 }})</span>
  50. <el-icon class="is-loading" v-if="methods2.loading.togetherMethod2and3">
  51. <Loading/>
  52. </el-icon>
  53. </el-button>
  54. </div>
  55. </template>
  56. <script lang="ts">
  57. import {createAsyncMethods, randomDelay} from "@/pages/message/createAsyncMethods";
  58. import {Loading} from '@element-plus/icons'
  59. import {defineComponent, reactive} from 'vue'
  60. export default defineComponent({
  61. components: {Loading},
  62. setup() {
  63. const state = reactive({
  64. method1: 0,
  65. method2: 0,
  66. method3: 0,
  67. togetherMethod2and3: 0,
  68. })
  69. const methods = createAsyncMethods({
  70. method1: async (id: string) => {
  71. console.log('任务一开始')
  72. await randomDelay(1000, 3000)
  73. console.log('任务一结束')
  74. state.method1++
  75. },
  76. method2: async (start: number, end: number) => {
  77. console.log('任务二开始')
  78. await randomDelay(1000, 2000)
  79. console.log('任务二结束')
  80. state.method2++
  81. return start + end
  82. },
  83. method3: async (result: any) => {
  84. console.log('任务三开始', {result})
  85. await randomDelay(2000, 3000)
  86. console.log('任务三结束')
  87. state.method3++
  88. },
  89. togetherMethod2and3: async () => {
  90. console.log('任务四开始')
  91. // const ret = await methods.method2() // 错误,缺少必须参数start以及end
  92. const ret = await methods.method2(2, 3)
  93. // await methods.method3(ret.charAt(0)) // 错误,返回值类型为数字
  94. await methods.method3(ret.toFixed(2))
  95. console.log('任务四结束')
  96. state.togetherMethod2and3++
  97. },
  98. })
  99. const state2 = reactive({
  100. method1: 0,
  101. method2: 0,
  102. method3: 0,
  103. togetherMethod2and3: 0,
  104. })
  105. const methods2 = createAsyncMethods((() => {
  106. const m = {
  107. method1: async (id: string) => {
  108. console.log('任务一开始')
  109. await randomDelay(1000, 3000)
  110. console.log('任务一结束')
  111. state2.method1++
  112. },
  113. method2: async (start: number, end: number) => {
  114. console.log('任务二开始')
  115. await randomDelay(1000, 2000)
  116. console.log('任务二结束')
  117. state2.method2++
  118. return start + end
  119. },
  120. method3: async (result: any) => {
  121. console.log('任务三开始', {result})
  122. await randomDelay(2000, 3000)
  123. console.log('任务三结束')
  124. state2.method3++
  125. },
  126. togetherMethod2and3: async () => {
  127. console.log('任务四开始')
  128. const ret = await m.method2(2, 3)
  129. await m.method3(ret.toFixed(2))
  130. console.log('任务四结束')
  131. state2.togetherMethod2and3++
  132. },
  133. }
  134. return m
  135. })(), true)
  136. return {
  137. state,
  138. methods,
  139. state2,
  140. methods2,
  141. }
  142. },
  143. })
  144. </script>

问题

methods2中的createAsyncMethods为什么要这样创建;

四、(React)实现使用弹框选择数据服务:pick函数

  • React同学专属题目,Vue同学请看第五题
  • pick函数是一个异步函数,参数是一个对象;返回值的类型,依据参数类型而定;参数类型如下所示
  1. interface iUsePickOption<Data> {
  2. data: Data[],
  3. render: (row: Data, index: number) => any
  4. }
  5. interface iUsePickOptionMultiple<Data> {
  6. data: Data[],
  7. render: (row: Data, index: number) => any
  8. multiple: true
  9. }
  • (单选)当参数符合iUsePickOption时,返回值为选项data数组中元素的类型;
  • (多选)当参数符合iUsePickOptionMultiple是,返回值类型等同于选项data,也是个数组;
  • 使用弹框渲染这个data数据,点击弹框取消按钮或者遮罩时,pick异步任务终止(Promise.reject);
  • 用户没有选择数据点击弹框确定按钮的时候,如果没有选中任何一条数据,提示选择数据,但是不得关闭弹框;
  • 用户选中数据,并且点击确定之后,异步任务执行完毕,返回用户选中数据;
  • pick函数还能够接收一个泛型,当传入这个泛型的时候,选项中的data的类型将忽略,返回值以这个泛型类型为主,如下列示例代码中的第三个示例为例;
    const pickWithCustomType = await pick<Student>(...) 得到的返回值类型为Student,如果是多选,则返回值为Student[]

提示:函数pick的类型为一个重载函数

测试代码如下所示:

  1. import {Button, Modal} from "antd"
  2. import {pick} from "@/pages/demo-pick/pick";
  3. import studentJsonData from './student.json'
  4. export default () => {
  5. interface Staff {
  6. name: string,
  7. age: number,
  8. avatar: string,
  9. }
  10. const staffData: Staff[] = [
  11. {
  12. "name": "张三",
  13. "age": 20,
  14. "avatar": "http://abc.com/zhangsan"
  15. },
  16. {
  17. "name": "李四",
  18. "age": 21,
  19. "avatar": "http://abc.com/lisi"
  20. },
  21. {
  22. "name": "王五",
  23. "age": 22,
  24. "avatar": "http://abc.com/wangwu"
  25. }
  26. ]
  27. /*---------------------------------------单选-------------------------------------------*/
  28. const pick1 = async () => {
  29. // pickPerson自动推导类型为 Staff
  30. const pickStaff = await pick({
  31. data: staffData,
  32. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  33. render: (row) => [row.name, row.age, row.avatar].join(',')
  34. })
  35. Modal.info({maskClosable: true, content: [pickStaff.name, pickStaff.age, pickStaff.avatar].join(',')})
  36. }
  37. /*---------------------------------------多选-------------------------------------------*/
  38. const pick2 = async () => {
  39. // pickPersonList自动推导类型为 Staff[]
  40. const pickStaffList = await pick({
  41. data: staffData,
  42. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  43. render: (row) => [row.name, row.age, row.avatar].join(','),
  44. multiple: true,
  45. })
  46. Modal.info({
  47. maskClosable: true,
  48. content:
  49. pickStaffList.map(staff => [staff.name, staff.age, staff.avatar].join(',')).join('\n')
  50. })
  51. }
  52. /*---------------------------------------多选,手动传递类型-------------------------------------------*/
  53. interface Student {
  54. name: string,
  55. code: string,
  56. grade: number
  57. }
  58. const pick3 = async () => {
  59. const pickWithCustomType = await pick<Student>({
  60. // 无法确定data的类型
  61. data: studentJsonData,
  62. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  63. render: (row) => [row.name, row.grade, row.code].join(','),
  64. multiple: true,
  65. })
  66. // pickWithCustomType推导类型为 Student[]
  67. Modal.info({
  68. maskClosable: true,
  69. content:
  70. pickWithCustomType.map(student => [student.name, student.grade, student.code].join(',')).join('\n')
  71. })
  72. }
  73. return (
  74. <div style={{backgroundColor: 'white', padding: '20px 10px'}}>
  75. <h1>TestPick</h1>
  76. <Button.Group>
  77. <Button onClick={pick1}>选中单条数据</Button>
  78. <Button onClick={pick2}>选中多条数据</Button>
  79. <Button onClick={pick3}>选中多条数据(手动传递类型)</Button>
  80. </Button.Group>
  81. </div>
  82. )
  83. }

student.json

  1. [
  2. {
  3. "name": "张三",
  4. "grade": 4,
  5. "code": "01001"
  6. },
  7. {
  8. "name": "李四",
  9. "grade": 5,
  10. "code": "01002"
  11. },
  12. {
  13. "name": "王五",
  14. "grade": 6,
  15. "code": "01003"
  16. }
  17. ]

实现:

  1. import {Modal} from 'antd'
  2. import {useState} from "react";
  3. type Deferred<T> = {
  4. promise: Promise<T>;
  5. resolve: (value?: T) => void;
  6. reject: (reason?: any) => void;
  7. };
  8. const defer = function <T>() {
  9. let dfd = {} as Deferred<T>;
  10. dfd.promise = new Promise((resolve, reject) => {
  11. dfd.resolve = resolve;
  12. dfd.reject = reject;
  13. })
  14. return dfd
  15. }
  16. type SimpleObject = Record<string, any>
  17. type NotUnknown<T, K> = unknown extends T ? K : T
  18. interface iUsePickOption<Data> {
  19. data: Data[],
  20. render: (row: Data, index: number) => any
  21. }
  22. interface iUsePickOptionMultiple<Data> extends iUsePickOption<Data> {
  23. multiple: true
  24. }
  25. /*函数类型: 单选*/
  26. export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOption<NotUnknown<T, ListT>>): Promise<NotUnknown<T, ListT>>
  27. /*函数类型: 多选*/
  28. export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOptionMultiple<NotUnknown<T, ListT>>): Promise<NotUnknown<T, ListT>[]>
  29. /*函数实现*/
  30. export function pick<T = unknown, ListT = SimpleObject>(option: iUsePickOption<NotUnknown<T, ListT>> | iUsePickOptionMultiple<NotUnknown<T, ListT>>) {
  31. const dfd = defer<NotUnknown<T, ListT> | NotUnknown<T, ListT>[]>()
  32. // 用来在beforeClose的时候判断是否已经选中任何内容
  33. const state = {
  34. checked: null as null | NotUnknown<T, ListT>,
  35. checkedList: [] as NotUnknown<T, ListT>[],
  36. }
  37. const Message = () => {
  38. const [checked, setChecked] = useState(null as null | NotUnknown<T, ListT>)
  39. const [checkedList, setCheckedList] = useState([] as NotUnknown<T, ListT>[])
  40. /*获取元素位置索引*/
  41. const getIndex = (row: any) => checkedList.indexOf(row)
  42. /*元素是否已经选中*/
  43. const isChecked = (row: any) => 'multiple' in option && option.multiple ? getIndex(row) > -1 : checked == row
  44. const onClick = (row: NotUnknown<T, ListT>) => {
  45. if ('multiple' in option && option.multiple) {
  46. if (isChecked(row)) {
  47. checkedList.splice(getIndex(row), 1)
  48. } else {
  49. checkedList.push(row as any)
  50. }
  51. state.checkedList = checkedList
  52. setCheckedList([...checkedList])
  53. } else {
  54. state.checked = row
  55. setChecked(row)
  56. }
  57. }
  58. return (
  59. <ul>
  60. {option.data.map((row, index) => (
  61. <li
  62. style={{
  63. padding: '10px 16px',
  64. backgroundColor: isChecked(row) ? 'aliceblue' : ''
  65. }}
  66. onClick={() => onClick(row as any)}
  67. key={index}
  68. >
  69. {option.render(row as any, index)}
  70. </li>
  71. ))}
  72. </ul>
  73. )
  74. }
  75. const modal = Modal.info({
  76. title: '选择',
  77. okText: '确定',
  78. cancelText: '取消',
  79. closable: true,
  80. maskClosable: true,
  81. content: <Message/>,
  82. onCancel: () => dfd.reject('cancel'),
  83. okButtonProps: {
  84. onClick: () => {
  85. if ('multiple' in option && option.multiple) {
  86. if (state.checkedList.length === 0) {
  87. return Modal.info({content: '请至少选中一条数据', maskClosable: true})
  88. } else {
  89. dfd.resolve(state.checkedList)
  90. }
  91. } else {
  92. if (!state.checked) {
  93. return Modal.info({content: '请选中一条数据', maskClosable: true})
  94. } else {
  95. dfd.resolve(state.checked)
  96. }
  97. }
  98. return modal.update({visible: false})
  99. },
  100. },
  101. })
  102. return dfd.promise
  103. }

五、(Vue3.0)实现使用弹框选择数据服务:pick函数

  • Vue同学专属题目,React同学请看第四题
  • pick函数是一个异步函数,参数是一个对象;返回值的类型,依据参数类型而定;参数类型如下所示
  1. interface iUsePickOption<Data> {
  2. data: Data[],
  3. render: (row: Data, index: number) => any
  4. }
  5. interface iUsePickOptionMultiple<Data> {
  6. data: Data[],
  7. render: (row: Data, index: number) => any
  8. multiple: true
  9. }
  • (单选)当参数符合iUsePickOption时,返回值为选项data数组中元素的类型;
  • (多选)当参数符合iUsePickOptionMultiple是,返回值类型等同于选项data,也是个数组;
  • 使用弹框渲染这个data数据,点击弹框取消按钮或者遮罩时,pick异步任务终止(Promise.reject);
  • 用户没有选择数据点击弹框确定按钮的时候,如果没有选中任何一条数据,提示选择数据,但是不得关闭弹框;
  • 用户选中数据,并且点击确定之后,异步任务执行完毕,返回用户选中数据;
  • pick函数还能够接收一个泛型,当传入这个泛型的时候,选项中的data的类型将忽略,返回值以这个泛型类型为主,如下列示例代码中的第三个示例为例;
    const pickWithCustomType = await pick<Student>(...) 得到的返回值类型为Student,如果是多选,则返回值为Student[]

提示:函数pick的类型为一个重载函数

测试代码如下所示:

  1. <template>
  2. <div style="background-color: white;padding: 20px 10px">
  3. <h1>TestPick</h1>
  4. <el-button-group>
  5. <el-button @click="pick1">选中单条数据</el-button>
  6. <el-button @click="pick2">选中多条数据</el-button>
  7. <el-button @click="pick3">选中多条数据(手动传递类型)</el-button>
  8. </el-button-group>
  9. </div>
  10. </template>
  11. <script lang="ts">
  12. import {pick} from "./pick";
  13. import studentJsonData from './student.json'
  14. import {ElMessageBox} from 'element-plus'
  15. export default {
  16. setup() {
  17. interface Staff {
  18. name: string,
  19. age: number,
  20. avatar: string,
  21. }
  22. const staffData: Staff[] = [
  23. {
  24. "name": "张三",
  25. "age": 20,
  26. "avatar": "http://abc.com/zhangsan"
  27. },
  28. {
  29. "name": "李四",
  30. "age": 21,
  31. "avatar": "http://abc.com/lisi"
  32. },
  33. {
  34. "name": "王五",
  35. "age": 22,
  36. "avatar": "http://abc.com/wangwu"
  37. }
  38. ]
  39. /*---------------------------------------单选-------------------------------------------*/
  40. const pick1 = async () => {
  41. // pickPerson自动推导类型为 Staff
  42. const pickStaff = await pick({
  43. data: staffData,
  44. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  45. render: (row) => [row.name, row.age, row.avatar].join(',')
  46. })
  47. ElMessageBox({message: [pickStaff.name, pickStaff.age, pickStaff.avatar].join(',')})
  48. }
  49. /*---------------------------------------多选-------------------------------------------*/
  50. const pick2 = async () => {
  51. // pickPersonList自动推导类型为 Staff[]
  52. const pickStaffList = await pick({
  53. data: staffData,
  54. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  55. render: (row) => [row.name, row.age, row.avatar].join(','),
  56. multiple: true,
  57. })
  58. ElMessageBox({
  59. message:
  60. pickStaffList.map(staff => [staff.name, staff.age, staff.avatar].join(',')).join('\n')
  61. })
  62. }
  63. /*---------------------------------------多选,手动传递类型-------------------------------------------*/
  64. interface Student {
  65. name: string,
  66. code: string,
  67. grade: number
  68. }
  69. const pick3 = async () => {
  70. const pickWithCustomType = await pick<Student>({
  71. // 无法确定data的类型
  72. data: studentJsonData,
  73. // render函数的row参数自动推导为Person,与data选项的persons对象类型保持一致
  74. render: (row) => [row.name, row.grade, row.code].join(','),
  75. multiple: true,
  76. })
  77. // pickWithCustomType推导类型为 Student[]
  78. ElMessageBox({
  79. message:
  80. pickWithCustomType.map(student => [student.name, student.grade, student.code].join(',')).join('\n')
  81. })
  82. }
  83. return {
  84. pick1,
  85. pick2,
  86. pick3,
  87. }
  88. },
  89. }
  90. </script>

student.json

  1. [
  2. {
  3. "name": "张三",
  4. "grade": 4,
  5. "code": "01001"
  6. },
  7. {
  8. "name": "李四",
  9. "grade": 5,
  10. "code": "01002"
  11. },
  12. {
  13. "name": "王五",
  14. "grade": 6,
  15. "code": "01003"
  16. }
  17. ]