• 这里以 ElDialog 组件编写自己的 Dialog 组件为示例
  1. 写组件前先将 @element-plus/utils 定义props类型的方法放到自己的 /src/utils/props.ts 文件中
import { warn } from 'vue'
import { fromPairs } from 'lodash-es'
import type { ExtractPropTypes, PropType } from 'vue'
import { isObject, hasOwn } from '@vue/shared'

const wrapperKey = Symbol()
export type PropWrapper<T> = { [wrapperKey]: T }

export const propKey = '__elPropsReservedKey'

type ResolveProp<T> = ExtractPropTypes<{
  key: { type: T; required: true }
}>['key']
type ResolvePropType<T> = ResolveProp<T> extends { type: infer V } ? V : ResolveProp<T>
type ResolvePropTypeWithReadonly<T> = Readonly<T> extends Readonly<Array<infer A>>
  ? ResolvePropType<A[]>
  : ResolvePropType<T>

type IfUnknown<T, V> = [unknown] extends [T] ? V : T

export type BuildPropOption<T, D extends BuildPropType<T, V, C>, R, V, C> = {
  type?: T
  values?: readonly V[]
  required?: R
  default?: R extends true ? never : D extends Record<string, unknown> | Array<any> ? () => D : (() => D) | D
  validator?: ((val: any) => val is C) | ((val: any) => boolean)
}

type _BuildPropType<T, V, C> =
  | (T extends PropWrapper<unknown>
      ? T[typeof wrapperKey]
      : [V] extends [never]
      ? ResolvePropTypeWithReadonly<T>
      : never)
  | V
  | C
export type BuildPropType<T, V, C> = _BuildPropType<
  IfUnknown<T, never>,
  IfUnknown<V, never>,
  IfUnknown<C, never>
>

type _BuildPropDefault<T, D> = [T] extends [
  // eslint-disable-next-line @typescript-eslint/ban-types
  Record<string, unknown> | Array<any> | Function
]
  ? D
  : D extends () => T
  ? ReturnType<D>
  : D

export type BuildPropDefault<T, D, R> = R extends true
  ? { readonly default?: undefined }
  : {
      readonly default: Exclude<D, undefined> extends never
        ? undefined
        : Exclude<_BuildPropDefault<T, D>, undefined>
    }
export type BuildPropReturn<T, D, R, V, C> = {
  readonly type: PropType<BuildPropType<T, V, C>>
  readonly required: IfUnknown<R, false>
  readonly validator: ((val: unknown) => boolean) | undefined
  [propKey]: true
} & BuildPropDefault<BuildPropType<T, V, C>, IfUnknown<D, never>, IfUnknown<R, false>>

/**
 * @description Build prop. It can better optimize prop types
 * @description 生成 prop,能更好地优化类型
 * @example
  // limited options
  // the type will be PropType<'light' | 'dark'>
  buildProp({
    type: String,
    values: ['light', 'dark'],
  } as const)
  * @example
  // limited options and other types
  // the type will be PropType<'small' | 'large' | number>
  buildProp({
    type: [String, Number],
    values: ['small', 'large'],
    validator: (val: unknown): val is number => typeof val === 'number',
  } as const)
  @link see more: https://github.com/element-plus/element-plus/pull/3341
 */
export function buildProp<
  T = never,
  D extends BuildPropType<T, V, C> = never,
  R extends boolean = false,
  V = never,
  C = never
>(option: BuildPropOption<T, D, R, V, C>, key?: string): BuildPropReturn<T, D, R, V, C> {
  // filter native prop type and nested prop, e.g `null`, `undefined` (from `buildProps`)
  if (!isObject(option) || !!option[propKey]) return option as any

  const { values, required, default: defaultValue, type, validator } = option

  const _validator =
    values || validator
      ? (val: unknown) => {
          let valid = false
          let allowedValues: unknown[] = []

          if (values) {
            allowedValues = Array.from(values)
            if (hasOwn(option, 'default')) {
              allowedValues.push(defaultValue)
            }
            valid ||= allowedValues.includes(val)
          }
          if (validator) valid ||= validator(val)

          if (!valid && allowedValues.length > 0) {
            const allowValuesText = [...new Set(allowedValues)]
              .map((value) => JSON.stringify(value))
              .join(', ')
            warn(
              `Invalid prop: validation failed${
                key ? ` for prop "${key}"` : ''
              }. Expected one of [${allowValuesText}], got value ${JSON.stringify(val)}.`
            )
          }
          return valid
        }
      : undefined

  const prop: any = {
    type: isObject(type) && Object.getOwnPropertySymbols(type).includes(wrapperKey) ? type[wrapperKey] : type,
    required: !!required,
    validator: _validator,
    [propKey]: true
  }
  if (hasOwn(option, 'default')) prop.default = defaultValue

  return prop as BuildPropReturn<T, D, R, V, C>
}

type NativePropType = [((...args: any) => any) | { new (...args: any): any } | undefined | null]

