Skip to content

Layer

组件介绍

基于Teleport useLayer的弹出层组件

弹出后可调整各个方向大小以及位置

使用示例

基本使用

点我查看代码
vue
<script lang="ts" setup>
import type { Rect } from '@summeruse/ui'
import { NButton, NCard } from 'naive-ui'
import { ref } from 'vue'
import Layer from './index.vue'

const props = defineProps<{
  initRect?: Rect
}>()

const show = ref(true)

const initRect = ref(props.initRect ?? {
  x: 500,
  y: 500,
  width: 200,
  height: 200,
})
setTimeout(() => {
  initRect.value = {
    x: 300,
    y: 500,
    width: 300,
    height: 300,
  }
}, 3000)

const teleport = ref(true)
</script>

<template>
  <NButton @click="show = !show">
    {{ show ? '关闭' : "打开" }}
  </NButton>
  <NButton @click="teleport = !teleport">
    {{ teleport ? '返回' : "弹出" }}
  </NButton>
  <Layer
    v-model:show="show" v-model:init-rect="initRect" :teleport="teleport" on-top
  >
    <template #default="{ close }">
      <slot :close :show>
        <NCard header-class="layer-header" title="标题" class="w-100% h-100%">
          <template #header-extra>
            <div class="h-20px flex cursor-pointer hover:bg-#abf5 w-20px items-center justify-center" @click="close">
              <div>x</div>
            </div>
          </template>
        </NCard>
      </slot>
    </template>
  </Layer>
</template>

组件代码

点我查看代码
vue
<script lang="ts" setup>
import type { StyleValue } from 'vue'
import type { LayerProps } from './props'
import { useResizeObserver } from '@vueuse/core'
import { computed, ref, toRefs, useTemplateRef, watch } from 'vue'
import { useLayerIndexManager } from './layer-provider'
import { useLayer } from './useLayer/index'

const props = withDefaults(defineProps<LayerProps>(), {
  to: 'body',
  destroyOnClose: true,
})

const propsRef = toRefs(props)

const show = defineModel<boolean>('show', {
  required: true,
})

const layerRef = useTemplateRef('layer')

const contentRef = useTemplateRef('content')

const rectModel = defineModel<{
  x?: number
  y?: number
  width?: number
  height?: number
}>('initRect', {
  required: true,
})

const initRect = ref({
  x: 0,
  y: 0,
  width: 0,
  height: 0,
  ...rectModel.value,
})

const layerIndexManager = useLayerIndexManager()

const zIndex = ref(propsRef.onTop.value ? layerIndexManager.nextZIndex() : layerIndexManager.defaultZIndex)

const hidden = computed(() => {
  if (!propsRef.teleport.value) {
    return false
  }
  return !show.value
})

const destroyed = computed(() => {
  if (!propsRef.teleport.value) {
    return false
  }
  return propsRef.destroyOnClose.value && !show.value
})

const disabledDrag = computed(() => {
  if (!propsRef.teleport.value) {
    return true
  }

  return propsRef.disabledDrag.value
})

const disabledResize = computed(() => {
  if (!propsRef.teleport.value) {
    return true
  }
  if (!rectModel.value.width || !rectModel.value.height) {
    return true
  }
  return propsRef.disabledResize.value
})

const { rect, check } = useLayer(layerRef, {
  ...propsRef,
  disabledDrag,
  disabledResize,
  initRect,
})

useResizeObserver(contentRef, (entries) => {
  const entry = entries[0]
  if (disabledResize.value && entry) {
    const { width, height } = entry.target.getBoundingClientRect()
    rect.value.width = width
    rect.value.height = height
    check()
  }
})

watch(propsRef.teleport, (teleport) => {
  if (teleport && show.value) {
    check()
  }
})

watch(() => rectModel.value.height, (value) => {
  if (value) {
    rect.value.height = value
    check()
  }
})
watch(() => rectModel.value.width, (value) => {
  if (value) {
    rect.value.width = value
    check()
  }
})
watch(() => rectModel.value.x, (value) => {
  if (value) {
    rect.value.x = value
    check()
  }
})
watch(() => rectModel.value.y, (value) => {
  if (value) {
    rect.value.y = value
    check()
  }
})

