init
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { DialogPortal, DialogRoot } from 'reka-ui'
|
||||
import { UiButton } from '../button'
|
||||
import UiDialogContent from './UiDialogContent.vue'
|
||||
import UiDialogDescription from './UiDialogDescription.vue'
|
||||
import UiDialogFooter from './UiDialogFooter.vue'
|
||||
import UiDialogHeader from './UiDialogHeader.vue'
|
||||
import UiDialogOverlay from './UiDialogOverlay.vue'
|
||||
import UiDialogTitle from './UiDialogTitle.vue'
|
||||
|
||||
export type UiActionDialogResult = 'ok' | 'reject' | 'cancel' | 'none'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible?: boolean
|
||||
header?: string | boolean
|
||||
mask?: boolean
|
||||
modal?: boolean
|
||||
drag?: 'header' | 'all' | 'none'
|
||||
okButton?: string | boolean
|
||||
rejectButton?: string | boolean
|
||||
cancelButton?: string | boolean
|
||||
closeOnEsc?: boolean
|
||||
defaultStatus?: UiActionDialogResult
|
||||
returnFocusElement?: HTMLElement | null
|
||||
scale?: number
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
header: true,
|
||||
mask: false,
|
||||
modal: false,
|
||||
drag: 'header',
|
||||
okButton: '确定',
|
||||
rejectButton: false,
|
||||
cancelButton: '取消',
|
||||
closeOnEsc: true,
|
||||
defaultStatus: 'cancel',
|
||||
scale: 1,
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
close: [status: UiActionDialogResult]
|
||||
ok: []
|
||||
reject: []
|
||||
cancel: []
|
||||
'update:visible': [visible: boolean]
|
||||
}>()
|
||||
|
||||
const result = ref<UiActionDialogResult>(props.defaultStatus)
|
||||
// Only a visible mask may make the dialog modal. A modal DialogRoot without an overlay
|
||||
// disables body pointer events and leaves the page looking inexplicably unresponsive.
|
||||
const hasBlockingOverlay = computed(() => props.mask)
|
||||
const contentStyle = computed<CSSProperties>(() => ({
|
||||
'--ui-dialog-scale': props.scale,
|
||||
'--ui-dialog-content-inline-size': 'max-content',
|
||||
'--ui-dialog-content-max-inline-size': 'var(--ui-dialog-viewport-inline-size)',
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) result.value = props.defaultStatus
|
||||
},
|
||||
)
|
||||
|
||||
function close(status: UiActionDialogResult = result.value) {
|
||||
result.value = status
|
||||
emits('update:visible', false)
|
||||
emits('close', status)
|
||||
}
|
||||
|
||||
function updateOpen(open: boolean) {
|
||||
if (open) emits('update:visible', true)
|
||||
else close()
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (!props.closeOnEsc) event.preventDefault()
|
||||
}
|
||||
|
||||
function handleInteractOutside(event: Event) {
|
||||
// With no mask and no outside-dismiss request, keep the window open while allowing
|
||||
// the underlying page to remain interactive. A visible mask always dismisses on click.
|
||||
if (!props.mask && !props.modal) event.preventDefault()
|
||||
}
|
||||
|
||||
function choose(status: Exclude<UiActionDialogResult, 'none'>) {
|
||||
if (status === 'ok') emits('ok')
|
||||
else if (status === 'reject') emits('reject')
|
||||
else emits('cancel')
|
||||
close(status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="visible" :modal="hasBlockingOverlay" @update:open="updateOpen">
|
||||
<DialogPortal>
|
||||
<UiDialogOverlay v-if="hasBlockingOverlay" />
|
||||
<UiDialogContent
|
||||
class="ui-action-dialog"
|
||||
:draggable="drag !== 'none'"
|
||||
:return-focus-element="returnFocusElement"
|
||||
:style="contentStyle"
|
||||
@escape-key-down="handleEscape"
|
||||
@interact-outside="handleInteractOutside"
|
||||
>
|
||||
<UiDialogHeader v-if="header" :closable="true">
|
||||
<UiDialogTitle>{{ header === true ? '提示' : header }}</UiDialogTitle>
|
||||
</UiDialogHeader>
|
||||
<UiDialogTitle v-else class="sr-only">提示</UiDialogTitle>
|
||||
<UiDialogDescription class="sr-only">{{
|
||||
header === true ? '提示' : header || '操作确认'
|
||||
}}</UiDialogDescription>
|
||||
|
||||
<div class="ui-action-dialog__body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<UiDialogFooter
|
||||
v-if="$slots.action || okButton || rejectButton || cancelButton"
|
||||
class="ui-action-dialog__footer"
|
||||
>
|
||||
<slot v-if="$slots.action" name="action" />
|
||||
<template v-else>
|
||||
<UiButton v-if="okButton" @click="choose('ok')">
|
||||
{{ okButton === true ? '确定' : okButton }}
|
||||
</UiButton>
|
||||
<UiButton v-if="rejectButton" variant="destructive" @click="choose('reject')">
|
||||
{{ rejectButton === true ? '拒绝' : rejectButton }}
|
||||
</UiButton>
|
||||
<UiButton v-if="cancelButton" variant="outline" @click="choose('cancel')">
|
||||
{{ cancelButton === true ? '取消' : cancelButton }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiDialogFooter>
|
||||
</UiDialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-action-dialog__body {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-width: min(100%, var(--ui-dialog-body-min-inline-size));
|
||||
min-height: var(--ui-dialog-body-min-block-size);
|
||||
max-width: var(--ui-dialog-viewport-inline-size);
|
||||
max-height: var(--ui-dialog-body-max-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ui-dialog-body-padding);
|
||||
overflow: auto;
|
||||
background: var(--ui-dialog-background);
|
||||
}
|
||||
|
||||
.ui-action-dialog__footer {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user