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:
2026-09-10 15:10:10 +03:30
co-authored by Cursor
parent c2f6f9d226
commit d4e3a99e61
12 changed files with 379 additions and 155 deletions
+1
View File
@@ -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
+16
View File
@@ -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
}
+12 -4
View File
@@ -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,
}
}
+46 -28
View File
@@ -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
View File
@@ -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));