const style = computed<StyleValue>(() => {
  const style: StyleValue = !hidden.value
    ? [{
        display: 'block',
      }]
    : [{
        display: 'none',
      }]

  if (propsRef.teleport.value) {
    style.push({
      position: 'fixed',
      zIndex: zIndex.value,
      left: `${rect.value.x}px`,
      top: `${rect.value.y}px`,
    })
    if (!propsRef.disabledResize.value) {
      style.push({
        width: `${rect.value.width}px`,
        height: `${rect.value.height}px`,
      })
    }
  }
  return style
})

function close() {
  show.value = false
}

function handleTop() {
  if (!propsRef.teleport.value) {
    return
  }

  if (!propsRef.onTop.value) {
    return
  }
  zIndex.value = layerIndexManager.nextZIndex()
}
</script>

<template>
  <Teleport :to="to" :disabled="!teleport">
    <div v-if="!destroyed" v-bind="$attrs" ref="layer" :style @mousedown="handleTop">
      <div ref="content" style="width: 100%; height: 100%;">
        <slot :close />
      </div>
    </div>
  </Teleport>
</template>

Props

点我查看代码
ts
import type { RendererElement, VNode } from 'vue'
import type { Directions, Rect } from './useLayer/index'

export interface LayerProps {
  /** 初始位置 */
  initRect?: {
    x?: number
    y?: number
    width?: number
    height?: number
  }
  /**  teleport 目标容器 */
  to?: string | RendererElement | null | undefined
  /** 是否弹出到Teleport */
  teleport?: boolean
  /** 方向 */
  directions?: Directions
  /** 拖动元素 */
  dragElement?: HTMLElement
  /** 禁止拉伸 */
  disabledResize?: boolean
  /** 禁止拖动 */
  disabledDrag?: boolean
  /** 最小宽度 */
  minWidth?: number
  /** 最小高度 */
  minHeight?: number
  /** 最大宽度 */
  maxWidth?: number
  /** 最大高度 */
  maxHeight?: number
  /** 宽高比 */
  ratio?: number
  /** 限位元素 */
  parent?: HTMLElement
  /** 允许拉伸到父元素外面 */
  allowOverParent?: boolean
  /** 保持置顶 */
  onTop?: boolean
  /** 关闭时销毁 */
  destroyOnClose?: boolean
}

export type UseLayerOptions = LayerProps & {
  initRect: Rect
  content?: ((close: () => void) => VNode) | string
}
ts
import type { MaybeRefOrGetter } from 'vue'

export type ResizeDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

export interface Directions {
  'left'?: boolean
  'right'?: boolean
  'top'?: boolean
  'bottom'?: boolean
  'top-left'?: boolean
  'top-right'?: boolean
  'bottom-left'?: boolean
  'bottom-right'?: boolean
}

export interface Rect {
  height: number
  width: number
  x: number
  y: number
}

export interface LayerOptions {
  // 方向
  directions?: MaybeRefOrGetter<Directions | undefined>
  // 初始位置
  initRect?: MaybeRefOrGetter<Rect>
  // 拖动元素
  dragElement?: MaybeRefOrGetter<HTMLElement | undefined>
  // 禁止拉伸
  disabledResize?: MaybeRefOrGetter<boolean>
  // 禁止拖动
  disabledDrag?: MaybeRefOrGetter<boolean>
  // 最小宽度
  minWidth?: MaybeRefOrGetter<number | undefined>
  // 最小高度
  minHeight?: MaybeRefOrGetter<number | undefined>
  // 最大宽度
  maxWidth?: MaybeRefOrGetter<number | undefined>
  // 最大高度
  maxHeight?: MaybeRefOrGetter<number | undefined>
  // 宽高比
  ratio?: MaybeRefOrGetter<number | undefined>
  // 限位元素
  parent?: MaybeRefOrGetter<HTMLElement | undefined>
  // 允许拉伸到父元素外面
  allowOverParent?: MaybeRefOrGetter<boolean>
}

Released under the ISC License.