fix(gallery): add ImageGallery to property create/edit page and modernize gallery UI
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -6,6 +6,7 @@ import { CategoryService } from '@/service/CategoryService';
|
|||||||
import { PropertyService } from '@/service/PropertyService';
|
import { PropertyService } from '@/service/PropertyService';
|
||||||
import { PropertyStatusService } from '@/service/PropertyStatusService';
|
import { PropertyStatusService } from '@/service/PropertyStatusService';
|
||||||
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
import ImageUploader from '@/views/uikit/ImageUploader.vue';
|
||||||
|
import ImageGallery from '@/views/uikit/ImageGallery.vue';
|
||||||
import KiwiChips from '@/views/uikit/KiwiChips.vue';
|
import KiwiChips from '@/views/uikit/KiwiChips.vue';
|
||||||
import MapComponent from '@/views/uikit/MapComponent.vue';
|
import MapComponent from '@/views/uikit/MapComponent.vue';
|
||||||
import { UTMtoLatLon } from '@/utm';
|
import { UTMtoLatLon } from '@/utm';
|
||||||
@@ -862,6 +863,10 @@ const saveProperty = async () => {
|
|||||||
<span>اسناد و مدارک تصویری / پیدیاف ملک</span>
|
<span>اسناد و مدارک تصویری / پیدیاف ملک</span>
|
||||||
</div>
|
</div>
|
||||||
<ImageUploader v-model="documentUrlsWithData" bucketName="documents" :maxFiles="10" />
|
<ImageUploader v-model="documentUrlsWithData" bucketName="documents" :maxFiles="10" />
|
||||||
|
<div v-if="documentUrlsWithData && documentUrlsWithData.length > 0" class="pt-2">
|
||||||
|
<label class="block text-xs font-semibold text-surface-600 dark:text-surface-400 mb-2">فایلها و اسناد بارگذاریشده ({{ documentUrlsWithData.length }} مورد)</label>
|
||||||
|
<ImageGallery v-model="documentUrlsWithData" galleryId="propertyDocumentsGallery" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -900,6 +905,10 @@ const saveProperty = async () => {
|
|||||||
<span>تصاویر ملک</span>
|
<span>تصاویر ملک</span>
|
||||||
</div>
|
</div>
|
||||||
<ImageUploader v-model="mainImageUrlsWithData" bucketName="properties" :maxFiles="10" />
|
<ImageUploader v-model="mainImageUrlsWithData" bucketName="properties" :maxFiles="10" />
|
||||||
|
<div v-if="mainImageUrlsWithData && mainImageUrlsWithData.length > 0" class="pt-2">
|
||||||
|
<label class="block text-xs font-semibold text-surface-600 dark:text-surface-400 mb-2">گالری تصاویر ({{ mainImageUrlsWithData.length }} مورد)</label>
|
||||||
|
<ImageGallery v-model="mainImageUrlsWithData" galleryId="propertyMainImagesGallery" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,86 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="galleryId" class="pswp-gallery pswp-gallery--single-column my-3 flex min-w-0 flex-row flex-nowrap overflow-x-auto rounded-md bg-brand-dark-3 p-3">
|
<div
|
||||||
<div v-for="(item, key) in itemsData" :key="key" class="relative mx-3 flex-shrink-0" draggable="true" @dragstart="startDrag($event, key)" @dragover.prevent @dragenter.prevent @drop="onDrop($event, key)">
|
:id="galleryId"
|
||||||
<!-- Image Display -->
|
class="pswp-gallery flex flex-wrap gap-3 p-3 rounded-xl bg-surface-50 dark:bg-surface-800/60 border border-surface-200 dark:border-surface-700/60 min-h-[90px] items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, key) in itemsData"
|
||||||
|
:key="key"
|
||||||
|
class="group relative flex flex-col items-center justify-between rounded-xl border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900 p-2 shadow-sm transition-all hover:shadow-md overflow-hidden"
|
||||||
|
:class="isImageFile(item.url) ? 'w-36 h-36 sm:w-40 sm:h-40' : 'w-44 h-36'"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="startDrag($event, key)"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent
|
||||||
|
@drop="onDrop($event, key)"
|
||||||
|
>
|
||||||
|
<!-- Image Thumbnail & PhotoSwipe Link -->
|
||||||
<template v-if="isImageFile(item.url)">
|
<template v-if="isImageFile(item.url)">
|
||||||
<a :href="item.url" :data-pswp-width="item.width" :data-pswp-height="item.height" target="_blank" rel="noreferrer" class="block">
|
<a
|
||||||
<img :src="item.url" alt="" class="max-h-60 rounded-md" :style="{ height: '150px' }" />
|
:href="item.url"
|
||||||
|
:data-pswp-width="item.width || 1200"
|
||||||
|
:data-pswp-height="item.height || 800"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="pswp-link w-full h-full flex items-center justify-center overflow-hidden rounded-lg relative cursor-pointer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="item.url"
|
||||||
|
:alt="item.name || 'Image'"
|
||||||
|
class="w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-105"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white">
|
||||||
|
<i class="pi pi-search-plus text-xl"></i>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- File Display -->
|
<!-- Non-Image File (PDF, DOC, etc.) -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex h-[150px] w-[200px] flex-col items-center justify-center rounded-md border-2 border-gray-200 bg-white p-4">
|
<div class="w-full h-full flex flex-col items-center justify-center p-2 text-center">
|
||||||
<!-- File Icon -->
|
<i :class="[getFileIconClass(item.url), getFileIconColor(item.url), 'text-3xl mb-2']"></i>
|
||||||
<font-awesome-icon :icon="getFileIcon(item.url)" class="mb-2 text-4xl" :class="getIconColor(item.url)" />
|
<p class="text-xs font-medium text-surface-700 dark:text-surface-200 truncate max-w-full px-1 mb-2" :title="getFileName(item.url)">
|
||||||
|
|
||||||
<!-- File Name -->
|
|
||||||
<p class="mb-2 max-w-full truncate text-sm font-medium text-gray-700">
|
|
||||||
{{ getFileName(item.url) }}
|
{{ getFileName(item.url) }}
|
||||||
</p>
|
</p>
|
||||||
|
<a
|
||||||
<!-- Download Button -->
|
:href="item.url"
|
||||||
<Button icon="pi pi-download" severity="secondary" size="small" @click="downloadFile(item.url)" class="p-button-sm"> دانلود </Button>
|
target="_blank"
|
||||||
|
download
|
||||||
|
class="inline-flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-lg bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 text-surface-700 dark:text-surface-200 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="pi pi-download text-[10px]"></i>
|
||||||
|
<span>دانلود</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Remove Button -->
|
<!-- Remove Button -->
|
||||||
<button class="absolute right-4 top-2 flex h-6 w-6 items-center justify-center rounded-full text-red-500" @click="removeItem(key, item.url)">
|
<button
|
||||||
<font-awesome-icon icon="fa-solid fa-x" />
|
type="button"
|
||||||
|
class="absolute top-1.5 right-1.5 z-10 w-6 h-6 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center shadow-md opacity-90 hover:opacity-100 transition-all cursor-pointer"
|
||||||
|
title="حذف"
|
||||||
|
@click.stop="removeItem(key, item.url)"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times text-[10px]"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
import 'photoswipe/style.css';
|
import 'photoswipe/style.css';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|
||||||
import { Button } from 'primevue';
|
|
||||||
|
|
||||||
// File type checking functions
|
const props = defineProps({
|
||||||
|
galleryId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'delete-image']);
|
||||||
|
|
||||||
|
const itemsData = ref([]);
|
||||||
|
let lightbox = null;
|
||||||
|
|
||||||
function isImageFile(url) {
|
function isImageFile(url) {
|
||||||
const extension = url.toLowerCase().split('.').pop();
|
if (!url || typeof url !== 'string') return false;
|
||||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension);
|
const cleanUrl = url.split('?')[0];
|
||||||
|
const ext = cleanUrl.toLowerCase().split('.').pop();
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileIcon(url) {
|
function getFileIconClass(url) {
|
||||||
const extension = url.toLowerCase().split('.').pop();
|
if (!url || typeof url !== 'string') return 'pi pi-file';
|
||||||
const iconMap = {
|
const cleanUrl = url.split('?')[0];
|
||||||
pdf: 'fa-solid fa-file-pdf',
|
const ext = cleanUrl.toLowerCase().split('.').pop();
|
||||||
doc: 'fa-solid fa-file-word',
|
if (ext === 'pdf') return 'pi pi-file-pdf';
|
||||||
docx: 'fa-solid fa-file-word',
|
if (['doc', 'docx'].includes(ext)) return 'pi pi-file-word';
|
||||||
xls: 'fa-solid fa-file-excel',
|
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'pi pi-file-excel';
|
||||||
xlsx: 'fa-solid fa-file-excel',
|
return 'pi pi-file';
|
||||||
txt: 'fa-solid fa-file-lines',
|
|
||||||
zip: 'fa-solid fa-file-zipper'
|
|
||||||
// Add more file types as needed
|
|
||||||
};
|
|
||||||
return iconMap[extension] || 'fa-solid fa-file';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconColor(url) {
|
function getFileIconColor(url) {
|
||||||
const extension = url.toLowerCase().split('.').pop();
|
if (!url || typeof url !== 'string') return 'text-surface-500';
|
||||||
const colorMap = {
|
const cleanUrl = url.split('?')[0];
|
||||||
pdf: 'text-red-500',
|
const ext = cleanUrl.toLowerCase().split('.').pop();
|
||||||
doc: 'text-blue-500',
|
if (ext === 'pdf') return 'text-red-500';
|
||||||
docx: 'text-blue-500',
|
if (['doc', 'docx'].includes(ext)) return 'text-blue-500';
|
||||||
xls: 'text-green-500',
|
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'text-emerald-500';
|
||||||
xlsx: 'text-green-500',
|
return 'text-primary-500';
|
||||||
txt: 'text-gray-500',
|
|
||||||
zip: 'text-yellow-600'
|
|
||||||
// Add more file types as needed
|
|
||||||
};
|
|
||||||
return colorMap[extension] || 'text-gray-400';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileName(url) {
|
function getFileName(url) {
|
||||||
return url.split('/').pop();
|
if (!url || typeof url !== 'string') return 'file';
|
||||||
|
const cleanUrl = url.split('?')[0];
|
||||||
|
const raw = cleanUrl.split('/').pop() || 'file';
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(raw);
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(url) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemsData(items) {
|
function updateItemsData(items) {
|
||||||
@@ -89,45 +133,49 @@ function updateItemsData(items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
if (item && item.url && isImageFile(item.url)) {
|
const itemObj = typeof item === 'string'
|
||||||
// Handle images
|
? { url: item, name: item.split('/').pop() || 'file' }
|
||||||
|
: { ...item };
|
||||||
|
|
||||||
|
if (itemObj && itemObj.url && isImageFile(itemObj.url)) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = item.url;
|
img.src = itemObj.url;
|
||||||
img.onload = function () {
|
img.onload = function () {
|
||||||
item.height = img.naturalHeight;
|
itemObj.height = img.naturalHeight || 800;
|
||||||
item.width = img.naturalWidth;
|
itemObj.width = img.naturalWidth || 1200;
|
||||||
};
|
};
|
||||||
|
if (!itemObj.width) itemObj.width = 1200;
|
||||||
|
if (!itemObj.height) itemObj.height = 800;
|
||||||
}
|
}
|
||||||
return item;
|
return itemObj;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Props
|
|
||||||
const props = defineProps({
|
function initLightbox() {
|
||||||
galleryId: {
|
if (lightbox) {
|
||||||
type: String,
|
lightbox.destroy();
|
||||||
required: true
|
lightbox = null;
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const itemsData = ref([]);
|
|
||||||
let lightbox = null;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!lightbox) {
|
|
||||||
lightbox = new PhotoSwipeLightbox({
|
lightbox = new PhotoSwipeLightbox({
|
||||||
gallery: '#' + props.galleryId,
|
gallery: '#' + props.galleryId,
|
||||||
children: 'a',
|
children: 'a.pswp-link',
|
||||||
pswpModule: () => import('photoswipe')
|
pswpModule: () => import('photoswipe')
|
||||||
});
|
});
|
||||||
lightbox.init();
|
lightbox.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initLightbox();
|
||||||
itemsData.value = updateItemsData(props.modelValue);
|
itemsData.value = updateItemsData(props.modelValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (lightbox) {
|
||||||
|
lightbox.destroy();
|
||||||
|
lightbox = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newItems) => {
|
(newItems) => {
|
||||||
@@ -136,8 +184,6 @@ watch(
|
|||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'delete-image']);
|
|
||||||
|
|
||||||
function removeItem(index, url) {
|
function removeItem(index, url) {
|
||||||
const newItems = [...props.modelValue];
|
const newItems = [...props.modelValue];
|
||||||
newItems.splice(index, 1);
|
newItems.splice(index, 1);
|
||||||
@@ -148,24 +194,18 @@ function removeItem(index, url) {
|
|||||||
function startDrag(event, index) {
|
function startDrag(event, index) {
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
event.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
event.dataTransfer.setData('imageIndex', index);
|
event.dataTransfer.setData('imageIndex', index.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(event, dropIndex) {
|
function onDrop(event, dropIndex) {
|
||||||
const dragIndex = event.dataTransfer.getData('imageIndex');
|
const dragIndexStr = event.dataTransfer.getData('imageIndex');
|
||||||
|
const dragIndex = parseInt(dragIndexStr, 10);
|
||||||
|
if (isNaN(dragIndex) || dragIndex === dropIndex) return;
|
||||||
|
|
||||||
if (dragIndex === dropIndex.toString()) return;
|
const newItems = [...props.modelValue];
|
||||||
|
const draggedItem = newItems.splice(dragIndex, 1)[0];
|
||||||
|
newItems.splice(dropIndex, 0, draggedItem);
|
||||||
|
|
||||||
// Create a copy of the images array
|
emit('update:modelValue', newItems);
|
||||||
const newImages = [...props.modelValue];
|
|
||||||
|
|
||||||
// Remove the dragged item
|
|
||||||
const draggedItem = newImages.splice(dragIndex, 1)[0];
|
|
||||||
|
|
||||||
// Insert it at the new position
|
|
||||||
newImages.splice(dropIndex, 0, draggedItem);
|
|
||||||
|
|
||||||
// Update the model
|
|
||||||
emit('update:modelValue', newImages);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user