1405/05/29-add tv page
This commit is contained in:
Vendored
+1
-5
@@ -13,20 +13,17 @@ declare module 'vue' {
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
AppCreditLink: typeof import('./src/components/AppCreditLink.vue')['default']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
|
||||
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
|
||||
@@ -78,6 +75,5 @@ declare module 'vue' {
|
||||
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
|
||||
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
||||
VanDialog: typeof import('vant/es')['Dialog']
|
||||
VanField: typeof import('vant/es')['Field']
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+139
-3
@@ -46,7 +46,7 @@
|
||||
</transition>
|
||||
|
||||
<div
|
||||
v-if="showSupportButton && !showSplash && route.name !== 'support'"
|
||||
v-if="showSupportButton && !showSplash && route.name !== 'support' && !hideAppChrome"
|
||||
id="support-button"
|
||||
ref="supportButtonEl"
|
||||
class="draggable-support-button"
|
||||
@@ -80,10 +80,24 @@
|
||||
<VersionUpdateModal />
|
||||
|
||||
<PwaInstallGuideModal
|
||||
v-if="!hideAppChrome"
|
||||
v-model:open="guideOpen"
|
||||
:platform="platform"
|
||||
@complete="completeGuide()"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!showSplash && !hideAppChrome && !isDesktop"
|
||||
class="app-credit-footer app-credit-footer--mobile"
|
||||
>
|
||||
<AppCreditLink variant="mobile" />
|
||||
</div>
|
||||
<div
|
||||
v-if="!showSplash && !hideAppChrome && showDesktopFloatingCredit"
|
||||
class="app-credit-footer app-credit-footer--desktop-float"
|
||||
>
|
||||
<AppCreditLink variant="float" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -103,7 +117,8 @@ import { useSupportMessenger } from '@/composables/useSupportMessenger'
|
||||
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
|
||||
import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue'
|
||||
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
|
||||
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
|
||||
import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import AppCreditLink from '@/components/AppCreditLink.vue'
|
||||
import { usePwaInstall } from '@/composables/usePwaInstall'
|
||||
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
@@ -125,11 +140,23 @@ const indicator = h(LoadingOutlined, {
|
||||
spin: true
|
||||
})
|
||||
const displayVersion = getDisplayAppVersion(pkg.version)
|
||||
const showSplash = ref(true)
|
||||
const showSplash = ref(!isTvBoardPath())
|
||||
const route = useRoute()
|
||||
const hideAppChrome = computed(() => route.meta.hideChrome === true || route.name === 'tvMonitor')
|
||||
|
||||
function isTvBoardPath() {
|
||||
const path = window.location.pathname.replace(/\/+$/, '')
|
||||
return path.endsWith('/tv') || path.endsWith('/monitor')
|
||||
}
|
||||
|
||||
useApplyDesktopLayoutClass()
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const showDesktopFloatingCredit = computed(() => {
|
||||
if (!isDesktop.value) return false
|
||||
return route.name === 'login' || !!route.meta.hideMenu
|
||||
})
|
||||
|
||||
const {
|
||||
guideOpen,
|
||||
platform,
|
||||
@@ -185,11 +212,19 @@ function handleSupportPointerUp(event: PointerEvent) {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSupportMessengerFromSettings()
|
||||
if (hideAppChrome.value) {
|
||||
showSplash.value = false
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
showSplash.value = false
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
watch(hideAppChrome, (hidden) => {
|
||||
if (hidden) showSplash.value = false
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { latestIncomingNotification } = useChatService()
|
||||
@@ -252,6 +287,107 @@ watch(latestIncomingNotification, (payload) => {
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.app-credit-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000000;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.app-credit-footer--mobile {
|
||||
width: 100%;
|
||||
max-width: 426px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
min-height: var(--app-credit-bar-inner);
|
||||
padding-bottom: var(--app-safe-bottom);
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.app-credit-footer--desktop-float {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
bottom: 16px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-credit-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&--mobile {
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
color: inherit;
|
||||
|
||||
.app-credit-link__logo {
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&--sidebar {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
color: #9ca3af;
|
||||
font-size: 10px;
|
||||
|
||||
.app-credit-link__logo {
|
||||
height: 15px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
&--float {
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||
|
||||
.app-credit-link__logo {
|
||||
height: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&--tv {
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
|
||||
.app-credit-link__logo {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.updateButton .van-dialog__confirm .van-button__text {
|
||||
color: @primary-color !important;
|
||||
font-weight: 600;
|
||||
|
||||
+15
-4
@@ -33,8 +33,17 @@ export function setupRouterGuards() {
|
||||
}
|
||||
}
|
||||
|
||||
function isGuestAllowed(to: RouteLocationNormalized) {
|
||||
if (to.meta?.public) {
|
||||
return true
|
||||
}
|
||||
return ['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tvMonitor'].includes(
|
||||
to.name as string,
|
||||
)
|
||||
}
|
||||
|
||||
function goToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
|
||||
if (to.name === 'buyProduct' || to.name === 'productDetail' || to.name === 'about' || to.name === 'cart' || to.name === 'tv') {
|
||||
if (isGuestAllowed(to)) {
|
||||
return next()
|
||||
}
|
||||
if (to.name !== 'login') {
|
||||
@@ -53,7 +62,9 @@ export function setupRouterGuards() {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
authStore.setLoading(true)
|
||||
clearFingerTimeout()
|
||||
setFingerTimeOut(to)
|
||||
if (to.name !== 'tvMonitor' && !to.meta?.hideChrome) {
|
||||
setFingerTimeOut(to)
|
||||
}
|
||||
|
||||
if (to.name === 'notifyTrigger' && getToken()) {
|
||||
const { useSettingsStore } = await import('@/store/settingsStore')
|
||||
@@ -112,7 +123,7 @@ export function setupRouterGuards() {
|
||||
return next()
|
||||
}
|
||||
} else {
|
||||
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tv'].includes(to.name as string)) {
|
||||
if (isGuestAllowed(to)) {
|
||||
return next()
|
||||
}
|
||||
goToLogin(to, next)
|
||||
@@ -126,7 +137,7 @@ export function setupRouterGuards() {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tv'].includes(to.name as string)) {
|
||||
if (isGuestAllowed(to)) {
|
||||
return next()
|
||||
}
|
||||
return next({ name: 'login' })
|
||||
|
||||
@@ -66,11 +66,11 @@ body {
|
||||
color-scheme: only light;
|
||||
--app-safe-top: env(safe-area-inset-top, 0px);
|
||||
--app-safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--app-credit-bar-inner: 0px;
|
||||
--app-credit-bar-inner: 28px;
|
||||
--app-home-header-offset: calc(4.25rem + var(--app-safe-top));
|
||||
--app-credit-footer-offset: var(--app-safe-bottom);
|
||||
--app-credit-footer-offset: calc(var(--app-credit-bar-inner) + var(--app-safe-bottom));
|
||||
--app-nav-height: 80px;
|
||||
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-safe-bottom));
|
||||
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-credit-footer-offset));
|
||||
--app-add-card-height: 88px;
|
||||
--wallet-card-dock-overlap: 52px;
|
||||
--app-z-nav: 5000;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<a
|
||||
href="https://montzar.ir"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="app-credit-link"
|
||||
:class="`app-credit-link--${variant}`"
|
||||
>
|
||||
<span class="app-credit-link__text">توسعه و طراحی توسط</span>
|
||||
<img src="/logo-montzar.png" alt="montzar" class="app-credit-link__logo" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'mobile' | 'sidebar' | 'float' | 'tv'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
</script>
|
||||
@@ -34,6 +34,10 @@
|
||||
@click="navigateTo(item.routeName)"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div class="desktop-sidebar__credit">
|
||||
<AppCreditLink variant="sidebar" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -47,6 +51,7 @@ import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { getPrimaryNavItems, getSecondaryNavItems } from '../config/navigation'
|
||||
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||
import DesktopNavItem from './DesktopNavItem.vue'
|
||||
import AppCreditLink from '@/components/AppCreditLink.vue'
|
||||
|
||||
const cartStore = useCartStore()
|
||||
const authenticated = computed(() => isToken())
|
||||
|
||||
@@ -85,6 +85,13 @@
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.desktop-sidebar__credit {
|
||||
margin-top: 12px;
|
||||
padding: 12px 8px 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-sidebar__divider {
|
||||
height: 1px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
.bottom-appbar {
|
||||
z-index: var(--app-z-nav, 5000);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: var(--app-credit-footer-offset, 28px);
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: calc(var(--app-nav-height, 80px) + var(--app-safe-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
padding-bottom: var(--app-safe-bottom, env(safe-area-inset-bottom, 0px));
|
||||
height: var(--app-nav-height, 80px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
.bottom-appbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--app-credit-bar-inner: 0px;
|
||||
--app-credit-footer-offset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
html.app-layout--desktop {
|
||||
|
||||
@@ -9,6 +9,16 @@ const router = createRouter({
|
||||
name: 'login',
|
||||
component: () => import('../views/login/index.vue')
|
||||
},
|
||||
{
|
||||
path: `/tv`,
|
||||
name: 'tvMonitor',
|
||||
alias: ['/monitor'],
|
||||
meta: {
|
||||
public: true,
|
||||
hideChrome: true,
|
||||
},
|
||||
component: () => import('../views/tv/index.vue')
|
||||
},
|
||||
/* {
|
||||
path: '/',
|
||||
name: 'null',
|
||||
|
||||
@@ -60,7 +60,13 @@ _axios.interceptors.response.use(
|
||||
|
||||
if (response?.status === 401) {
|
||||
removeToken()
|
||||
if (router.currentRoute.value.name !== 'buyProduct') {
|
||||
const routeName = router.currentRoute.value.name
|
||||
const isPublicRoute =
|
||||
router.currentRoute.value.meta?.public === true ||
|
||||
['buyProduct', 'productDetail', 'about', 'cart', 'tvMonitor', 'login', '404'].includes(
|
||||
routeName as string,
|
||||
)
|
||||
if (!isPublicRoute) {
|
||||
await router.push({name: 'login'})
|
||||
}
|
||||
return Promise.reject(error)
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getTvProducts } from '@/api/products'
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
import { getProductBuyPrice, getProductSellPrice, isProductPriceInactive } 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 { priceSocketConnected, priceStreamLifecycle } from '@/utils/priceSocketState'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import logoFallback from '@/assets/img/logo_full_.png'
|
||||
//@ts-ignore
|
||||
import moment from 'moment-jalaali'
|
||||
//@ts-ignore
|
||||
import fa from 'moment/src/locale/fa'
|
||||
|
||||
moment.locale('fa', fa)
|
||||
moment.loadPersian({ usePersianDigits: false })
|
||||
|
||||
const CURSOR_HIDE_MS = 2500
|
||||
const TV_HTML_CLASS = 'app-layout--tv'
|
||||
const REFRESH_MS = 15000
|
||||
|
||||
function productText(product: Product) {
|
||||
return `${product.group_type ?? ''} ${product.title ?? ''}`
|
||||
}
|
||||
|
||||
function isShowOnTv(product: Product) {
|
||||
const value = product.show_on_tv as unknown
|
||||
return value === true || value === 1 || value === '1'
|
||||
}
|
||||
|
||||
function isParsianCoin(product: Product) {
|
||||
return productText(product).includes('پارسیان')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function bankCoinRank(product: Product) {
|
||||
const text = productText(product)
|
||||
if (text.includes('نیم')) return 2
|
||||
if (text.includes('ربع')) return 3
|
||||
if (text.includes('تمام') || text.includes('امامی') || text.includes('بهار')) return 1
|
||||
return 99
|
||||
}
|
||||
|
||||
function isBankCoinDenomination(product: Product) {
|
||||
return bankCoinRank(product) <= 3
|
||||
}
|
||||
|
||||
function isSimorghBar(product: Product) {
|
||||
return productText(product).includes('سیمرغ')
|
||||
}
|
||||
|
||||
export function useTvMonitorPage() {
|
||||
const rootEl = ref<HTMLElement | null>(null)
|
||||
const { isFullscreen, enter: enterFullscreen } = useFullscreen(rootEl)
|
||||
|
||||
const products = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const nowLabel = ref('')
|
||||
const dateLabel = ref('')
|
||||
const cursorHidden = ref(false)
|
||||
const wakeLockSentinel = ref<{ release: () => Promise<void> } | null>(null)
|
||||
|
||||
let clockTimer: ReturnType<typeof setInterval> | undefined
|
||||
let cursorTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let listTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const brandName = computed(
|
||||
() => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'),
|
||||
)
|
||||
const brandLogo = computed(() => String(getConfigApp('avatar') || logoFallback))
|
||||
|
||||
const catalogProducts = computed(() => products.value.filter(isShowOnTv))
|
||||
|
||||
const parsianCoins = computed(() =>
|
||||
catalogProducts.value
|
||||
.filter(isParsianCoin)
|
||||
.sort((a, b) => Number(a.amount ?? 0) - Number(b.amount ?? 0)),
|
||||
)
|
||||
|
||||
const bankCoins = computed(() =>
|
||||
catalogProducts.value
|
||||
.filter(isBankCoin)
|
||||
.filter(isBankCoinDenomination)
|
||||
.sort((a, b) => bankCoinRank(a) - bankCoinRank(b) || Number(b.amount ?? 0) - Number(a.amount ?? 0)),
|
||||
)
|
||||
|
||||
const hasCoinBoard = computed(
|
||||
() => parsianCoins.value.length > 0 || bankCoins.value.length > 0,
|
||||
)
|
||||
|
||||
const coinRowCount = computed(() =>
|
||||
Math.max(parsianCoins.value.length, bankCoins.value.length, 1),
|
||||
)
|
||||
|
||||
const simorghBars = computed(() =>
|
||||
catalogProducts.value
|
||||
.filter(isSimorghBar)
|
||||
.sort((a, b) => Number(a.amount ?? 0) - Number(b.amount ?? 0)),
|
||||
)
|
||||
|
||||
const statusBanner = computed(() => {
|
||||
void lastPrice.value
|
||||
const state = getPriceDisplayState()
|
||||
return state === 'open' ? '' : getPriceDisplayMessage(state)
|
||||
})
|
||||
|
||||
const liveBuy = computed(() => getTradeBuyPrice(lastPrice.value))
|
||||
const liveSell = computed(() => getTradeSellPrice(lastPrice.value))
|
||||
|
||||
const isLive = computed(
|
||||
() => priceSocketConnected.value || priceStreamLifecycle.value === 'open',
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function updateClock() {
|
||||
const now = moment()
|
||||
nowLabel.value = now.format('HH:mm:ss')
|
||||
dateLabel.value = now.format('dddd jD jMMMM jYYYY')
|
||||
}
|
||||
|
||||
function hideCursorSoon() {
|
||||
cursorHidden.value = false
|
||||
if (cursorTimer) clearTimeout(cursorTimer)
|
||||
cursorTimer = setTimeout(() => {
|
||||
cursorHidden.value = true
|
||||
}, CURSOR_HIDE_MS)
|
||||
}
|
||||
|
||||
async function requestWakeLock() {
|
||||
try {
|
||||
const nav = navigator as Navigator & {
|
||||
wakeLock?: { request: (type: 'screen') => Promise<{ release: () => Promise<void> }> }
|
||||
}
|
||||
wakeLockSentinel.value = (await nav.wakeLock?.request('screen')) ?? null
|
||||
} catch {
|
||||
wakeLockSentinel.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function releaseWakeLock() {
|
||||
try {
|
||||
await wakeLockSentinel.value?.release()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
wakeLockSentinel.value = null
|
||||
}
|
||||
|
||||
async function onVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
await requestWakeLock()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBoardClick() {
|
||||
hideCursorSoon()
|
||||
if (!isFullscreen.value) {
|
||||
try {
|
||||
await enterFullscreen()
|
||||
} catch {
|
||||
/* browser may require a later gesture */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function productWeightLabel(product: Product) {
|
||||
if (isMeltedTradeGroup(product.group_type)) {
|
||||
return 'قیمت هر گرم'
|
||||
}
|
||||
const amount = Number(product.amount ?? 0)
|
||||
if (!amount) return ''
|
||||
if (amount < 1) return `${amount * 1000} سوت`
|
||||
return `${amount} گرم`
|
||||
}
|
||||
|
||||
function simorghWeightLabel(product: Product) {
|
||||
const amount = Number(product.amount ?? 0)
|
||||
if (!amount) return product.title
|
||||
return `${amount} گرم`
|
||||
}
|
||||
|
||||
function productBuyLabel(product: Product) {
|
||||
if (isProductPriceInactive(product)) return NO_QUOTE_MESSAGE
|
||||
const value = getProductBuyPrice(product)
|
||||
return value > 0 ? usePrice(value) : '—'
|
||||
}
|
||||
|
||||
function productSellLabel(product: Product) {
|
||||
if (isProductPriceInactive(product)) return NO_QUOTE_MESSAGE
|
||||
const value = getProductSellPrice(product)
|
||||
return value > 0 ? usePrice(value) : '—'
|
||||
}
|
||||
|
||||
function liveQuoteLabel(value: number) {
|
||||
return value > 0 ? usePrice(value) : '—'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.documentElement.classList.add(TV_HTML_CLASS)
|
||||
updateClock()
|
||||
hideCursorSoon()
|
||||
clockTimer = setInterval(updateClock, 1000)
|
||||
window.addEventListener('mousemove', hideCursorSoon)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
await requestWakeLock()
|
||||
|
||||
startLastPricePolling()
|
||||
void getLastPrices()
|
||||
void loadProducts()
|
||||
listTimer = setInterval(() => {
|
||||
void loadProducts()
|
||||
}, REFRESH_MS)
|
||||
|
||||
document.title = `${brandName.value} | تابلو قیمت`
|
||||
|
||||
try {
|
||||
await enterFullscreen()
|
||||
} catch {
|
||||
/* ignored until user clicks */
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove(TV_HTML_CLASS)
|
||||
document.title = String(getConfigApp('name_fa') || getConfigApp('name') || '')
|
||||
if (clockTimer) clearInterval(clockTimer)
|
||||
if (cursorTimer) clearTimeout(cursorTimer)
|
||||
if (listTimer) clearInterval(listTimer)
|
||||
window.removeEventListener('mousemove', hideCursorSoon)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
void releaseWakeLock()
|
||||
stopLastPricePolling()
|
||||
})
|
||||
|
||||
return {
|
||||
rootEl,
|
||||
brandName,
|
||||
brandLogo,
|
||||
nowLabel,
|
||||
dateLabel,
|
||||
parsianCoins,
|
||||
bankCoins,
|
||||
hasCoinBoard,
|
||||
coinRowCount,
|
||||
simorghBars,
|
||||
statusBanner,
|
||||
liveBuy,
|
||||
liveSell,
|
||||
isLive,
|
||||
isFullscreen,
|
||||
cursorHidden,
|
||||
loading,
|
||||
productWeightLabel,
|
||||
simorghWeightLabel,
|
||||
productBuyLabel,
|
||||
productSellLabel,
|
||||
liveQuoteLabel,
|
||||
handleBoardClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { colors } from '@/themeConfig'
|
||||
|
||||
export type TvThemeMode = 'light' | 'dark'
|
||||
|
||||
const STORAGE_KEY = 'ghatreh_tv_theme'
|
||||
|
||||
type Rgb = { r: number; g: number; b: number }
|
||||
|
||||
/** رنگ بنفش اپ کاربر — همان primary در UnoCSS و @App-primary-color */
|
||||
const appPrimary = '#a72b77'
|
||||
const appSecondary = String(colors.secondary)
|
||||
const appTertiary = String(colors.tertiary)
|
||||
const appWhite = String(colors.white)
|
||||
const appGray50 = String(colors.gray[50])
|
||||
const appGray100 = String(colors.gray[100])
|
||||
const appGray500 = String(colors.gray[500])
|
||||
const appGray700 = String(colors.gray[700])
|
||||
|
||||
function normalizeHex(hex: unknown): string | null {
|
||||
if (!hex || typeof hex !== 'string') return null
|
||||
let value = hex.trim()
|
||||
if (!value) return null
|
||||
if (value[0] !== '#') value = `#${value}`
|
||||
if (/^#[0-9a-fA-F]{3}$/.test(value)) {
|
||||
value = `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`
|
||||
}
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(value)) return null
|
||||
return value.toLowerCase()
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): Rgb | null {
|
||||
const normalized = normalizeHex(hex)
|
||||
if (!normalized) return null
|
||||
const num = parseInt(normalized.slice(1), 16)
|
||||
return {
|
||||
r: (num >> 16) & 255,
|
||||
g: (num >> 8) & 255,
|
||||
b: num & 255,
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHex(r: number, g: number, b: number) {
|
||||
const toHex = (value: number) =>
|
||||
Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, '0')
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function mix(hexA: string, hexB: string, weight: number) {
|
||||
const a = hexToRgb(hexA)
|
||||
const b = hexToRgb(hexB)
|
||||
if (!a || !b) return hexA
|
||||
return rgbToHex(
|
||||
a.r * (1 - weight) + b.r * weight,
|
||||
a.g * (1 - weight) + b.g * weight,
|
||||
a.b * (1 - weight) + b.b * weight,
|
||||
)
|
||||
}
|
||||
|
||||
function brightness(hex: string) {
|
||||
const rgb = hexToRgb(hex)
|
||||
if (!rgb) return 0
|
||||
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
|
||||
}
|
||||
|
||||
function hexToRgba(hex: string, alpha: number) {
|
||||
const rgb = hexToRgb(hex)
|
||||
if (!rgb) return `rgba(15, 23, 42, ${alpha})`
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`
|
||||
}
|
||||
|
||||
function toneForSurface(hex: string, surface: 'light' | 'dark') {
|
||||
const value = brightness(hex)
|
||||
if (surface === 'light') {
|
||||
return value > 165 ? mix(hex, '#1e2229', 0.42) : hex
|
||||
}
|
||||
return value < 95 ? mix(hex, appWhite, 0.42) : hex
|
||||
}
|
||||
|
||||
function readStoredTheme(): TvThemeMode | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored === 'light' || stored === 'dark') return stored
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function buildThemeVars(mode: TvThemeMode) {
|
||||
const isDark = mode === 'dark'
|
||||
|
||||
const pageBg = isDark ? mix('#160910', appPrimary, 0.22) : appWhite
|
||||
const pageBgSoft = isDark
|
||||
? mix(pageBg, appPrimary, 0.18)
|
||||
: mix(appGray50, appPrimary, 0.08)
|
||||
|
||||
const text = isDark ? appWhite : '#1e2229'
|
||||
const muted = isDark ? hexToRgba(appWhite, 0.72) : appGray500
|
||||
const card = isDark ? mix(pageBg, appWhite, 0.08) : appWhite
|
||||
const cardText = isDark ? appWhite : '#1e2229'
|
||||
const cardMuted = isDark ? hexToRgba(appWhite, 0.68) : appGray500
|
||||
const border = isDark ? hexToRgba(appWhite, 0.12) : hexToRgba(appPrimary, 0.14)
|
||||
const mediaBg = isDark ? hexToRgba(appWhite, 0.08) : mix(appGray100, appPrimary, 0.06)
|
||||
const shadow = isDark
|
||||
? `0 16px 40px ${hexToRgba(appPrimary, 0.28)}`
|
||||
: `0 14px 32px ${hexToRgba(appPrimary, 0.14)}`
|
||||
|
||||
const sell = toneForSurface(appSecondary, isDark ? 'dark' : 'light')
|
||||
const buy = toneForSurface(appPrimary, isDark ? 'dark' : 'light')
|
||||
const sellBg = isDark ? hexToRgba(appSecondary, 0.2) : mix(appWhite, appSecondary, 0.14)
|
||||
const buyBg = isDark ? hexToRgba(appPrimary, 0.22) : mix(appWhite, appPrimary, 0.12)
|
||||
|
||||
return {
|
||||
'--tv-bg': pageBg,
|
||||
'--tv-bg-soft': pageBgSoft,
|
||||
'--tv-text': text,
|
||||
'--tv-muted': muted,
|
||||
'--tv-card': card,
|
||||
'--tv-card-text': cardText,
|
||||
'--tv-card-muted': cardMuted,
|
||||
'--tv-border': border,
|
||||
'--tv-primary': appPrimary,
|
||||
'--tv-secondary': appSecondary,
|
||||
'--tv-tertiary': appTertiary,
|
||||
'--tv-sell': sell,
|
||||
'--tv-buy': buy,
|
||||
'--tv-sell-bg': sellBg,
|
||||
'--tv-buy-bg': buyBg,
|
||||
'--tv-media': mediaBg,
|
||||
'--tv-logo-bg': appWhite,
|
||||
'--tv-shadow': shadow,
|
||||
'--tv-orb-1': hexToRgba(appPrimary, isDark ? 0.28 : 0.14),
|
||||
'--tv-orb-2': hexToRgba(appTertiary, isDark ? 0.16 : 0.1),
|
||||
'--tv-quote-bg': isDark ? hexToRgba(appWhite, 0.07) : hexToRgba(appWhite, 0.86),
|
||||
'--tv-dot': isDark ? hexToRgba(appWhite, 0.28) : hexToRgba(appPrimary, 0.22),
|
||||
'--tv-badge': isDark ? mix(appPrimary, '#1e2229', 0.35) : mix(appPrimary, '#1e2229', 0.2),
|
||||
}
|
||||
}
|
||||
|
||||
export function useTvTheme() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function resolveTheme(): TvThemeMode {
|
||||
const query = String(route.query.theme || '').toLowerCase()
|
||||
if (query === 'light' || query === 'dark') return query
|
||||
return readStoredTheme() ?? 'dark'
|
||||
}
|
||||
|
||||
const theme = ref<TvThemeMode>(resolveTheme())
|
||||
|
||||
const themeVars = computed(() => buildThemeVars(theme.value))
|
||||
|
||||
watch(
|
||||
theme,
|
||||
(mode) => {
|
||||
document.documentElement.classList.toggle('app-layout--tv-dark', mode === 'dark')
|
||||
document.documentElement.classList.toggle('app-layout--tv-light', mode === 'light')
|
||||
document.documentElement.style.setProperty('--tv-page-bg', themeVars.value['--tv-bg'])
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.theme,
|
||||
(value) => {
|
||||
if (value === 'light' || value === 'dark') {
|
||||
theme.value = value
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove('app-layout--tv-dark', 'app-layout--tv-light')
|
||||
document.documentElement.style.removeProperty('--tv-page-bg')
|
||||
})
|
||||
|
||||
function persistTheme(next: TvThemeMode) {
|
||||
theme.value = next
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, next)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const nextQuery = { ...route.query, theme: next }
|
||||
void router.replace({ query: nextQuery })
|
||||
}
|
||||
|
||||
function toggleTheme(event?: Event) {
|
||||
event?.stopPropagation()
|
||||
persistTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
themeVars,
|
||||
toggleTheme,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { Product } from '@/models/products'
|
||||
import type { Wallet } from '@/models/wallet'
|
||||
|
||||
/** وقتی true باشد تابلو از داده تستی استفاده میکند؛ بعداً false شود تا از سرور بخواند. */
|
||||
export const TV_USE_STATIC_PRODUCTS = true
|
||||
|
||||
function mockWallet(): Wallet {
|
||||
return {
|
||||
owner: 0,
|
||||
treasure: 0,
|
||||
treasure_sum: 0,
|
||||
id: 0,
|
||||
gold: 0,
|
||||
gold_block: 0,
|
||||
cash: 0,
|
||||
gold_sum: 0,
|
||||
product: {} as Product,
|
||||
cash_block: 0,
|
||||
count_cash: 0,
|
||||
count_cash_wallet: 0,
|
||||
count_cash_wallets: 0,
|
||||
count_gold: 0,
|
||||
count_gold_wallet: 0,
|
||||
count_gold_wallets: 0,
|
||||
count_product: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mockCoin(input: {
|
||||
id: number
|
||||
title: string
|
||||
group_type: string
|
||||
amount: number
|
||||
price: number
|
||||
sellPrice: number
|
||||
}): Product {
|
||||
return {
|
||||
id: input.id,
|
||||
img: undefined,
|
||||
title: input.title,
|
||||
amount: input.amount,
|
||||
metal_id: 1,
|
||||
file_id: null,
|
||||
unit: 'گرم',
|
||||
active: true,
|
||||
show: true,
|
||||
open_sell: true,
|
||||
open_buy: true,
|
||||
group_type: input.group_type,
|
||||
percent_fee_buy: 0,
|
||||
percent_fee_sell: 0,
|
||||
cash_fee_buy: 0,
|
||||
cash_fee_sell: 0,
|
||||
reading_method: null,
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
deleted_at: null,
|
||||
price: input.price,
|
||||
sellPrice: input.sellPrice,
|
||||
description: '',
|
||||
wallet: mockWallet(),
|
||||
inventory: 1,
|
||||
trade_by_quantity: true,
|
||||
}
|
||||
}
|
||||
|
||||
const PARSIAN_WEIGHTS = [
|
||||
{ amount: 0.05, title: 'سکه پارسیان ۵۰ سوت', price: 48_500_000, sellPrice: 46_800_000 },
|
||||
{ amount: 0.1, title: 'سکه پارسیان ۱۰۰ سوت', price: 96_200_000, sellPrice: 93_400_000 },
|
||||
{ amount: 0.15, title: 'سکه پارسیان ۱۵۰ سوت', price: 143_800_000, sellPrice: 139_900_000 },
|
||||
{ amount: 0.2, title: 'سکه پارسیان ۲۰۰ سوت', price: 191_500_000, sellPrice: 186_200_000 },
|
||||
{ amount: 0.25, title: 'سکه پارسیان ۲۵۰ سوت', price: 239_000_000, sellPrice: 232_400_000 },
|
||||
{ amount: 0.3, title: 'سکه پارسیان ۳۰۰ سوت', price: 286_700_000, sellPrice: 278_800_000 },
|
||||
{ amount: 0.4, title: 'سکه پارسیان ۴۰۰ سوت', price: 382_000_000, sellPrice: 371_500_000 },
|
||||
{ amount: 0.5, title: 'سکه پارسیان ۵۰۰ سوت', price: 477_200_000, sellPrice: 464_000_000 },
|
||||
{ amount: 1, title: 'سکه پارسیان ۱ گرم', price: 952_000_000, sellPrice: 926_000_000 },
|
||||
] as const
|
||||
|
||||
const BANK_COINS = [
|
||||
{ amount: 8.13, title: 'تمام سکه بانکی', price: 8_150_000_000, sellPrice: 7_980_000_000 },
|
||||
{ amount: 4.06, title: 'نیم سکه بانکی', price: 4_120_000_000, sellPrice: 4_020_000_000 },
|
||||
{ amount: 2.03, title: 'ربع سکه بانکی', price: 2_180_000_000, sellPrice: 2_110_000_000 },
|
||||
] as const
|
||||
|
||||
const SIMORGH_BARS = [
|
||||
{ amount: 0.5, title: 'شمش سیمرغ ۰٫۵ گرم', price: 478_000_000, sellPrice: 465_000_000 },
|
||||
{ amount: 1, title: 'شمش سیمرغ ۱ گرم', price: 952_000_000, sellPrice: 926_000_000 },
|
||||
{ amount: 2.5, title: 'شمش سیمرغ ۲٫۵ گرم', price: 2_380_000_000, sellPrice: 2_315_000_000 },
|
||||
{ amount: 5, title: 'شمش سیمرغ ۵ گرم', price: 4_760_000_000, sellPrice: 4_630_000_000 },
|
||||
{ amount: 10, title: 'شمش سیمرغ ۱۰ گرم', price: 9_520_000_000, sellPrice: 9_260_000_000 },
|
||||
{ amount: 20, title: 'شمش سیمرغ ۲۰ گرم', price: 19_040_000_000, sellPrice: 18_520_000_000 },
|
||||
{ amount: 50, title: 'شمش سیمرغ ۵۰ گرم', price: 47_600_000_000, sellPrice: 46_300_000_000 },
|
||||
{ amount: 100, title: 'شمش سیمرغ ۱۰۰ گرم', price: 95_200_000_000, sellPrice: 92_600_000_000 },
|
||||
] as const
|
||||
|
||||
export const STATIC_TV_PRODUCTS: Product[] = [
|
||||
...PARSIAN_WEIGHTS.map((item, index) =>
|
||||
mockCoin({
|
||||
id: 100 + index,
|
||||
title: item.title,
|
||||
group_type: 'سکه پارسیان',
|
||||
amount: item.amount,
|
||||
price: item.price,
|
||||
sellPrice: item.sellPrice,
|
||||
}),
|
||||
),
|
||||
...BANK_COINS.map((item, index) =>
|
||||
mockCoin({
|
||||
id: 200 + index,
|
||||
title: item.title,
|
||||
group_type: 'سکه',
|
||||
amount: item.amount,
|
||||
price: item.price,
|
||||
sellPrice: item.sellPrice,
|
||||
}),
|
||||
),
|
||||
...SIMORGH_BARS.map((item, index) =>
|
||||
mockCoin({
|
||||
id: 300 + index,
|
||||
title: item.title,
|
||||
group_type: 'شمش سیمرغ',
|
||||
amount: item.amount,
|
||||
price: item.price,
|
||||
sellPrice: item.sellPrice,
|
||||
}),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,923 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="tv-monitor"
|
||||
:class="[`tv-monitor--${theme}`, { 'tv-monitor--idle': cursorHidden }]"
|
||||
:style="themeVars"
|
||||
dir="rtl"
|
||||
@click="handleBoardClick"
|
||||
>
|
||||
<header class="tv-monitor__header">
|
||||
<div class="tv-monitor__brand">
|
||||
<img :src="brandLogo" :alt="brandName" class="tv-monitor__logo" />
|
||||
<div>
|
||||
<h1 class="tv-monitor__title">{{ brandName }}</h1>
|
||||
<p class="tv-monitor__subtitle">تابلو قیمت محصولات</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tv-monitor__quote">
|
||||
<span class="tv-monitor__quote-label">مظنه لحظهای</span>
|
||||
<div class="tv-monitor__quote-prices">
|
||||
<div class="tv-monitor__quote-item tv-monitor__quote-item--sell">
|
||||
<span>فروش</span>
|
||||
<strong>{{ liveQuoteLabel(liveBuy) }}</strong>
|
||||
</div>
|
||||
<div class="tv-monitor__quote-item tv-monitor__quote-item--buy">
|
||||
<span>خرید</span>
|
||||
<strong>{{ liveQuoteLabel(liveSell) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tv-monitor__clock">
|
||||
<button
|
||||
type="button"
|
||||
class="tv-monitor__theme-toggle"
|
||||
:title="theme === 'dark' ? 'تم روشن' : 'تم تیره'"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<BasicIcon :icon="theme === 'dark' ? 'vuesax-linear:sun-1' : 'vuesax-linear:moon'" size="18" />
|
||||
<span>{{ theme === 'dark' ? 'تم روشن' : 'تم تیره' }}</span>
|
||||
</button>
|
||||
<div class="tv-monitor__time">{{ nowLabel }}</div>
|
||||
<div class="tv-monitor__date">{{ dateLabel }}</div>
|
||||
<div class="tv-monitor__live" :class="{ 'is-on': isLive }">
|
||||
<span class="tv-monitor__live-dot" />
|
||||
{{ isLive ? 'زنده' : 'بهروزرسانی خودکار' }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p v-if="statusBanner" class="tv-monitor__banner">{{ statusBanner }}</p>
|
||||
|
||||
<main class="tv-monitor__board">
|
||||
<a-spin :spinning="loading" class="tv-monitor__spin">
|
||||
<div
|
||||
v-if="hasCoinBoard"
|
||||
class="tv-monitor__columns"
|
||||
:style="{ '--tv-row-count': coinRowCount }"
|
||||
>
|
||||
<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 parsianCoins"
|
||||
: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="!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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading" class="tv-monitor__empty">
|
||||
محصولی برای نمایش روی تابلو انتخاب نشده است
|
||||
</div>
|
||||
</a-spin>
|
||||
</main>
|
||||
|
||||
<p v-if="!isFullscreen" class="tv-monitor__hint">
|
||||
برای نمایش تمامصفحه روی صفحه کلیک کنید
|
||||
</p>
|
||||
|
||||
<footer class="tv-monitor__footer">
|
||||
<div
|
||||
v-if="simorghBars.length"
|
||||
class="tv-ticker"
|
||||
:style="{ '--tv-ticker-duration': `${Math.max(simorghBars.length * 7, 36)}s` }"
|
||||
>
|
||||
<div class="tv-ticker__label">
|
||||
<strong>شمش سیمرغ</strong>
|
||||
<span>خرید و فروش</span>
|
||||
</div>
|
||||
<div class="tv-ticker__viewport">
|
||||
<div class="tv-ticker__track">
|
||||
<article
|
||||
v-for="item in simorghBars"
|
||||
:key="item.id"
|
||||
class="tv-ticker__item"
|
||||
>
|
||||
<span class="tv-ticker__weight">{{ simorghWeightLabel(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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-monitor__credit" @click.stop>
|
||||
<AppCreditLink variant="tv" />
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { useTvTheme } from '@/views/tv/composables/useTvTheme'
|
||||
|
||||
const { theme, themeVars, toggleTheme } = useTvTheme()
|
||||
|
||||
const {
|
||||
rootEl,
|
||||
brandName,
|
||||
brandLogo,
|
||||
nowLabel,
|
||||
dateLabel,
|
||||
parsianCoins,
|
||||
bankCoins,
|
||||
hasCoinBoard,
|
||||
coinRowCount,
|
||||
simorghBars,
|
||||
statusBanner,
|
||||
liveBuy,
|
||||
liveSell,
|
||||
isLive,
|
||||
isFullscreen,
|
||||
cursorHidden,
|
||||
loading,
|
||||
productWeightLabel,
|
||||
simorghWeightLabel,
|
||||
productBuyLabel,
|
||||
productSellLabel,
|
||||
liveQuoteLabel,
|
||||
handleBoardClick,
|
||||
} = useTvMonitorPage()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tv-monitor {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: clamp(12px, 1.6vh, 24px) clamp(16px, 2vw, 32px);
|
||||
background:
|
||||
radial-gradient(1200px 500px at 100% -10%, var(--tv-orb-1), transparent 55%),
|
||||
radial-gradient(900px 420px at -10% 110%, var(--tv-orb-2), transparent 50%),
|
||||
linear-gradient(160deg, var(--tv-bg) 0%, var(--tv-bg-soft) 48%, var(--tv-bg) 100%);
|
||||
color: var(--tv-text);
|
||||
font-family: iran-sans, sans-serif;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
span,
|
||||
strong,
|
||||
div,
|
||||
article {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&--idle {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__header {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1.4fr 0.9fr;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: clamp(10px, 1.4vh, 20px);
|
||||
}
|
||||
|
||||
.tv-monitor__banner {
|
||||
margin: 0 0 clamp(10px, 1.2vh, 16px);
|
||||
padding: 10px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--tv-quote-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-muted);
|
||||
font-size: clamp(12px, 1.05vw, 15px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tv-monitor__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tv-monitor__logo {
|
||||
width: clamp(52px, 6.5vh, 76px);
|
||||
height: clamp(52px, 6.5vh, 76px);
|
||||
object-fit: contain;
|
||||
background: var(--tv-logo-bg);
|
||||
border-radius: 16px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tv-monitor__title {
|
||||
margin: 0;
|
||||
font-size: clamp(20px, 2.4vw, 36px);
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.tv-monitor__subtitle {
|
||||
margin: 2px 0 0;
|
||||
color: var(--tv-muted);
|
||||
font-size: clamp(12px, 1.1vw, 16px);
|
||||
}
|
||||
|
||||
.tv-monitor__quote {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tv-monitor__quote-label {
|
||||
font-size: 13px;
|
||||
color: var(--tv-muted);
|
||||
}
|
||||
|
||||
.tv-monitor__quote-prices {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tv-monitor__quote-item {
|
||||
min-width: 180px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
background: var(--tv-quote-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--tv-muted);
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: clamp(16px, 1.5vw, 22px);
|
||||
font-weight: 700;
|
||||
font-family: iran-sans, sans-serif;
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
&--sell strong {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
|
||||
&--buy strong {
|
||||
color: var(--tv-buy);
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__clock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tv-monitor__theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 999px;
|
||||
background: var(--tv-quote-bg);
|
||||
color: var(--tv-text);
|
||||
font-family: iran-sans, sans-serif;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(svg path) {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__time {
|
||||
font-size: clamp(28px, 3.4vw, 48px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
font-family: iran-sans, sans-serif;
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
line-height: 1;
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.tv-monitor__date {
|
||||
margin-top: 6px;
|
||||
color: var(--tv-muted);
|
||||
font-size: clamp(12px, 1.15vw, 16px);
|
||||
}
|
||||
|
||||
.tv-monitor__live {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
color: var(--tv-muted);
|
||||
font-size: 12px;
|
||||
|
||||
&.is-on {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--tv-muted);
|
||||
|
||||
.is-on & {
|
||||
background: var(--tv-sell);
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--tv-sell) 70%, transparent);
|
||||
animation: tv-pulse 1.6s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__board {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.tv-monitor__spin),
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: clamp(12px, 1.6vw, 22px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tv-col {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 22px;
|
||||
background: var(--tv-quote-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
}
|
||||
|
||||
.tv-col__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 4px 10px;
|
||||
border-bottom: 1px solid var(--tv-border);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(18px, 1.7vw, 28px);
|
||||
font-weight: 700;
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--tv-muted);
|
||||
font-size: clamp(12px, 1vw, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
.tv-col__labels {
|
||||
display: grid;
|
||||
grid-template-columns: 56px minmax(0, 0.9fr) minmax(0, 2fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tv-col__labels-gap {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.tv-col__price-heads {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tv-col__label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: clamp(13px, 1.1vw, 16px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tv-col__label--sell {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
|
||||
.tv-col__label--buy {
|
||||
color: var(--tv-buy);
|
||||
}
|
||||
|
||||
.tv-col__list {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
grid-auto-rows: minmax(
|
||||
52px,
|
||||
calc((100% - (var(--tv-row-count) - 1) * 8px) / var(--tv-row-count))
|
||||
);
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tv-col__empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--tv-muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tv-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(44px, 56px) minmax(0, 0.9fr) minmax(0, 2fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 6px 10px;
|
||||
border-radius: 16px;
|
||||
background: var(--tv-card);
|
||||
color: var(--tv-card-text);
|
||||
border: 1px solid var(--tv-border);
|
||||
box-shadow: var(--tv-shadow);
|
||||
container-type: size;
|
||||
|
||||
&--off {
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-row__badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 2;
|
||||
padding: 2px 7px;
|
||||
border-radius: 8px;
|
||||
background: var(--tv-badge);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tv-row__media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--tv-media);
|
||||
}
|
||||
|
||||
.tv-row__img {
|
||||
max-width: 86%;
|
||||
max-height: 86%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tv-row__meta {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: clamp(13px, 1.2vw, 20px);
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--tv-card-text);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--tv-card-muted);
|
||||
font-size: clamp(11px, 1vw, 15px);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-height: 64px) {
|
||||
.tv-row__meta p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-row__prices {
|
||||
display: grid;
|
||||
align-self: stretch;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tv-row__price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 6px 8px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
container-type: inline-size;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--tv-card-muted);
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
font-size: clamp(10px, 12cqi, 16px);
|
||||
font-weight: 700;
|
||||
font-family: iran-sans, sans-serif;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
&--sell {
|
||||
background: var(--tv-sell-bg);
|
||||
|
||||
strong {
|
||||
color: var(--tv-sell);
|
||||
}
|
||||
}
|
||||
|
||||
&--buy {
|
||||
background: var(--tv-buy-bg);
|
||||
|
||||
strong {
|
||||
color: var(--tv-buy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tv-monitor__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--tv-muted);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.tv-monitor__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
margin-top: 12px;
|
||||
margin-inline: calc(-1 * clamp(16px, 2vw, 32px));
|
||||
margin-bottom: calc(-1 * clamp(12px, 1.6vh, 24px));
|
||||
}
|
||||
|
||||
.tv-monitor__hint {
|
||||
position: absolute;
|
||||
top: clamp(12px, 1.6vh, 24px);
|
||||
left: 50%;
|
||||
z-index: 4;
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--tv-card) 88%, transparent);
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-muted);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tv-monitor__credit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px));
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||
}
|
||||
|
||||
.tv-ticker {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 52px;
|
||||
background: var(--tv-primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 -8px 24px color-mix(in srgb, var(--tv-primary) 28%, transparent);
|
||||
}
|
||||
|
||||
.tv-ticker__label {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 132px;
|
||||
padding: 8px 18px 8px 22px;
|
||||
background: color-mix(in srgb, #1e2229 42%, var(--tv-primary));
|
||||
|
||||
strong {
|
||||
font-size: clamp(14px, 1.2vw, 18px);
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -18px;
|
||||
width: 18px;
|
||||
background: inherit;
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 50%);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.tv-ticker__viewport {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
direction: ltr;
|
||||
container-type: inline-size;
|
||||
mask-image: linear-gradient(to left, transparent 0, #000 36px);
|
||||
}
|
||||
|
||||
.tv-ticker__track {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
direction: rtl;
|
||||
will-change: transform;
|
||||
animation: tv-ticker var(--tv-ticker-duration, 40s) linear infinite;
|
||||
}
|
||||
|
||||
.tv-ticker__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 28px;
|
||||
white-space: nowrap;
|
||||
border-left: 1px solid color-mix(in srgb, #fff 28%, transparent);
|
||||
}
|
||||
|
||||
.tv-ticker__weight {
|
||||
font-size: clamp(14px, 1.15vw, 18px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tv-ticker__price {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
|
||||
b {
|
||||
font-size: clamp(13px, 1.05vw, 16px);
|
||||
font-weight: 700;
|
||||
font-family: iran-sans, sans-serif;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
&--sell b {
|
||||
color: #ffe08a;
|
||||
}
|
||||
|
||||
&--buy b {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tv-ticker {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100cqw);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tv-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--tv-sell) 55%, transparent);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 8px transparent;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.tv-monitor__header {
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.2fr) auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tv-monitor__logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.tv-monitor__quote,
|
||||
.tv-monitor__quote-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tv-col__labels,
|
||||
.tv-row {
|
||||
grid-template-columns: minmax(40px, 48px) minmax(0, 0.85fr) minmax(0, 2.1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tv-monitor__header {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.tv-monitor__quote {
|
||||
grid-column: 1 / -1;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.tv-monitor__columns {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tv-col {
|
||||
min-height: 240px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
html.app-layout--tv,
|
||||
html.app-layout--tv body,
|
||||
html.app-layout--tv #app,
|
||||
html.app-layout--tv .desktopConfig,
|
||||
html.app-layout--tv .ant-config-provider,
|
||||
html.app-layout--tv .app-main,
|
||||
html.app-layout--tv .tv-monitor {
|
||||
font-family: iran-sans, sans-serif !important;
|
||||
}
|
||||
|
||||
html.app-layout--tv,
|
||||
html.app-layout--tv body,
|
||||
html.app-layout--tv #app,
|
||||
html.app-layout--tv .desktopConfig,
|
||||
html.app-layout--tv .ant-config-provider,
|
||||
html.app-layout--tv .app-main {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: 100dvh !important;
|
||||
min-height: 100dvh !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
html.app-layout--tv .tv-monitor {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
html.app-layout--tv body {
|
||||
display: block !important;
|
||||
background: var(--tv-page-bg, #0b1220) !important;
|
||||
}
|
||||
|
||||
html.app-layout--tv .app-splash-screen,
|
||||
html.app-layout--tv #support-button,
|
||||
html.app-layout--tv #goftino_w {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
Vendored
+11
-1
@@ -10,4 +10,14 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
public?: boolean
|
||||
hideChrome?: boolean
|
||||
hideMenu?: boolean
|
||||
hideBottomNav?: boolean
|
||||
transition?: string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user