Keep gold quotes live from last and apply SSE catalog list so cards refresh without a page reload.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -467,13 +467,38 @@ function handleSettingsPayload(payload: unknown) {
|
||||
void applySettingsFromStream(payload)
|
||||
}
|
||||
|
||||
function coerceStreamProductList(payload: unknown): Product[] {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as Product[]
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return []
|
||||
}
|
||||
const root = payload as Record<string, unknown>
|
||||
if (Array.isArray(root.data)) return root.data as Product[]
|
||||
if (Array.isArray(root.products)) return root.products as Product[]
|
||||
if (Array.isArray(root.items)) return root.items as Product[]
|
||||
if (root.id != null && ('price' in root || 'title' in root || 'group_type' in root)) {
|
||||
return [payload as Product]
|
||||
}
|
||||
const values = Object.values(root)
|
||||
if (
|
||||
values.length > 0 &&
|
||||
values.every((item) => item && typeof item === 'object' && 'id' in (item as object))
|
||||
) {
|
||||
return values as Product[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function handleListPayload(payload: unknown) {
|
||||
if (!Array.isArray(payload) || !payload.length) {
|
||||
const products = coerceStreamProductList(payload)
|
||||
if (!products.length) {
|
||||
return
|
||||
}
|
||||
|
||||
touchStreamActivity()
|
||||
applyProductsListFromStream(payload as Product[])
|
||||
applyProductsListFromStream(products)
|
||||
}
|
||||
|
||||
function handleAppNotificationPayload(payload: unknown) {
|
||||
|
||||
@@ -101,26 +101,13 @@ export function getProductBaseBuyPrice(product?: Product | null): number {
|
||||
}
|
||||
|
||||
if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) {
|
||||
const apiPrice = Number(product.price ?? 0)
|
||||
const useLast = productUsesLastPriceIfZero(product)
|
||||
|
||||
if (!useLast) {
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
if (liveBuy > 0) {
|
||||
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (apiPrice > 0) {
|
||||
return apiPrice
|
||||
}
|
||||
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
if (liveBuy > 0) {
|
||||
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
|
||||
}
|
||||
|
||||
if (productUsesLastPriceIfZero(product) && Number(product.price ?? 0) > 0) {
|
||||
return Number(product.price ?? 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -195,26 +182,13 @@ export function getProductBaseSellPrice(product?: Product | null): number {
|
||||
}
|
||||
|
||||
if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) {
|
||||
const apiPrice = Number(product.price ?? 0)
|
||||
const useLast = productUsesLastPriceIfZero(product)
|
||||
|
||||
if (!useLast) {
|
||||
const liveSell = getTradeSellPrice(lastPrice.value)
|
||||
if (liveSell > 0) {
|
||||
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (apiPrice > 0) {
|
||||
return apiPrice
|
||||
}
|
||||
|
||||
const liveSell = getTradeSellPrice(lastPrice.value)
|
||||
if (liveSell > 0) {
|
||||
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
|
||||
}
|
||||
|
||||
if (productUsesLastPriceIfZero(product) && Number(product.price ?? 0) > 0) {
|
||||
return Number(product.price ?? 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,6 @@ import {
|
||||
getProductsCatalogRef,
|
||||
} from '@/composables/useProductsCatalog'
|
||||
import { subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
import { usesQuantityTrade } from '@/utils/productCategories'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
@@ -456,8 +455,8 @@ const sellPagination = ref<TableEngineInput>({
|
||||
|
||||
const unitPrice = computed(() => {
|
||||
const product = tradeProduct.value
|
||||
if (usesQuantityTrade(product)) {
|
||||
return isBuy.value ? getProductBuyPrice(product!) : getProductSellPrice(product!)
|
||||
if (product) {
|
||||
return isBuy.value ? getProductBuyPrice(product) : getProductSellPrice(product)
|
||||
}
|
||||
return isBuy.value ? getTradeBuyPrice(lastPrice.value) : getTradeSellPrice(lastPrice.value)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getLastStreamProductsList, subscribeProductListStream } from '@/utils/p
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
let productListInterval: ReturnType<typeof setInterval> | undefined
|
||||
let catalogStreamRefreshTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let providerMountCount = 0
|
||||
let pricePollingMountCount = 0
|
||||
let unsubscribeProductStream: (() => void) | undefined
|
||||
@@ -72,6 +73,10 @@ function pruneEmptyTypeItems() {
|
||||
productsTypeItems.value = productsTypeItems.value.filter((key) => present.has(key))
|
||||
}
|
||||
|
||||
function productRowId(product: Product): number {
|
||||
return Number(product.id)
|
||||
}
|
||||
|
||||
function productFileId(product: Product): number {
|
||||
return Number(product.file_id ?? (product.file as { id?: number } | undefined)?.id ?? 0)
|
||||
}
|
||||
@@ -85,43 +90,47 @@ function productHasDisplayImage(product: Product): boolean {
|
||||
}
|
||||
|
||||
/** استریم گاهی file خالی با همان file_id میفرستد — عکس خوب HTTP را پاک نکن. */
|
||||
function mergeLiveProduct(prev: Product, incoming: Product): Product {
|
||||
const next: Product = {
|
||||
...prev,
|
||||
...incoming,
|
||||
open_buy: 'open_buy' in incoming ? incoming.open_buy : prev.open_buy,
|
||||
open_sell: 'open_sell' in incoming ? incoming.open_sell : prev.open_sell,
|
||||
}
|
||||
|
||||
if (!productHasDisplayImage(incoming) && productHasDisplayImage(prev)) {
|
||||
next.file = prev.file
|
||||
next.file_id = prev.file_id ?? (prev.file as { id?: number } | undefined)?.id ?? incoming.file_id
|
||||
next.img = prev.img
|
||||
} else if (productFileId(prev) > productFileId(incoming)) {
|
||||
next.file = prev.file
|
||||
next.file_id = prev.file_id
|
||||
next.img = prev.img
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
function mergeLiveProductList(incoming: Product[], current?: Product[]): Product[] {
|
||||
if (!current?.length) return incoming
|
||||
|
||||
const currentById = new Map(current.map((item) => [item.id, item]))
|
||||
const currentById = new Map(current.map((item) => [productRowId(item), item]))
|
||||
const incomingById = new Map(incoming.map((item) => [productRowId(item), item]))
|
||||
const incomingIds = incoming.map(productRowId)
|
||||
const isPatch =
|
||||
incoming.length < current.length &&
|
||||
incomingIds.every((id) => currentById.has(id))
|
||||
|
||||
if (isPatch) {
|
||||
return current.map((item) => {
|
||||
const updated = incomingById.get(productRowId(item))
|
||||
return updated ? mergeLiveProduct(item, updated) : item
|
||||
})
|
||||
}
|
||||
|
||||
return incoming.map((item) => {
|
||||
const prev = currentById.get(item.id)
|
||||
if (!prev) return item
|
||||
|
||||
const prevId = productFileId(prev)
|
||||
const nextId = productFileId(item)
|
||||
|
||||
const keepTradeFlags = {
|
||||
open_buy: 'open_buy' in item ? item.open_buy : prev.open_buy,
|
||||
open_sell: 'open_sell' in item ? item.open_sell : prev.open_sell,
|
||||
}
|
||||
|
||||
if (!productHasDisplayImage(item) && productHasDisplayImage(prev)) {
|
||||
return {
|
||||
...item,
|
||||
...keepTradeFlags,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id ?? (prev.file as { id?: number } | undefined)?.id ?? item.file_id,
|
||||
img: prev.img,
|
||||
}
|
||||
}
|
||||
|
||||
if (prevId > nextId) {
|
||||
return {
|
||||
...item,
|
||||
...keepTradeFlags,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id,
|
||||
img: prev.img,
|
||||
}
|
||||
}
|
||||
|
||||
return { ...item, ...keepTradeFlags }
|
||||
const prev = currentById.get(productRowId(item))
|
||||
return prev ? mergeLiveProduct(prev, item) : item
|
||||
})
|
||||
}
|
||||
|
||||
@@ -141,16 +150,18 @@ function isListedProduct(product: Product): boolean {
|
||||
return flagOn(product.active)
|
||||
}
|
||||
|
||||
/** لیست SSE فروشگاه دیگر را روی این صفحه ننشان */
|
||||
/** لیست SSE فروشگاه دیگر را روی این صفحه ننشان؛ پچ یکمحصوله همین فروشگاه را بپذیر */
|
||||
function isSameShopCatalog(incoming: Product[], current?: Product[]): boolean {
|
||||
if (!current?.length) return true
|
||||
const incomingIds = new Set(incoming.map((item) => item.id))
|
||||
const currentIds = new Set(current.map((item) => item.id))
|
||||
const incomingIds = incoming.map((item) => productRowId(item))
|
||||
const currentIds = new Set(current.map((item) => productRowId(item)))
|
||||
let overlap = 0
|
||||
incomingIds.forEach((id) => {
|
||||
if (currentIds.has(id)) overlap += 1
|
||||
})
|
||||
return overlap / Math.max(incomingIds.size, currentIds.size) >= 0.5
|
||||
if (overlap === 0) return false
|
||||
if (overlap === incomingIds.length) return true
|
||||
return overlap / Math.max(incomingIds.length, currentIds.size) >= 0.5
|
||||
}
|
||||
|
||||
function applyProductsList(products: Product[]) {
|
||||
@@ -171,10 +182,20 @@ function applyProductsList(products: Product[]) {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
|
||||
/** قیمت enrich شده فقط در HTTP کامل است؛ slim SSE برای پرچمهاست */
|
||||
function scheduleCatalogRefreshFromStream() {
|
||||
if (catalogStreamRefreshTimer) {
|
||||
clearTimeout(catalogStreamRefreshTimer)
|
||||
}
|
||||
catalogStreamRefreshTimer = setTimeout(() => {
|
||||
catalogStreamRefreshTimer = undefined
|
||||
void refreshProductsPageData()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE همیشه لیست کامل محصولات فعال را میفرستد.
|
||||
* عضو جدید/حذفشده را از همان لیست بگیر؛ فقط عکس را از نسخهٔ قبلی نگه دار.
|
||||
* آپدیت قیمت روی همان آبجکتها تا گرید نپرد.
|
||||
* SSE لیست کامل یا پچ میفرستد.
|
||||
* slim فوری برای open_buy/show؛ قیمت کارت مثل رفرش از /customer/product/list.
|
||||
*/
|
||||
function applySocketProductList(incoming: Product[], current?: Product[]) {
|
||||
if (!isSameShopCatalog(incoming, current)) {
|
||||
@@ -184,35 +205,13 @@ function applySocketProductList(incoming: Product[], current?: Product[]) {
|
||||
const next = sortProductsByInventory(
|
||||
mergeLiveProductList(incoming, current).filter(isListedProduct),
|
||||
)
|
||||
|
||||
if (!current?.length) {
|
||||
if (next.length) {
|
||||
applyProductsList(next)
|
||||
return
|
||||
} else {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
|
||||
const nextById = new Map(next.map((item) => [item.id, item]))
|
||||
const currentIds = new Set(current.map((item) => item.id))
|
||||
const nextIds = new Set(next.map((item) => item.id))
|
||||
const membershipChanged =
|
||||
next.length !== current.length ||
|
||||
next.some((item) => !currentIds.has(item.id)) ||
|
||||
current.some((item) => !nextIds.has(item.id))
|
||||
|
||||
if (membershipChanged) {
|
||||
applyProductsList(next)
|
||||
return
|
||||
}
|
||||
|
||||
current.forEach((item) => {
|
||||
const updated = nextById.get(item.id)
|
||||
if (updated) {
|
||||
Object.assign(item, updated)
|
||||
}
|
||||
})
|
||||
|
||||
getProductsCatalogRef().value = current
|
||||
changeProductsTab()
|
||||
productsPageLoading.value = false
|
||||
scheduleCatalogRefreshFromStream()
|
||||
}
|
||||
|
||||
export async function refreshProductsPageData() {
|
||||
@@ -305,6 +304,11 @@ export function onProductsPageProviderUnmounted() {
|
||||
|
||||
if (providerMountCount === 0) {
|
||||
stopProductListPolling()
|
||||
initialized.value = false
|
||||
if (catalogStreamRefreshTimer) {
|
||||
clearTimeout(catalogStreamRefreshTimer)
|
||||
catalogStreamRefreshTimer = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user