Files
admin-web/src/shared/components/AppDialog.vue
T

152 lines
5.7 KiB
Vue

<script setup lang="ts">
import { Maximize2, Minimize2 } from '@lucide/vue'
import { computed, ref, watch } from 'vue'
export type DialogConfirmType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<{
title: string
width?: string | number
loading?: boolean
confirmLoading?: boolean
confirmDisabled?: boolean
confirmText?: string
cancelText?: string
confirmType?: DialogConfirmType
showFooter?: boolean
showCancel?: boolean
showConfirm?: boolean
showClose?: boolean
closeOnClickModal?: boolean
closeOnPressEscape?: boolean
destroyOnClose?: boolean
lockWhileSubmitting?: boolean
fullscreenable?: boolean
defaultFullscreen?: boolean
beforeClose?: (done: () => void) => void | Promise<void>
}>(), {
width: 'min(560px, calc(100vw - 28px))',
loading: false,
confirmLoading: false,
confirmDisabled: false,
confirmText: '确定',
cancelText: '取消',
confirmType: 'primary',
showFooter: true,
showCancel: true,
showConfirm: true,
showClose: true,
closeOnClickModal: false,
closeOnPressEscape: true,
destroyOnClose: false,
lockWhileSubmitting: true,
fullscreenable: false,
defaultFullscreen: false,
})
const open = defineModel<boolean>({ default: false })
const fullscreen = ref(props.defaultFullscreen)
const emit = defineEmits<{
confirm: []
cancel: []
}>()
const submittingLocked = computed(() => props.lockWhileSubmitting && props.confirmLoading)
watch(open, (value) => {
if (value) fullscreen.value = props.defaultFullscreen
})
function toggleFullscreen() {
fullscreen.value = !fullscreen.value
}
function closeDialog() {
if (submittingLocked.value) return
const done = () => {
open.value = false
emit('cancel')
}
if (props.beforeClose) void props.beforeClose(done)
else done()
}
</script>
<template>
<el-dialog
v-bind="$attrs"
v-model="open"
class="app-dialog"
:title="title"
:width="width"
:show-close="showClose && !submittingLocked"
:close-on-click-modal="closeOnClickModal && !submittingLocked"
:close-on-press-escape="closeOnPressEscape && !submittingLocked"
:destroy-on-close="destroyOnClose"
:before-close="beforeClose"
:fullscreen="fullscreenable && fullscreen"
:class="{ 'has-native-fullscreen-control': fullscreenable && !$slots.header }"
>
<template v-if="$slots.header" #header>
<div class="app-dialog__header">
<div class="app-dialog__header-content">
<slot name="header" :fullscreen="fullscreen" :toggle-fullscreen="toggleFullscreen" />
</div>
<el-tooltip v-if="fullscreenable" :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
<el-button text :aria-label="fullscreen ? '退出全屏' : '展开全屏'" @click="toggleFullscreen">
<Minimize2 v-if="fullscreen" :size="18" />
<Maximize2 v-else :size="18" />
</el-button>
</el-tooltip>
</div>
</template>
<el-tooltip v-if="fullscreenable && !$slots.header" :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
<el-button
class="app-dialog__native-fullscreen-control"
:class="{ 'is-without-close': !showClose || submittingLocked }"
text
:aria-label="fullscreen ? '退出全屏' : '展开全屏'"
@click="toggleFullscreen"
>
<Minimize2 v-if="fullscreen" :size="18" />
<Maximize2 v-else :size="18" />
</el-button>
</el-tooltip>
<div v-loading="loading" class="app-dialog__body" :aria-busy="loading || confirmLoading">
<slot />
</div>
<template v-if="showFooter" #footer>
<slot name="footer" :close="closeDialog" :submitting="confirmLoading">
<div class="app-dialog__footer">
<el-button v-if="showCancel" :disabled="submittingLocked" @click="closeDialog">{{ cancelText }}</el-button>
<el-button
v-if="showConfirm"
:type="confirmType"
:loading="confirmLoading"
:disabled="loading || confirmDisabled"
@click="emit('confirm')"
>{{ confirmText }}</el-button>
</div>
</slot>
</template>
</el-dialog>
</template>
<style scoped>
:global(.app-dialog.el-dialog) { position: relative; display: flex; max-height: min(90vh, 900px); flex-direction: column; overflow: hidden; }
:global(.app-dialog.el-dialog.is-fullscreen) { width: 100%; max-width: 100%; height: 100vh; max-height: 100vh; margin: 0 !important; border-radius: 0; }
:global(.app-dialog .el-dialog__header), :global(.app-dialog .el-dialog__footer) { flex: 0 0 auto; }
:global(.app-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
:global(.app-dialog.has-native-fullscreen-control .el-dialog__title) { display: block; padding-right: 44px; }
.app-dialog__header { display: flex; width: 100%; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; color: var(--cc-text); font-size: 18px; font-weight: 600; line-height: 24px; }
.app-dialog__header-content { min-width: 0; flex: 1; }
.app-dialog__header > :deep(.el-button) { flex: 0 0 auto; margin-right: 24px; color: var(--cc-muted); }
.app-dialog__native-fullscreen-control { position: absolute; z-index: 2; top: 10px; right: 48px; color: var(--cc-muted); }
.app-dialog__native-fullscreen-control.is-without-close { right: 12px; }
.app-dialog__body { min-height: 0; }
.app-dialog__footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
.app-dialog__footer :deep(.el-button) { margin-left: 0; }
@media (max-width: 720px) { :global(.app-dialog.el-dialog) { max-height: calc(100svh - 20px); margin-block: 10px !important; } .app-dialog__footer :deep(.el-button) { min-height: 44px; } }
</style>