163 lines
5.0 KiB
Vue
163 lines
5.0 KiB
Vue
<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>
|