feat: enhance media preview functionality in catalog components by adding support for multiple image URLs and custom titles
This commit is contained in:
parent
3792424d52
commit
f1f9162304
@ -1,16 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Package } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
|
||||
const props = defineProps<{
|
||||
items?: MediaItem[];
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items?: MediaItem[];
|
||||
customPreview?: boolean;
|
||||
title?: string;
|
||||
}>(),
|
||||
{
|
||||
items: () => [],
|
||||
customPreview: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'click-thumb': [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const previewOpen = ref(false);
|
||||
const previewUrl = ref<string | null>(null);
|
||||
|
||||
const urls = computed(() => props.items?.map((item) => item.url) ?? []);
|
||||
|
||||
function openPreview() {
|
||||
if (!props.items?.length) {
|
||||
return;
|
||||
@ -19,6 +33,14 @@ function openPreview() {
|
||||
previewUrl.value = props.items[0].url;
|
||||
previewOpen.value = true;
|
||||
}
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (props.customPreview) {
|
||||
emit('click-thumb', e);
|
||||
} else {
|
||||
openPreview();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -27,7 +49,7 @@ function openPreview() {
|
||||
class="size-10 shrink-0 overflow-hidden rounded border bg-muted/30"
|
||||
:class="items?.length ? 'cursor-zoom-in' : 'cursor-default'"
|
||||
:disabled="!items?.length"
|
||||
@click.stop="openPreview"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<img
|
||||
v-if="items?.length"
|
||||
@ -43,5 +65,5 @@ function openPreview() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" />
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="urls" :title="title" />
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -11,28 +13,153 @@ const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
'update:url': [url: string];
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
url: string | null;
|
||||
title?: string;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
url: string | null;
|
||||
urls?: string[];
|
||||
title?: string;
|
||||
}>(),
|
||||
{
|
||||
urls: () => [],
|
||||
title: 'Pratinjau Foto',
|
||||
}
|
||||
);
|
||||
|
||||
const activeUrl = ref<string | null>(props.url);
|
||||
|
||||
watch(
|
||||
() => props.url,
|
||||
(newUrl) => {
|
||||
activeUrl.value = newUrl;
|
||||
}
|
||||
);
|
||||
|
||||
watch(open, (val) => {
|
||||
if (!val) {
|
||||
emit('close');
|
||||
} else {
|
||||
activeUrl.value = props.url;
|
||||
}
|
||||
});
|
||||
|
||||
const allUrls = computed(() => {
|
||||
if (props.urls && props.urls.length > 0) {
|
||||
return props.urls;
|
||||
}
|
||||
|
||||
return props.url ? [props.url] : [];
|
||||
});
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
if (!activeUrl.value) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return allUrls.value.indexOf(activeUrl.value);
|
||||
});
|
||||
|
||||
const hasMultiple = computed(() => allUrls.value.length > 1);
|
||||
|
||||
function nextImage() {
|
||||
if (!hasMultiple.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIdx = (currentIndex.value + 1) % allUrls.value.length;
|
||||
const newUrl = allUrls.value[nextIdx];
|
||||
activeUrl.value = newUrl;
|
||||
emit('update:url', newUrl);
|
||||
}
|
||||
|
||||
function prevImage() {
|
||||
if (!hasMultiple.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevIdx = (currentIndex.value - 1 + allUrls.value.length) % allUrls.value.length;
|
||||
const newUrl = allUrls.value[prevIdx];
|
||||
activeUrl.value = newUrl;
|
||||
emit('update:url', newUrl);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!open.value || !hasMultiple.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
nextImage();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
prevImage();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}
|
||||
|
||||
function handleTouchEnd(e: TouchEvent) {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
const diff = touchStartX - touchEndX;
|
||||
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (diff > 0) {
|
||||
nextImage();
|
||||
} else {
|
||||
prevImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-wd p-2 sm:p-4">
|
||||
<DialogHeader class="sr-only">
|
||||
<DialogTitle>{{ title ?? 'Pratinjau Gambar' }}</DialogTitle>
|
||||
<DialogContent class="sm:max-w-2xl p-4 overflow-hidden gap-4">
|
||||
<DialogHeader class="pb-2 border-b">
|
||||
<DialogTitle class="text-base font-semibold">{{ title ?? 'Pratinjau Gambar' }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<img v-if="url" :src="url" :alt="title ?? 'Pratinjau gambar'"
|
||||
class="max-h-[80vh] w-full rounded-md object-contain">
|
||||
|
||||
<div class="relative flex items-center justify-center min-h-[300px] select-none"
|
||||
@touchstart="handleTouchStart" @touchend="handleTouchEnd">
|
||||
<img v-if="activeUrl" :key="activeUrl" :src="activeUrl" :alt="title ?? 'Pratinjau gambar'"
|
||||
class="max-h-[70vh] w-full rounded-md object-contain transition-all duration-300">
|
||||
</div>
|
||||
|
||||
<!-- Bottom Navigation Bar -->
|
||||
<div v-if="hasMultiple" class="mt-4 flex items-center justify-between gap-4 border-t pt-3">
|
||||
<Button type="button" variant="outline" size="sm" class="flex items-center gap-1 shrink-0"
|
||||
@click.stop="prevImage">
|
||||
<ChevronLeft class="size-4" />
|
||||
Sebelumnya
|
||||
</Button>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-1.5">
|
||||
<button v-for="(_, idx) in allUrls" :key="idx" type="button"
|
||||
class="size-2 rounded-full transition-all duration-200"
|
||||
:class="idx === currentIndex ? 'bg-primary w-4' : 'bg-muted-foreground/30'"
|
||||
@click="activeUrl = allUrls[idx]; emit('update:url', allUrls[idx]);" />
|
||||
</div>
|
||||
|
||||
<Button type="button" variant="outline" size="sm" class="flex items-center gap-1 shrink-0"
|
||||
@click.stop="nextImage">
|
||||
Berikutnya
|
||||
<ChevronRight class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@ -12,6 +12,7 @@ import type { MediaItem } from '@/types/media';
|
||||
const props = defineProps<{
|
||||
items: MediaItem[];
|
||||
maxVisible?: number;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const previewOpen = ref(false);
|
||||
@ -118,5 +119,5 @@ function openGallery() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" @close="onPreviewClose" />
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="items.map(item => item.url)" :title="title" @close="onPreviewClose" />
|
||||
</template>
|
||||
|
||||
@ -184,7 +184,7 @@ watch(open, (value) => {
|
||||
class="flex flex-col gap-2 rounded-md border p-2 sm:flex-row sm:items-center"
|
||||
:class="item.is_initial ? 'border-primary bg-primary/5' : ''">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<PosCatalogVariantThumb :items="item.images" />
|
||||
<PosCatalogVariantThumb :items="item.images" :title="`${item.raw_material_name} - ${item.variant}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ item.raw_material_name }} - {{ item.variant }}
|
||||
@ -238,7 +238,7 @@ watch(open, (value) => {
|
||||
:class="[
|
||||
isSelected(price.id) ? 'border-2 border-primary bg-primary/5' : 'cursor-pointer hover:bg-muted/30',
|
||||
]" @click="!isSelected(price.id) && toggleVariant(rawMaterial, price)">
|
||||
<PosCatalogVariantThumb :items="price.images" />
|
||||
<PosCatalogVariantThumb :items="price.images" :title="`${rawMaterial.name} - ${price.variant}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm">{{ price.variant }}</p>
|
||||
<p class="text-xs tabular-nums text-muted-foreground">
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Layers, Minus, Plus, Search } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
||||
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -27,7 +28,7 @@ import CuttingPosCombinationDialog from './CuttingPosCombinationDialog.vue';
|
||||
import QuickCreateRawMaterialModal from './QuickCreateRawMaterialModal.vue';
|
||||
import type { CuttingCatalogPrice } from './useCuttingPosCart';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
filteredRawMaterials: CuttingRawMaterialCatalogItem[];
|
||||
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
||||
materialCart: CuttingMaterialCartItem[];
|
||||
@ -35,6 +36,47 @@ defineProps<{
|
||||
units: EnumOption[];
|
||||
}>();
|
||||
|
||||
const previewOpen = ref(false);
|
||||
const previewUrl = ref<string | null>(null);
|
||||
const previewTitle = ref<string>('');
|
||||
|
||||
const allVariantImages = computed(() => {
|
||||
const list: { priceId: number; title: string; url: string }[] = [];
|
||||
props.filteredRawMaterials.forEach((rawMaterial) => {
|
||||
rawMaterial.prices.forEach((price) => {
|
||||
if (price.images && price.images.length > 0) {
|
||||
list.push({
|
||||
priceId: price.id,
|
||||
title: `${rawMaterial.name} - ${price.variant}`,
|
||||
url: price.images[0].url,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
const previewUrls = computed(() => allVariantImages.value.map((item) => item.url));
|
||||
|
||||
function handleThumbClick(priceId: number) {
|
||||
const idx = allVariantImages.value.findIndex((item) => item.priceId === priceId);
|
||||
|
||||
if (idx !== -1) {
|
||||
previewUrl.value = allVariantImages.value[idx].url;
|
||||
previewTitle.value = allVariantImages.value[idx].title;
|
||||
previewOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
watch(previewUrl, (newUrl) => {
|
||||
const matched = allVariantImages.value.find((item) => item.url === newUrl);
|
||||
|
||||
if (matched) {
|
||||
previewTitle.value = matched.title;
|
||||
}
|
||||
});
|
||||
|
||||
const materialSearch = defineModel<string>('materialSearch', { required: true });
|
||||
const selectedRawMaterialId = defineModel<string>('selectedRawMaterialId', { required: true });
|
||||
|
||||
@ -102,67 +144,63 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
|
||||
|
||||
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
|
||||
<template v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id">
|
||||
<PosCatalogCard v-if="rawMaterial.prices.length > 0"
|
||||
:title="rawMaterial.name" :cover-image="getFirstCoverImage(rawMaterial.prices)">
|
||||
<template #header-extra>
|
||||
<Badge variant="secondary" class="mt-1.5">
|
||||
{{ rawMaterial.unit_label }}
|
||||
</Badge>
|
||||
</template>
|
||||
<PosCatalogCard v-if="rawMaterial.prices.length > 0" :title="rawMaterial.name"
|
||||
:cover-image="getFirstCoverImage(rawMaterial.prices)">
|
||||
<template #header-extra>
|
||||
<Badge variant="secondary" class="mt-1.5">
|
||||
{{ rawMaterial.unit_label }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
|
||||
Belum ada varian
|
||||
</p>
|
||||
<div v-for="price in rawMaterial.prices" :key="price.id"
|
||||
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" :class="[
|
||||
'cursor-pointer hover:bg-muted/30',
|
||||
getMaterialCartItem(price.id)
|
||||
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
||||
: '',
|
||||
]" @click="!getMaterialCartItem(price.id) && emit('add-material', rawMaterial, price)">
|
||||
<PosCatalogVariantThumb :items="price.images" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ price.variant }}
|
||||
</p>
|
||||
<p class="text-xs tabular-nums text-muted-foreground">
|
||||
Stok: {{ price.stock_formatted }}
|
||||
</p>
|
||||
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
|
||||
Belum ada varian
|
||||
</p>
|
||||
<div v-for="price in rawMaterial.prices" :key="price.id"
|
||||
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" :class="[
|
||||
'cursor-pointer hover:bg-muted/30',
|
||||
getMaterialCartItem(price.id)
|
||||
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
||||
: '',
|
||||
]" @click="!getMaterialCartItem(price.id) && emit('add-material', rawMaterial, price)">
|
||||
<PosCatalogVariantThumb :items="price.images" custom-preview
|
||||
@click-thumb="handleThumbClick(price.id)" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ price.variant }}
|
||||
</p>
|
||||
<p class="text-xs tabular-nums text-muted-foreground">
|
||||
Stok: {{ price.stock_formatted }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="getMaterialCartItem(price.id)" class="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="icon-sm" title="Kurangi Pemakaian"
|
||||
@click.stop="emit('decrease-material-qty', price.id)">
|
||||
<Minus class="size-3.5" />
|
||||
</Button>
|
||||
<span class="min-w-5 text-center text-xs font-semibold tabular-nums">
|
||||
{{ getMaterialCartItem(price.id)!.material_usage }}
|
||||
</span>
|
||||
<Button type="button" variant="outline" size="icon-sm" title="Tambah ke Keranjang"
|
||||
@click.stop="emit('add-material', rawMaterial, price)">
|
||||
<Plus class="size-3.5" />
|
||||
</Button>
|
||||
<Button type="button" variant="default" size="icon-sm" title="Gunakan Kombinasi"
|
||||
@click.stop="openCombination(rawMaterial, price)">
|
||||
<Layers class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="icon-sm" title="Tambah ke Keranjang"
|
||||
@click.stop="emit('add-material', rawMaterial, price)">
|
||||
<Plus class="size-3.5" />
|
||||
</Button>
|
||||
<Button type="button" variant="default" size="icon-sm" title="Gunakan Kombinasi"
|
||||
@click.stop="openCombination(rawMaterial, price)">
|
||||
<Layers class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getMaterialCartItem(price.id)" class="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="icon-sm"
|
||||
title="Kurangi Pemakaian"
|
||||
@click.stop="emit('decrease-material-qty', price.id)">
|
||||
<Minus class="size-3.5" />
|
||||
</Button>
|
||||
<span class="min-w-5 text-center text-xs font-semibold tabular-nums">
|
||||
{{ getMaterialCartItem(price.id)!.material_usage }}
|
||||
</span>
|
||||
<Button type="button" variant="outline" size="icon-sm"
|
||||
title="Tambah ke Keranjang"
|
||||
@click.stop="emit('add-material', rawMaterial, price)">
|
||||
<Plus class="size-3.5" />
|
||||
</Button>
|
||||
<Button type="button" variant="default" size="icon-sm"
|
||||
title="Gunakan Kombinasi"
|
||||
@click.stop="openCombination(rawMaterial, price)">
|
||||
<Layers class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="icon-sm"
|
||||
title="Tambah ke Keranjang"
|
||||
@click.stop="emit('add-material', rawMaterial, price)">
|
||||
<Plus class="size-3.5" />
|
||||
</Button>
|
||||
<Button type="button" variant="default" size="icon-sm"
|
||||
title="Gunakan Kombinasi"
|
||||
@click.stop="openCombination(rawMaterial, price)">
|
||||
<Layers class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PosCatalogCard>
|
||||
</PosCatalogCard>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@ -175,4 +213,6 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
|
||||
<CuttingPosCombinationDialog v-model:open="combinationOpen" :raw-material-catalog="rawMaterialCatalog"
|
||||
:existing-cart-items="materialCart" :initial-variant="combinationInitialVariant"
|
||||
@combination-created="emit('combination-created', $event)" />
|
||||
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" />
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Minus, Plus, Search } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
||||
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
@ -18,7 +19,7 @@ import type { CategoryOption } from '@/types/product';
|
||||
import QuickCreateProductModal from './QuickCreateProductModal.vue';
|
||||
import type { CuttingCatalogVariant } from './useCuttingPosCart';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
filteredProducts: CuttingProductCatalogItem[];
|
||||
getResultCartItem: (variantId: number) => CuttingResultCartItem | undefined;
|
||||
isCreateMode: boolean;
|
||||
@ -26,6 +27,47 @@ defineProps<{
|
||||
materialCart: CuttingMaterialCartItem[];
|
||||
}>();
|
||||
|
||||
const previewOpen = ref(false);
|
||||
const previewUrl = ref<string | null>(null);
|
||||
const previewTitle = ref<string>('');
|
||||
|
||||
const allVariantImages = computed(() => {
|
||||
const list: { variantId: number; title: string; url: string }[] = [];
|
||||
props.filteredProducts.forEach((product) => {
|
||||
product.variants.forEach((variant) => {
|
||||
if (variant.images && variant.images.length > 0) {
|
||||
list.push({
|
||||
variantId: variant.id,
|
||||
title: `${product.name} - ${variant.name}`,
|
||||
url: variant.images[0].url,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
const previewUrls = computed(() => allVariantImages.value.map((item) => item.url));
|
||||
|
||||
function handleThumbClick(variantId: number) {
|
||||
const idx = allVariantImages.value.findIndex((item) => item.variantId === variantId);
|
||||
|
||||
if (idx !== -1) {
|
||||
previewUrl.value = allVariantImages.value[idx].url;
|
||||
previewTitle.value = allVariantImages.value[idx].title;
|
||||
previewOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
watch(previewUrl, (newUrl) => {
|
||||
const matched = allVariantImages.value.find((item) => item.url === newUrl);
|
||||
|
||||
if (matched) {
|
||||
previewTitle.value = matched.title;
|
||||
}
|
||||
});
|
||||
|
||||
const productSearch = defineModel<string>('productSearch', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -77,7 +119,8 @@ const quickCreateOpen = ref(false);
|
||||
? 'mx-1 my-0.5 rounded-md border-2 border-primary bg-primary/5'
|
||||
: '',
|
||||
]" @click="!getResultCartItem(variant.id) && emit('add-result', product, variant)">
|
||||
<PosCatalogVariantThumb :items="variant.images" />
|
||||
<PosCatalogVariantThumb :items="variant.images" custom-preview
|
||||
@click-thumb="handleThumbClick(variant.id)" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ variant.name }}
|
||||
@ -124,4 +167,6 @@ const quickCreateOpen = ref(false);
|
||||
|
||||
<QuickCreateProductModal v-model:open="quickCreateOpen" :categories="categories" :selected-materials="materialCart"
|
||||
@created="emit('product-created', $event)" />
|
||||
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="previewUrls" :title="previewTitle" />
|
||||
</template>
|
||||
|
||||
@ -265,8 +265,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
{{ material.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="material.images ?? []
|
||||
" :max-visible="1" />
|
||||
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" :title="`${group.rawMaterialName} - ${material.variant}`" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{
|
||||
@ -323,9 +322,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
{{ result.product_variant?.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="result.product_variant
|
||||
?.images ?? []
|
||||
" :max-visible="1" />
|
||||
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" :title="`${group.productName} - ${result.product_variant?.name}`" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ result.cutting_result }} pcs
|
||||
|
||||
@ -101,7 +101,7 @@ const emit = defineEmits<{
|
||||
]"
|
||||
@click="getVariantPrice(variant) && !getCartItem(variant.id) && emit('add-to-cart', product, variant)"
|
||||
>
|
||||
<PosCatalogVariantThumb :items="variant.images" />
|
||||
<PosCatalogVariantThumb :items="variant.images" :title="`${product.name} - ${variant.name}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ variant.name }}
|
||||
|
||||
@ -91,7 +91,7 @@ const emit = defineEmits<{
|
||||
]"
|
||||
@click="!getCartItem(price.id) && emit('add-to-cart', rawMaterial, price)"
|
||||
>
|
||||
<PosCatalogVariantThumb :items="price.images" />
|
||||
<PosCatalogVariantThumb :items="price.images" :title="`${rawMaterial.name} - ${price.variant}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ price.variant }}
|
||||
|
||||
@ -148,7 +148,7 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
{{ variant.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" :title="`${product.name} - ${variant.name}`" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ variant.stock_formatted }}
|
||||
|
||||
@ -130,7 +130,7 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
{{ price.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" :title="`${material.name} - ${price.variant}`" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.stock_formatted }}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user