export const buildProps = <
  O extends {
    [K in keyof O]: O[K] extends BuildPropReturn<any, any, any, any, any>
      ? O[K]
      : [O[K]] extends NativePropType
      ? O[K]
      : O[K] extends BuildPropOption<infer T, infer D, infer R, infer V, infer C>
      ? D extends BuildPropType<T, V, C>
        ? BuildPropOption<T, D, R, V, C>
        : never
      : never
  }
>(
  props: O
) =>
  fromPairs(
    Object.entries(props).map(([key, option]) => [key, buildProp(option as any, key)])
  ) as unknown as {
    [K in keyof O]: O[K] extends { [propKey]: boolean }
      ? O[K]
      : [O[K]] extends NativePropType
      ? O[K]
      : O[K] extends BuildPropOption<
          infer T,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          infer _D,
          infer R,
          infer V,
          infer C
        >
      ? BuildPropReturn<T, O[K]['default'], R, V, C>
      : never
  }

export const definePropType = <T>(val: any) => ({ [wrapperKey]: val } as PropWrapper<T>)
  1. 写组件前将定义emits类型的EmitFn类型放到/src/utils/emits.ts文件中
type ObjectEmitsOptions = Record<string, ((...args: any[]) => any) | null>
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
  ? I
  : never

export type EmitFn<
  Options = ObjectEmitsOptions,
  Event extends keyof Options = keyof Options
> = Options extends Array<infer V>
  ? (event: V, ...args: any[]) => void
  : {} extends Options
  ? (event: string, ...args: any[]) => void
  : UnionToIntersection<
      {
        [key in Event]: Options[key] extends (...args: infer Args) => any
          ? (event: key, ...args: Args) => void
          : (event: key, ...args: any[]) => void
      }[Event]
    >
  1. 开始编写自己的 /src/components/Dialog/index.vue 弹窗组件
<template>
  <ElDialog 
  	v-model="isShow" 
  	append-to-body 
  	destroy-on-close 
  	custom-class="dialog-wrapper"
    :draggable="draggable" 
    :width="width" 
    :open-delay="50" 
    :close-on-click-modal="false"
    :close-on-press-escape="false" 
    @close="handleClose">
    <template #header>
      <div class="w-full h-6 text-lg">{{ title }}</div>
    </template>
    <template #footer v-if="showFooter">
      <slot name="footer">
        <ElButton @click="handleClose">取消</ElButton>
        <ElButton type="primary" @click="handleConfirm">确认</ElButton>
      </slot>
    </template>
    <div class="w-full">
      <slot></slot>
    </div>
  </ElDialog>
</template>

<script lang="ts" setup>
import { ElDialog, ElButton } from 'element-plus'
import { dialogProps, dialogEmit } from './type'
import { useIndex } from './index'

const props = defineProps(dialogProps)
const emit = defineEmits(dialogEmit)

const { isShow, handleClose, handleConfirm } = useIndex(props, emit)
</script>
  1. /src/components/Dialog/index.ts文件中处理组件相关逻辑
import { DialogProps, DialogEmits } from './type'
import { ref, onMounted, onUnmounted } from 'vue'

export const useIndex = (props: DialogProps, emit: DialogEmits) => {
  const isShow = ref<boolean>(false)
  onMounted(() => {
    isShow.value = true
  })
  onUnmounted(() => {
    isShow.value = false
  })
  const handleClose= () => {
    isShow.value = false
    emit('close')
  }
  const handleConfirm = () => {
    isShow.value = false
    emit('confirm', isShow.value)
  }
  return {
    isShow,
    handleClose,
    handleConfirm
  }
}
  1. /components/Dialog/type.ts 中定义组件参数类型
import { buildProps } from '@/utils/props'
import { EmitFn } from '@/utils/emits'
import { ExtractPropTypes } from 'vue'
import { dialogProps as _dialogProps } from 'element-plus'
import { isBoolean } from 'lodash-es'

// 组件props参数类型
 export const dialogProps = buildProps({
 // 其余参数跟 ElDialog 参数保持一致
  ..._dialogProps,
  title: {
    type: String,
    default: ''
  },
  draggable: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '580px'
  },
  showFooter: {
    type: Boolean,
    default: true
  }
} as const)

export type DialogProps = ExtractPropTypes<typeof dialogProps>

// 组件emit回调方法类型
export const dialogEmit = {
  confirm: (type: boolean) => isBoolean(type),
  close: () => true
}

export type DialogEmits = EmitFn<typeof dialogEmit>
  1. 在任意页面中使用
<template>
<ElButton @click="show = true">显示弹框</ElButton>
<Dialog title="弹框标题" @close="show = false" @confirm="confirm" v-if="show">
    我是自定义内容
  </Dialog>
</template>

<script lang="ts" setup>
import Dialog from '@/components/Dialog/index.vue'
import { ElButton } from 'element-plus'
import { ref } from 'vue'

const show = ref(false)
const confirm = (e) => {
  console.log('确认了', e)
  show.value = false
}
</script>
Logo

前往低代码交流专区

更多推荐