import type { CSSProperties, ExtractPropTypes, PropType } from 'vue' import { inject, nextTick, defineComponent, ref, onMounted, provide, onUnmounted, watch, computed } from 'vue' import { getPropsSlot, initDefaultProps } from 'ant-design-vue/es/_util/props-util' import classnames from 'ant-design-vue/es/_util/classNames' import VcDrawer from 'ant-design-vue/es/vc-drawer' import PropTypes from 'ant-design-vue/es/_util/vue-types' import { CloseOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue' import { tuple } from 'ant-design-vue/es/_util/type' import omit from 'ant-design-vue/es/_util/omit' import devWarning from 'ant-design-vue/es/vc-util/devWarning' import type { KeyboardEventHandler, MouseEventHandler } from 'ant-design-vue/es/_util/EventInterface' import './style/drawer.less' type ILevelMove = number | [number, number] const PlacementTypes = tuple('top', 'right', 'bottom', 'left') export type placementType = typeof PlacementTypes[number] const SizeTypes = tuple('default', 'large') export type sizeType = typeof SizeTypes[number] export interface PushState { distance: string | number } const defaultPushState: PushState = { distance: 180 } export const drawerProps = () => ({ autofocus: { type: Boolean, default: undefined }, closable: { type: Boolean, default: undefined }, closeIcon: PropTypes.any, destroyOnClose: { type: Boolean, default: undefined }, forceRender: { type: Boolean, default: undefined }, getContainer: PropTypes.any, maskClosable: { type: Boolean, default: undefined }, mask: { type: Boolean, default: undefined }, maskStyle: { type: Object as PropType, default: undefined }, /** @deprecated Use `style` instead */ wrapStyle: { type: Object as PropType, default: undefined }, style: { type: Object as PropType, default: undefined }, class: PropTypes.any, /** @deprecated Use `class` instead */ wrapClassName: String, size: { type: String as PropType }, drawerStyle: { type: Object as PropType, default: undefined }, headerStyle: { type: Object as PropType, default: {} }, bodyStyle: { type: Object as PropType, default: {} }, contentWrapperStyle: { type: Object as PropType, default: undefined }, title: PropTypes.any, visible: { type: Boolean, default: undefined }, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), zIndex: Number, prefixCls: String, push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType }]), placement: PropTypes.oneOf(PlacementTypes), keyboard: { type: Boolean, default: undefined }, extra: PropTypes.any, footer: PropTypes.any, footerStyle: { type: Object as PropType, default: undefined }, level: PropTypes.any, levelMove: { type: [Number, Array, Function] as PropType ILevelMove)> }, handle: PropTypes.any, /** @deprecated Use `@afterVisibleChange` instead */ afterVisibleChange: Function as PropType<(visible: boolean) => void>, onAfterVisibleChange: Function as PropType<(visible: boolean) => void>, 'onUpdate:visible': Function as PropType<(visible: boolean) => void>, onClose: Function as PropType, showBottom: { type: Boolean, default: true } }) export type DrawerProps = Partial>> export const DcDrawer = defineComponent({ name: 'DcDrawer', inheritAttrs: false, props: initDefaultProps(drawerProps(), { closable: true, placement: 'right' as placementType, maskClosable: true, mask: true, level: null, keyboard: true, push: defaultPushState }), slots: ['closeIcon', 'title', 'extra', 'footer', 'handle'], emits: ['update:visible', 'close', 'afterVisibleChange'], setup(props, { emit, slots, attrs }) { const sPush = ref(false) const destroyClose = ref(false) const vcDrawer = ref(null) const parentDrawerOpts = inject('parentDrawerOpts', null) const prefixCls = 'ant-drawer' // 是否是全屏 const isFullscreen = ref(false) devWarning(!props.afterVisibleChange, 'Drawer', '`afterVisibleChange` prop is deprecated, please use `@afterVisibleChange` event instead') devWarning(props.wrapStyle === undefined, 'Drawer', '`wrapStyle` prop is deprecated, please use `style` instead') devWarning(props.wrapClassName === undefined, 'Drawer', '`wrapClassName` prop is deprecated, please use `class` instead') const setPush = () => { sPush.value = true } const setPull = () => { sPush.value = false nextTick(() => { domFocus() }) } provide('parentDrawerOpts', { setPush, setPull }) onMounted(() => { const { visible } = props if (visible && parentDrawerOpts) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parentDrawerOpts.setPush() } }) onUnmounted(() => { if (parentDrawerOpts) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parentDrawerOpts.setPull() } }) watch( () => props.visible, visible => { if (parentDrawerOpts) { if (visible) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parentDrawerOpts.setPush() } else { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore parentDrawerOpts.setPull() } } }, { flush: 'post' } ) const domFocus = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore vcDrawer.value?.domFocus?.() } const expand = () => { isFullscreen.value = !isFullscreen.value } const close = (e: Event) => { isFullscreen.value = false emit('update:visible', false) emit('close', e) } const afterVisibleChange = (visible: boolean) => { props.afterVisibleChange?.(visible) emit('afterVisibleChange', visible) } const destroyOnClose = computed(() => props.destroyOnClose && !props.visible) const onDestroyTransitionEnd = () => { const isDestroyOnClose = destroyOnClose.value if (!isDestroyOnClose) { return } if (!props.visible) { destroyClose.value = true } } const pushTransform = computed(() => { const { push, placement } = props let distance: number | string if (typeof push === 'boolean') { distance = push ? defaultPushState.distance : 0 } else { distance = push!.distance } distance = parseFloat(String(distance || 0)) if (placement === 'left' || placement === 'right') { return `translateX(${placement === 'left' ? distance : -distance}px)` } if (placement === 'top' || placement === 'bottom') { return `translateY(${placement === 'top' ? distance : -distance}px)` } return null }) const offsetStyle = computed(() => { // https://github.com/ant-design/ant-design/issues/24287 const { visible, mask, placement, size = 'default', width, height } = props if (!visible && !mask) { return {} } const val: CSSProperties = {} if (placement === 'left' || placement === 'right') { const defaultWidth = size === 'large' ? 736 : 378 val.width = typeof width === 'undefined' ? defaultWidth : width val.width = typeof val.width === 'string' ? val.width : `${val.width}px` } else { const defaultHeight = size === 'large' ? 736 : 378 val.height = typeof height === 'undefined' ? defaultHeight : height val.height = typeof val.height === 'string' ? val.height : `${val.height}px` } return val }) const drawerStyle = computed(() => { const { zIndex, wrapStyle, mask, style } = props const val = mask ? {} : offsetStyle.value return { zIndex, transform: sPush.value ? pushTransform.value : undefined, ...val, ...wrapStyle, ...style } }) const renderHeader = (prefixCls: string) => { const { closable, headerStyle } = props const extra = getPropsSlot(slots, props, 'extra') const title = getPropsSlot(slots, props, 'title') if (!title && !closable) { return null } if (!props.showBottom) { headerStyle.borderWidth = 0 } else { headerStyle.borderWidth = '1px' } return (
{renderExpandIcon(prefixCls)}
{title &&
{title}
}
{extra &&
{extra}
} {renderCloseIcon(prefixCls)}
) } const renderExpandIcon = (prefixCls: string) => { const className = `${prefixCls}-expand-icon` if (isFullscreen.value) { return } else { return } } const renderCloseIcon = (prefixCls: string) => { const { closable } = props const $closeIcon = slots.closeIcon ? slots.closeIcon?.() : props.closeIcon return ( closable && ( ) ) } const renderBody = (prefixCls: string) => { if (destroyClose.value && !props.visible) { return null } destroyClose.value = false const { bodyStyle, drawerStyle } = props const containerStyle: CSSProperties = {} const isDestroyOnClose = destroyOnClose.value if (isDestroyOnClose) { // Increase the opacity transition, delete children after closing. containerStyle.opacity = 0 containerStyle.transition = 'opacity .3s' } // 如果不显示底部线条,那就不需要添加高度 if (!props.showBottom) { bodyStyle.paddingTop = 0 } else { bodyStyle.paddingTop = '24px' } return (
{renderHeader(prefixCls)}
{slots.default?.()}
{renderFooter(prefixCls)}
) } const renderFooter = (prefixCls: string) => { const footer = getPropsSlot(slots, props, 'footer') if (!footer) { return null } const footerClassName = `${prefixCls}-footer` return (
{footer}
) } return () => { const { visible, placement, mask, wrapClassName, class: className, ...rest } = props const val = mask ? offsetStyle.value : {} const haveMask = mask ? '' : 'no-mask' const vcDrawerProps: any = { ...attrs, ...omit(rest, [ 'size', 'closeIcon', 'closable', 'destroyOnClose', 'drawerStyle', 'headerStyle', 'bodyStyle', 'title', 'push', 'wrapStyle', 'onAfterVisibleChange', 'onClose', 'onUpdate:visible' ]), ...val, onClose: close, afterVisibleChange, handler: false, prefixCls: prefixCls, open: visible, showMask: mask, placement, class: classnames({ [className]: className, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore [wrapClassName]: !!wrapClassName, [haveMask]: !!haveMask }), style: drawerStyle.value, ref: vcDrawer } // 开启全屏 if (isFullscreen.value) { vcDrawerProps.width = '100%' } return ( props.handle : slots.handle, default: () => renderBody(prefixCls) }} > ) } } }) // export default withInstall(Drawer) // export { Drawer as DcDrawer }