Files
ghatreh_user/src/views/base/tv/index.vue
T
2026-09-14 12:57:53 +03:30

211 lines
5.7 KiB
Vue

<template>
<div class="tv-board">
<header class="tv-board__header">
<h1 class="tv-board__title">{{ shopName }}</h1>
<time class="tv-board__clock" dir="ltr">{{ clockText }}</time>
</header>
<p v-if="statusBanner" class="tv-board__banner">{{ statusBanner }}</p>
<div v-if="loading" class="tv-board__state">
<a-spin />
</div>
<div v-else-if="!rows.length" class="tv-board__state">
<BasicIcon icon="vuesax-linear:monitor" size="42" />
<span>محصولی برای نمایش روی تابلو انتخاب نشده است</span>
</div>
<div v-else class="tv-board__table">
<div class="tv-board__row tv-board__row--head">
<span>محصول</span>
<span>خرید</span>
<span>فروش</span>
</div>
<div v-for="row in rows" :key="row.id" class="tv-board__row">
<span class="tv-board__name">{{ row.title }}</span>
<span class="tv-board__buy" dir="ltr">{{ row.buyText }}</span>
<span class="tv-board__sell" dir="ltr">{{ row.sellText }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { getTvProducts, getTvServerTime } from '@/api/products'
import type { Product } from '@/models/products'
import { getConfigApp } from '@/utils/config'
import { getTvProductBuyPrice, getTvProductSellPrice, isTvProductPriceInactive } from '@/utils/productPrice'
import { lastPrice, getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours'
import { usePriceWithoutType } from '@/utils/usePrice'
import { initPriceRealtimeChannel } from '@/utils/priceSocket'
import { useSettingsStore } from '@/store/settingsStore'
const REFRESH_MS = 15000
const loading = ref(true)
const products = ref<Product[]>([])
const now = ref(Date.now())
let serverOffsetMs = 0
let clockTimer: ReturnType<typeof setInterval> | undefined
let listTimer: ReturnType<typeof setInterval> | undefined
const shopName = computed(
() => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'),
)
const clockText = computed(() =>
new Date(now.value).toLocaleTimeString('fa-IR', {
timeZone: 'Asia/Tehran',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}),
)
const statusBanner = computed(() => {
void lastPrice.value
const state = getPriceDisplayState()
return state === 'open' ? '' : getPriceDisplayMessage(state)
})
function isShowOnTv(product: Product): boolean {
const value = product.show_on_tv as unknown
return value === true || value === 1 || value === '1' || value === 'true'
}
const rows = computed(() => {
void lastPrice.value
// API فقط محصولات تابلو را می‌دهد؛ اگر فیلد show_on_tv نبود، همه را نشان بده
const list = products.value
const marked = list.filter(isShowOnTv)
const source = marked.length ? marked : list
return source.map((product) => {
if (isTvProductPriceInactive(product)) {
return {
id: product.id,
title: product.title,
buyText: NO_QUOTE_MESSAGE,
sellText: NO_QUOTE_MESSAGE,
}
}
return {
id: product.id,
title: product.title,
buyText: usePriceWithoutType(getTvProductBuyPrice(product)),
sellText: usePriceWithoutType(getTvProductSellPrice(product)),
}
})
})
function normalizeTvProduct(raw: Product): Product {
const image = (raw as Product & { image?: string | null }).image
if (image && !raw.img) {
return { ...raw, img: image }
}
return raw
}
async function syncServerClock() {
const t0 = Date.now()
const payload = await getTvServerTime()
const t1 = Date.now()
if (!payload?.unix) return
const rttHalf = (t1 - t0) / 2
serverOffsetMs = payload.unix * 1000 + rttHalf - t1
}
async function loadProducts() {
try {
const list = await getTvProducts()
products.value = (Array.isArray(list) ? list : []).map(normalizeTvProduct)
} catch {
if (!products.value.length) {
products.value = []
}
} finally {
loading.value = false
}
}
onMounted(async () => {
const settingsStore = useSettingsStore()
initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key))
startLastPricePolling()
void getLastPrices()
await syncServerClock()
now.value = Date.now() + serverOffsetMs
void loadProducts()
clockTimer = setInterval(() => {
now.value = Date.now() + serverOffsetMs
}, 1000)
listTimer = setInterval(() => {
void loadProducts()
void syncServerClock()
}, REFRESH_MS)
})
onUnmounted(() => {
stopLastPricePolling()
if (clockTimer) {
clearInterval(clockTimer)
}
if (listTimer) {
clearInterval(listTimer)
}
})
</script>
<style scoped>
.tv-board {
@apply min-h-screen w-full flex flex-col gap-5 p-6 text-white bg-gray-950;
}
.tv-board__header {
@apply flex items-center justify-between gap-4;
}
.tv-board__title {
@apply m-0 text-2xl font-bold;
}
.tv-board__clock {
@apply text-xl font-bold text-gray-300;
}
.tv-board__banner {
@apply m-0 rounded-xl bg-gray-800 px-4 py-3 text-center text-sm text-gray-200;
}
.tv-board__state {
@apply flex flex-1 flex-col items-center justify-center gap-3 text-gray-400;
}
.tv-board__table {
@apply flex flex-col gap-2;
}
.tv-board__row {
@apply grid grid-cols-3 items-center gap-4 rounded-xl bg-gray-900 px-4 py-3;
}
.tv-board__row--head {
@apply bg-gray-800 text-sm text-gray-400;
}
.tv-board__name {
@apply text-lg font-bold;
}
.tv-board__buy {
@apply text-xl font-bold text-green-400;
}
.tv-board__sell {
@apply text-xl font-bold text-red-400;
}
</style>