feat: show selected products on public /tv board
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<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 } from '@/api/products'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
import { getProductBuyPrice, getProductSellPrice, isProductPriceInactive } 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 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', {
|
||||
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'
|
||||
}
|
||||
|
||||
const rows = computed(() => {
|
||||
void lastPrice.value
|
||||
return products.value.filter(isShowOnTv).map((product) => {
|
||||
if (isProductPriceInactive(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(getProductBuyPrice(product)),
|
||||
sellText: usePriceWithoutType(getProductSellPrice(product)),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
const list = await getTvProducts()
|
||||
products.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
if (!products.value.length) {
|
||||
products.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const settingsStore = useSettingsStore()
|
||||
initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key))
|
||||
startLastPricePolling()
|
||||
void getLastPrices()
|
||||
void loadProducts()
|
||||
clockTimer = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, 1000)
|
||||
listTimer = setInterval(() => {
|
||||
void loadProducts()
|
||||
}, 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>
|
||||
Reference in New Issue
Block a user