feat: enhance MediaThumbnailCell and MediaPreviewDialog components with gallery functionality and improved item display; add close event emission for better dialog management

This commit is contained in:
Yoga Pangestu 2026-07-02 10:37:02 +07:00
parent fff6b147be
commit 78e30c2aba
4 changed files with 107 additions and 7 deletions

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { watch } from 'vue';
import {
Dialog,
DialogContent,
@ -8,10 +9,20 @@ import {
const open = defineModel<boolean>('open', { default: false });
const emit = defineEmits<{
close: [];
}>();
defineProps<{
url: string | null;
title?: string;
}>();
watch(open, (val) => {
if (!val) {
emit('close');
}
});
</script>
<template>

View File

@ -1,6 +1,12 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import type { MediaItem } from '@/types/media';
const props = defineProps<{
@ -10,14 +16,78 @@ const props = defineProps<{
const previewOpen = ref(false);
const previewUrl = ref<string | null>(null);
const galleryOpen = ref(false);
const cameFromGallery = ref(false);
const visibleItems = computed(() => props.items.slice(0, props.maxVisible ?? 3));
const hiddenCount = computed(() => Math.max(props.items.length - visibleItems.value.length, 0));
const galleryWidthClass = computed(() => {
const count = props.items.length;
if (count <= 1) {
return 'sm:max-w-xs';
}
if (count <= 2) {
return 'sm:max-w-sm';
}
if (count <= 4) {
return 'sm:max-w-md';
}
if (count <= 8) {
return 'sm:max-w-lg';
}
return 'sm:max-w-xl';
});
const galleryGridClass = computed(() => {
const count = props.items.length;
if (count === 1) {
return 'grid-cols-1';
}
if (count === 2) {
return 'grid-cols-2';
}
if (count === 3) {
return 'grid-cols-3';
}
return 'grid-cols-4';
});
function openPreview(url: string) {
previewUrl.value = url;
previewOpen.value = true;
}
function openPreviewFromGallery(url: string) {
cameFromGallery.value = true;
galleryOpen.value = false;
setTimeout(() => {
previewUrl.value = url;
previewOpen.value = true;
}, 200);
}
function onPreviewClose() {
if (cameFromGallery.value) {
cameFromGallery.value = false;
setTimeout(() => {
galleryOpen.value = true;
}, 200);
}
}
function openGallery() {
galleryOpen.value = true;
}
</script>
<template>
@ -26,10 +96,27 @@ function openPreview(url: string) {
class="size-8 overflow-hidden rounded border bg-muted/30" @click="openPreview(item.url)">
<img :src="item.thumb_url" alt="Foto" class="size-full object-cover">
</button>
<span v-if="hiddenCount > 0" class="text-muted-foreground text-xs">
<button v-if="hiddenCount > 0" type="button"
class="flex size-8 items-center justify-center rounded border bg-muted/30 text-muted-foreground text-xs font-medium hover:bg-muted/50 transition-colors"
@click="openGallery">
+{{ hiddenCount }}
</span>
</button>
</div>
<MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" />
<Dialog v-model:open="galleryOpen">
<DialogContent :class="galleryWidthClass">
<DialogHeader>
<DialogTitle>Semua Foto ({{ items.length }})</DialogTitle>
</DialogHeader>
<div class="grid gap-2 max-h-[60vh] overflow-y-auto p-1" :class="galleryGridClass">
<button v-for="item in items" :key="item.id" type="button"
class="aspect-square overflow-hidden rounded border bg-muted/30"
@click="openPreviewFromGallery(item.url)">
<img :src="item.thumb_url" alt="Foto" class="size-full object-cover">
</button>
</div>
</DialogContent>
</Dialog>
<MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" @close="onPreviewClose" />
</template>

View File

@ -86,8 +86,10 @@ function openVerificationDetail(requestId: number | undefined) {
</h3>
</div>
<p v-if="product.has_pending_request" class="text-sm text-amber-600">
{{ product.pending_request_submitted_by_name }} mengajukan {{ product.pending_request_action_label?.toLowerCase() }} produk ini
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(product.pending_request_id)">lihat</button>
{{ product.pending_request_submitted_by_name }} mengajukan {{
product.pending_request_action_label?.toLowerCase() }} produk ini
<button type="button" class="underline-offset-2 hover:underline"
@click="openVerificationDetail(product.pending_request_id)">lihat</button>
</p>
<div v-if="product.categories.length" class="flex flex-wrap gap-1">
<Badge v-for="category in product.categories" :key="category.id" variant="outline">
@ -142,7 +144,7 @@ function openVerificationDetail(requestId: number | undefined) {
{{ variant.name }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="variant.images ?? []" />
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" />
</TableCell>
<TableCell class="tabular-nums">
{{ variant.stock_formatted }}

View File

@ -126,7 +126,7 @@ function openVerificationDetail(requestId: number | undefined) {
{{ price.variant }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="price.images ?? []" />
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" />
</TableCell>
<TableCell class="tabular-nums">
{{ price.stock_formatted }}