Fix product performance grouped JSON build
This commit is contained in:
@@ -25,6 +25,7 @@ type ProductImageItem struct {
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
ThumbURL string `json:"thumb_url,omitempty"`
|
||||
FullURL string `json:"full_url,omitempty"`
|
||||
StoredInDB bool `json:"stored_in_db,omitempty"`
|
||||
}
|
||||
|
||||
type ProductImageBatchItem struct {
|
||||
@@ -136,6 +137,24 @@ func extractImageUUID(storagePath, fileName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func enrichProductImageItem(it *ProductImageItem) {
|
||||
if it == nil {
|
||||
return
|
||||
}
|
||||
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
||||
it.UUID = u
|
||||
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
|
||||
it.FullURL = "/uploads/image/" + u + ".jpg"
|
||||
}
|
||||
if it.StoredInDB {
|
||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
||||
return
|
||||
}
|
||||
if resolved, _ := resolveStoragePath(it.Storage); resolved != "" {
|
||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/product-images/batch
|
||||
func PostProductImagesBatchHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -208,6 +227,7 @@ ranked AS (
|
||||
COALESCE(b.file_name,'') AS file_name,
|
||||
COALESCE(b.file_size,0) AS file_size,
|
||||
COALESCE(b.storage_path,'') AS storage_path,
|
||||
COALESCE(b.stored_in_db,false) AS stored_in_db,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY mi.key
|
||||
ORDER BY
|
||||
@@ -225,7 +245,7 @@ ranked AS (
|
||||
AND b.typ='img'
|
||||
AND b.src_id=mi.mmitem_id
|
||||
)
|
||||
SELECT key, id, file_name, file_size, storage_path
|
||||
SELECT key, id, file_name, file_size, storage_path, stored_in_db
|
||||
FROM ranked
|
||||
WHERE rn <= 1
|
||||
ORDER BY key, rn
|
||||
@@ -241,15 +261,10 @@ ORDER BY key, rn
|
||||
for rows.Next() {
|
||||
var key string
|
||||
var it ProductImageItem
|
||||
if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil {
|
||||
if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB); err != nil {
|
||||
continue
|
||||
}
|
||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
||||
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
||||
it.UUID = u
|
||||
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
|
||||
it.FullURL = "/uploads/image/" + u + ".jpg"
|
||||
}
|
||||
enrichProductImageItem(&it)
|
||||
grouped[key] = append(grouped[key], it)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -339,7 +354,8 @@ SELECT
|
||||
id,
|
||||
COALESCE(file_name,'') AS file_name,
|
||||
COALESCE(file_size,0) AS file_size,
|
||||
COALESCE(storage_path,'') AS storage_path
|
||||
COALESCE(storage_path,'') AS storage_path,
|
||||
COALESCE(stored_in_db,false) AS stored_in_db
|
||||
FROM dfblob
|
||||
WHERE typ='img'
|
||||
AND src_table='mmitem'
|
||||
@@ -370,15 +386,10 @@ ORDER BY
|
||||
items := make([]ProductImageItem, 0, 16)
|
||||
for rows.Next() {
|
||||
var it ProductImageItem
|
||||
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil {
|
||||
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB); err != nil {
|
||||
continue
|
||||
}
|
||||
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
|
||||
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
|
||||
it.UUID = u
|
||||
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
|
||||
it.FullURL = "/uploads/image/" + u + ".jpg"
|
||||
}
|
||||
enrichProductImageItem(&it)
|
||||
items = append(items, it)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -529,6 +540,9 @@ WHERE id = $1
|
||||
}
|
||||
|
||||
resolved, _ := resolveStoragePath(storagePath)
|
||||
if resolved == "" {
|
||||
resolved, _ = resolveStoragePath(fileName)
|
||||
}
|
||||
if resolved == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="topScrollbarRef" class="performance-top-scrollbar q-mb-xs">
|
||||
<div ref="topScrollbarInnerRef" class="performance-top-scrollbar-inner"></div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
v-if="activeTab === 'general'"
|
||||
flat
|
||||
@@ -111,7 +115,7 @@
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
@@ -224,7 +228,7 @@
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'order_usd', descending: true }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
@@ -558,7 +562,7 @@
|
||||
:pagination="pagination"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
@@ -714,7 +718,7 @@
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
@@ -1008,7 +1012,7 @@
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'performance_score_total', descending: true }"
|
||||
:sort-method="sortProductGroupedTableRows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="64"
|
||||
:virtual-scroll-item-size="productThumbVirtualItemSize"
|
||||
:virtual-scroll-slice-size="tableVirtualSliceSize"
|
||||
>
|
||||
<template #header-cell="props">
|
||||
@@ -1506,7 +1510,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { useProductPerformanceStore } from 'src/stores/productPerformanceStore'
|
||||
@@ -1582,8 +1586,13 @@ const filterBusy = ref(false)
|
||||
const bulkExpandLoading = ref(false)
|
||||
const generalRowsLoaded = ref(false)
|
||||
const detailLevelMenuOpen = ref(false)
|
||||
const topScrollbarRef = ref(null)
|
||||
const topScrollbarInnerRef = ref(null)
|
||||
let backendGroupedTimer = null
|
||||
let imagePrimeActiveRequests = 0
|
||||
let activeTableMiddleEl = null
|
||||
let topScrollbarResizeObserver = null
|
||||
let topScrollbarSyncing = false
|
||||
|
||||
const columnFilters = reactive({})
|
||||
const columnFilterSearch = reactive({})
|
||||
@@ -1602,9 +1611,10 @@ const selectedExpandLevelKeysByTab = reactive({
|
||||
})
|
||||
const productKpiFetchLimit = 50000
|
||||
const reportFetchLimit = 50000
|
||||
const maxBulkExpandKeys = 500
|
||||
const maxBackendExpandedKeys = 120
|
||||
const maxBulkExpandKeys = 1000
|
||||
const maxBackendExpandedKeys = 1000
|
||||
const tableVirtualSliceSize = 160
|
||||
const productThumbVirtualItemSize = 236
|
||||
const maxAutoExpandLevelByTab = {
|
||||
products: 8,
|
||||
product_detail: 4,
|
||||
@@ -2579,10 +2589,10 @@ function maxAutoExpandThroughLevelForTab (tabKey = activeTab.value) {
|
||||
}
|
||||
|
||||
function maxBackendExpandedKeysForTab (tabKey = activeTab.value) {
|
||||
if (tabKey === 'product_detail') return 260
|
||||
if (tabKey === 'products' || tabKey === 'idle') return 260
|
||||
if (salesBreakdownTabKeys.includes(tabKey)) return 180
|
||||
return 100
|
||||
if (tabKey === 'product_detail') return 1000
|
||||
if (tabKey === 'products' || tabKey === 'idle') return 1000
|
||||
if (salesBreakdownTabKeys.includes(tabKey)) return 1000
|
||||
return 1000
|
||||
}
|
||||
|
||||
const autoExpandThroughLevel = computed(() => {
|
||||
@@ -2776,12 +2786,14 @@ function buildGroupedTableRows (tableKey, sourceRows) {
|
||||
function backendRowsForTab (tabKey, fallbackRows) {
|
||||
const rows = backendGroupedRows.value[tabKey]
|
||||
const backendRows = Array.isArray(rows) && rows.length ? rows : []
|
||||
if (hasActiveTableFilters(tabKey)) {
|
||||
return tabKey === 'product_detail'
|
||||
? filterRowsBySelectedDetailMainGroup(fallbackRows)
|
||||
: fallbackRows
|
||||
}
|
||||
const sourceRows = tabKey === 'product_detail'
|
||||
? filterRowsBySelectedDetailMainGroup(backendRows.length ? backendRows : fallbackRows)
|
||||
: (backendRows.length ? backendRows : fallbackRows)
|
||||
if (hasActiveTableFilters(tabKey)) {
|
||||
return filterRowsForTable(tabKey, sourceRows, columnsForTableKey(tabKey))
|
||||
}
|
||||
return sourceRows
|
||||
}
|
||||
|
||||
@@ -3378,23 +3390,53 @@ function isGroupExpanded (key) {
|
||||
return expandedGroups.value[key] === true
|
||||
}
|
||||
|
||||
function groupKeyDepth (key) {
|
||||
const parts = String(key || '').split('|').filter(Boolean)
|
||||
return Math.max(-1, parts.length - 2)
|
||||
}
|
||||
|
||||
function activeTabPrefix () {
|
||||
return `tab:${activeTab.value}|`
|
||||
}
|
||||
|
||||
function expandedLevelKeysThrough (level) {
|
||||
if (level < 0) return []
|
||||
const keys = []
|
||||
collectGroupKeys(keys, activeGroupSourceRows.value, 0, [`tab:${activeTab.value}`], activeGroupLevels.value, level)
|
||||
return keys
|
||||
}
|
||||
|
||||
function replaceActiveTabExpandedGroups (keys) {
|
||||
const tabPrefix = activeTabPrefix()
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(tabPrefix))
|
||||
)
|
||||
for (const key of keys) next[key] = true
|
||||
expandedGroups.value = next
|
||||
}
|
||||
|
||||
function setSelectedExpandLevelsThrough (level) {
|
||||
activeSelectedExpandLevelKeys.value = level < 0
|
||||
? []
|
||||
: activeGroupLevels.value.slice(0, level + 1).map(item => item.key)
|
||||
}
|
||||
|
||||
function applyActiveExpansionThroughLevel (level) {
|
||||
setSelectedExpandLevelsThrough(level)
|
||||
replaceActiveTabExpandedGroups(expandedLevelKeysThrough(level))
|
||||
lastManualExpandedGroupKey.value = ''
|
||||
}
|
||||
|
||||
function toggleGroup (key) {
|
||||
lastManualExpandedGroupKey.value = key
|
||||
expandedGroups.value = {
|
||||
...expandedGroups.value,
|
||||
[key]: !isGroupExpanded(key)
|
||||
}
|
||||
const level = groupKeyDepth(key)
|
||||
applyActiveExpansionThroughLevel(isGroupExpanded(key) ? level - 1 : level)
|
||||
scheduleLoadBackendGroupedRows()
|
||||
}
|
||||
|
||||
async function collapseAllProductGroups () {
|
||||
if (bulkExpandLoading.value) return
|
||||
detailLevelMenuOpen.value = false
|
||||
const tabPrefix = `tab:${activeTab.value}|`
|
||||
expandedGroups.value = Object.fromEntries(
|
||||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(tabPrefix))
|
||||
)
|
||||
lastManualExpandedGroupKey.value = ''
|
||||
applyActiveExpansionThroughLevel(-1)
|
||||
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
|
||||
bulkExpandLoading.value = true
|
||||
try {
|
||||
@@ -3410,12 +3452,7 @@ async function expandSelectedProductGroups () {
|
||||
const keys = productAutoExpandKeys.value
|
||||
const maxKeys = Math.min(maxBulkExpandKeys, maxBackendExpandedKeysForTab(activeTab.value))
|
||||
const expandableKeys = keys.slice(0, maxKeys)
|
||||
const tabPrefix = `tab:${activeTab.value}|`
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith(tabPrefix))
|
||||
)
|
||||
for (const key of expandableKeys) next[key] = true
|
||||
expandedGroups.value = next
|
||||
replaceActiveTabExpandedGroups(expandableKeys)
|
||||
lastManualExpandedGroupKey.value = ''
|
||||
if (keys.length > expandableKeys.length) {
|
||||
Notify.create({
|
||||
@@ -3590,6 +3627,7 @@ function runWithFilterBusy (apply) {
|
||||
filterBusy.value = true
|
||||
const done = () => {
|
||||
filterBusy.value = false
|
||||
syncExpansionAfterFilterChange()
|
||||
}
|
||||
const execute = () => {
|
||||
apply()
|
||||
@@ -3608,6 +3646,17 @@ function runWithFilterBusy (apply) {
|
||||
window.requestAnimationFrame(execute)
|
||||
}
|
||||
|
||||
function syncExpansionAfterFilterChange () {
|
||||
if (!backendGroupedSupportedTab(activeTab.value)) return
|
||||
const level = selectedExpandThroughLevel.value
|
||||
if (level < 0) {
|
||||
scheduleLoadBackendGroupedRows()
|
||||
return
|
||||
}
|
||||
applyActiveExpansionThroughLevel(autoExpandThroughLevel.value)
|
||||
scheduleLoadBackendGroupedRows()
|
||||
}
|
||||
|
||||
function filterRowsForTable (tableKey, sourceRows, sourceColumns) {
|
||||
return sourceRows.filter(row => {
|
||||
return sourceColumns.every(col => {
|
||||
@@ -4044,13 +4093,6 @@ function normalizeUploadsPath (storagePath) {
|
||||
function resolveProductImageUrl (item) {
|
||||
if (!item || typeof item !== 'object') return ''
|
||||
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) return contentURL
|
||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||||
|
||||
const imageId = Number(item.id || item.ID || 0)
|
||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||
|
||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||
if (thumbURL) return thumbURL
|
||||
|
||||
@@ -4061,7 +4103,15 @@ function resolveProductImageUrl (item) {
|
||||
if (uploadsPath) return uploadsPath
|
||||
|
||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
||||
return fileName ? `/uploads/image/${fileName}` : ''
|
||||
if (fileName) return `/uploads/image/${fileName}`
|
||||
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) return contentURL
|
||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||||
|
||||
const imageId = Number(item.id || item.ID || 0)
|
||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||
return ''
|
||||
}
|
||||
|
||||
async function fetchProductImagesForRow (row) {
|
||||
@@ -4122,6 +4172,15 @@ function getCachedProductImageUrl (row) {
|
||||
function markProductImageFailed (row) {
|
||||
const key = productImageKey(row)
|
||||
if (!key) return
|
||||
const currentUrl = imageUrlByKey.value[key] || performanceStore.imageUrl(key) || ''
|
||||
const list = imageListByKey.value[key] || performanceStore.imageList(key) || []
|
||||
const nextList = Array.isArray(list) ? list.filter(url => url && url !== currentUrl) : []
|
||||
if (nextList.length) {
|
||||
performanceStore.setImageCache(key, nextList)
|
||||
imageListByKey.value = { ...imageListByKey.value, [key]: nextList }
|
||||
imageUrlByKey.value = { ...imageUrlByKey.value, [key]: nextList[0] || '' }
|
||||
return
|
||||
}
|
||||
imageFailedKeys.value = new Set([...imageFailedKeys.value, key])
|
||||
performanceStore.setImageCache(key, [])
|
||||
imageListByKey.value = { ...imageListByKey.value, [key]: [] }
|
||||
@@ -4282,6 +4341,63 @@ function scheduleLoadBackendGroupedRows () {
|
||||
}, 260)
|
||||
}
|
||||
|
||||
function cleanupTopScrollbarSync () {
|
||||
if (activeTableMiddleEl) {
|
||||
activeTableMiddleEl.removeEventListener('scroll', syncTopScrollbarFromTable)
|
||||
}
|
||||
if (topScrollbarRef.value) {
|
||||
topScrollbarRef.value.removeEventListener('scroll', syncTableFromTopScrollbar)
|
||||
}
|
||||
if (topScrollbarResizeObserver) {
|
||||
topScrollbarResizeObserver.disconnect()
|
||||
topScrollbarResizeObserver = null
|
||||
}
|
||||
activeTableMiddleEl = null
|
||||
}
|
||||
|
||||
function updateTopScrollbarSize () {
|
||||
const outer = topScrollbarRef.value
|
||||
const inner = topScrollbarInnerRef.value
|
||||
const tableMiddle = activeTableMiddleEl
|
||||
if (!outer || !inner || !tableMiddle) return
|
||||
inner.style.width = `${tableMiddle.scrollWidth}px`
|
||||
outer.scrollLeft = tableMiddle.scrollLeft
|
||||
outer.style.display = tableMiddle.scrollWidth > tableMiddle.clientWidth + 2 ? 'block' : 'none'
|
||||
}
|
||||
|
||||
function syncTopScrollbarFromTable () {
|
||||
if (topScrollbarSyncing || !topScrollbarRef.value || !activeTableMiddleEl) return
|
||||
topScrollbarSyncing = true
|
||||
topScrollbarRef.value.scrollLeft = activeTableMiddleEl.scrollLeft
|
||||
topScrollbarSyncing = false
|
||||
}
|
||||
|
||||
function syncTableFromTopScrollbar () {
|
||||
if (topScrollbarSyncing || !topScrollbarRef.value || !activeTableMiddleEl) return
|
||||
topScrollbarSyncing = true
|
||||
activeTableMiddleEl.scrollLeft = topScrollbarRef.value.scrollLeft
|
||||
topScrollbarSyncing = false
|
||||
}
|
||||
|
||||
function setupTopScrollbarSync () {
|
||||
if (typeof document === 'undefined') return
|
||||
void nextTick(() => {
|
||||
cleanupTopScrollbarSync()
|
||||
activeTableMiddleEl = document.querySelector('.product-performance-page .performance-table .q-table__middle')
|
||||
if (!activeTableMiddleEl || !topScrollbarRef.value) return
|
||||
activeTableMiddleEl.addEventListener('scroll', syncTopScrollbarFromTable, { passive: true })
|
||||
topScrollbarRef.value.addEventListener('scroll', syncTableFromTopScrollbar, { passive: true })
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
topScrollbarResizeObserver = new ResizeObserver(updateTopScrollbarSize)
|
||||
topScrollbarResizeObserver.observe(activeTableMiddleEl)
|
||||
const table = activeTableMiddleEl.querySelector('table')
|
||||
if (table) topScrollbarResizeObserver.observe(table)
|
||||
}
|
||||
updateTopScrollbarSize()
|
||||
window.setTimeout(updateTopScrollbarSize, 120)
|
||||
})
|
||||
}
|
||||
|
||||
function switchPerformanceTab (tabName) {
|
||||
if (!tabName || tabName === activeTab.value || loading.value) return
|
||||
backendGroupedLoading.value = true
|
||||
@@ -4565,22 +4681,38 @@ function bucketColor (value) {
|
||||
watch(activeTab, () => {
|
||||
void ensureActiveTabData(false)
|
||||
scheduleLoadBackendGroupedRows()
|
||||
setupTopScrollbarSync()
|
||||
})
|
||||
|
||||
watch([
|
||||
displayProductKpiTableRows,
|
||||
displayIdleTableRows,
|
||||
displayOrderProductCustomerTableRows,
|
||||
displayOrderMarketDetailTableRows,
|
||||
displayActiveSalesBreakdownTableRows,
|
||||
filteredGeneralRows,
|
||||
filteredMarketRows,
|
||||
filteredCountryRows,
|
||||
filteredCustomerRows
|
||||
], () => {
|
||||
setupTopScrollbarSync()
|
||||
}, { flush: 'post' })
|
||||
|
||||
watch(detailMainGroupOptions, () => {
|
||||
ensureDetailMainGroupSelection()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(selectedDetailMainGroup, () => {
|
||||
if (activeTab.value === 'product_detail') {
|
||||
expandedGroups.value = Object.fromEntries(
|
||||
Object.entries(expandedGroups.value).filter(([key]) => !key.startsWith('tab:product_detail|'))
|
||||
)
|
||||
backendGroupedRows.value = {
|
||||
...backendGroupedRows.value,
|
||||
product_detail: []
|
||||
}
|
||||
scheduleLoadBackendGroupedRows()
|
||||
void nextTick(() => {
|
||||
const level = selectedExpandThroughLevel.value
|
||||
applyActiveExpansionThroughLevel(level >= 0 ? autoExpandThroughLevel.value : -1)
|
||||
scheduleLoadBackendGroupedRows()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4593,7 +4725,15 @@ watch(productTableRows, tableRows => {
|
||||
void primeProductImages(imageRows)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(reload)
|
||||
onMounted(() => {
|
||||
void reload()
|
||||
setupTopScrollbarSync()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupTopScrollbarSync()
|
||||
if (backendGroupedTimer) window.clearTimeout(backendGroupedTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -4793,11 +4933,29 @@ onMounted(reload)
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.performance-top-scrollbar {
|
||||
display: none;
|
||||
height: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #d5dce5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.performance-top-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table__middle) {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table__middle::-webkit-scrollbar:horizontal) {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.performance-table :deep(.q-table__bottom) {
|
||||
display: none;
|
||||
}
|
||||
@@ -4916,7 +5074,7 @@ onMounted(reload)
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table) {
|
||||
min-width: 3400px;
|
||||
min-width: 3170px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
@@ -4938,14 +5096,14 @@ onMounted(reload)
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(1)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(1)) {
|
||||
left: 0;
|
||||
width: 420px;
|
||||
min-width: 420px;
|
||||
max-width: 420px;
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
max-width: 190px;
|
||||
}
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(2)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(2)) {
|
||||
left: 420px;
|
||||
left: 190px;
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
@@ -4953,7 +5111,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(3)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(3)) {
|
||||
left: 560px;
|
||||
left: 330px;
|
||||
width: 110px;
|
||||
min-width: 110px;
|
||||
max-width: 110px;
|
||||
@@ -4961,7 +5119,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(4)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(4)) {
|
||||
left: 670px;
|
||||
left: 440px;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
@@ -4969,7 +5127,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(5)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(5)) {
|
||||
left: 820px;
|
||||
left: 590px;
|
||||
width: 170px;
|
||||
min-width: 170px;
|
||||
max-width: 170px;
|
||||
@@ -4977,7 +5135,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(6)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(6)) {
|
||||
left: 990px;
|
||||
left: 760px;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
@@ -4985,7 +5143,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(7)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(7)) {
|
||||
left: 1140px;
|
||||
left: 910px;
|
||||
width: 130px;
|
||||
min-width: 130px;
|
||||
max-width: 130px;
|
||||
@@ -4993,7 +5151,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(8)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(8)) {
|
||||
left: 1270px;
|
||||
left: 1040px;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
max-width: 90px;
|
||||
@@ -5001,7 +5159,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(9)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(9)) {
|
||||
left: 1360px;
|
||||
left: 1130px;
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
@@ -5009,7 +5167,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table :deep(.q-table th:nth-child(10)),
|
||||
.product-breakdown-table :deep(.q-table td:nth-child(10)) {
|
||||
left: 1440px;
|
||||
left: 1210px;
|
||||
width: 130px;
|
||||
min-width: 130px;
|
||||
max-width: 130px;
|
||||
@@ -5017,7 +5175,7 @@ onMounted(reload)
|
||||
}
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table) {
|
||||
min-width: 2850px;
|
||||
min-width: 2620px;
|
||||
}
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(n+8)) {
|
||||
@@ -5051,14 +5209,14 @@ onMounted(reload)
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(1)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(1)) {
|
||||
left: 0;
|
||||
width: 420px;
|
||||
min-width: 420px;
|
||||
max-width: 420px;
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
max-width: 190px;
|
||||
}
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(2)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(2)) {
|
||||
left: 420px;
|
||||
left: 190px;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
@@ -5066,7 +5224,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(3)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(3)) {
|
||||
left: 570px;
|
||||
left: 340px;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
@@ -5074,7 +5232,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(4)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(4)) {
|
||||
left: 720px;
|
||||
left: 490px;
|
||||
width: 130px;
|
||||
min-width: 130px;
|
||||
max-width: 130px;
|
||||
@@ -5082,7 +5240,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(5)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(5)) {
|
||||
left: 850px;
|
||||
left: 620px;
|
||||
width: 170px;
|
||||
min-width: 170px;
|
||||
max-width: 170px;
|
||||
@@ -5090,7 +5248,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(6)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(6)) {
|
||||
left: 1020px;
|
||||
left: 790px;
|
||||
width: 170px;
|
||||
min-width: 170px;
|
||||
max-width: 190px;
|
||||
@@ -5098,7 +5256,7 @@ onMounted(reload)
|
||||
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table th:nth-child(7)),
|
||||
.product-breakdown-table.product-detail-table :deep(.q-table td:nth-child(7)) {
|
||||
left: 1190px;
|
||||
left: 960px;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
@@ -5494,21 +5652,21 @@ onMounted(reload)
|
||||
}
|
||||
|
||||
.product-image-cell {
|
||||
width: 420px;
|
||||
min-width: 420px;
|
||||
max-width: 420px;
|
||||
width: 190px;
|
||||
min-width: 190px;
|
||||
max-width: 190px;
|
||||
padding: 10px !important;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.product-thumb-button {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
width: 162px;
|
||||
height: 216px;
|
||||
border: 1px solid #d9dde3;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -5518,8 +5676,8 @@ onMounted(reload)
|
||||
|
||||
.product-thumb,
|
||||
.product-thumb-placeholder {
|
||||
width: 384px;
|
||||
height: 384px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-thumb {
|
||||
|
||||
@@ -17,13 +17,6 @@ function normalizeUploadsPath (storagePath) {
|
||||
function resolveProductImageUrl (item) {
|
||||
if (!item || typeof item !== 'object') return ''
|
||||
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) return contentURL
|
||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||||
|
||||
const imageId = Number(item.id || item.ID || 0)
|
||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||
|
||||
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
|
||||
if (thumbURL) return thumbURL
|
||||
|
||||
@@ -34,7 +27,15 @@ function resolveProductImageUrl (item) {
|
||||
if (uploadsPath) return uploadsPath
|
||||
|
||||
const fileName = String(item.file_name || item.FileName || '').trim()
|
||||
return fileName ? `/uploads/image/${fileName}` : ''
|
||||
if (fileName) return `/uploads/image/${fileName}`
|
||||
|
||||
const contentURL = String(item.content_url || item.ContentURL || '').trim()
|
||||
if (contentURL.startsWith('/api/')) return contentURL
|
||||
if (contentURL.startsWith('/')) return `/api${contentURL}`
|
||||
|
||||
const imageId = Number(item.id || item.ID || 0)
|
||||
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizedList (value, sort = false) {
|
||||
|
||||
Reference in New Issue
Block a user