tv changes

This commit is contained in:
2026-09-11 16:44:28 +03:30
parent b78fbbdc4e
commit b211c8773b
2 changed files with 154 additions and 28 deletions
+52 -7
View File
@@ -3,12 +3,13 @@ import { useFullscreen } from '@vueuse/core'
import type { Product } from '@/models/products'
import { getTvProducts } 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 { usePriceWithoutType } from '@/utils/usePrice'
import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours'
import { isMeltedTradeGroup, isMetalCommodityGroup } from '@/utils/productCategories'
import { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState'
import { usePrice } from '@/utils/usePrice'
import logoFallback from '@/assets/img/logo_full_.png'
//@ts-ignore
import moment from 'moment-jalaali'
@@ -21,6 +22,37 @@ moment.loadPersian({ usePersianDigits: false })
const CURSOR_HIDE_MS = 2500
const TV_HTML_CLASS = 'app-layout--tv'
const REFRESH_MS = 15000
/** دقت تابلو: هزار تومان = ۱۰٬۰۰۰ ریال — باقی‌مانده به پایین حذف می‌شود */
const TV_TOMAN_STEP = 1000
function parseTvNews(raw: unknown): string[] {
const text = String(raw ?? '').trim()
if (!text) return []
try {
const decoded = JSON.parse(text)
if (Array.isArray(decoded)) {
return decoded.map((item) => String(item ?? '').trim()).filter(Boolean)
}
} catch {
// هر خط یک خبر
}
return text
.split(/\r?\n/)
.map((line) => line.replace(/^[-•*]\s+/, '').trim())
.filter(Boolean)
}
function floorTvToman(rial: number): number {
if (!Number.isFinite(rial) || rial <= 0) return 0
return Math.floor(rial / 10 / TV_TOMAN_STEP) * TV_TOMAN_STEP
}
function formatTvPrice(rial: number): string {
const toman = floorTvToman(rial)
return toman > 0 ? usePriceWithoutType(toman) : '—'
}
function productText(product: Product) {
return `${product.group_type ?? ''} ${product.title ?? ''}`
@@ -70,6 +102,7 @@ function isGoldBar(product: Product) {
}
export function useTvMonitorPage() {
const settingsStore = useSettingsStore()
const rootEl = ref<HTMLElement | null>(null)
const { isFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(rootEl)
@@ -143,11 +176,23 @@ export function useTvMonitorPage() {
extraTickerProducts.value.length ? 'سایر مظنه‌ها' : '',
)
const tvNewsItems = computed(() => {
void settingsStore.settingsRevision
const raw = settingsStore.getSettingByKey('tv_news') ?? getConfigApp('tv_news')
if (raw == null) {
return ['تا حالا از اپ کشکول طلای آنلاین خریدی؟']
}
return parseTvNews(raw)
})
const newsTickerTitle = computed(() => (tvNewsItems.value.length ? 'اخبار' : ''))
const hasBoardContent = computed(
() =>
hasCoinBoard.value
|| tickerProducts.value.length > 0
|| extraTickerProducts.value.length > 0,
|| extraTickerProducts.value.length > 0
|| tvNewsItems.value.length > 0,
)
const statusBanner = computed(() => {
@@ -258,18 +303,16 @@ export function useTvMonitorPage() {
function productBuyLabel(product: Product) {
if (isProductTvInactive(product)) return NO_QUOTE_MESSAGE
const value = getTvProductBuyPrice(product)
return value > 0 ? usePrice(value) : '—'
return formatTvPrice(getTvProductBuyPrice(product))
}
function productSellLabel(product: Product) {
if (isProductTvInactive(product)) return NO_QUOTE_MESSAGE
const value = getTvProductSellPrice(product)
return value > 0 ? usePrice(value) : '—'
return formatTvPrice(getTvProductSellPrice(product))
}
function liveQuoteLabel(value: number) {
return value > 0 ? usePrice(value) : '—'
return formatTvPrice(value)
}
onMounted(async () => {
@@ -318,6 +361,8 @@ export function useTvMonitorPage() {
tickerTitle,
extraTickerProducts,
extraTickerTitle,
tvNewsItems,
newsTickerTitle,
statusBanner,
liveBuy,
liveSell,
+102 -21
View File
@@ -21,10 +21,12 @@
<div class="tv-monitor__quote-item tv-monitor__quote-item--sell">
<span>فروش</span>
<strong>{{ liveQuoteLabel(liveBuy) }}</strong>
<em>تومان</em>
</div>
<div class="tv-monitor__quote-item tv-monitor__quote-item--buy">
<span>خرید</span>
<strong>{{ liveQuoteLabel(liveSell) }}</strong>
<em>تومان</em>
</div>
</div>
</div>
@@ -119,12 +121,14 @@
:class="{ 'tv-row__price--empty': isProductTvInactive(item) }"
>
<strong>{{ productBuyLabel(item) }}</strong>
<em v-if="!isProductTvInactive(item) && productBuyLabel(item) !== '—'">تومان</em>
</div>
<div
class="tv-row__price tv-row__price--buy"
:class="{ 'tv-row__price--empty': isProductTvInactive(item) }"
>
<strong>{{ productSellLabel(item) }}</strong>
<em v-if="!isProductTvInactive(item) && productSellLabel(item) !== '—'">تومان</em>
</div>
</div>
</article>
@@ -146,7 +150,7 @@
<div
v-if="tickerProducts.length"
class="tv-ticker"
:style="{ '--tv-ticker-duration': `${Math.max(tickerProducts.length * 7, 36)}s` }"
:style="{ '--tv-ticker-duration': `${Math.max(tickerProducts.length * 14, 64)}s` }"
>
<div class="tv-ticker__label">
<strong>{{ tickerTitle }}</strong>
@@ -163,19 +167,42 @@
<span class="tv-ticker__price tv-ticker__price--sell">
فروش
<b>{{ productBuyLabel(entry.product) }}</b>
<i>تومان</i>
</span>
<span class="tv-ticker__price tv-ticker__price--buy">
خرید
<b>{{ productSellLabel(entry.product) }}</b>
<i>تومان</i>
</span>
</article>
</div>
</div>
</div>
<div
v-if="extraTickerProducts.length"
v-if="tvNewsItems.length"
class="tv-ticker tv-ticker--extra tv-ticker--news"
:style="{ '--tv-ticker-duration': `${newsTickerDuration}s` }"
>
<div class="tv-ticker__label">
<strong>{{ newsTickerTitle }}</strong>
<span>اطلاع‌رسانی</span>
</div>
<div class="tv-ticker__viewport">
<div class="tv-ticker__track">
<article
v-for="entry in newsTickerLoop"
:key="`news-${entry.copy}-${entry.index}`"
class="tv-ticker__item tv-ticker__item--news"
>
<span class="tv-ticker__headline">{{ entry.text }}</span>
</article>
</div>
</div>
</div>
<div
v-else-if="extraTickerProducts.length"
class="tv-ticker tv-ticker--extra"
:style="{ '--tv-ticker-duration': `${Math.max(extraTickerProducts.length * 7, 36)}s` }"
:style="{ '--tv-ticker-duration': `${Math.max(extraTickerProducts.length * 18, 80)}s` }"
>
<div class="tv-ticker__label">
<strong>{{ extraTickerTitle }}</strong>
@@ -192,10 +219,12 @@
<span class="tv-ticker__price tv-ticker__price--sell">
فروش
<b>{{ productBuyLabel(entry.product) }}</b>
<i>تومان</i>
</span>
<span class="tv-ticker__price tv-ticker__price--buy">
خرید
<b>{{ productSellLabel(entry.product) }}</b>
<i>تومان</i>
</span>
</article>
</div>
@@ -235,6 +264,8 @@ const {
tickerTitle,
extraTickerProducts,
extraTickerTitle,
tvNewsItems,
newsTickerTitle,
statusBanner,
liveBuy,
liveSell,
@@ -268,8 +299,21 @@ function loopTicker(items: Product[]) {
return copies.flatMap((copy) => items.map((product) => ({ product, copy })))
}
function loopNews(items: string[]) {
if (!items.length) return []
const copies = items.length >= 1 && !prefersReducedMotion() ? [0, 1] : [0]
return copies.flatMap((copy) => items.map((text, index) => ({ text, copy, index })))
}
function newsDuration(items: string[]) {
const chars = items.reduce((sum, text) => sum + text.length, 0)
return Math.max(items.length * 22, Math.ceil(chars / 3), 90)
}
const shamsTickerLoop = computed(() => loopTicker(tickerProducts.value))
const extraTickerLoop = computed(() => loopTicker(extraTickerProducts.value))
const newsTickerLoop = computed(() => loopNews(tvNewsItems.value))
const newsTickerDuration = computed(() => newsDuration(tvNewsItems.value))
const coinColumns = computed(() => [
{
@@ -391,8 +435,8 @@ const coinColumns = computed(() => [
}
.tv-monitor__quote-item {
min-width: 180px;
padding: 8px 14px;
min-width: 220px;
padding: 10px 18px;
border-radius: 14px;
text-align: center;
background: var(--tv-quote-bg);
@@ -400,18 +444,27 @@ const coinColumns = computed(() => [
span {
display: block;
font-size: 12px;
font-size: 13px;
color: var(--tv-muted);
}
strong {
display: block;
margin-top: 2px;
font-size: clamp(16px, 1.5vw, 22px);
font-weight: 700;
font-size: clamp(22px, 2.4vw, 38px);
font-weight: 800;
font-family: iran-sans, sans-serif;
direction: ltr;
unicode-bidi: isolate;
line-height: 1.15;
}
em {
display: block;
margin-top: 2px;
font-style: normal;
font-size: clamp(11px, 0.95vw, 14px);
color: var(--tv-muted);
}
&--sell strong {
@@ -554,7 +607,7 @@ const coinColumns = computed(() => [
.tv-col__labels {
display: grid;
grid-template-columns: 56px minmax(0, 0.9fr) minmax(0, 2fr);
grid-template-columns: 52px minmax(0, 0.7fr) minmax(0, 2.4fr);
gap: 10px;
align-items: center;
margin-bottom: 8px;
@@ -574,7 +627,7 @@ const coinColumns = computed(() => [
.tv-col__label {
display: block;
text-align: center;
font-size: clamp(13px, 1.1vw, 16px);
font-size: clamp(14px, 1.2vw, 18px);
font-weight: 700;
}
@@ -648,13 +701,13 @@ const coinColumns = computed(() => [
.tv-row {
position: relative;
display: grid;
grid-template-columns: minmax(44px, 56px) minmax(0, 0.9fr) minmax(0, 2fr);
grid-template-columns: minmax(44px, 52px) minmax(0, 0.7fr) minmax(0, 2.4fr);
align-items: center;
gap: 10px;
flex: 0 0 auto;
height: clamp(64px, 8vh, 80px);
max-height: 80px;
min-height: 64px;
height: clamp(76px, 10vh, 104px);
max-height: 104px;
min-height: 76px;
min-width: 0;
overflow: hidden;
padding: 8px 12px;
@@ -706,7 +759,7 @@ const coinColumns = computed(() => [
h3 {
margin: 0;
overflow: hidden;
font-size: clamp(13px, 1.2vw, 20px);
font-size: clamp(14px, 1.25vw, 22px);
font-weight: 700;
line-height: 1.3;
text-overflow: ellipsis;
@@ -740,11 +793,12 @@ const coinColumns = computed(() => [
.tv-row__price {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
padding: 6px 8px;
padding: 6px 6px;
border-radius: 12px;
text-align: center;
background: transparent;
@@ -761,12 +815,22 @@ const coinColumns = computed(() => [
max-width: 100%;
margin-top: 0;
overflow: hidden;
font-size: clamp(10px, 12cqi, 16px);
font-weight: 700;
font-size: clamp(18px, 22cqi, 34px);
font-weight: 800;
font-family: iran-sans, sans-serif;
white-space: nowrap;
direction: ltr;
unicode-bidi: isolate;
line-height: 1.1;
}
em {
display: block;
margin-top: 2px;
font-style: normal;
font-size: clamp(10px, 8cqi, 13px);
color: var(--tv-card-muted);
line-height: 1;
}
&--sell strong {
@@ -916,6 +980,17 @@ const coinColumns = computed(() => [
font-weight: 700;
}
.tv-ticker__item--news {
gap: 18px;
padding: 0 36px;
}
.tv-ticker__headline {
font-size: clamp(16px, 1.35vw, 22px);
font-weight: 700;
letter-spacing: 0.01em;
}
.tv-ticker__price {
display: inline-flex;
align-items: baseline;
@@ -923,14 +998,20 @@ const coinColumns = computed(() => [
font-size: 13px;
b {
font-size: clamp(13px, 1.05vw, 16px);
font-weight: 700;
font-size: clamp(16px, 1.35vw, 22px);
font-weight: 800;
font-family: iran-sans, sans-serif;
white-space: nowrap;
direction: ltr;
unicode-bidi: isolate;
}
i {
font-style: normal;
font-size: 11px;
opacity: 0.78;
}
&--sell b {
color: #ffe08a;
}
@@ -979,7 +1060,7 @@ const coinColumns = computed(() => [
.tv-col__labels,
.tv-row {
grid-template-columns: minmax(40px, 48px) minmax(0, 0.85fr) minmax(0, 2.1fr);
grid-template-columns: minmax(40px, 48px) minmax(0, 0.65fr) minmax(0, 2.5fr);
}
}