From 9cb0979606e92bb952bac8fb8e61c53f2551803d Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 28 Sep 2026 18:04:49 +0330 Subject: [PATCH] chore: sync price monitor updates --- src/models/products.ts | 2 - src/utils/priceSocket.ts | 56 ++++++++----- src/utils/priceSocketState.ts | 9 +++ src/utils/productPrice.ts | 17 ++-- .../composables/useProductsPageData.ts | 7 +- src/views/tv/composables/useTvMonitorPage.ts | 78 +++++++------------ src/views/tv/index.vue | 31 ++------ 7 files changed, 88 insertions(+), 112 deletions(-) diff --git a/src/models/products.ts b/src/models/products.ts index 882f3e7..4682868 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -35,8 +35,6 @@ export interface Product { updated_at: any deleted_at: any price?:number | undefined - price_buy?: number | null - price_sell?: number | null price_profit?: number | null price_offset_percent?: number | null sellPrice?:number | undefined diff --git a/src/utils/priceSocket.ts b/src/utils/priceSocket.ts index 2a0738c..630f89d 100644 --- a/src/utils/priceSocket.ts +++ b/src/utils/priceSocket.ts @@ -7,6 +7,9 @@ import { priceSocketConnected, priceStreamConnectingSince, priceStreamLifecycle, + priceStreamList, + priceStreamProducts, + priceStreamServerUnix, priceWssEnabled, } from '@/utils/priceSocketState' import { @@ -282,28 +285,21 @@ export function notifyPriceLastFetchFailed() { void maybeShowNetworkAlert() } -function onTvBoard() { - if (typeof window === 'undefined') return false - const path = window.location.pathname.replace(/\/+$/, '') || '/' - return path === '/tv' || path.endsWith('/tv') || path === '/monitor' || path.endsWith('/monitor') -} - async function runFallbackPoll() { - if (onTvBoard() || tvStreamKey || !priceWssEnabled.value || priceSocketConnected.value) { + if (tvStreamKey || !priceWssEnabled.value || priceSocketConnected.value) { return } try { const { getLastPrices, hasRecentPriceFromSource } = await import('@/utils/price') - const jobs: Promise[] = [getLastPrices()] - if (!tvStreamKey) { - const { refreshProductsPageData } = await import( - '@/views/base/products/composables/useProductsPageData' - ) - jobs.push(refreshProductsPageData()) - } + const { refreshProductsPageData } = await import( + '@/views/base/products/composables/useProductsPageData' + ) - const [priceResult] = await Promise.allSettled(jobs) + const [priceResult] = await Promise.allSettled([ + getLastPrices(), + refreshProductsPageData(), + ]) const priceOk = priceResult.status === 'fulfilled' @@ -508,6 +504,7 @@ function handleListPayload(payload: unknown) { } touchStreamActivity() + priceStreamList.value = products as Array> applyProductsListFromStream(products) } @@ -572,6 +569,31 @@ function bindStreamEvents(source: AuthPriceStream) { } }) + source.addEventListener('products', (event) => { + try { + const rows = JSON.parse(event.data) + if (Array.isArray(rows) && rows.length) { + touchStreamActivity() + priceStreamProducts.value = rows + } + } catch (error) { + console.warn('Price SSE products parse failed', error) + } + }) + + source.addEventListener('time', (event) => { + try { + const payload = JSON.parse(event.data) as { unix?: number } + const unix = Number(payload?.unix) + if (unix > 0) { + touchStreamActivity() + priceStreamServerUnix.value = unix + } + } catch (error) { + console.warn('Price SSE time parse failed', error) + } + }) + source.addEventListener('app_notification', (event) => { try { handleAppNotificationPayload(JSON.parse(event.data)) @@ -750,10 +772,6 @@ function unbindBrowserNetworkListeners() { window.removeEventListener('offline', onBrowserOffline) } -export function isTvStreamActive() { - return Boolean(tvStreamKey) -} - export function startTvPriceStream( settingGetter: (key: string) => unknown, key: string, diff --git a/src/utils/priceSocketState.ts b/src/utils/priceSocketState.ts index 1e19353..d6dc2b7 100644 --- a/src/utils/priceSocketState.ts +++ b/src/utils/priceSocketState.ts @@ -9,6 +9,15 @@ export const priceStreamLifecycle = ref('idle') export const priceStreamConnectingSince = ref(0) +/** آخرین رویداد list استریم */ +export const priceStreamList = ref>>([]) + +/** آخرین بدنهٔ tv/products که از SSE آمده */ +export const priceStreamProducts = ref>>([]) + +/** unix ساعت سرور از رویداد time استریم */ +export const priceStreamServerUnix = ref(0) + const CONNECTING_POLL_AFTER_MS = 8000 export function shouldUsePricePolling(): boolean { diff --git a/src/utils/productPrice.ts b/src/utils/productPrice.ts index cffe05e..e558bb3 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -61,15 +61,6 @@ function sellCashFee(product: Product, base: number) { return resolveProductCashFeeSellRial(product, base, cashFeeOptions(product, 'sell')) } -/** سمت خرید/فروش همکار؛ اگر سرور نفرستاده باشد همان قیمت وسط قبلی */ -function colleagueSideUnit(product: Product, side: 'buy' | 'sell', live: number): number { - const sided = side === 'buy' ? Number(product.price_buy ?? 0) : Number(product.price_sell ?? 0) - if (sided > 0) { - return sided - } - return getColleagueSourcedUnitPrice(product, live) -} - /** قیمت منبع همکار؛ اگر خالی بود آبشده همان مظنهٔ صفحه اصلی را می‌گیرد */ function getColleagueSourcedUnitPrice(product: Product, live: number, apiPrice = Number(product.price ?? 0)): number { if (apiPrice > 0) { @@ -143,7 +134,7 @@ export function getProductBaseBuyPrice(product?: Product | null): number { } if (isColleaguePriceProduct(product)) { - return colleagueSideUnit(product, 'buy', getTradeBuyPrice(lastPrice.value)) + return getColleagueSourcedUnitPrice(product, getTradeBuyPrice(lastPrice.value)) } if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) { @@ -220,7 +211,11 @@ export function getProductBaseSellPrice(product?: Product | null): number { } if (isColleaguePriceProduct(product)) { - return colleagueSideUnit(product, 'sell', getTradeSellPrice(lastPrice.value)) + return getColleagueSourcedUnitPrice( + product, + getTradeSellPrice(lastPrice.value), + Number(product.sellPrice ?? product.price ?? 0), + ) } if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) { diff --git a/src/views/base/products/composables/useProductsPageData.ts b/src/views/base/products/composables/useProductsPageData.ts index 2f1a7ac..3224b67 100644 --- a/src/views/base/products/composables/useProductsPageData.ts +++ b/src/views/base/products/composables/useProductsPageData.ts @@ -182,13 +182,13 @@ function applyProductsList(products: Product[]) { productsPageLoading.value = false } +/** قیمت enrich شده فقط در HTTP کامل است؛ slim SSE برای پرچم‌هاست */ function onTvBoard() { if (typeof window === 'undefined') return false const path = window.location.pathname.replace(/\/+$/, '') || '/' return path === '/tv' || path.endsWith('/tv') || path === '/monitor' || path.endsWith('/monitor') } -/** قیمت enrich شده فقط در HTTP کامل است؛ slim SSE برای پرچم‌هاست */ function scheduleCatalogRefreshFromStream() { if (onTvBoard()) return if (catalogStreamRefreshTimer) { @@ -196,10 +196,7 @@ function scheduleCatalogRefreshFromStream() { } catalogStreamRefreshTimer = setTimeout(() => { catalogStreamRefreshTimer = undefined - void import('@/utils/priceSocket').then(({ isTvStreamActive }) => { - if (isTvStreamActive()) return - void refreshProductsPageData() - }) + void refreshProductsPageData() }, 150) } diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts index df87140..4ea5ffb 100644 --- a/src/views/tv/composables/useTvMonitorPage.ts +++ b/src/views/tv/composables/useTvMonitorPage.ts @@ -4,11 +4,12 @@ import type { Product } from '@/models/products' import { getTvProducts, getTvServerTime } from '@/api/products' import { getConfigApp } from '@/utils/config' import { useSettingsStore } from '@/store/settingsStore' -import { getTvProductBuyPrice, getTvProductSellPrice } from '@/utils/productPrice' +import { getTvProductBuyPrice, getTvProductSellPrice, isTvProductPriceInactive } from '@/utils/productPrice' +import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, startLastPricePolling, stopLastPricePolling } from '@/utils/price' import { usePriceWithoutType } from '@/utils/usePrice' -import { getStoreClosedMessage, isStoreClosedBySchedule, NO_QUOTE_MESSAGE } from '@/utils/storeHours' +import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours' import { isMeltedTradeGroup, isMetalCommodityGroup } from '@/utils/productCategories' -import { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState' +import { priceSocketConnected, priceStreamLifecycle, priceStreamProducts, priceStreamServerUnix } from '@/utils/priceSocketState' import { startTvPriceStream, stopTvPriceStream } from '@/utils/priceSocket' import logoFallback from '@/assets/img/logo_full_.png' //@ts-ignore @@ -130,6 +131,16 @@ export function useTvMonitorPage() { startTvPriceStream((settingKey) => settingsStore.getSettingByKey(settingKey), secret) } + watch(priceStreamProducts, (rows) => { + if (!rows.length) return + products.value = (rows as Product[]).map(normalizeTvProduct) + }) + + watch(priceStreamServerUnix, (unix) => { + if (!unix) return + serverOffsetMs = unix * 1000 - Date.now() + }) + const brandName = computed( () => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'), ) @@ -222,44 +233,30 @@ export function useTvMonitorPage() { || tvNewsItems.value.length > 0, ) - const statusBanner = computed(() => (isStoreClosedBySchedule() ? getStoreClosedMessage() : '')) - - const mainTvProduct = computed(() => { - void settingsStore.settingsRevision - const raw = String(settingsStore.getSettingByKey('main_product') ?? '').trim() || '13' - const match = catalogProducts.value.find( - (product) => String(product.id) === raw || String(product.reading_method ?? '') === raw, - ) - if (!match) return null - if (products.value.some(isShowOnTv) && !isShowOnTv(match)) return null - return match + const statusBanner = computed(() => { + void lastPrice.value + const state = getPriceDisplayState() + return state === 'open' ? '' : getPriceDisplayMessage(state) }) - const showMainQuote = computed(() => !!mainTvProduct.value) - - const liveBuy = computed(() => Number(mainTvProduct.value?.tv_price ?? 0) || 0) - const liveSell = computed(() => Number(mainTvProduct.value?.tv_sell_price ?? 0) || 0) - - const mainQuoteReady = computed(() => liveBuy.value > 0 || liveSell.value > 0) + const liveBuy = computed(() => getTradeBuyPrice(lastPrice.value)) + const liveSell = computed(() => getTradeSellPrice(lastPrice.value)) const isLive = computed( () => priceSocketConnected.value || priceStreamLifecycle.value === 'open', ) - let productsInFlight = false - async function loadProducts() { - if (productsInFlight) return - productsInFlight = true + if (priceStreamProducts.value.length) return try { const list = await getTvProducts() + if (priceStreamProducts.value.length) return products.value = (Array.isArray(list) ? list : []).map(normalizeTvProduct) } catch { if (!products.value.length) { products.value = [] } } finally { - productsInFlight = false loading.value = false } } @@ -340,12 +337,8 @@ export function useTvMonitorPage() { return simorghWeightLabel(product) } - function tvQuoteMissing(product: Product) { - return Number(product.tv_price ?? 0) <= 0 && Number(product.tv_sell_price ?? 0) <= 0 - } - function isProductTvInactive(product: Product) { - return tvQuoteMissing(product) + return isTvProductPriceInactive(product) } function productBuyLabel(product: Product) { @@ -359,24 +352,9 @@ export function useTvMonitorPage() { } function liveQuoteLabel(value: number) { - if (!mainQuoteReady.value) return NO_QUOTE_MESSAGE - const label = formatTvPrice(value) - return label === '—' ? NO_QUOTE_MESSAGE : label + return formatTvPrice(value) } - function streamIsUp() { - return priceSocketConnected.value || priceStreamLifecycle.value === 'open' - } - - function pollTvProducts() { - if (streamIsUp()) return - void loadProducts() - } - - watch(priceSocketConnected, (up) => { - if (!up) pollTvProducts() - }) - onMounted(async () => { document.documentElement.classList.add(TV_HTML_CLASS) await syncServerClock() @@ -387,10 +365,13 @@ export function useTvMonitorPage() { document.addEventListener('visibilitychange', onVisibilityChange) await requestWakeLock() + startLastPricePolling() + void getLastPrices() void loadProducts() listTimer = setInterval(() => { + if (isLive.value) return + void loadProducts() void syncServerClock() - pollTvProducts() }, REFRESH_MS) document.title = `${brandName.value} | تابلو قیمت` @@ -405,6 +386,7 @@ export function useTvMonitorPage() { window.removeEventListener('mousemove', hideCursorSoon) document.removeEventListener('visibilitychange', onVisibilityChange) void releaseWakeLock() + stopLastPricePolling() stopTvPriceStream() }) @@ -426,8 +408,6 @@ export function useTvMonitorPage() { tvNewsItems, newsTickerTitle, statusBanner, - showMainQuote, - mainQuoteReady, liveBuy, liveSell, isLive, diff --git a/src/views/tv/index.vue b/src/views/tv/index.vue index fb240b4..278fe7c 100644 --- a/src/views/tv/index.vue +++ b/src/views/tv/index.vue @@ -15,18 +15,18 @@ -
+
مظنه لحظه‌ای
فروش - {{ liveQuoteLabel(liveBuy) }} - تومان + {{ liveQuoteLabel(liveBuy) }} + تومان
خرید - {{ liveQuoteLabel(liveSell) }} - تومان + {{ liveQuoteLabel(liveSell) }} + تومان
@@ -267,9 +267,6 @@ const { tvNewsItems, newsTickerTitle, statusBanner, - showMainQuote, - mainQuoteReady, - mainQuoteReady, liveBuy, liveSell, isLive, @@ -392,7 +389,6 @@ const coinColumns = computed(() => [ } .tv-monitor__brand { - grid-column: 1; display: flex; align-items: center; gap: 14px; @@ -423,7 +419,6 @@ const coinColumns = computed(() => [ } .tv-monitor__quote { - grid-column: 2; display: flex; flex-direction: column; align-items: center; @@ -481,17 +476,9 @@ const coinColumns = computed(() => [ &--buy strong { color: var(--tv-buy); } - - strong.is-empty { - font-size: clamp(13px, 1.15vw, 18px); - font-weight: 600; - direction: rtl; - color: var(--tv-muted); - } } .tv-monitor__clock { - grid-column: 3; display: flex; flex-direction: column; align-items: flex-end; @@ -1105,14 +1092,6 @@ const coinColumns = computed(() => [ order: 3; } - .tv-monitor__clock { - grid-column: 2; - } - - .tv-monitor__clock { - grid-column: 2; - } - .tv-monitor__columns { grid-template-columns: 1fr; overflow: auto;