ui: update ProductPerformanceProfitability page to enhance table views and tabs management

This commit is contained in:
M_Kececi
2026-07-03 00:30:54 +03:00
parent c536b85a4c
commit be6feb8aa8
3 changed files with 710 additions and 71 deletions
+166
View File
@@ -0,0 +1,166 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
const GROUPED_TTL_MS = 2 * 60 * 1000
const IMAGE_TTL_MS = 15 * 60 * 1000
function normalizeUploadsPath (storagePath) {
const raw = String(storagePath || '').trim()
if (!raw) return ''
const normalized = raw.replace(/\\/g, '/')
const idx = normalized.toLowerCase().indexOf('/uploads/')
if (idx >= 0) return normalized.slice(idx)
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
return ''
}
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
const fullURL = String(item.full_url || item.fullUrl || '').trim()
if (fullURL) return fullURL
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
if (uploadsPath) return uploadsPath
const fileName = String(item.file_name || item.FileName || '').trim()
return fileName ? `/uploads/image/${fileName}` : ''
}
function sortedList (value) {
if (!Array.isArray(value)) return []
return value.map(x => String(x || '').trim()).filter(Boolean).sort()
}
export const useProductPerformanceStore = defineStore('product-performance-store', {
state: () => ({
groupedRowsByKey: {},
groupedLoadedAtByKey: {},
groupedLoadingByKey: {},
groupedInFlightByKey: {},
imageUrlByKey: {},
imageListByKey: {},
imageLoadedAtByKey: {}
}),
getters: {
groupedRows: state => key => state.groupedRowsByKey[key] || [],
hasImageKey: state => key => Object.prototype.hasOwnProperty.call(state.imageListByKey, key),
imageUrl: state => key => state.imageUrlByKey[key] || '',
imageList: state => key => state.imageListByKey[key] || []
},
actions: {
groupedCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
groupLevels: sortedList(params.groupLevels),
expandedKeys: sortedList(params.expandedKeys),
limit: Number(params.limit || 0)
})
},
async fetchGroupedRows (params = {}, options = {}) {
const key = this.groupedCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedLoadedAtByKey[key] || 0)
const cached = this.groupedRowsByKey[key]
if (!options.force && Array.isArray(cached) && now - loadedAt < GROUPED_TTL_MS) return cached
if (this.groupedInFlightByKey[key]) return this.groupedInFlightByKey[key]
this.groupedLoadingByKey = { ...this.groupedLoadingByKey, [key]: true }
const request = api.get('/pricing/product-performance/grouped', {
params: {
mode: params.mode,
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels.join(',') : params.groupLevels,
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys.join(',') : params.expandedKeys,
limit: params.limit
},
timeout: 90000
}).then(resp => {
const rows = Array.isArray(resp?.data) ? resp.data : []
this.groupedRowsByKey = { ...this.groupedRowsByKey, [key]: rows }
this.groupedLoadedAtByKey = { ...this.groupedLoadedAtByKey, [key]: Date.now() }
return rows
}).finally(() => {
const nextLoading = { ...this.groupedLoadingByKey }
const nextInFlight = { ...this.groupedInFlightByKey }
delete nextLoading[key]
delete nextInFlight[key]
this.groupedLoadingByKey = nextLoading
this.groupedInFlightByKey = nextInFlight
})
this.groupedInFlightByKey = { ...this.groupedInFlightByKey, [key]: request }
return request
},
setImageCache (key, urls = []) {
const normalizedKey = String(key || '').trim()
if (!normalizedKey) return
const list = Array.isArray(urls) ? urls.filter(Boolean) : []
this.imageListByKey = { ...this.imageListByKey, [normalizedKey]: list }
this.imageUrlByKey = { ...this.imageUrlByKey, [normalizedKey]: list[0] || '' }
this.imageLoadedAtByKey = { ...this.imageLoadedAtByKey, [normalizedKey]: Date.now() }
},
async fetchImageBatch (items = [], options = {}) {
const now = Date.now()
const pending = []
const seen = new Set()
for (const item of items) {
const key = String(item?.key || '').trim()
if (!key || seen.has(key)) continue
seen.add(key)
const loadedAt = Number(this.imageLoadedAtByKey[key] || 0)
if (!options.force && Object.prototype.hasOwnProperty.call(this.imageListByKey, key) && now - loadedAt < IMAGE_TTL_MS) continue
pending.push({
key,
code: String(item?.code || '').trim(),
dim1: String(item?.dim1 || '').trim(),
dim3: String(item?.dim3 || '').trim()
})
}
if (!pending.length) {
const keys = Array.from(seen.values())
return {
lists: Object.fromEntries(keys.map(key => [key, this.imageListByKey[key] || []])),
urls: Object.fromEntries(keys.map(key => [key, this.imageUrlByKey[key] || '']))
}
}
const resp = await api.post('/product-images/batch', { items: pending }, { timeout: 60000 })
const returned = new Set()
const lists = {}
const urlsByKey = {}
for (const item of Array.isArray(resp?.data) ? resp.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const urls = (Array.isArray(item.images) ? item.images : []).map(resolveProductImageUrl).filter(Boolean)
this.setImageCache(key, urls)
lists[key] = urls
urlsByKey[key] = urls[0] || ''
}
for (const item of pending) {
if (returned.has(item.key)) continue
this.setImageCache(item.key, [])
lists[item.key] = []
urlsByKey[item.key] = ''
}
return { lists, urls: urlsByKey }
}
}
})