From 2a46b2942d665e8f855fa4e47dc36d3be9a118f3 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Sun, 15 Mar 2026 23:47:37 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- ui/src/pages/ProductStockByAttributes.vue | 228 ++++++++++++++++----- ui/src/pages/ProductStockQuery.vue | 236 +++++++++++++++++----- 2 files changed, 368 insertions(+), 96 deletions(-) diff --git a/ui/src/pages/ProductStockByAttributes.vue b/ui/src/pages/ProductStockByAttributes.vue index 6bcdf52..b686cdd 100644 --- a/ui/src/pages/ProductStockByAttributes.vue +++ b/ui/src/pages/ProductStockByAttributes.vue @@ -275,8 +275,8 @@ swipeable navigation arrows - height="560px" - class="product-card-carousel rounded-borders bg-grey-2" + height="70vh" + class="product-card-carousel rounded-borders" > - +
Urun Fotografi
@@ -321,14 +321,38 @@
-
- -
+ + +
+ +
+
+
@@ -401,7 +425,15 @@ const productCardImages = ref([]) const productCardSlide = ref(0) const productImageFullscreenDialog = ref(false) const productImageFullscreenSrc = ref('') +const productImageFullscreenSlide = ref(0) const productImageFullscreenZoom = ref(1) +const productImageFullscreenOffsetX = ref(0) +const productImageFullscreenOffsetY = ref(0) +const productImageFullscreenDragging = ref(false) +const productImageFullscreenDragStartX = ref(0) +const productImageFullscreenDragStartY = ref(0) +const productImageFullscreenDragOriginX = ref(0) +const productImageFullscreenDragOriginY = ref(0) const IMAGE_LIST_CONCURRENCY = 8 const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000 const FILTER_OPTIONS_DEBOUNCE_MS = 250 @@ -438,11 +470,19 @@ const gridHeaderHeight = computed(() => showGridHeader.value ? '56px' : '0px' ) const fullscreenImageStyle = computed(() => ({ - transform: `scale(${productImageFullscreenZoom.value})`, + transform: `translate(${productImageFullscreenOffsetX.value}px, ${productImageFullscreenOffsetY.value}px) scale(${productImageFullscreenZoom.value})`, transformOrigin: 'center center', - transition: 'transform 0.15s ease-out' + transition: productImageFullscreenDragging.value ? 'none' : 'transform 0.1s ease-out', + cursor: productImageFullscreenZoom.value > 1 ? (productImageFullscreenDragging.value ? 'grabbing' : 'grab') : 'zoom-in' })) +const fullscreenImages = computed(() => { + const arr = Array.isArray(productCardImages.value) ? productCardImages.value : [] + if (arr.length) return arr + const single = String(productImageFullscreenSrc.value || '').trim() + return single ? [single] : [] +}) + function emptySizeTotals() { const map = {} for (const s of sizeLabels.value) map[s] = 0 @@ -587,6 +627,25 @@ function resolveProductImageUrl(item) { return { contentUrl, publicUrl, thumbUrl, fullUrl } } +function extractImageOrder(fileName, fallbackIndex) { + const name = String(fileName || '').trim() + const m = name.match(/\((\d+)\)(?=\.[a-z0-9]+$)/i) + if (m) return Number(m[1] || 999999) + const m2 = name.match(/[-_ ](\d+)(?=\.[a-z0-9]+$)/i) + if (m2) return Number(m2[1] || 999999) + return 1000000 + Number(fallbackIndex || 0) +} + +function sortImagesForDisplay(list) { + return (Array.isArray(list) ? list : []) + .map((item, idx) => ({ item, idx, order: extractImageOrder(item?.file_name || item?.FileName || '', idx) })) + .sort((a, b) => { + if (a.order !== b.order) return a.order - b.order + return a.idx - b.idx + }) + .map((x) => x.item) +} + async function resolveProductImageUrlForCarousel(item) { const resolved = resolveProductImageUrl(item) const contentUrl = String(resolved.contentUrl || '').trim() @@ -674,12 +733,12 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di } } } - const list = productImageListByCode.value[listKey] || [] + const list = sortImagesForDisplay(productImageListByCode.value[listKey] || []) const first = list[0] || null const resolved = resolveProductImageUrl(first) - productImageCache.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.contentUrl || resolved.publicUrl || '' + productImageCache.value[key] = resolved.fullUrl || resolved.publicUrl || resolved.thumbUrl || resolved.contentUrl || '' productImageFallbackByKey.value[key] = resolved.contentUrl || '' } catch (err) { console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err }) @@ -1153,8 +1212,9 @@ async function openProductCard(grp1, grp2) { } } + const sortedList = sortImagesForDisplay(list) const imageCandidates = await Promise.all( - list.map((item) => resolveProductImageUrlForCarousel(item)) + sortedList.map((item) => resolveProductImageUrlForCarousel(item)) ) const images = imageCandidates.filter((x) => String(x || '').trim() !== '') console.info('[ProductStockByAttributes][openProductCard] render', { @@ -1196,7 +1256,12 @@ function openProductImageFullscreen(src) { const value = String(src || '').trim() if (!value) return productImageFullscreenSrc.value = value + const idx = Math.max(0, fullscreenImages.value.findIndex((x) => String(x || '').trim() === value)) + productImageFullscreenSlide.value = idx productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false productImageFullscreenDialog.value = true } @@ -1204,7 +1269,57 @@ function toggleFullscreenImageZoom() { const current = Number(productImageFullscreenZoom.value || 1) if (current < 1.5) productImageFullscreenZoom.value = 1.8 else if (current < 2.3) productImageFullscreenZoom.value = 2.6 + else if (current < 3.2) productImageFullscreenZoom.value = 3.2 else productImageFullscreenZoom.value = 1 + if (productImageFullscreenZoom.value <= 1) { + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + } +} + +function onFullscreenWheel(evt) { + if (!evt) return + evt.preventDefault() + const delta = Number(evt.deltaY || 0) + if (productImageFullscreenZoom.value > 1.01 && !evt.ctrlKey) { + productImageFullscreenOffsetY.value -= delta * 0.45 + return + } + const current = Number(productImageFullscreenZoom.value || 1) + const next = delta < 0 ? current + 0.2 : current - 0.2 + productImageFullscreenZoom.value = Math.min(4, Math.max(1, Number(next.toFixed(2)))) + if (productImageFullscreenZoom.value <= 1) { + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + } +} + +function onFullscreenMouseDown(evt) { + if (productImageFullscreenZoom.value <= 1) return + productImageFullscreenDragging.value = true + productImageFullscreenDragStartX.value = Number(evt?.clientX || 0) + productImageFullscreenDragStartY.value = Number(evt?.clientY || 0) + productImageFullscreenDragOriginX.value = Number(productImageFullscreenOffsetX.value || 0) + productImageFullscreenDragOriginY.value = Number(productImageFullscreenOffsetY.value || 0) +} + +function onFullscreenMouseMove(evt) { + if (!productImageFullscreenDragging.value) return + const dx = Number(evt?.clientX || 0) - productImageFullscreenDragStartX.value + const dy = Number(evt?.clientY || 0) - productImageFullscreenDragStartY.value + productImageFullscreenOffsetX.value = productImageFullscreenDragOriginX.value + dx + productImageFullscreenOffsetY.value = productImageFullscreenDragOriginY.value + dy +} + +function onFullscreenMouseUp() { + productImageFullscreenDragging.value = false +} + +function onFullscreenSlideChange() { + productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false } function resetForm() { @@ -1236,16 +1351,24 @@ function resetForm() { productCardSlide.value = 0 productImageFullscreenDialog.value = false productImageFullscreenSrc.value = '' + productImageFullscreenSlide.value = 0 productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false filterOptionsCache.value = {} void loadFilterOptions(true) } onMounted(() => { void loadFilterOptions(true) + window.addEventListener('mousemove', onFullscreenMouseMove) + window.addEventListener('mouseup', onFullscreenMouseUp) }) onUnmounted(() => { + window.removeEventListener('mousemove', onFullscreenMouseMove) + window.removeEventListener('mouseup', onFullscreenMouseUp) if (filterOptionsDebounceTimer) { clearTimeout(filterOptionsDebounceTimer) filterOptionsDebounceTimer = null @@ -1637,14 +1760,16 @@ onUnmounted(() => { } .product-card-dialog { - background: #fffef9; + --pc-media-h: min(70vh, 900px); + --pc-media-w: min(74vw, 1220px); + background: #f6f8fc; } .product-card-stock { - background: #f8f5e7; - border: 1px solid #e2d9b6; - border-radius: 10px; - padding: 12px; + background: linear-gradient(180deg, #eef3fb 0%, #f8fbff 100%); + border: 1px solid #c8d6ec; + border-radius: 12px; + padding: 14px; } .stock-size-grid { @@ -1654,9 +1779,9 @@ onUnmounted(() => { } .stock-size-chip { - border: 1px solid #d8cca6; - border-radius: 8px; - background: #fff; + border: 1px solid #d7e2f2; + border-radius: 10px; + background: #ffffff; padding: 6px 8px; display: flex; justify-content: space-between; @@ -1669,8 +1794,8 @@ onUnmounted(() => { .product-card-content { display: grid; - grid-template-columns: minmax(360px, 1fr) 420px; - gap: 12px; + grid-template-columns: minmax(360px, 420px) minmax(760px, 1fr); + gap: 14px; align-items: stretch; justify-content: start; } @@ -1678,16 +1803,18 @@ onUnmounted(() => { .product-card-images { grid-column: 2; grid-row: 1; - min-height: 560px; + min-height: var(--pc-media-h); display: flex; flex-direction: column; - align-items: flex-start; + align-items: stretch; justify-content: flex-start; } .product-card-carousel { - width: 420px; + width: var(--pc-media-w); max-width: 100%; + background: linear-gradient(180deg, #edf2fb 0%, #e4ebf9 100%); + border: 1px solid #c8d6ec; } .dialog-image { @@ -1696,27 +1823,27 @@ onUnmounted(() => { } .dialog-image-stage { - width: 420px; + width: var(--pc-media-w); max-width: 100%; - height: 560px; + height: var(--pc-media-h); overflow: hidden; - border-radius: 8px; - background: #f7f4e9; + border-radius: 10px; + background: linear-gradient(180deg, #edf2fb 0%, #e4ebf9 100%); display: flex; align-items: center; justify-content: center; } .dialog-image-empty { - width: 420px; + width: var(--pc-media-w); max-width: 100%; - height: 560px; - border: 1px dashed #cabf9a; - border-radius: 8px; + height: var(--pc-media-h); + border: 1px dashed #9db5de; + border-radius: 10px; display: flex; align-items: center; justify-content: center; - background: #faf7ee; + background: #f2f6fd; } .image-fullscreen-dialog { @@ -1730,6 +1857,10 @@ onUnmounted(() => { justify-content: center; } +.image-fullscreen-carousel { + width: min(98vw, 1500px); +} + .image-fullscreen-stage { width: min(96vw, 1400px); height: calc(100vh - 120px); @@ -1749,11 +1880,11 @@ onUnmounted(() => { .product-card-fields { grid-column: 1; grid-row: 1; - border: 1px solid #e2d9b6; - border-radius: 10px; - background: #fff; - padding: 10px; - height: 560px; + border: 1px solid #c8d6ec; + border-radius: 12px; + background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%); + padding: 12px; + height: var(--pc-media-h); overflow: auto; } @@ -1761,8 +1892,8 @@ onUnmounted(() => { display: grid; grid-template-columns: 150px 1fr; gap: 8px; - padding: 7px 0; - border-bottom: 1px solid #f0ead7; + padding: 8px 0; + border-bottom: 1px solid #e4ebf7; font-size: 13px; } @@ -1771,7 +1902,7 @@ onUnmounted(() => { } .field-row .k { - color: #5a4f2c; + color: var(--q-primary, #1976d2); font-weight: 700; } @@ -1801,6 +1932,11 @@ onUnmounted(() => { .product-card-fields { height: auto; } + + .product-card-dialog { + --pc-media-h: min(62vh, 760px); + --pc-media-w: min(96vw, 900px); + } } .order-sub-header.level-2 .sub-right .top-total { diff --git a/ui/src/pages/ProductStockQuery.vue b/ui/src/pages/ProductStockQuery.vue index 97cbe86..729faa2 100644 --- a/ui/src/pages/ProductStockQuery.vue +++ b/ui/src/pages/ProductStockQuery.vue @@ -273,8 +273,8 @@ swipeable navigation arrows - height="560px" - class="product-card-carousel rounded-borders bg-grey-2" + height="70vh" + class="product-card-carousel rounded-borders" >
- +
Urun Fotografi
@@ -319,14 +319,38 @@
-
- -
+ + +
+ +
+
+
@@ -376,7 +400,15 @@ const productCardImages = ref([]) const productCardSlide = ref(0) const productImageFullscreenDialog = ref(false) const productImageFullscreenSrc = ref('') +const productImageFullscreenSlide = ref(0) const productImageFullscreenZoom = ref(1) +const productImageFullscreenOffsetX = ref(0) +const productImageFullscreenOffsetY = ref(0) +const productImageFullscreenDragging = ref(false) +const productImageFullscreenDragStartX = ref(0) +const productImageFullscreenDragStartY = ref(0) +const productImageFullscreenDragOriginX = ref(0) +const productImageFullscreenDragOriginY = ref(0) const IMAGE_LIST_CONCURRENCY = 8 let imageListActiveRequests = 0 const imageListWaitQueue = [] @@ -405,11 +437,19 @@ const gridHeaderHeight = computed(() => showGridHeader.value ? '56px' : '0px' ) const fullscreenImageStyle = computed(() => ({ - transform: `scale(${productImageFullscreenZoom.value})`, + transform: `translate(${productImageFullscreenOffsetX.value}px, ${productImageFullscreenOffsetY.value}px) scale(${productImageFullscreenZoom.value})`, transformOrigin: 'center center', - transition: 'transform 0.15s ease-out' + transition: productImageFullscreenDragging.value ? 'none' : 'transform 0.1s ease-out', + cursor: productImageFullscreenZoom.value > 1 ? (productImageFullscreenDragging.value ? 'grabbing' : 'grab') : 'zoom-in' })) +const fullscreenImages = computed(() => { + const arr = Array.isArray(productCardImages.value) ? productCardImages.value : [] + if (arr.length) return arr + const single = String(productImageFullscreenSrc.value || '').trim() + return single ? [single] : [] +}) + function emptySizeTotals() { const map = {} for (const s of sizeLabels.value) map[s] = 0 @@ -574,6 +614,25 @@ function resolveProductImageUrl(item) { return { contentUrl, publicUrl, thumbUrl, fullUrl } } +function extractImageOrder(fileName, fallbackIndex) { + const name = String(fileName || '').trim() + const m = name.match(/\((\d+)\)(?=\.[a-z0-9]+$)/i) + if (m) return Number(m[1] || 999999) + const m2 = name.match(/[-_ ](\d+)(?=\.[a-z0-9]+$)/i) + if (m2) return Number(m2[1] || 999999) + return 1000000 + Number(fallbackIndex || 0) +} + +function sortImagesForDisplay(list) { + return (Array.isArray(list) ? list : []) + .map((item, idx) => ({ item, idx, order: extractImageOrder(item?.file_name || item?.FileName || '', idx) })) + .sort((a, b) => { + if (a.order !== b.order) return a.order - b.order + return a.idx - b.idx + }) + .map((x) => x.item) +} + async function resolveProductImageUrlForCarousel(item) { const resolved = resolveProductImageUrl(item) const contentUrl = String(resolved.contentUrl || '').trim() @@ -659,11 +718,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di } } - const list = productImageListByCode.value[listKey] || [] + const list = sortImagesForDisplay(productImageListByCode.value[listKey] || []) const first = list[0] || null const resolved = resolveProductImageUrl(first) - productImageCache.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.contentUrl || resolved.publicUrl || '' + productImageCache.value[key] = resolved.fullUrl || resolved.publicUrl || resolved.thumbUrl || resolved.contentUrl || '' productImageFallbackByKey.value[key] = resolved.contentUrl || '' } catch (err) { console.warn('[ProductStockQuery] product image fetch failed', { code, color, err }) @@ -1028,8 +1087,9 @@ async function openProductCard(grp1, grp2) { } } + const sortedList = sortImagesForDisplay(list) const imageCandidates = await Promise.all( - list.map((item) => resolveProductImageUrlForCarousel(item)) + sortedList.map((item) => resolveProductImageUrlForCarousel(item)) ) const images = imageCandidates.filter((x) => String(x || '').trim() !== '') console.info('[ProductStockQuery][openProductCard] render', { @@ -1071,7 +1131,12 @@ function openProductImageFullscreen(src) { const value = String(src || '').trim() if (!value) return productImageFullscreenSrc.value = value + const idx = Math.max(0, fullscreenImages.value.findIndex((x) => String(x || '').trim() === value)) + productImageFullscreenSlide.value = idx productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false productImageFullscreenDialog.value = true } @@ -1079,7 +1144,57 @@ function toggleFullscreenImageZoom() { const current = Number(productImageFullscreenZoom.value || 1) if (current < 1.5) productImageFullscreenZoom.value = 1.8 else if (current < 2.3) productImageFullscreenZoom.value = 2.6 + else if (current < 3.2) productImageFullscreenZoom.value = 3.2 else productImageFullscreenZoom.value = 1 + if (productImageFullscreenZoom.value <= 1) { + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + } +} + +function onFullscreenWheel(evt) { + if (!evt) return + evt.preventDefault() + const delta = Number(evt.deltaY || 0) + if (productImageFullscreenZoom.value > 1.01 && !evt.ctrlKey) { + productImageFullscreenOffsetY.value -= delta * 0.45 + return + } + const current = Number(productImageFullscreenZoom.value || 1) + const next = delta < 0 ? current + 0.2 : current - 0.2 + productImageFullscreenZoom.value = Math.min(4, Math.max(1, Number(next.toFixed(2)))) + if (productImageFullscreenZoom.value <= 1) { + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + } +} + +function onFullscreenMouseDown(evt) { + if (productImageFullscreenZoom.value <= 1) return + productImageFullscreenDragging.value = true + productImageFullscreenDragStartX.value = Number(evt?.clientX || 0) + productImageFullscreenDragStartY.value = Number(evt?.clientY || 0) + productImageFullscreenDragOriginX.value = Number(productImageFullscreenOffsetX.value || 0) + productImageFullscreenDragOriginY.value = Number(productImageFullscreenOffsetY.value || 0) +} + +function onFullscreenMouseMove(evt) { + if (!productImageFullscreenDragging.value) return + const dx = Number(evt?.clientX || 0) - productImageFullscreenDragStartX.value + const dy = Number(evt?.clientY || 0) - productImageFullscreenDragStartY.value + productImageFullscreenOffsetX.value = productImageFullscreenDragOriginX.value + dx + productImageFullscreenOffsetY.value = productImageFullscreenDragOriginY.value + dy +} + +function onFullscreenMouseUp() { + productImageFullscreenDragging.value = false +} + +function onFullscreenSlideChange() { + productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false } function resetForm() { @@ -1101,19 +1216,27 @@ function resetForm() { productCardSlide.value = 0 productImageFullscreenDialog.value = false productImageFullscreenSrc.value = '' + productImageFullscreenSlide.value = 0 productImageFullscreenZoom.value = 1 + productImageFullscreenOffsetX.value = 0 + productImageFullscreenOffsetY.value = 0 + productImageFullscreenDragging.value = false } -onMounted(() => { - loadProductOptions() -}) - onUnmounted(() => { + window.removeEventListener('mousemove', onFullscreenMouseMove) + window.removeEventListener('mouseup', onFullscreenMouseUp) for (const url of productImageBlobUrls.value) { try { URL.revokeObjectURL(url) } catch {} } productImageBlobUrls.value = [] }) + +onMounted(() => { + loadProductOptions() + window.addEventListener('mousemove', onFullscreenMouseMove) + window.addEventListener('mouseup', onFullscreenMouseUp) +})