feat: show selected products on public /tv board

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 17:16:59 +03:30
co-authored by Cursor
parent a9d0cba909
commit 9d391aeb95
5 changed files with 198 additions and 3 deletions
+4
View File
@@ -6,6 +6,10 @@ import type { PWallet, Wallet } from '@/models/wallet'
export const getProducts = async (): Promise<any> => {
return await useAxios.get<Product[]>(`/customer/product/list`, { timeout: 15000 })
}
export const getTvProducts = async (): Promise<Product[]> => {
const response = await useAxios.get<unknown>(`/customer/tv/products`, { timeout: 15000 })
return Array.isArray(response) ? (response as Product[]) : []
}
export const getProductById = async ( id:number|null): Promise<any> => {
return await useAxios.get<Product[]>(`/customer/product/list/${id}`, { timeout: 15000 })
}
+3 -3
View File
@@ -34,7 +34,7 @@ export function setupRouterGuards() {
}
function goToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
if (to.name === 'buyProduct' || to.name === 'productDetail' || to.name === 'about' || to.name === 'cart') {
if (to.name === 'buyProduct' || to.name === 'productDetail' || to.name === 'about' || to.name === 'cart' || to.name === 'tv') {
return next()
}
if (to.name !== 'login') {
@@ -112,7 +112,7 @@ export function setupRouterGuards() {
return next()
}
} else {
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart'].includes(to.name as string)) {
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tv'].includes(to.name as string)) {
return next()
}
goToLogin(to, next)
@@ -126,7 +126,7 @@ export function setupRouterGuards() {
return next()
}
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart'].includes(to.name as string)) {
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tv'].includes(to.name as string)) {
return next()
}
return next({ name: 'login' })
+1
View File
@@ -11,6 +11,7 @@ export interface Product {
active: boolean
show: boolean
show_in_assets?: boolean
show_on_tv?: boolean
open_sell: boolean
open_buy: boolean
group_type:string
+6
View File
@@ -168,6 +168,12 @@ const router = createRouter({
},
]
},
{
path: '/tv',
name: 'tv',
meta: { hideMenu: true },
component: () => import('@/views/base/tv/index.vue')
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
+184
View File
@@ -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>