diff --git a/src/api/products.ts b/src/api/products.ts index 77583ea..9ffc645 100644 --- a/src/api/products.ts +++ b/src/api/products.ts @@ -15,6 +15,7 @@ export type TvServerTime = { unix: number iso: string timezone: string + stream_key?: string } export const getTvServerTime = async (): Promise => { diff --git a/src/utils/priceSocket.ts b/src/utils/priceSocket.ts index ad2c88f..f0b79b7 100644 --- a/src/utils/priceSocket.ts +++ b/src/utils/priceSocket.ts @@ -44,9 +44,9 @@ class AuthPriceStream extends EventTarget { onmessage: (() => void) | null = null private abort = new AbortController() - constructor(url: string) { + constructor(url: string, tvKey = '') { super() - void this.start(url) + void this.start(url, tvKey) } close() { @@ -54,10 +54,12 @@ class AuthPriceStream extends EventTarget { this.abort.abort() } - private async start(url: string) { + private async start(url: string, tvKey = '') { try { + const headers: Record = { Accept: 'text/event-stream' } + if (tvKey) headers['X-Tv-Key'] = tvKey const response = await fetch(url, { - headers: { Accept: 'text/event-stream' }, + headers, cache: 'no-store', signal: this.abort.signal, }) @@ -119,6 +121,7 @@ class AuthPriceStream extends EventTarget { let eventSource: AuthPriceStream | null = null let subscribers = 0 +let tvStreamKey: string | null = null let stopped = false let getSetting: ((key: string) => unknown) | null = null let reconnectTimer: ReturnType | undefined @@ -655,7 +658,7 @@ function connect() { markConnectingLifecycle() try { - eventSource = new AuthPriceStream(url) + eventSource = new AuthPriceStream(url, tvStreamKey || '') } catch (error) { console.warn('Price SSE connect failed', error) handleStreamLost() @@ -740,6 +743,22 @@ function unbindBrowserNetworkListeners() { window.removeEventListener('offline', onBrowserOffline) } +export function startTvPriceStream( + settingGetter: (key: string) => unknown, + key: string, +) { + const secret = key.trim() + if (!secret) return + tvStreamKey = secret + startPriceSocket(settingGetter) +} + +export function stopTvPriceStream() { + if (!tvStreamKey) return + tvStreamKey = null + stopPriceSocket() +} + export function startPriceSocket(settingGetter: (key: string) => unknown) { getSetting = settingGetter subscribers += 1 diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts index 330b920..46fce6f 100644 --- a/src/views/tv/composables/useTvMonitorPage.ts +++ b/src/views/tv/composables/useTvMonitorPage.ts @@ -10,6 +10,7 @@ import { usePriceWithoutType } from '@/utils/usePrice' import { getStoreClosedMessage, isStoreClosedBySchedule, NO_QUOTE_MESSAGE } from '@/utils/storeHours' import { isMeltedTradeGroup, isMetalCommodityGroup } from '@/utils/productCategories' import { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState' +import { startTvPriceStream, stopTvPriceStream } from '@/utils/priceSocket' import logoFallback from '@/assets/img/logo_full_.png' //@ts-ignore import moment from 'moment-jalaali' @@ -21,7 +22,7 @@ moment.loadPersian({ usePersianDigits: false }) const CURSOR_HIDE_MS = 2500 const TV_HTML_CLASS = 'app-layout--tv' -const REFRESH_MS = 3000 +const REFRESH_MS = 15000 /** دقت تابلو: هزار تومان = ۱۰٬۰۰۰ ریال — باقی‌مانده به پایین حذف می‌شود */ const TV_TOMAN_STEP = 1000 @@ -121,6 +122,14 @@ export function useTvMonitorPage() { let listTimer: ReturnType | undefined /** اختلاف میلی‌ثانیهٔ ساعت سرور (Asia/Tehran) با Date.now محلی */ let serverOffsetMs = 0 + let tvStreamReady = false + + function openTvStream(key: string) { + const secret = key.trim() + if (!secret || tvStreamReady) return + tvStreamReady = true + startTvPriceStream((settingKey) => settingsStore.getSettingByKey(settingKey), secret) + } const brandName = computed( () => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'), @@ -150,6 +159,7 @@ export function useTvMonitorPage() { if (!payload?.unix) return const rttHalf = (t1 - t0) / 2 serverOffsetMs = payload.unix * 1000 + rttHalf - t1 + if (payload.stream_key) openTvStream(payload.stream_key) } const parsianCoins = computed(() => @@ -246,7 +256,11 @@ export function useTvMonitorPage() { () => priceSocketConnected.value || priceStreamLifecycle.value === 'open', ) + let productsInFlight = false + async function loadProducts() { + if (productsInFlight) return + productsInFlight = true try { const list = await getTvProducts() products.value = (Array.isArray(list) ? list : []).map(normalizeTvProduct) @@ -255,6 +269,7 @@ export function useTvMonitorPage() { products.value = [] } } finally { + productsInFlight = false loading.value = false } } @@ -390,6 +405,7 @@ export function useTvMonitorPage() { document.removeEventListener('visibilitychange', onVisibilityChange) void releaseWakeLock() stopLastPricePolling() + stopTvPriceStream() }) return { diff --git a/src/views/tv/index.vue b/src/views/tv/index.vue index ddcece5..fb240b4 100644 --- a/src/views/tv/index.vue +++ b/src/views/tv/index.vue @@ -20,12 +20,12 @@
فروش - {{ liveQuoteLabel(liveSell) }} + {{ liveQuoteLabel(liveBuy) }} تومان
خرید - {{ liveQuoteLabel(liveBuy) }} + {{ liveQuoteLabel(liveSell) }} تومان