feat: show selected products on public /tv board
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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