Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -181,21 +181,29 @@
|
||||
</div>
|
||||
|
||||
<div class="sub-image level2-image">
|
||||
<q-card flat bordered class="product-image-card">
|
||||
<q-card flat bordered class="product-image-card cursor-pointer" @click.stop="openProductCard(grp1, grp2)">
|
||||
<q-card-section class="q-pa-xs product-image-wrap">
|
||||
<q-img
|
||||
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)"
|
||||
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)"
|
||||
fit="cover"
|
||||
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
:src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
fit="contain"
|
||||
class="product-image"
|
||||
loading="lazy"
|
||||
@error="onProductImageError(grp1.productCode, grp2.colorCode)"
|
||||
@error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
/>
|
||||
<div v-else class="product-image-placeholder">
|
||||
<q-icon name="image_not_supported" size="22px" color="grey-6" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
color="primary"
|
||||
label="Urun Detayi Gor"
|
||||
class="detail-open-btn"
|
||||
@click.stop="openProductCard(grp1, grp2)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +239,99 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="productCardDialog" maximized>
|
||||
<q-card class="product-card-dialog">
|
||||
<q-card-section class="row items-center q-pb-sm">
|
||||
<div class="text-h6">Urun Karti</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="product-card-stock">
|
||||
<div class="text-subtitle1 text-weight-bold">
|
||||
{{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }}
|
||||
</div>
|
||||
<div class="text-caption">Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}</div>
|
||||
<div class="stock-size-grid q-mt-sm">
|
||||
<div v-for="sz in sizeLabels" :key="'dlg-sz-' + sz" class="stock-size-chip">
|
||||
<span class="label">{{ sz }}</span>
|
||||
<span class="value">{{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
|
||||
<div class="product-card-content">
|
||||
<div class="product-card-images">
|
||||
<q-carousel
|
||||
v-if="productCardImages.length"
|
||||
v-model="productCardSlide"
|
||||
animated
|
||||
swipeable
|
||||
navigation
|
||||
arrows
|
||||
height="560px"
|
||||
class="product-card-carousel rounded-borders bg-grey-2"
|
||||
>
|
||||
<q-carousel-slide
|
||||
v-for="(img, idx) in productCardImages"
|
||||
:key="'img-' + idx"
|
||||
:name="idx"
|
||||
class="column no-wrap flex-center"
|
||||
>
|
||||
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
|
||||
<q-img :src="img" fit="contain" class="dialog-image" />
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
<div v-else class="dialog-image-empty">
|
||||
<q-icon name="image_not_supported" size="36px" color="grey-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-card-fields">
|
||||
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Renk</span><span class="v">{{ productCardData.colorCode || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun 2.Renk</span><span class="v">{{ productCardData.secondColor || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Icerigi</span><span class="v">{{ productCardData.urunIcerigi || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Fit</span><span class="v">{{ productCardData.fit || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Drop</span><span class="v">{{ productCardData.drop || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Kumas</span><span class="v">{{ productCardData.kumas || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="productImageFullscreenDialog" maximized>
|
||||
<q-card class="image-fullscreen-dialog">
|
||||
<q-card-section class="row items-center q-pb-sm">
|
||||
<div class="text-h6">Urun Fotografi</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="image-fullscreen-body">
|
||||
<div class="image-fullscreen-stage cursor-pointer" @click="toggleFullscreenImageZoom">
|
||||
<q-img
|
||||
:src="productImageFullscreenSrc"
|
||||
fit="contain"
|
||||
class="image-fullscreen-img"
|
||||
:style="fullscreenImageStyle"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
@@ -284,6 +385,7 @@ const filters = ref({
|
||||
})
|
||||
const optionLists = ref({})
|
||||
const filteredOptionLists = ref({})
|
||||
const filterOptionsCache = ref({})
|
||||
const rawRows = ref([])
|
||||
const productImageCache = ref({})
|
||||
const productImageLoading = ref({})
|
||||
@@ -293,8 +395,19 @@ const productImageFallbackByKey = ref({})
|
||||
const productImageContentLoading = ref({})
|
||||
const productImageBlobUrls = ref([])
|
||||
const productImageListBlockedUntil = ref(0)
|
||||
const productCardDialog = ref(false)
|
||||
const productCardData = ref({})
|
||||
const productCardImages = ref([])
|
||||
const productCardSlide = ref(0)
|
||||
const productImageFullscreenDialog = ref(false)
|
||||
const productImageFullscreenSrc = ref('')
|
||||
const productImageFullscreenZoom = ref(1)
|
||||
const IMAGE_LIST_CONCURRENCY = 8
|
||||
const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000
|
||||
const FILTER_OPTIONS_DEBOUNCE_MS = 250
|
||||
let imageListActiveRequests = 0
|
||||
let filterOptionsDebounceTimer = null
|
||||
let filterOptionsRequestSeq = 0
|
||||
const imageListWaitQueue = []
|
||||
const activeSchema = ref(storeSchemaByKey.tak)
|
||||
const activeGrpKey = ref('tak')
|
||||
@@ -324,6 +437,11 @@ const allDetailsExpanded = computed(() => {
|
||||
const gridHeaderHeight = computed(() =>
|
||||
showGridHeader.value ? '56px' : '0px'
|
||||
)
|
||||
const fullscreenImageStyle = computed(() => ({
|
||||
transform: `scale(${productImageFullscreenZoom.value})`,
|
||||
transformOrigin: 'center center',
|
||||
transition: 'transform 0.15s ease-out'
|
||||
}))
|
||||
|
||||
function emptySizeTotals() {
|
||||
const map = {}
|
||||
@@ -347,8 +465,20 @@ function sortByTotalQtyDesc(a, b) {
|
||||
return String(a?.key || '').localeCompare(String(b?.key || ''), 'tr', { sensitivity: 'base' })
|
||||
}
|
||||
|
||||
function buildImageKey(code, color) {
|
||||
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}`
|
||||
function buildImageKey(code, color, secondColor = '') {
|
||||
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}::${String(secondColor || '').trim().toUpperCase()}`
|
||||
}
|
||||
|
||||
function imageNameMatches(fileName, color, secondColor) {
|
||||
const text = String(fileName || '').toUpperCase()
|
||||
if (!text) return false
|
||||
const tokens = text.replace(/[^A-Z0-9_]+/g, ' ').trim().split(/\s+/).filter(Boolean)
|
||||
if (!tokens.length) return false
|
||||
const colorTrim = String(color || '').trim().toUpperCase()
|
||||
const secondTrim = String(secondColor || '').trim().toUpperCase()
|
||||
if (colorTrim && !tokens.includes(colorTrim)) return false
|
||||
if (secondTrim && !tokens.includes(secondTrim)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function normalizeUploadsPath(storagePath) {
|
||||
@@ -403,17 +533,17 @@ function resolveProductImageUrl(item) {
|
||||
return { contentUrl, publicUrl }
|
||||
}
|
||||
|
||||
function getProductImageUrl(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
function getProductImageUrl(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const existing = productImageCache.value[key]
|
||||
if (existing !== undefined) return existing || ''
|
||||
|
||||
void ensureProductImage(code, color)
|
||||
void ensureProductImage(code, color, secondColor)
|
||||
return ''
|
||||
}
|
||||
|
||||
async function onProductImageError(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
async function onProductImageError(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const fallback = String(productImageFallbackByKey.value[key] || '')
|
||||
if (fallback && !productImageContentLoading.value[key]) {
|
||||
productImageContentLoading.value[key] = true
|
||||
@@ -438,10 +568,12 @@ async function onProductImageError(code, color) {
|
||||
productImageCache.value[key] = ''
|
||||
}
|
||||
|
||||
async function ensureProductImage(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
async function ensureProductImage(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const codeTrim = String(code || '').trim().toUpperCase()
|
||||
const colorTrim = String(color || '').trim().toUpperCase()
|
||||
const secondTrim = String(secondColor || '').trim().toUpperCase()
|
||||
const listKey = buildImageKey(codeTrim, colorTrim, secondTrim)
|
||||
if (!codeTrim) {
|
||||
productImageCache.value[key] = ''
|
||||
return ''
|
||||
@@ -455,44 +587,44 @@ async function ensureProductImage(code, color) {
|
||||
|
||||
productImageLoading.value[key] = true
|
||||
try {
|
||||
if (!productImageListByCode.value[codeTrim]) {
|
||||
if (!productImageListLoading.value[codeTrim]) {
|
||||
productImageListLoading.value[codeTrim] = true
|
||||
if (!productImageListByCode.value[listKey]) {
|
||||
if (!productImageListLoading.value[listKey]) {
|
||||
productImageListLoading.value[listKey] = true
|
||||
try {
|
||||
if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) {
|
||||
await new Promise((resolve) => imageListWaitQueue.push(resolve))
|
||||
}
|
||||
imageListActiveRequests++
|
||||
const res = await api.get('/product-images', { params: { code: codeTrim } })
|
||||
productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : []
|
||||
const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim }
|
||||
const res = await api.get('/product-images', { params })
|
||||
productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : []
|
||||
} catch (err) {
|
||||
productImageListByCode.value[codeTrim] = []
|
||||
productImageListByCode.value[listKey] = []
|
||||
const status = Number(err?.response?.status || 0)
|
||||
if (status >= 500 || status === 403 || status === 0) {
|
||||
// Backend dengesizken istek firtinasini kisaca kes.
|
||||
productImageListBlockedUntil.value = Date.now() + 30 * 1000
|
||||
}
|
||||
console.warn('[ProductStockByAttributes] product image list fetch failed', { code: codeTrim, err })
|
||||
console.warn('[ProductStockByAttributes] product image list fetch failed', { code: codeTrim, color: colorTrim, secondColor: secondTrim, err })
|
||||
} finally {
|
||||
imageListActiveRequests = Math.max(0, imageListActiveRequests - 1)
|
||||
const nextInQueue = imageListWaitQueue.shift()
|
||||
if (nextInQueue) nextInQueue()
|
||||
delete productImageListLoading.value[codeTrim]
|
||||
delete productImageListLoading.value[listKey]
|
||||
}
|
||||
} else {
|
||||
// Ayni code icin baska bir istek zaten calisiyorsa tamamlanmasini bekle.
|
||||
while (productImageListLoading.value[codeTrim]) {
|
||||
while (productImageListLoading.value[listKey]) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||
}
|
||||
}
|
||||
}
|
||||
const list = productImageListByCode.value[codeTrim] || []
|
||||
const list = productImageListByCode.value[listKey] || []
|
||||
|
||||
let first = null
|
||||
if (colorTrim) {
|
||||
const needle = `-${colorTrim.toLowerCase()}-`
|
||||
if (colorTrim || secondTrim) {
|
||||
first = list.find((item) =>
|
||||
String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle)
|
||||
imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim)
|
||||
) || null
|
||||
}
|
||||
if (!first) first = list[0] || null
|
||||
@@ -625,6 +757,11 @@ const level1Groups = computed(() => {
|
||||
const depoAdi = String(item.Depo_Adi || '').trim()
|
||||
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
|
||||
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
|
||||
const urunIcerigi = String(item.URUN_ICERIGI || item.KISA_KAR || '').trim()
|
||||
const fit = String(item.BIRINCI_PARCA_FIT || '').trim()
|
||||
const drop = String(item.DR || '').trim()
|
||||
const kumas = String(item.BIRINCI_PARCA_KUMAS || '').trim()
|
||||
const karisim = String(item.BIRINCI_PARCA_KARISIM || '').trim()
|
||||
const aciklama = String(item.Madde_Aciklamasi || '').trim()
|
||||
const beden = normalizeSize(item.Beden || '')
|
||||
const qty = parseNumber(item.Kullanilabilir_Envanter)
|
||||
@@ -654,6 +791,11 @@ const level1Groups = computed(() => {
|
||||
secondColor,
|
||||
urunAnaGrubu,
|
||||
urunAltGrubu,
|
||||
urunIcerigi,
|
||||
fit,
|
||||
drop,
|
||||
kumas,
|
||||
karisim,
|
||||
aciklama,
|
||||
sizeTotals: emptySizeTotals(),
|
||||
totalQty: 0,
|
||||
@@ -714,6 +856,11 @@ function buildFilterParams() {
|
||||
return out
|
||||
}
|
||||
|
||||
function buildFilterCacheKey(params) {
|
||||
const keys = Object.keys(params || {}).sort()
|
||||
return keys.map((k) => `${k}=${String(params[k] || '').trim()}`).join('&')
|
||||
}
|
||||
|
||||
function isFilterDisabled(key) {
|
||||
if (key === 'kategori') return false
|
||||
if (key === 'urun_ana_grubu') {
|
||||
@@ -747,7 +894,12 @@ function onFilterValueChange(changedKey) {
|
||||
filters.value.beden = ''
|
||||
}
|
||||
|
||||
void loadFilterOptions()
|
||||
if (filterOptionsDebounceTimer) {
|
||||
clearTimeout(filterOptionsDebounceTimer)
|
||||
}
|
||||
filterOptionsDebounceTimer = setTimeout(() => {
|
||||
void loadFilterOptions()
|
||||
}, FILTER_OPTIONS_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
function filterOptions(field, val, update) {
|
||||
@@ -768,12 +920,26 @@ function filterOptions(field, val, update) {
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFilterOptions() {
|
||||
async function loadFilterOptions(force = false) {
|
||||
const params = buildFilterParams()
|
||||
const cacheKey = buildFilterCacheKey(params)
|
||||
const now = Date.now()
|
||||
if (!force) {
|
||||
const cached = filterOptionsCache.value[cacheKey]
|
||||
if (cached && Number(cached.expiresAt || 0) > now && cached.payload) {
|
||||
optionLists.value = cached.payload.optionLists
|
||||
filteredOptionLists.value = cached.payload.filteredOptionLists
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const reqSeq = ++filterOptionsRequestSeq
|
||||
loadingFilterOptions.value = true
|
||||
try {
|
||||
const res = await api.get('/product-stock-attribute-options', {
|
||||
params: buildFilterParams()
|
||||
params
|
||||
})
|
||||
if (reqSeq !== filterOptionsRequestSeq) return
|
||||
const payload = res?.data && typeof res.data === 'object' ? res.data : {}
|
||||
const next = {}
|
||||
const nextFiltered = {}
|
||||
@@ -795,11 +961,21 @@ async function loadFilterOptions() {
|
||||
|
||||
optionLists.value = next
|
||||
filteredOptionLists.value = nextFiltered
|
||||
filterOptionsCache.value[cacheKey] = {
|
||||
expiresAt: now + FILTER_OPTIONS_CACHE_TTL_MS,
|
||||
payload: {
|
||||
optionLists: next,
|
||||
filteredOptionLists: nextFiltered
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (reqSeq !== filterOptionsRequestSeq) return
|
||||
errorMessage.value = 'Urun ozellik secenekleri alinamadi.'
|
||||
console.error('loadFilterOptions error:', err)
|
||||
} finally {
|
||||
loadingFilterOptions.value = false
|
||||
if (reqSeq === filterOptionsRequestSeq) {
|
||||
loadingFilterOptions.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,10 +1047,65 @@ async function fetchStockByAttributes() {
|
||||
function onLevel2Click(productCode, grp2) {
|
||||
toggleOpen(grp2.key)
|
||||
if (isOpen(grp2.key)) {
|
||||
void ensureProductImage(productCode, grp2.colorCode)
|
||||
void ensureProductImage(productCode, grp2.colorCode, grp2.secondColor)
|
||||
}
|
||||
}
|
||||
|
||||
async function openProductCard(grp1, grp2) {
|
||||
const productCode = String(grp1?.productCode || '').trim()
|
||||
const colorCode = String(grp2?.colorCode || '').trim()
|
||||
const secondColor = String(grp2?.secondColor || '').trim()
|
||||
const listKey = buildImageKey(productCode, colorCode, secondColor)
|
||||
|
||||
await ensureProductImage(productCode, colorCode, secondColor)
|
||||
const list = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : []
|
||||
const images = list
|
||||
.map((item) => {
|
||||
const resolved = resolveProductImageUrl(item)
|
||||
return resolved.contentUrl || resolved.publicUrl || ''
|
||||
})
|
||||
.filter((x) => String(x || '').trim() !== '')
|
||||
|
||||
if (!images.length) {
|
||||
const single = getProductImageUrl(productCode, colorCode, secondColor)
|
||||
if (single) images.push(single)
|
||||
}
|
||||
|
||||
productCardImages.value = images
|
||||
productCardSlide.value = 0
|
||||
productCardData.value = {
|
||||
productCode,
|
||||
colorCode,
|
||||
secondColor,
|
||||
kategori: String(filters.value?.kategori || '').trim(),
|
||||
urunAnaGrubu: String(grp2?.urunAnaGrubu || '').trim(),
|
||||
urunAltGrubu: String(grp2?.urunAltGrubu || '').trim(),
|
||||
urunIcerigi: String(grp2?.urunIcerigi || '').trim(),
|
||||
fit: String(grp2?.fit || '').trim(),
|
||||
drop: String(grp2?.drop || '').trim(),
|
||||
kumas: String(grp2?.kumas || '').trim(),
|
||||
karisim: String(grp2?.karisim || '').trim(),
|
||||
sizeTotals: grp2?.sizeTotals || {},
|
||||
totalQty: Number(grp2?.totalQty || 0)
|
||||
}
|
||||
productCardDialog.value = true
|
||||
}
|
||||
|
||||
function openProductImageFullscreen(src) {
|
||||
const value = String(src || '').trim()
|
||||
if (!value) return
|
||||
productImageFullscreenSrc.value = value
|
||||
productImageFullscreenZoom.value = 1
|
||||
productImageFullscreenDialog.value = true
|
||||
}
|
||||
|
||||
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 productImageFullscreenZoom.value = 1
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
filters.value = {
|
||||
kategori: '',
|
||||
@@ -898,14 +1129,26 @@ function resetForm() {
|
||||
productImageFallbackByKey.value = {}
|
||||
productImageContentLoading.value = {}
|
||||
productImageListBlockedUntil.value = 0
|
||||
void loadFilterOptions()
|
||||
productCardDialog.value = false
|
||||
productCardData.value = {}
|
||||
productCardImages.value = []
|
||||
productCardSlide.value = 0
|
||||
productImageFullscreenDialog.value = false
|
||||
productImageFullscreenSrc.value = ''
|
||||
productImageFullscreenZoom.value = 1
|
||||
filterOptionsCache.value = {}
|
||||
void loadFilterOptions(true)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFilterOptions()
|
||||
void loadFilterOptions(true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (filterOptionsDebounceTimer) {
|
||||
clearTimeout(filterOptionsDebounceTimer)
|
||||
filterOptionsDebounceTimer = null
|
||||
}
|
||||
for (const url of productImageBlobUrls.value) {
|
||||
try { URL.revokeObjectURL(url) } catch {}
|
||||
}
|
||||
@@ -920,7 +1163,7 @@ onUnmounted(() => {
|
||||
--grp-title-w: 44px;
|
||||
--psq-header-h: 56px;
|
||||
--psq-col-adet: calc(var(--col-adet) + var(--beden-w));
|
||||
--psq-col-img: 126px;
|
||||
--psq-col-img: 190px;
|
||||
--psq-l1-lift: 42px;
|
||||
}
|
||||
|
||||
@@ -1003,8 +1246,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.order-sub-header.level-2 {
|
||||
min-height: 82px !important;
|
||||
height: 82px !important;
|
||||
min-height: 252px !important;
|
||||
height: 252px !important;
|
||||
background: #fff9c4 !important;
|
||||
border-top: 1px solid #d4c79f !important;
|
||||
border-bottom: 1px solid #d4c79f !important;
|
||||
@@ -1250,16 +1493,18 @@ onUnmounted(() => {
|
||||
.order-sub-header.level-2 .sub-image.level2-image {
|
||||
grid-column: 8 / 9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #d4c79f;
|
||||
padding: 0 6px;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #fffef7;
|
||||
}
|
||||
|
||||
.product-image-card {
|
||||
width: 108px;
|
||||
height: 66px;
|
||||
width: 162px;
|
||||
height: 216px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -1272,6 +1517,7 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
@@ -1284,6 +1530,178 @@ onUnmounted(() => {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.detail-open-btn {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.product-card-dialog {
|
||||
background: #fffef9;
|
||||
}
|
||||
|
||||
.product-card-stock {
|
||||
background: #f8f5e7;
|
||||
border: 1px solid #e2d9b6;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stock-size-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stock-size-chip {
|
||||
border: 1px solid #d8cca6;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stock-size-chip .label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-card-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 1fr) 420px;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.product-card-images {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-height: 560px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.product-card-carousel {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dialog-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-image-stage {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
height: 560px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f7f4e9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-image-empty {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
height: 560px;
|
||||
border: 1px dashed #cabf9a;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #faf7ee;
|
||||
}
|
||||
|
||||
.image-fullscreen-dialog {
|
||||
background: #f4f0e2;
|
||||
}
|
||||
|
||||
.image-fullscreen-body {
|
||||
height: calc(100vh - 72px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-fullscreen-stage {
|
||||
width: min(96vw, 1400px);
|
||||
height: calc(100vh - 120px);
|
||||
border-radius: 10px;
|
||||
background: #efe7cc;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-fullscreen-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-card-fields {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border: 1px solid #e2d9b6;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
height: 560px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 1fr;
|
||||
gap: 8px;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid #f0ead7;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.field-row .k {
|
||||
color: #5a4f2c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-row .v {
|
||||
color: #1f1f1f;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.q-btn,
|
||||
.q-icon,
|
||||
.product-image-card,
|
||||
.cursor-pointer {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.product-card-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.product-card-images,
|
||||
.product-card-fields {
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.product-card-fields {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.order-sub-header.level-2 .sub-right .top-total {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1;
|
||||
|
||||
@@ -179,21 +179,29 @@
|
||||
</div>
|
||||
|
||||
<div class="sub-image level2-image">
|
||||
<q-card flat bordered class="product-image-card">
|
||||
<q-card flat bordered class="product-image-card cursor-pointer" @click.stop="openProductCard(grp1, grp2)">
|
||||
<q-card-section class="q-pa-xs product-image-wrap">
|
||||
<q-img
|
||||
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)"
|
||||
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)"
|
||||
fit="cover"
|
||||
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
:src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
fit="contain"
|
||||
class="product-image"
|
||||
loading="lazy"
|
||||
@error="onProductImageError(grp1.productCode, grp2.colorCode)"
|
||||
@error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
|
||||
/>
|
||||
<div v-else class="product-image-placeholder">
|
||||
<q-icon name="image_not_supported" size="22px" color="grey-6" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
color="primary"
|
||||
label="Urun Detayi Gor"
|
||||
class="detail-open-btn"
|
||||
@click.stop="openProductCard(grp1, grp2)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -229,6 +237,99 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="productCardDialog" maximized>
|
||||
<q-card class="product-card-dialog">
|
||||
<q-card-section class="row items-center q-pb-sm">
|
||||
<div class="text-h6">Urun Karti</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<div class="product-card-stock">
|
||||
<div class="text-subtitle1 text-weight-bold">
|
||||
{{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }}
|
||||
</div>
|
||||
<div class="text-caption">Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}</div>
|
||||
<div class="stock-size-grid q-mt-sm">
|
||||
<div v-for="sz in sizeLabels" :key="'dlg-sz-' + sz" class="stock-size-chip">
|
||||
<span class="label">{{ sz }}</span>
|
||||
<span class="value">{{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
|
||||
<div class="product-card-content">
|
||||
<div class="product-card-images">
|
||||
<q-carousel
|
||||
v-if="productCardImages.length"
|
||||
v-model="productCardSlide"
|
||||
animated
|
||||
swipeable
|
||||
navigation
|
||||
arrows
|
||||
height="560px"
|
||||
class="product-card-carousel rounded-borders bg-grey-2"
|
||||
>
|
||||
<q-carousel-slide
|
||||
v-for="(img, idx) in productCardImages"
|
||||
:key="'img-' + idx"
|
||||
:name="idx"
|
||||
class="column no-wrap flex-center"
|
||||
>
|
||||
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
|
||||
<q-img :src="img" fit="contain" class="dialog-image" />
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
<div v-else class="dialog-image-empty">
|
||||
<q-icon name="image_not_supported" size="36px" color="grey-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-card-fields">
|
||||
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Renk</span><span class="v">{{ productCardData.colorCode || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun 2.Renk</span><span class="v">{{ productCardData.secondColor || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Urun Icerigi</span><span class="v">{{ productCardData.urunIcerigi || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Fit</span><span class="v">{{ productCardData.fit || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Drop</span><span class="v">{{ productCardData.drop || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Kumas</span><span class="v">{{ productCardData.kumas || '-' }}</span></div>
|
||||
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="productImageFullscreenDialog" maximized>
|
||||
<q-card class="image-fullscreen-dialog">
|
||||
<q-card-section class="row items-center q-pb-sm">
|
||||
<div class="text-h6">Urun Fotografi</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="image-fullscreen-body">
|
||||
<div class="image-fullscreen-stage cursor-pointer" @click="toggleFullscreenImageZoom">
|
||||
<q-img
|
||||
:src="productImageFullscreenSrc"
|
||||
fit="contain"
|
||||
class="image-fullscreen-img"
|
||||
:style="fullscreenImageStyle"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
@@ -269,6 +370,13 @@ const productImageFallbackByKey = ref({})
|
||||
const productImageContentLoading = ref({})
|
||||
const productImageBlobUrls = ref([])
|
||||
const productImageListBlockedUntil = ref(0)
|
||||
const productCardDialog = ref(false)
|
||||
const productCardData = ref({})
|
||||
const productCardImages = ref([])
|
||||
const productCardSlide = ref(0)
|
||||
const productImageFullscreenDialog = ref(false)
|
||||
const productImageFullscreenSrc = ref('')
|
||||
const productImageFullscreenZoom = ref(1)
|
||||
const IMAGE_LIST_CONCURRENCY = 8
|
||||
let imageListActiveRequests = 0
|
||||
const imageListWaitQueue = []
|
||||
@@ -296,6 +404,11 @@ const allDetailsExpanded = computed(() => {
|
||||
const gridHeaderHeight = computed(() =>
|
||||
showGridHeader.value ? '56px' : '0px'
|
||||
)
|
||||
const fullscreenImageStyle = computed(() => ({
|
||||
transform: `scale(${productImageFullscreenZoom.value})`,
|
||||
transformOrigin: 'center center',
|
||||
transition: 'transform 0.15s ease-out'
|
||||
}))
|
||||
|
||||
function emptySizeTotals() {
|
||||
const map = {}
|
||||
@@ -312,8 +425,53 @@ function parseNumber(value) {
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
function buildImageKey(code, color) {
|
||||
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}`
|
||||
function sortByColorCodeAsc(a, b) {
|
||||
const compareCodeLike = (va, vb) => {
|
||||
const sa = String(va || '').trim()
|
||||
const sb = String(vb || '').trim()
|
||||
const pa = sa.match(/^(\d+)(?:_(\d+))?$/)
|
||||
const pb = sb.match(/^(\d+)(?:_(\d+))?$/)
|
||||
if (pa && pb) {
|
||||
const a1 = Number.parseInt(pa[1], 10)
|
||||
const b1 = Number.parseInt(pb[1], 10)
|
||||
if (a1 !== b1) return a1 - b1
|
||||
const a2 = Number.parseInt(pa[2] || '0', 10)
|
||||
const b2 = Number.parseInt(pb[2] || '0', 10)
|
||||
if (a2 !== b2) return a2 - b2
|
||||
}
|
||||
return sa.localeCompare(sb, 'tr', { sensitivity: 'base' })
|
||||
}
|
||||
|
||||
const ca = String(a?.colorCode || '').trim()
|
||||
const cb = String(b?.colorCode || '').trim()
|
||||
const na = Number.parseInt(ca, 10)
|
||||
const nb = Number.parseInt(cb, 10)
|
||||
const aNum = Number.isFinite(na)
|
||||
const bNum = Number.isFinite(nb)
|
||||
|
||||
if (aNum && bNum && na !== nb) return na - nb
|
||||
if (aNum !== bNum) return aNum ? -1 : 1
|
||||
|
||||
const cmp = compareCodeLike(ca, cb)
|
||||
if (cmp !== 0) return cmp
|
||||
|
||||
return compareCodeLike(a?.secondColor, b?.secondColor)
|
||||
}
|
||||
|
||||
function buildImageKey(code, color, secondColor = '') {
|
||||
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}::${String(secondColor || '').trim().toUpperCase()}`
|
||||
}
|
||||
|
||||
function imageNameMatches(fileName, color, secondColor) {
|
||||
const text = String(fileName || '').toUpperCase()
|
||||
if (!text) return false
|
||||
const tokens = text.replace(/[^A-Z0-9_]+/g, ' ').trim().split(/\s+/).filter(Boolean)
|
||||
if (!tokens.length) return false
|
||||
const colorTrim = String(color || '').trim().toUpperCase()
|
||||
const secondTrim = String(secondColor || '').trim().toUpperCase()
|
||||
if (colorTrim && !tokens.includes(colorTrim)) return false
|
||||
if (secondTrim && !tokens.includes(secondTrim)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function normalizeUploadsPath(storagePath) {
|
||||
@@ -358,16 +516,16 @@ function resolveProductImageUrl(item) {
|
||||
return { contentUrl, publicUrl }
|
||||
}
|
||||
|
||||
function getProductImageUrl(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
function getProductImageUrl(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const existing = productImageCache.value[key]
|
||||
if (existing !== undefined) return existing || ''
|
||||
void ensureProductImage(code, color)
|
||||
void ensureProductImage(code, color, secondColor)
|
||||
return ''
|
||||
}
|
||||
|
||||
async function onProductImageError(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
async function onProductImageError(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const fallback = String(productImageFallbackByKey.value[key] || '')
|
||||
if (fallback && !productImageContentLoading.value[key]) {
|
||||
productImageContentLoading.value[key] = true
|
||||
@@ -389,10 +547,12 @@ async function onProductImageError(code, color) {
|
||||
productImageCache.value[key] = ''
|
||||
}
|
||||
|
||||
async function ensureProductImage(code, color) {
|
||||
const key = buildImageKey(code, color)
|
||||
async function ensureProductImage(code, color, secondColor = '') {
|
||||
const key = buildImageKey(code, color, secondColor)
|
||||
const codeTrim = String(code || '').trim().toUpperCase()
|
||||
const colorTrim = String(color || '').trim().toUpperCase()
|
||||
const secondTrim = String(secondColor || '').trim().toUpperCase()
|
||||
const listKey = buildImageKey(codeTrim, colorTrim, secondTrim)
|
||||
if (!codeTrim) {
|
||||
productImageCache.value[key] = ''
|
||||
return ''
|
||||
@@ -406,42 +566,42 @@ async function ensureProductImage(code, color) {
|
||||
|
||||
productImageLoading.value[key] = true
|
||||
try {
|
||||
if (!productImageListByCode.value[codeTrim]) {
|
||||
if (!productImageListLoading.value[codeTrim]) {
|
||||
productImageListLoading.value[codeTrim] = true
|
||||
if (!productImageListByCode.value[listKey]) {
|
||||
if (!productImageListLoading.value[listKey]) {
|
||||
productImageListLoading.value[listKey] = true
|
||||
try {
|
||||
if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) {
|
||||
await new Promise((resolve) => imageListWaitQueue.push(resolve))
|
||||
}
|
||||
imageListActiveRequests++
|
||||
const res = await api.get('/product-images', { params: { code: codeTrim } })
|
||||
productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : []
|
||||
const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim }
|
||||
const res = await api.get('/product-images', { params })
|
||||
productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : []
|
||||
} catch (err) {
|
||||
productImageListByCode.value[codeTrim] = []
|
||||
productImageListByCode.value[listKey] = []
|
||||
const status = Number(err?.response?.status || 0)
|
||||
if (status >= 500 || status === 403 || status === 0) {
|
||||
productImageListBlockedUntil.value = Date.now() + 30 * 1000
|
||||
}
|
||||
console.warn('[ProductStockQuery] product image list fetch failed', { code: codeTrim, err })
|
||||
console.warn('[ProductStockQuery] product image list fetch failed', { code: codeTrim, color: colorTrim, secondColor: secondTrim, err })
|
||||
} finally {
|
||||
imageListActiveRequests = Math.max(0, imageListActiveRequests - 1)
|
||||
const nextInQueue = imageListWaitQueue.shift()
|
||||
if (nextInQueue) nextInQueue()
|
||||
delete productImageListLoading.value[codeTrim]
|
||||
delete productImageListLoading.value[listKey]
|
||||
}
|
||||
} else {
|
||||
while (productImageListLoading.value[codeTrim]) {
|
||||
while (productImageListLoading.value[listKey]) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const list = productImageListByCode.value[codeTrim] || []
|
||||
const list = productImageListByCode.value[listKey] || []
|
||||
let first = null
|
||||
if (colorTrim) {
|
||||
const needle = `-${colorTrim.toLowerCase()}-`
|
||||
if (colorTrim || secondTrim) {
|
||||
first = list.find((item) =>
|
||||
String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle)
|
||||
imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim)
|
||||
) || null
|
||||
}
|
||||
if (!first) first = list[0] || null
|
||||
@@ -571,8 +731,14 @@ const level1Groups = computed(() => {
|
||||
const secondColor = String(item.Yaka || '').trim()
|
||||
const depoKodu = String(item.Depo_Kodu || '').trim()
|
||||
const depoAdi = String(item.Depo_Adi || '').trim()
|
||||
const kategori = String(item.YETISKIN_GARSON || '').trim()
|
||||
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
|
||||
const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim()
|
||||
const urunIcerigi = String(item.URUN_ICERIGI || item.KISA_KAR || '').trim()
|
||||
const fit = String(item.BIRINCI_PARCA_FIT || '').trim()
|
||||
const drop = String(item.DR || '').trim()
|
||||
const kumas = String(item.BIRINCI_PARCA_KUMAS || '').trim()
|
||||
const karisim = String(item.BIRINCI_PARCA_KARISIM || '').trim()
|
||||
const aciklama = String(item.Madde_Aciklamasi || '').trim()
|
||||
const beden = normalizeSize(item.Beden || '')
|
||||
const qty = parseNumber(item.Kullanilabilir_Envanter)
|
||||
@@ -600,8 +766,14 @@ const level1Groups = computed(() => {
|
||||
colorCode,
|
||||
colorDesc,
|
||||
secondColor,
|
||||
kategori,
|
||||
urunAnaGrubu,
|
||||
urunAltGrubu,
|
||||
urunIcerigi,
|
||||
fit,
|
||||
drop,
|
||||
kumas,
|
||||
karisim,
|
||||
aciklama,
|
||||
sizeTotals: emptySizeTotals(),
|
||||
totalQty: 0,
|
||||
@@ -638,10 +810,12 @@ const level1Groups = computed(() => {
|
||||
|
||||
return Array.from(l1Map.values()).map((l1) => ({
|
||||
...l1,
|
||||
children: Array.from(l1.childrenMap.values()).map((l2) => ({
|
||||
...l2,
|
||||
children: Array.from(l2.childrenMap.values())
|
||||
}))
|
||||
children: Array.from(l1.childrenMap.values())
|
||||
.map((l2) => ({
|
||||
...l2,
|
||||
children: Array.from(l2.childrenMap.values())
|
||||
}))
|
||||
.sort(sortByColorCodeAsc)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -740,10 +914,65 @@ async function fetchStockByCode() {
|
||||
function onLevel2Click(productCode, grp2) {
|
||||
toggleOpen(grp2.key)
|
||||
if (isOpen(grp2.key)) {
|
||||
void ensureProductImage(productCode, grp2.colorCode)
|
||||
void ensureProductImage(productCode, grp2.colorCode, grp2.secondColor)
|
||||
}
|
||||
}
|
||||
|
||||
async function openProductCard(grp1, grp2) {
|
||||
const productCode = String(grp1?.productCode || '').trim()
|
||||
const colorCode = String(grp2?.colorCode || '').trim()
|
||||
const secondColor = String(grp2?.secondColor || '').trim()
|
||||
const listKey = buildImageKey(productCode, colorCode, secondColor)
|
||||
|
||||
await ensureProductImage(productCode, colorCode, secondColor)
|
||||
const list = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : []
|
||||
const images = list
|
||||
.map((item) => {
|
||||
const resolved = resolveProductImageUrl(item)
|
||||
return resolved.contentUrl || resolved.publicUrl || ''
|
||||
})
|
||||
.filter((x) => String(x || '').trim() !== '')
|
||||
|
||||
if (!images.length) {
|
||||
const single = getProductImageUrl(productCode, colorCode, secondColor)
|
||||
if (single) images.push(single)
|
||||
}
|
||||
|
||||
productCardImages.value = images
|
||||
productCardSlide.value = 0
|
||||
productCardData.value = {
|
||||
productCode,
|
||||
colorCode,
|
||||
secondColor,
|
||||
kategori: String(grp2?.kategori || '').trim(),
|
||||
urunAnaGrubu: String(grp2?.urunAnaGrubu || '').trim(),
|
||||
urunAltGrubu: String(grp2?.urunAltGrubu || '').trim(),
|
||||
urunIcerigi: String(grp2?.urunIcerigi || '').trim(),
|
||||
fit: String(grp2?.fit || '').trim(),
|
||||
drop: String(grp2?.drop || '').trim(),
|
||||
kumas: String(grp2?.kumas || '').trim(),
|
||||
karisim: String(grp2?.karisim || '').trim(),
|
||||
sizeTotals: grp2?.sizeTotals || {},
|
||||
totalQty: Number(grp2?.totalQty || 0)
|
||||
}
|
||||
productCardDialog.value = true
|
||||
}
|
||||
|
||||
function openProductImageFullscreen(src) {
|
||||
const value = String(src || '').trim()
|
||||
if (!value) return
|
||||
productImageFullscreenSrc.value = value
|
||||
productImageFullscreenZoom.value = 1
|
||||
productImageFullscreenDialog.value = true
|
||||
}
|
||||
|
||||
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 productImageFullscreenZoom.value = 1
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
selectedProductCode.value = ''
|
||||
rawRows.value = []
|
||||
@@ -757,6 +986,13 @@ function resetForm() {
|
||||
productImageFallbackByKey.value = {}
|
||||
productImageContentLoading.value = {}
|
||||
productImageListBlockedUntil.value = 0
|
||||
productCardDialog.value = false
|
||||
productCardData.value = {}
|
||||
productCardImages.value = []
|
||||
productCardSlide.value = 0
|
||||
productImageFullscreenDialog.value = false
|
||||
productImageFullscreenSrc.value = ''
|
||||
productImageFullscreenZoom.value = 1
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -777,7 +1013,7 @@ onUnmounted(() => {
|
||||
--grp-title-w: 44px;
|
||||
--psq-header-h: 56px;
|
||||
--psq-col-adet: calc(var(--col-adet) + var(--beden-w));
|
||||
--psq-col-img: 126px;
|
||||
--psq-col-img: 190px;
|
||||
--psq-l1-lift: 42px;
|
||||
}
|
||||
|
||||
@@ -860,8 +1096,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.order-sub-header.level-2 {
|
||||
min-height: 82px !important;
|
||||
height: 82px !important;
|
||||
min-height: 252px !important;
|
||||
height: 252px !important;
|
||||
background: #fff9c4 !important;
|
||||
border-top: 1px solid #d4c79f !important;
|
||||
border-bottom: 1px solid #d4c79f !important;
|
||||
@@ -1107,15 +1343,17 @@ onUnmounted(() => {
|
||||
.order-sub-header.level-2 .sub-image.level2-image {
|
||||
grid-column: 8 / 9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-left: 1px solid #d4c79f;
|
||||
}
|
||||
|
||||
.product-image-card {
|
||||
width: 110px;
|
||||
height: 68px;
|
||||
width: 162px;
|
||||
height: 216px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1128,6 +1366,7 @@ onUnmounted(() => {
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
@@ -1139,6 +1378,178 @@ onUnmounted(() => {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.detail-open-btn {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.product-card-dialog {
|
||||
background: #fffef9;
|
||||
}
|
||||
|
||||
.product-card-stock {
|
||||
background: #f8f5e7;
|
||||
border: 1px solid #e2d9b6;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stock-size-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stock-size-chip {
|
||||
border: 1px solid #d8cca6;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stock-size-chip .label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-card-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 1fr) 420px;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.product-card-images {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-height: 560px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.product-card-carousel {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dialog-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-image-stage {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
height: 560px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f7f4e9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-image-empty {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
height: 560px;
|
||||
border: 1px dashed #cabf9a;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #faf7ee;
|
||||
}
|
||||
|
||||
.image-fullscreen-dialog {
|
||||
background: #f4f0e2;
|
||||
}
|
||||
|
||||
.image-fullscreen-body {
|
||||
height: calc(100vh - 72px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-fullscreen-stage {
|
||||
width: min(96vw, 1400px);
|
||||
height: calc(100vh - 120px);
|
||||
border-radius: 10px;
|
||||
background: #efe7cc;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-fullscreen-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-card-fields {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border: 1px solid #e2d9b6;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
height: 560px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 1fr;
|
||||
gap: 8px;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid #f0ead7;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.field-row .k {
|
||||
color: #5a4f2c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-row .v {
|
||||
color: #1f1f1f;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.q-btn,
|
||||
.q-icon,
|
||||
.product-image-card,
|
||||
.cursor-pointer {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.product-card-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.product-card-images,
|
||||
.product-card-fields {
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.product-card-fields {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.order-sub-header.level-2 .sub-right .top-total {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1;
|
||||
|
||||
Reference in New Issue
Block a user