diff --git a/src/models/products.ts b/src/models/products.ts index 6f06565..1c530fe 100644 --- a/src/models/products.ts +++ b/src/models/products.ts @@ -41,6 +41,7 @@ export interface Product { inventory:number app_price?: boolean | number | null tv_price?: number | null + tv_sell_price?: number | null direct_price?: number | null trade_by_quantity?: boolean | number | null use_last_price_if_zero?: boolean | null diff --git a/src/utils/productCategories.ts b/src/utils/productCategories.ts index 1c9a15b..4f6c119 100644 --- a/src/utils/productCategories.ts +++ b/src/utils/productCategories.ts @@ -85,3 +85,19 @@ export function isMeltedProductUsingAppPrice(product?: { return Number(product.price ?? 0) <= 0 } + +/** آبشده موجودی انبار فروشگاهی ندارد */ +export function productTracksStoreInventory(product?: { group_type?: string | null } | null) { + return !isMeltedTradeGroup(product?.group_type) +} + +export function isProductOutOfStock(product?: { + group_type?: string | null + inventory?: number | null +} | null) { + if (!product || !productTracksStoreInventory(product)) { + return false + } + + return Number(product.inventory ?? 0) <= 0 +} diff --git a/src/utils/productPrice.ts b/src/utils/productPrice.ts index 220293c..d9aa070 100644 --- a/src/utils/productPrice.ts +++ b/src/utils/productPrice.ts @@ -249,15 +249,23 @@ export function isProductPriceInactive(product?: Product | null): boolean { return getProductBaseBuyPrice(product) <= 0 } -function tvUnitPrice(product: Product): number { +function tvBuyUnitPrice(product: Product): number { return Number(product.tv_price ?? 0) } +function tvSellUnitPrice(product: Product): number { + const sell = Number(product.tv_sell_price ?? 0) + if (sell > 0) { + return sell + } + return tvBuyUnitPrice(product) +} + export function getTvProductBuyPrice(product?: Product | null): number { if (!product) { return 0 } - const unit = tvUnitPrice(product) + const unit = tvBuyUnitPrice(product) if (unit > 0) { return unit + resolveProductCashFeeBuyRial(product, unit) } @@ -268,7 +276,7 @@ export function getTvProductSellPrice(product?: Product | null): number { if (!product) { return 0 } - const unit = tvUnitPrice(product) + const unit = tvSellUnitPrice(product) if (unit > 0) { return unit - resolveProductCashFeeSellRial(product, unit) } @@ -279,7 +287,7 @@ export function isTvProductPriceInactive(product?: Product | null): boolean { if (!product) { return true } - if (tvUnitPrice(product) > 0) { + if (tvBuyUnitPrice(product) > 0 || tvSellUnitPrice(product) > 0) { return false } return isProductPriceInactive(product) diff --git a/src/views/base/products/components/ProductItem.vue b/src/views/base/products/components/ProductItem.vue index 14ab016..5f94ede 100644 --- a/src/views/base/products/components/ProductItem.vue +++ b/src/views/base/products/components/ProductItem.vue @@ -105,6 +105,7 @@ import { computed } from 'vue' import type { Product } from '@/models/products' import { usePrice } from '@/utils/usePrice' import { getProductBuyPrice } from '@/utils/productPrice' +import { isProductOutOfStock } from '@/utils/productCategories' import { isProductTradingHoursOpen } from '@/utils/productTradeOpen' import ProductImage from '@/components/ProductImage.vue' @@ -114,9 +115,7 @@ interface Props { const props = defineProps() -const isOutOfStock = computed( - () => !props.ProductItem?.inventory || props.ProductItem.inventory === 0 -) +const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem)) const amountProduct = computed(() => { if (!props.ProductItem.amount) return '' diff --git a/src/views/base/products/components/ProductItemVertical.vue b/src/views/base/products/components/ProductItemVertical.vue index fc64800..6cba488 100644 --- a/src/views/base/products/components/ProductItemVertical.vue +++ b/src/views/base/products/components/ProductItemVertical.vue @@ -37,6 +37,7 @@ import { computed } from 'vue' import type { Product } from '@/models/products' import { usePrice } from '@/utils/usePrice' import { getProductBuyPrice } from '@/utils/productPrice' +import { isProductOutOfStock } from '@/utils/productCategories' import { isProductTradingHoursOpen } from '@/utils/productTradeOpen' import ProductImage from '@/components/ProductImage.vue' @@ -46,9 +47,7 @@ interface Props { const props = defineProps() -const isOutOfStock = computed( - () => !props.ProductItem?.inventory || props.ProductItem.inventory === 0 -) +const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem)) const amountProduct = computed(() => { if (!props.ProductItem.amount) return '' diff --git a/src/views/base/products/composables/useProductsPageData.ts b/src/views/base/products/composables/useProductsPageData.ts index de046b1..ab95dc3 100644 --- a/src/views/base/products/composables/useProductsPageData.ts +++ b/src/views/base/products/composables/useProductsPageData.ts @@ -2,6 +2,7 @@ import { ref, watch } from 'vue' import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog' import type { Product } from '@/models/products' import { loadProductCategoryTabNames } from '@/utils/categoryTabs' +import { isProductOutOfStock } from '@/utils/productCategories' import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price' import { priceSocketConnected, @@ -27,8 +28,8 @@ export const productsPageLoading = ref(true) function sortProductsByInventory(products: Product[]) { return [...products].sort((a, b) => { - const aInStock = Number(a.inventory ?? 0) > 0 ? 0 : 1 - const bInStock = Number(b.inventory ?? 0) > 0 ? 0 : 1 + const aInStock = isProductOutOfStock(a) ? 1 : 0 + const bInStock = isProductOutOfStock(b) ? 1 : 0 if (aInStock !== bInStock) return aInStock - bInStock return Number(b.inventory ?? 0) - Number(a.inventory ?? 0) }) diff --git a/src/views/base/products/product/composables/useProductDetailPage.ts b/src/views/base/products/product/composables/useProductDetailPage.ts index 359a0db..35d968b 100644 --- a/src/views/base/products/product/composables/useProductDetailPage.ts +++ b/src/views/base/products/product/composables/useProductDetailPage.ts @@ -6,6 +6,7 @@ import { getProductById } from '@/api/products' import { findProductInCatalog, getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog' import { getLastPrices, stopPriceInterval } from '@/utils/price' import { getProductBuyPrice } from '@/utils/productPrice' +import { isProductOutOfStock } from '@/utils/productCategories' import { isProductTradingHoursOpen } from '@/utils/productTradeOpen' import { loading } from '@/utils' import type { Product } from '@/models/products' @@ -34,7 +35,7 @@ export function useProductDetailPage() { return typeof id === 'string' ? parseInt(id) : Number(id) }) - const isOutOfStock = computed(() => !product.value?.inventory || product.value.inventory === 0) + const isOutOfStock = computed(() => isProductOutOfStock(product.value)) const hoursOpen = computed(() => isProductTradingHoursOpen(product.value)) watch(productId, (newId, oldId) => { diff --git a/src/views/base/products/product/desktop/DesktopProductDetailView.vue b/src/views/base/products/product/desktop/DesktopProductDetailView.vue index 848e5a8..9d8a506 100644 --- a/src/views/base/products/product/desktop/DesktopProductDetailView.vue +++ b/src/views/base/products/product/desktop/DesktopProductDetailView.vue @@ -18,7 +18,7 @@
{{ usePrice(getProductBuyPrice(product)) }}
@@ -70,6 +70,7 @@ import ProductImage from '@/components/ProductImage.vue' import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue' import { loading } from '@/utils' import { usePrice } from '@/utils/usePrice' +import { isProductOutOfStock } from '@/utils/productCategories' import { useProductDetailPageContext } from '../composables/useProductDetailPage' const { product, isOutOfStock, hoursOpen, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext() diff --git a/src/views/base/products/product/mobile/MobileProductDetailView.vue b/src/views/base/products/product/mobile/MobileProductDetailView.vue index bd0ea31..9fe6d19 100644 --- a/src/views/base/products/product/mobile/MobileProductDetailView.vue +++ b/src/views/base/products/product/mobile/MobileProductDetailView.vue @@ -17,7 +17,7 @@
@@ -71,6 +71,7 @@ import img from '@/assets/img/user_ly1.svg' import BasicButton from '@/components/BasicPacks/BasicButton.vue' import ProductImage from '@/components/ProductImage.vue' import { loading } from '@/utils' +import { isProductOutOfStock } from '@/utils/productCategories' import { usePrice } from '@/utils/usePrice' import { Swiper, SwiperSlide } from 'swiper/vue' import 'swiper/swiper-bundle.css' diff --git a/src/views/tv/composables/useTvColAutoScroll.ts b/src/views/tv/composables/useTvColAutoScroll.ts new file mode 100644 index 0000000..7316a31 --- /dev/null +++ b/src/views/tv/composables/useTvColAutoScroll.ts @@ -0,0 +1,73 @@ +import { onMounted, onUnmounted, reactive } from 'vue' + +const AUTO_RESUME_MS = 6000 +const AUTO_PX_PER_FRAME = 0.45 + +export function useTvColAutoScroll(keys: string[]) { + const paused = reactive(Object.fromEntries(keys.map((key) => [key, false])) as Record) + const listEls: Record = Object.fromEntries(keys.map((key) => [key, null])) + const resumeTimers: Record | undefined> = {} + let rafId = 0 + + function reduceMotion() { + return typeof window !== 'undefined' + && window.matchMedia('(prefers-reduced-motion: reduce)').matches + } + + function setColListRef(key: string, el: unknown) { + listEls[key] = el instanceof HTMLElement ? el : null + } + + function pauseColAuto(key: string) { + paused[key] = true + if (resumeTimers[key]) { + clearTimeout(resumeTimers[key]) + resumeTimers[key] = undefined + } + } + + function scheduleColAuto(key: string) { + if (resumeTimers[key]) { + clearTimeout(resumeTimers[key]) + } + resumeTimers[key] = setTimeout(() => { + paused[key] = false + resumeTimers[key] = undefined + }, AUTO_RESUME_MS) + } + + function tick() { + if (!reduceMotion()) { + for (const key of keys) { + if (paused[key]) continue + const el = listEls[key] + if (!el) continue + const half = el.scrollHeight / 2 + if (half <= el.clientHeight + 8) continue + el.scrollTop += AUTO_PX_PER_FRAME + if (el.scrollTop >= half) { + el.scrollTop -= half + } + } + } + rafId = requestAnimationFrame(tick) + } + + onMounted(() => { + rafId = requestAnimationFrame(tick) + }) + + onUnmounted(() => { + cancelAnimationFrame(rafId) + for (const key of keys) { + if (resumeTimers[key]) clearTimeout(resumeTimers[key]) + } + }) + + return { + pausedCols: paused, + setColListRef, + pauseColAuto, + scheduleColAuto, + } +} diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts index 6e467c8..bce0708 100644 --- a/src/views/tv/composables/useTvMonitorPage.ts +++ b/src/views/tv/composables/useTvMonitorPage.ts @@ -6,7 +6,7 @@ import { getConfigApp } from '@/utils/config' import { getTvProductBuyPrice, getTvProductSellPrice, isTvProductPriceInactive } from '@/utils/productPrice' import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, startLastPricePolling, stopLastPricePolling } from '@/utils/price' import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours' -import { isMeltedTradeGroup } from '@/utils/productCategories' +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' @@ -37,10 +37,8 @@ function isParsianCoin(product: Product) { function isBankCoin(product: Product) { if (isParsianCoin(product)) return false - const text = productText(product) const group = (product.group_type ?? '').trim() - if (group === 'سکه' || group === 'سکه بانکی' || group.includes('بانکی')) return true - return text.includes('سکه') && /تمام|نیم|ربع/.test(text) + return group === 'سکه' || group === 'سکه بانکی' || (group.includes('سکه') && !group.includes('پارسیان')) } function bankCoinRank(product: Product) { @@ -51,14 +49,23 @@ function bankCoinRank(product: Product) { return 99 } -function isBankCoinDenomination(product: Product) { - return bankCoinRank(product) <= 3 -} - function isSimorghBar(product: Product) { return productText(product).includes('سیمرغ') } +function isGoldBar(product: Product) { + if (isMeltedTradeGroup(product.group_type) || isMetalCommodityGroup(product.group_type)) { + return false + } + + const group = (product.group_type ?? '').trim() + if (group.includes('شمش') || isSimorghBar(product)) { + return true + } + + return (product.title ?? '').includes('شمش') +} + export function useTvMonitorPage() { const rootEl = ref(null) const { isFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(rootEl) @@ -96,8 +103,7 @@ export function useTvMonitorPage() { const bankCoins = computed(() => catalogProducts.value .filter(isBankCoin) - .filter(isBankCoinDenomination) - .sort((a, b) => bankCoinRank(a) - bankCoinRank(b) || Number(b.amount ?? 0) - Number(a.amount ?? 0)), + .sort((a, b) => bankCoinRank(a) - bankCoinRank(b) || Number(a.amount ?? 0) - Number(b.amount ?? 0)), ) const hasCoinBoard = computed( @@ -114,23 +120,31 @@ export function useTvMonitorPage() { .sort((a, b) => Number(a.amount ?? 0) - Number(b.amount ?? 0)), ) - /** تیکر پایین: شمش / آبشده / هر محصول غیرسکه */ + /** تیکر بالا: فقط شمش‌های تابلو */ const tickerProducts = computed(() => catalogProducts.value - .filter((p) => !isParsianCoin(p) && !isBankCoin(p)) + .filter(isGoldBar) .sort((a, b) => Number(a.amount ?? 0) - Number(b.amount ?? 0)), ) - const tickerTitle = computed(() => { - const items = tickerProducts.value - if (!items.length) return '' - if (items.every(isSimorghBar)) return 'شمش سیمرغ' - if (items.every((p) => isMeltedTradeGroup(p.group_type))) return 'آبشده' - return 'محصولات تابلو' - }) + const tickerTitle = computed(() => (tickerProducts.value.length ? 'مظنه شمش‌ها' : '')) + + /** تیکر پایین: بقیه — نه سکه، نه پارسیان، نه شمش */ + const extraTickerProducts = computed(() => + catalogProducts.value + .filter((product) => !isParsianCoin(product) && !isBankCoin(product) && !isGoldBar(product)) + .sort((a, b) => (a.title ?? '').localeCompare(b.title ?? '', 'fa')), + ) + + const extraTickerTitle = computed(() => + extraTickerProducts.value.length ? 'سایر مظنه‌ها' : '', + ) const hasBoardContent = computed( - () => hasCoinBoard.value || tickerProducts.value.length > 0, + () => + hasCoinBoard.value + || tickerProducts.value.length > 0 + || extraTickerProducts.value.length > 0, ) const statusBanner = computed(() => { @@ -230,22 +244,23 @@ export function useTvMonitorPage() { } function tickerItemLabel(product: Product) { - if (isSimorghBar(product)) return simorghWeightLabel(product) - const amount = Number(product.amount ?? 0) - if (isMeltedTradeGroup(product.group_type)) { - return amount > 0 ? `${product.title} · ${amount} گرم` : product.title - } - return amount > 0 ? `${product.title} · ${amount} گرم` : product.title + const title = (product.title ?? '').trim() + if (title) return title + return simorghWeightLabel(product) + } + + function isProductTvInactive(product: Product) { + return isTvProductPriceInactive(product) } function productBuyLabel(product: Product) { - if (isTvProductPriceInactive(product)) return NO_QUOTE_MESSAGE + if (isProductTvInactive(product)) return NO_QUOTE_MESSAGE const value = getTvProductBuyPrice(product) return value > 0 ? usePrice(value) : '—' } function productSellLabel(product: Product) { - if (isTvProductPriceInactive(product)) return NO_QUOTE_MESSAGE + if (isProductTvInactive(product)) return NO_QUOTE_MESSAGE const value = getTvProductSellPrice(product) return value > 0 ? usePrice(value) : '—' } @@ -298,6 +313,8 @@ export function useTvMonitorPage() { coinRowCount, tickerProducts, tickerTitle, + extraTickerProducts, + extraTickerTitle, statusBanner, liveBuy, liveSell, @@ -307,6 +324,7 @@ export function useTvMonitorPage() { loading, productWeightLabel, tickerItemLabel, + isProductTvInactive, productBuyLabel, productSellLabel, liveQuoteLabel, diff --git a/src/views/tv/index.vue b/src/views/tv/index.vue index 1c8126f..98b4ac0 100644 --- a/src/views/tv/index.vue +++ b/src/views/tv/index.vue @@ -69,12 +69,15 @@
-
+
-

سکه پارسیان

- تمام اوزان +

{{ col.title }}

+ {{ col.hint }}
@@ -83,67 +86,51 @@ خرید
-
-
+
-
- +
+
+
+ +
+
+

{{ item.title }}

+

{{ productWeightLabel(item) }}

+
+
+
+ {{ productBuyLabel(item) }} +
+
+ {{ productSellLabel(item) }} +
+
+
-
-

{{ item.title }}

-

{{ productWeightLabel(item) }}

-
-
-
- {{ productBuyLabel(item) }} -
-
- {{ productSellLabel(item) }} -
-
-
-
سکه‌ای برای نمایش نیست
-
- - -
-
-

سکه بانکی

- تمام · نیم · ربع -
-
- -
- فروش - خرید
-
-
-
-
- -
-
-

{{ item.title }}

-

{{ productWeightLabel(item) }}

-
-
-
- {{ productBuyLabel(item) }} -
-
- {{ productSellLabel(item) }} -
-
-
-
سکه‌ای برای نمایش نیست
+
{{ col.empty }}
@@ -155,32 +142,63 @@