diff --git a/src/models/products.ts b/src/models/products.ts index 4682868..882f3e7 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -35,6 +35,8 @@ 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/productPrice.ts b/src/utils/productPrice.ts index e558bb3..cffe05e 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -61,6 +61,15 @@ 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) { @@ -134,7 +143,7 @@ export function getProductBaseBuyPrice(product?: Product | null): number { } if (isColleaguePriceProduct(product)) { - return getColleagueSourcedUnitPrice(product, getTradeBuyPrice(lastPrice.value)) + return colleagueSideUnit(product, 'buy', getTradeBuyPrice(lastPrice.value)) } if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) { @@ -211,11 +220,7 @@ export function getProductBaseSellPrice(product?: Product | null): number { } if (isColleaguePriceProduct(product)) { - return getColleagueSourcedUnitPrice( - product, - getTradeSellPrice(lastPrice.value), - Number(product.sellPrice ?? product.price ?? 0), - ) + return colleagueSideUnit(product, 'sell', getTradeSellPrice(lastPrice.value)) } if (isMeltedProductUsingAppPrice(product) || isAppPriceProduct(product)) { diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts index fc19425..330b920 100644 --- a/src/views/tv/composables/useTvMonitorPage.ts +++ b/src/views/tv/composables/useTvMonitorPage.ts @@ -4,10 +4,10 @@ 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, isTvProductPriceInactive } from '@/utils/productPrice' -import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, startLastPricePolling, stopLastPricePolling } from '@/utils/price' +import { getProductBuyPrice, getProductSellPrice, getTvProductBuyPrice, getTvProductSellPrice } from '@/utils/productPrice' +import { getLastPrices, lastPrice, startLastPricePolling, stopLastPricePolling } from '@/utils/price' import { usePriceWithoutType } from '@/utils/usePrice' -import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours' +import { getStoreClosedMessage, isStoreClosedBySchedule, NO_QUOTE_MESSAGE } from '@/utils/storeHours' import { isMeltedTradeGroup, isMetalCommodityGroup } from '@/utils/productCategories' import { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState' import logoFallback from '@/assets/img/logo_full_.png' @@ -21,7 +21,7 @@ moment.loadPersian({ usePersianDigits: false }) const CURSOR_HIDE_MS = 2500 const TV_HTML_CLASS = 'app-layout--tv' -const REFRESH_MS = 15000 +const REFRESH_MS = 3000 /** دقت تابلو: هزار تومان = ۱۰٬۰۰۰ ریال — باقی‌مانده به پایین حذف می‌شود */ const TV_TOMAN_STEP = 1000 @@ -213,14 +213,34 @@ export function useTvMonitorPage() { || tvNewsItems.value.length > 0, ) - const statusBanner = computed(() => { - void lastPrice.value - const state = getPriceDisplayState() - return state === 'open' ? '' : getPriceDisplayMessage(state) + const statusBanner = computed(() => (isStoreClosedBySchedule() ? getStoreClosedMessage() : '')) + + const mainTvProduct = computed(() => { + void settingsStore.settingsRevision + const raw = String(settingsStore.getSettingByKey('main_product') ?? '').trim() + if (!raw) return null + 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 liveBuy = computed(() => getTradeBuyPrice(lastPrice.value)) - const liveSell = computed(() => getTradeSellPrice(lastPrice.value)) + const showMainQuote = computed(() => !!mainTvProduct.value) + + const liveBuy = computed(() => { + void lastPrice.value + const product = mainTvProduct.value + return product ? getProductBuyPrice(product) : 0 + }) + const liveSell = computed(() => { + void lastPrice.value + const product = mainTvProduct.value + return product ? getProductSellPrice(product) : 0 + }) + + const mainQuoteReady = computed(() => liveBuy.value > 0 || liveSell.value > 0) const isLive = computed( () => priceSocketConnected.value || priceStreamLifecycle.value === 'open', @@ -315,8 +335,12 @@ 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 isTvProductPriceInactive(product) + return tvQuoteMissing(product) } function productBuyLabel(product: Product) { @@ -330,7 +354,9 @@ export function useTvMonitorPage() { } function liveQuoteLabel(value: number) { - return formatTvPrice(value) + if (!mainQuoteReady.value) return NO_QUOTE_MESSAGE + const label = formatTvPrice(value) + return label === '—' ? NO_QUOTE_MESSAGE : label } onMounted(async () => { @@ -384,6 +410,8 @@ 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 278fe7c..ddcece5 100644 --- a/src/views/tv/index.vue +++ b/src/views/tv/index.vue @@ -15,18 +15,18 @@ -
+
مظنه لحظه‌ای
فروش - {{ liveQuoteLabel(liveBuy) }} - تومان + {{ liveQuoteLabel(liveSell) }} + تومان
خرید - {{ liveQuoteLabel(liveSell) }} - تومان + {{ liveQuoteLabel(liveBuy) }} + تومان
@@ -267,6 +267,9 @@ const { tvNewsItems, newsTickerTitle, statusBanner, + showMainQuote, + mainQuoteReady, + mainQuoteReady, liveBuy, liveSell, isLive, @@ -389,6 +392,7 @@ const coinColumns = computed(() => [ } .tv-monitor__brand { + grid-column: 1; display: flex; align-items: center; gap: 14px; @@ -419,6 +423,7 @@ const coinColumns = computed(() => [ } .tv-monitor__quote { + grid-column: 2; display: flex; flex-direction: column; align-items: center; @@ -476,9 +481,17 @@ 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; @@ -1092,6 +1105,14 @@ 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;