Files
dcalc/frontend-ui/src/components/ui/dialog/UiActionDialog.vue
T
2026-08-09 00:33:21 +08:00

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>