chore: sync price monitor updates
This commit is contained in:
@@ -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
|
||||
|
||||
+37
-19
@@ -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<unknown>[] = [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<Record<string, unknown>>
|
||||
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,
|
||||
|
||||
@@ -9,6 +9,15 @@ export const priceStreamLifecycle = ref<PriceStreamLifecycle>('idle')
|
||||
|
||||
export const priceStreamConnectingSince = ref(0)
|
||||
|
||||
/** آخرین رویداد list استریم */
|
||||
export const priceStreamList = ref<Array<Record<string, unknown>>>([])
|
||||
|
||||
/** آخرین بدنهٔ tv/products که از SSE آمده */
|
||||
export const priceStreamProducts = ref<Array<Record<string, unknown>>>([])
|
||||
|
||||
/** unix ساعت سرور از رویداد time استریم */
|
||||
export const priceStreamServerUnix = ref(0)
|
||||
|
||||
const CONNECTING_POLL_AFTER_MS = 8000
|
||||
|
||||
export function shouldUsePricePolling(): boolean {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-26
@@ -15,18 +15,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMainQuote" class="tv-monitor__quote">
|
||||
<div class="tv-monitor__quote">
|
||||
<span class="tv-monitor__quote-label">مظنه لحظهای</span>
|
||||
<div class="tv-monitor__quote-prices">
|
||||
<div class="tv-monitor__quote-item tv-monitor__quote-item--sell">
|
||||
<span>فروش</span>
|
||||
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveBuy) }}</strong>
|
||||
<em v-if="mainQuoteReady">تومان</em>
|
||||
<strong>{{ liveQuoteLabel(liveBuy) }}</strong>
|
||||
<em>تومان</em>
|
||||
</div>
|
||||
<div class="tv-monitor__quote-item tv-monitor__quote-item--buy">
|
||||
<span>خرید</span>
|
||||
<strong :class="{ 'is-empty': !mainQuoteReady }">{{ liveQuoteLabel(liveSell) }}</strong>
|
||||
<em v-if="mainQuoteReady">تومان</em>
|
||||
<strong>{{ liveQuoteLabel(liveSell) }}</strong>
|
||||
<em>تومان</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user