Let melted products trade without store inventory, and split TV sell prices with extra tickers.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Props>()
|
||||
|
||||
const isOutOfStock = computed(
|
||||
() => !props.ProductItem?.inventory || props.ProductItem.inventory === 0
|
||||
)
|
||||
const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem))
|
||||
|
||||
const amountProduct = computed(() => {
|
||||
if (!props.ProductItem.amount) return ''
|
||||
|
||||
@@ -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<Props>()
|
||||
|
||||
const isOutOfStock = computed(
|
||||
() => !props.ProductItem?.inventory || props.ProductItem.inventory === 0
|
||||
)
|
||||
const isOutOfStock = computed(() => isProductOutOfStock(props.ProductItem))
|
||||
|
||||
const amountProduct = computed(() => {
|
||||
if (!props.ProductItem.amount) return ''
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="dproduct-info__price-row">
|
||||
<span class="dproduct-info__price">{{ usePrice(getProductBuyPrice(product)) }}</span>
|
||||
<BasicButton
|
||||
v-if="hoursOpen && product.open_buy && product.inventory"
|
||||
v-if="hoursOpen && product.open_buy && !isOutOfStock"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="dproduct-similar-card__image">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="!item.inventory"
|
||||
:dimmed="isProductOutOfStock(item)"
|
||||
img-class="h-20 object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -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()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BasicButton
|
||||
v-if="hoursOpen && product?.open_buy && product?.inventory"
|
||||
v-if="hoursOpen && product?.open_buy && !isOutOfStock"
|
||||
class="w-full py-3"
|
||||
type="primary"
|
||||
size="large"
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="bg-gray-100 rounded-md flex justify-center p-4 mb-2">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="!item.inventory"
|
||||
:dimmed="isProductOutOfStock(item)"
|
||||
img-class="h-20 object-contain"
|
||||
/>
|
||||
</div>
|
||||
@@ -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'
|
||||
|
||||
@@ -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<string, boolean>)
|
||||
const listEls: Record<string, HTMLElement | null> = Object.fromEntries(keys.map((key) => [key, null]))
|
||||
const resumeTimers: Record<string, ReturnType<typeof setTimeout> | 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,
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLElement | null>(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,
|
||||
|
||||
+216
-110
@@ -69,12 +69,15 @@
|
||||
<div
|
||||
v-if="hasCoinBoard"
|
||||
class="tv-monitor__columns"
|
||||
:style="{ '--tv-row-count': coinRowCount }"
|
||||
>
|
||||
<section class="tv-col">
|
||||
<section
|
||||
v-for="col in coinColumns"
|
||||
:key="col.key"
|
||||
class="tv-col"
|
||||
>
|
||||
<header class="tv-col__head">
|
||||
<h2>سکه پارسیان</h2>
|
||||
<span>تمام اوزان</span>
|
||||
<h2>{{ col.title }}</h2>
|
||||
<span>{{ col.hint }}</span>
|
||||
</header>
|
||||
<div class="tv-col__labels">
|
||||
<span class="tv-col__labels-gap" />
|
||||
@@ -83,67 +86,51 @@
|
||||
<span class="tv-col__label tv-col__label--buy">خرید</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-col__list">
|
||||
<article
|
||||
v-for="item in parsianCoins"
|
||||
:key="item.id"
|
||||
class="tv-row"
|
||||
<div
|
||||
:ref="(el) => setColListRef(col.key, el)"
|
||||
class="tv-col__list"
|
||||
@mouseenter="pauseColAuto(col.key)"
|
||||
@mouseleave="scheduleColAuto(col.key)"
|
||||
>
|
||||
<div
|
||||
v-if="col.items.length"
|
||||
class="tv-col__track"
|
||||
>
|
||||
<div class="tv-row__media">
|
||||
<ProductImage :product="item" img-class="tv-row__img" />
|
||||
<div
|
||||
v-for="copy in col.copies"
|
||||
:key="copy"
|
||||
class="tv-col__group"
|
||||
>
|
||||
<article
|
||||
v-for="item in col.items"
|
||||
:key="`${copy}-${item.id}`"
|
||||
class="tv-row"
|
||||
>
|
||||
<div class="tv-row__media">
|
||||
<ProductImage :product="item" img-class="tv-row__img" />
|
||||
</div>
|
||||
<div class="tv-row__meta">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p>{{ productWeightLabel(item) }}</p>
|
||||
</div>
|
||||
<div class="tv-row__prices">
|
||||
<div
|
||||
class="tv-row__price tv-row__price--sell"
|
||||
:class="{ 'tv-row__price--empty': isProductTvInactive(item) }"
|
||||
>
|
||||
<strong>{{ productBuyLabel(item) }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="tv-row__price tv-row__price--buy"
|
||||
:class="{ 'tv-row__price--empty': isProductTvInactive(item) }"
|
||||
>
|
||||
<strong>{{ productSellLabel(item) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tv-row__meta">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p>{{ productWeightLabel(item) }}</p>
|
||||
</div>
|
||||
<div class="tv-row__prices">
|
||||
<div class="tv-row__price tv-row__price--sell">
|
||||
<strong>{{ productBuyLabel(item) }}</strong>
|
||||
</div>
|
||||
<div class="tv-row__price tv-row__price--buy">
|
||||
<strong>{{ productSellLabel(item) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!parsianCoins.length" class="tv-col__empty">سکهای برای نمایش نیست</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tv-col">
|
||||
<header class="tv-col__head">
|
||||
<h2>سکه بانکی</h2>
|
||||
<span>تمام · نیم · ربع</span>
|
||||
</header>
|
||||
<div class="tv-col__labels">
|
||||
<span class="tv-col__labels-gap" />
|
||||
<div class="tv-col__price-heads">
|
||||
<span class="tv-col__label tv-col__label--sell">فروش</span>
|
||||
<span class="tv-col__label tv-col__label--buy">خرید</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-col__list">
|
||||
<article
|
||||
v-for="item in bankCoins"
|
||||
:key="item.id"
|
||||
class="tv-row"
|
||||
>
|
||||
<div class="tv-row__media">
|
||||
<ProductImage :product="item" img-class="tv-row__img" />
|
||||
</div>
|
||||
<div class="tv-row__meta">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p>{{ productWeightLabel(item) }}</p>
|
||||
</div>
|
||||
<div class="tv-row__prices">
|
||||
<div class="tv-row__price tv-row__price--sell">
|
||||
<strong>{{ productBuyLabel(item) }}</strong>
|
||||
</div>
|
||||
<div class="tv-row__price tv-row__price--buy">
|
||||
<strong>{{ productSellLabel(item) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!bankCoins.length" class="tv-col__empty">سکهای برای نمایش نیست</div>
|
||||
<div v-else class="tv-col__empty">{{ col.empty }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -155,32 +142,63 @@
|
||||
</main>
|
||||
|
||||
<footer class="tv-monitor__footer">
|
||||
<div
|
||||
v-if="tickerProducts.length"
|
||||
class="tv-ticker"
|
||||
:style="{ '--tv-ticker-duration': `${Math.max(tickerProducts.length * 7, 36)}s` }"
|
||||
>
|
||||
<div class="tv-ticker__label">
|
||||
<strong>{{ tickerTitle }}</strong>
|
||||
<span>خرید و فروش</span>
|
||||
<div class="tv-monitor__tickers">
|
||||
<div
|
||||
v-if="tickerProducts.length"
|
||||
class="tv-ticker"
|
||||
:style="{ '--tv-ticker-duration': `${Math.max(tickerProducts.length * 7, 36)}s` }"
|
||||
>
|
||||
<div class="tv-ticker__label">
|
||||
<strong>{{ tickerTitle }}</strong>
|
||||
<span>خرید و فروش</span>
|
||||
</div>
|
||||
<div class="tv-ticker__viewport">
|
||||
<div class="tv-ticker__track">
|
||||
<article
|
||||
v-for="entry in shamsTickerLoop"
|
||||
:key="`shams-${entry.copy}-${entry.product.id}`"
|
||||
class="tv-ticker__item"
|
||||
>
|
||||
<span class="tv-ticker__weight">{{ tickerItemLabel(entry.product) }}</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--sell">
|
||||
فروش
|
||||
<b>{{ productBuyLabel(entry.product) }}</b>
|
||||
</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--buy">
|
||||
خرید
|
||||
<b>{{ productSellLabel(entry.product) }}</b>
|
||||
</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-ticker__viewport">
|
||||
<div class="tv-ticker__track">
|
||||
<article
|
||||
v-for="item in tickerProducts"
|
||||
:key="item.id"
|
||||
class="tv-ticker__item"
|
||||
>
|
||||
<span class="tv-ticker__weight">{{ tickerItemLabel(item) }}</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--sell">
|
||||
فروش
|
||||
<b>{{ productBuyLabel(item) }}</b>
|
||||
</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--buy">
|
||||
خرید
|
||||
<b>{{ productSellLabel(item) }}</b>
|
||||
</span>
|
||||
</article>
|
||||
<div
|
||||
v-if="extraTickerProducts.length"
|
||||
class="tv-ticker tv-ticker--extra"
|
||||
:style="{ '--tv-ticker-duration': `${Math.max(extraTickerProducts.length * 7, 36)}s` }"
|
||||
>
|
||||
<div class="tv-ticker__label">
|
||||
<strong>{{ extraTickerTitle }}</strong>
|
||||
<span>بقیه قیمتها</span>
|
||||
</div>
|
||||
<div class="tv-ticker__viewport">
|
||||
<div class="tv-ticker__track">
|
||||
<article
|
||||
v-for="entry in extraTickerLoop"
|
||||
:key="`extra-${entry.copy}-${entry.product.id}`"
|
||||
class="tv-ticker__item"
|
||||
>
|
||||
<span class="tv-ticker__weight">{{ tickerItemLabel(entry.product) }}</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--sell">
|
||||
فروش
|
||||
<b>{{ productBuyLabel(entry.product) }}</b>
|
||||
</span>
|
||||
<span class="tv-ticker__price tv-ticker__price--buy">
|
||||
خرید
|
||||
<b>{{ productSellLabel(entry.product) }}</b>
|
||||
</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,10 +210,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import AppCreditLink from '@/components/AppCreditLink.vue'
|
||||
import { useTvMonitorPage } from '@/views/tv/composables/useTvMonitorPage'
|
||||
import { useTvColAutoScroll } from '@/views/tv/composables/useTvColAutoScroll'
|
||||
import { useTvTheme } from '@/views/tv/composables/useTvTheme'
|
||||
|
||||
const { theme, themeVars, toggleTheme } = useTvTheme()
|
||||
@@ -210,9 +231,10 @@ const {
|
||||
bankCoins,
|
||||
hasCoinBoard,
|
||||
hasBoardContent,
|
||||
coinRowCount,
|
||||
tickerProducts,
|
||||
tickerTitle,
|
||||
extraTickerProducts,
|
||||
extraTickerTitle,
|
||||
statusBanner,
|
||||
liveBuy,
|
||||
liveSell,
|
||||
@@ -222,11 +244,50 @@ const {
|
||||
loading,
|
||||
productWeightLabel,
|
||||
tickerItemLabel,
|
||||
isProductTvInactive,
|
||||
productBuyLabel,
|
||||
productSellLabel,
|
||||
liveQuoteLabel,
|
||||
toggleFullscreen,
|
||||
} = useTvMonitorPage()
|
||||
|
||||
const { setColListRef, pauseColAuto, scheduleColAuto } = useTvColAutoScroll(['parsian', 'bank'])
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return typeof window !== 'undefined'
|
||||
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
function marqueeCopies(count: number) {
|
||||
return count >= 2 && !prefersReducedMotion() ? [0, 1] : [0]
|
||||
}
|
||||
|
||||
function loopTicker(items: Product[]) {
|
||||
if (!items.length) return []
|
||||
return [0, 1].flatMap((copy) => items.map((product) => ({ product, copy })))
|
||||
}
|
||||
|
||||
const shamsTickerLoop = computed(() => loopTicker(tickerProducts.value))
|
||||
const extraTickerLoop = computed(() => loopTicker(extraTickerProducts.value))
|
||||
|
||||
const coinColumns = computed(() => [
|
||||
{
|
||||
key: 'parsian',
|
||||
title: 'سکه پارسیان',
|
||||
hint: 'تمام اوزان',
|
||||
empty: 'سکهای برای نمایش نیست',
|
||||
items: parsianCoins.value,
|
||||
copies: marqueeCopies(parsianCoins.value.length),
|
||||
},
|
||||
{
|
||||
key: 'bank',
|
||||
title: 'سکه بانکی',
|
||||
hint: 'دسته سکه',
|
||||
empty: 'سکهای برای نمایش نیست',
|
||||
items: bankCoins.value,
|
||||
copies: marqueeCopies(bankCoins.value.length),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -525,17 +586,39 @@ const {
|
||||
}
|
||||
|
||||
.tv-col__list {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
/* با آیتم کم کش نیاید؛ با آیتم زیاد فضای ستون را پر کند */
|
||||
grid-auto-rows: minmax(
|
||||
64px,
|
||||
min(88px, calc((100% - (var(--tv-row-count) - 1) * 8px) / var(--tv-row-count)))
|
||||
);
|
||||
align-content: start;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--tv-primary) 42%, transparent) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 99px;
|
||||
background: color-mix(in srgb, var(--tv-primary) 42%, transparent);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-col__track {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tv-col__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.tv-col__empty {
|
||||
@@ -553,9 +636,10 @@ const {
|
||||
grid-template-columns: minmax(44px, 56px) minmax(0, 0.9fr) minmax(0, 2fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
max-height: 88px;
|
||||
min-height: 0;
|
||||
flex: 0 0 auto;
|
||||
height: clamp(64px, 8vh, 80px);
|
||||
max-height: 80px;
|
||||
min-height: 64px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
@@ -633,10 +717,9 @@ const {
|
||||
|
||||
.tv-row__prices {
|
||||
display: grid;
|
||||
align-self: stretch;
|
||||
align-self: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -649,6 +732,7 @@ const {
|
||||
padding: 6px 8px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
container-type: inline-size;
|
||||
|
||||
span {
|
||||
@@ -670,19 +754,27 @@ const {
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
&--sell {
|
||||
background: var(--tv-sell-bg);
|
||||
|
||||
strong {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
&--sell strong {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
|
||||
&--buy {
|
||||
&--buy strong {
|
||||
color: var(--tv-buy);
|
||||
}
|
||||
|
||||
&--sell:not(.tv-row__price--empty) {
|
||||
background: var(--tv-sell-bg);
|
||||
}
|
||||
|
||||
&--buy:not(.tv-row__price--empty) {
|
||||
background: var(--tv-buy-bg);
|
||||
}
|
||||
|
||||
&--empty {
|
||||
background: transparent;
|
||||
|
||||
strong {
|
||||
color: var(--tv-buy);
|
||||
color: var(--tv-card-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,6 +797,11 @@ const {
|
||||
margin-bottom: calc(-1 * clamp(12px, 1.6vh, 24px));
|
||||
}
|
||||
|
||||
.tv-monitor__tickers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tv-monitor__credit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -724,6 +821,15 @@ const {
|
||||
box-shadow: 0 -8px 24px color-mix(in srgb, var(--tv-primary) 28%, transparent);
|
||||
}
|
||||
|
||||
.tv-ticker--extra {
|
||||
background: var(--tv-secondary);
|
||||
box-shadow: 0 -8px 24px color-mix(in srgb, var(--tv-secondary) 28%, transparent);
|
||||
|
||||
.tv-ticker__label {
|
||||
background: color-mix(in srgb, #1e2229 42%, var(--tv-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
.tv-ticker__label {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -732,7 +838,7 @@ const {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 132px;
|
||||
min-width: 148px;
|
||||
padding: 8px 18px 8px 22px;
|
||||
background: color-mix(in srgb, #1e2229 42%, var(--tv-primary));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user