Files
workspace/code/base-project/base-vue/packages/ao-design/src/drawer/index.tsx
T
2025-09-18 17:28:22 +08:00

203 lines
5.4 KiB
TypeScript

import type { PropType } from 'vue'
import { defineComponent, onMounted, ref, Teleport } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Button from '../button/index'
import Icon from '../icon/index'
type DrawerSize = 'small' | 'medium' | 'large'
type SizeType = number | string | DrawerSize
export default defineComponent({
name: 'Drawer',
props: {
visible: {
type: Boolean,
default: false,
},
placement: {
type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
default: 'right',
},
size: {
type: [Number, String] as PropType<SizeType>,
default: 'medium',
},
title: {
type: String,
default: '',
},
showClose: {
type: Boolean,
default: false,
},
confirmBtn: {
type: null,
default: () => ({ label: '确认', type: 'solid', status: 'primary' }),
},
cancelBtn: {
type: null,
default: () => ({ label: '取消', type: 'solid', status: 'default' }),
},
beforeConfirm: {
type: Function as PropType<
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
>,
},
maskClose: {
type: Boolean,
default: true,
},
appendTo: {
type: [String, Object] as PropType<string | HTMLElement>,
default: 'body',
},
},
emits: ['update:visible', 'confirm', 'cancel'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('drawer')
const confirmLoading = ref<boolean>(false)
const close = () => {
emit('update:visible', false)
confirmLoading.value = false
}
const handleMaskClick = () => {
if (props.maskClose) {
close()
}
}
const resolveSize = (value: SizeType, defaultPx = 300) => {
if (typeof value === 'number')
return `${value}px`
if (typeof value === 'string') {
const presetMap: Record<DrawerSize, string> = {
small: '300px',
medium: '500px',
large: '700px',
}
if (presetMap[value as DrawerSize])
return presetMap[value as DrawerSize]
return value
}
return `${defaultPx}px`
}
const handleConfirm = async () => {
let allow = true
if (props.beforeConfirm) {
confirmLoading.value = true
try {
allow = await new Promise<boolean>((resolve) => {
const result = props.beforeConfirm!(ok => resolve(ok))
if (typeof result === 'boolean') {
resolve(result)
}
else if (result instanceof Promise) {
result.then(ok => resolve(ok)).catch(() => resolve(false))
}
// 如果是用 done 回调,就等调用方触发 resolve
})
}
catch {
allow = false
}
}
if (allow) {
close()
emit('confirm')
}
confirmLoading.value = false
}
const handleCancel = () => {
close()
emit('cancel')
}
// 挂载目标位置
const targetEl = ref<HTMLElement>(document.body)
onMounted(() => {
if (props.appendTo instanceof HTMLElement) {
targetEl.value = props.appendTo
}
else if (typeof props.appendTo === 'string') {
const el = document.querySelector(props.appendTo)
if (el)
targetEl.value = el as HTMLElement
}
})
// 判断是否是 body
const isBody = props.appendTo === 'body'
return () => (
<Teleport to={targetEl.value}>
<div
class={[prefixCls, `${prefixCls}-${props.placement}`, props.visible && `${prefixCls}-open`]}
style={{
position: isBody ? 'fixed' : 'absolute',
}}
>
<div class={`${prefixCls}-mask`} onClick={handleMaskClick}></div>
<div
class={[`${prefixCls}-wrapper`]}
style={{
width: ['left', 'right'].includes(props.placement)
? resolveSize(props.size)
: '100%',
height: ['top', 'bottom'].includes(props.placement)
? resolveSize(props.size)
: '100%',
}}
>
<div class={`${prefixCls}-header`}>
{slots.header
? slots.header()
: (
<>
<span>{props.title}</span>
{props.showClose && (
<Icon
class={`${prefixCls}-close`}
icon="ix:close"
onClick={() => emit('update:visible', false)}
/>
)}
</>
)}
</div>
<div class={`${prefixCls}-body`}>
{slots.default ? slots.default() : null}
</div>
<div class={`${prefixCls}-footer`}>
{slots.footer
? slots.footer()
: (
<>
<Button {...props.cancelBtn} block onClick={handleCancel}>
{props.cancelBtn.label}
</Button>
<Button loading={confirmLoading.value} block {...props.confirmBtn} onClick={handleConfirm}>
{props.confirmBtn.label}
</Button>
</>
)}
</div>
</div>
</div>
</Teleport>
)
},
})