This commit is contained in:
oneao committed 2025-09-11 17:31:55 +08:00
1 parent b24015fbae
commit 6d4f1ee402
19 files changed
+782 -108

No files matched your search

@@ -0,0 +1,143 @@
@use '../_styles/_variables.scss' as *;
$drawer-prefix: '#{$prefix}-drawer';
.#{$drawer-prefix} {
z-index: 1500;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
outline: none;
box-sizing: border-box;
margin: 0;
padding: 0;
&-mask {
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.6);
opacity: 0;
transition: opacity 0.25s ease;
pointer-events: none;
}
&-wrapper {
position: absolute;
background: #fff;
display: flex;
flex-direction: column;
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
transition:
transform 0.25s ease,
opacity 0.25s ease;
}
&-header {
display: flex;
align-items: center;
min-height: 46px;
padding: 16px;
font-size: 16px;
font-weight: 600;
color: $text-color-base;
border-bottom: 1px solid #e8e8e8;
box-sizing: border-box;
.#{$drawer-prefix}-close {
flex-shrink: 0;
margin-left: auto;
color: $text-color-assist;
cursor: pointer;
border-radius: $border-radius;
width: 20px;
height: 20px;
&:hover {
background-color: $default-color-hover;
}
}
}
&-body {
flex: 1;
padding: 16px;
overflow: auto;
font-size: 14px;
}
&-footer {
width: 100%;
padding: 16px;
text-align: left;
border-top: 1px solid #e8e8e8;
box-sizing: border-box;
background-color: $bg-color-control;
display: flex;
gap: 8px;
}
&-right {
top: 0;
right: 0;
}
&-left {
top: 0;
left: 0;
}
&-top {
top: 0;
left: 0;
}
&-bottom {
bottom: 0;
left: 0;
}
&-open {
pointer-events: auto;
.#{$drawer-prefix}-mask {
opacity: 1;
pointer-events: auto;
}
&.#{$drawer-prefix}-right .#{$drawer-prefix}-wrapper {
transform: translateX(0);
}
&.#{$drawer-prefix}-left .#{$drawer-prefix}-wrapper {
transform: translateX(0);
}
&.#{$drawer-prefix}-top .#{$drawer-prefix}-wrapper {
transform: translateY(0);
}
&.#{$drawer-prefix}-bottom .#{$drawer-prefix}-wrapper {
transform: translateY(0);
}
}
&-right .#{$drawer-prefix}-wrapper {
top: 0;
right: 0;
height: 100%;
transform: translateX(100%);
}
&-left .#{$drawer-prefix}-wrapper {
top: 0;
left: 0;
height: 100%;
transform: translateX(-100%);
}
&-top .#{$drawer-prefix}-wrapper {
top: 0;
left: 0;
width: 100%;
transform: translateY(-100%);
}
&-bottom .#{$drawer-prefix}-wrapper {
bottom: 0;
left: 0;
width: 100%;
transform: translateY(100%);
}
}
@@ -0,0 +1,182 @@
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' }),
},
asyncConfirm: {
type: Boolean,
default: false,
},
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 = () => {
if (props.asyncConfirm) {
confirmLoading.value = true
emit('confirm', close, () => {
confirmLoading.value = false
})
}
else {
close()
emit('confirm')
}
}
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>
)
},
})