This commit is contained in:
2026-01-28 22:37:27 +03:00
parent cbaa1cda9d
commit 53ca63ec91
64 changed files with 4375 additions and 2980 deletions

View File

@@ -1,98 +1,125 @@
<template>
<q-dialog
v-model="modelValue"
maximized
transition-show="slide-up"
transition-hide="slide-down"
position="bottom"
>
>
<q-card
class="fix-card-width flex column no-scroll no-wrap q-px-none"
ref="cardRef"
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
style="
border-top-left-radius: var(--top-radius) !important;
border-top-right-radius: var(--top-radius) !important;
max-height: calc(100vh - 48px);
max-height: calc(100vh - 48px - var(--tg-content-safe-area-inset-top) - var(--tg-safe-area-inset-top));
"
>
<div
ref="cardHeaderRef"
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
>
<div class="text-h6 q-mx-xs ellipsis">{{ $t(title) }}</div>
<div class="flex items-center justify-between no-wrap">
<slot name="btnSlot">
<q-btn
icon="mdi-close"
@click="modelValue = false"
flat round
/>
</slot>
</div>
</div>
<div
ref="cardBodyRef"
class="q-px-none q-ma-none"
>
<pn-shadow-scroll
:hideShadows="false"
:height="bodyHeight"
>
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
<q-resize-observer @resize="updateDimensions" />
<slot/>
</div>
</pn-shadow-scroll>
</div>
<div
style="overflow: hidden"
:style="{ height: footerHeight + 'px'}"
>
<div ref="cardFooterInnerRef" class="q-pa-md">
<q-resize-observer @resize="updateDimensions" />
<slot name="footer"/>
<div ref="cardHeaderRef" class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md">
<q-resize-observer @resize="onHeaderResize" />
<div class="flex column q-mx-xs">
<span class="text-h6" style="line-height: 1.5rem;">
{{ $t(title) }}
</span>
<span v-if="caption" class="text-grey text-caption">
{{ $t(caption) }}
</span>
</div>
<div class="flex items-center justify-between no-wrap">
<slot name="btnSlot">
<q-btn icon="mdi-close" @click="modelValue = false" flat round />
</slot>
</div>
</div>
<div class="q-px-none q-ma-none">
<pn-shadow-scroll
:height="bodyHeight"
:bottomOffset
:topOffset
>
<div class="q-pa-none q-ma-none">
<q-resize-observer @resize="onInnerBodyResize" />
<slot />
</div>
</pn-shadow-scroll>
</div>
<div v-if="hasFooter" class="absolute-bottom">
<q-resize-observer @resize="onFooterResize" />
<div class="q-pa-md">
<slot name="footer" />
</div>
<div :style="{ height: tgInsetBottom + 'px' }" />
</div>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useSlots } from 'vue'
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
const modelValue = defineModel<boolean>({
required: true
})
defineProps<{
const modelValue = defineModel<boolean>({ required: true })
const props = withDefaults(defineProps<{
title: string
}>()
caption?: string
maximized?: boolean
}>(), { maximized: true })
const slots = useSlots()
const cardHeaderRef = ref<HTMLElement | null>(null)
const cardFooterInnerRef = ref<HTMLElement | null>(null)
const cardBodyInnerRef = ref<HTMLElement | null>(null)
const cardRef = ref()
const headerHeight = ref(0)
const footerHeight = ref(0)
const bodyInnerHeight = ref(0)
const bodyHeight = ref(0)
const tgInsetBottom = ref(0)
const topOffset = 16
const updateDimensions = async () => {
await nextTick(() => {
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
footerHeight.value = cardFooterInnerRef.value?.offsetHeight || 0
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
bodyHeight.value = needScroll
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
: bodyInnerHeight.value + 16
})
const windowHeight = ref(window.innerHeight)
const hasFooter = computed(() => !!slots['footer'])
const bottomOffset = computed(() => hasFooter.value ? footerHeight.value : tgInsetBottom.value)
const bodyHeight = computed(() => {
// hack for update computed params on window resize
const _trigger = windowHeight.value //eslint-disable-line
const el = cardRef.value?.$el
if (!el) return 0
const maxH = parseFloat(window.getComputedStyle(el).maxHeight) || 0
return props.maximized
? maxH - headerHeight.value
: Math.min(
bodyInnerHeight.value + topOffset + (hasFooter.value ? footerHeight.value : 0),
maxH - headerHeight.value
)
})
interface SizeParams { height: number; width: number }
const onHeaderResize = (size: SizeParams) => headerHeight.value = size.height
const onFooterResize = (size: SizeParams) => footerHeight.value = hasFooter.value ? size.height : 0
const onInnerBodyResize = (size: SizeParams) => bodyInnerHeight.value = size.height
const handleWindowResize = () => {
windowHeight.value = window.innerHeight
}
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
watch(modelValue, updateDimensions)
const getTgInsetBottom = () => {
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--tg-safe-area-inset-bottom')
.trim()
tgInsetBottom.value = parseFloat(value) || 0
}
onMounted(() => getTgInsetBottom())
onMounted(() => {
window.addEventListener('resize', handleWindowResize)
getTgInsetBottom()
})
onUnmounted(() => window.removeEventListener('resize', handleWindowResize))
</script>