diff --git a/src/views/login/components/Register.vue b/src/views/login/components/Register.vue
index 64d7727..6e7cbdb 100644
--- a/src/views/login/components/Register.vue
+++ b/src/views/login/components/Register.vue
@@ -77,6 +77,7 @@
/>
ثبت نام
+
+ درباره نرمافزار
+
{
+onMounted(async () => {
+ if (!settingsStore.loaded) {
+ await settingsStore.fetchSettings()
+ }
if (router.currentRoute.value.query.id){
userFormState.referral_id = router.currentRoute.value.query.id
disableForm.value=true
@@ -267,7 +277,11 @@ function registerAccount() {
// if (!stepTwoVerify.value) {
URLUserType() === 'single' ? userFormState.type = 2 : userFormState.type = 1
// console.log(userFormState)
- if (!userFormState.referral_id || userFormState.referral_id === '') delete userFormState.referral_id
+ if (!settingsStore.isReferralFieldVisible && !disableForm.value) {
+ delete userFormState.referral_id
+ } else if (!userFormState.referral_id || userFormState.referral_id === '') {
+ delete userFormState.referral_id
+ }
normalizeAuthFormFields(userFormState)
const { id } = await registerCustomer(userFormState)
registeredInfo.value = true
@@ -297,6 +311,10 @@ async function goToAboutUs(){
await router.push('/aboutUs')
}
+async function goToAboutSoftware() {
+ await router.push({ name: 'aboutSoftware' })
+}
+
const goToEducate = () => {
router.push({ name: "educate" })
}
diff --git a/src/views/login/composables/useLoginForm.ts b/src/views/login/composables/useLoginForm.ts
index 89f5b96..928da78 100644
--- a/src/views/login/composables/useLoginForm.ts
+++ b/src/views/login/composables/useLoginForm.ts
@@ -16,7 +16,7 @@ import { useWalletStore } from '@/store/walletStore'
import { useSettingsStore } from '@/store/settingsStore'
import { setTruth } from '@/app/authBoot'
import { completeLoginNavigation } from '@/layouts/composables/useMobileScrollReset'
-import { normalizeAuthFormFields } from '@/utils/persianDigits'
+import { normalizeAuthFormFields, persianToEnglishDigits } from '@/utils/persianDigits'
import { beginSmsOtpCapture, clearPendingSmsOtp } from '@/utils/webOtp'
import { usePwaInstall } from '@/composables/usePwaInstall'
import {
@@ -79,12 +79,12 @@ export function useLoginForm() {
}
function handleOnChange(value: string) {
- userFormState.token = value
+ userFormState.token = persianToEnglishDigits(String(value ?? ''))
normalizeAuthFormFields(userFormState)
}
async function handleOnComplete(value: string) {
- userFormState.token = value
+ userFormState.token = persianToEnglishDigits(String(value ?? ''))
normalizeAuthFormFields(userFormState)
phoneVerification()
}
diff --git a/src/views/login/composables/useRegisterForm.ts b/src/views/login/composables/useRegisterForm.ts
index ce3410e..3965450 100644
--- a/src/views/login/composables/useRegisterForm.ts
+++ b/src/views/login/composables/useRegisterForm.ts
@@ -6,10 +6,12 @@ import { loading, URLUserType } from '@/utils'
import { registerCustomer } from '@/api/login'
import router from '@/router'
import { normalizeAuthFormFields } from '@/utils/persianDigits'
+import { useSettingsStore } from '@/store/settingsStore'
export function useRegisterForm() {
const registeredInfo = ref(false)
const disableForm = ref(false)
+ const settingsStore = useSettingsStore()
const userFormState = reactive
({
access_token: '',
@@ -17,7 +19,10 @@ export function useRegisterForm() {
const formRef = ref()
- onMounted(() => {
+ onMounted(async () => {
+ if (!settingsStore.loaded) {
+ await settingsStore.fetchSettings()
+ }
if (router.currentRoute.value.query.id) {
userFormState.referral_id = router.currentRoute.value.query.id as string
disableForm.value = true
@@ -29,7 +34,10 @@ export function useRegisterForm() {
try {
loading.value = true
URLUserType() === 'single' ? (userFormState.type = 2) : (userFormState.type = 1)
- if (!userFormState.referral_id || userFormState.referral_id === '') {
+ // فیلد مخفی + بدون لینک دعوت → کد را نفرست
+ if (!settingsStore.isReferralFieldVisible && !disableForm.value) {
+ delete userFormState.referral_id
+ } else if (!userFormState.referral_id || userFormState.referral_id === '') {
delete userFormState.referral_id
}
normalizeAuthFormFields(userFormState)
diff --git a/src/views/login/desktop/components/DesktopLoginForm.vue b/src/views/login/desktop/components/DesktopLoginForm.vue
index 14065e7..9f8f718 100644
--- a/src/views/login/desktop/components/DesktopLoginForm.vue
+++ b/src/views/login/desktop/components/DesktopLoginForm.vue
@@ -44,6 +44,10 @@ function goToEducate() {
function goToAboutUs() {
router.push('/aboutUs')
}
+
+function goToAboutSoftware() {
+ router.push({ name: 'aboutSoftware' })
+}
@@ -162,11 +166,14 @@ function goToAboutUs() {
>
ورود با اثر انگشت
+
+ درباره نرمافزار
+
diff --git a/src/views/tv/composables/useTvMonitorPage.ts b/src/views/tv/composables/useTvMonitorPage.ts
new file mode 100644
index 0000000..fb9166b
--- /dev/null
+++ b/src/views/tv/composables/useTvMonitorPage.ts
@@ -0,0 +1,315 @@
+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/boursemes_logofull.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' || value === 'true'
+}
+
+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
(null)
+ const { isFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(rootEl)
+
+ const products = ref([])
+ const loading = ref(true)
+ const nowLabel = ref('')
+ const dateLabel = ref('')
+ const cursorHidden = ref(false)
+ const wakeLockSentinel = ref<{ release: () => Promise } | null>(null)
+
+ let clockTimer: ReturnType | undefined
+ let cursorTimer: ReturnType | undefined
+ let listTimer: ReturnType | undefined
+
+ const brandName = computed(
+ () => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'),
+ )
+ const brandLogo = computed(() => String(getConfigApp('avatar') || logoFallback))
+
+ // API فقط show_on_tv میدهد؛ فیلتر کلاینت برای دادهٔ ناقص است
+ const catalogProducts = computed(() => {
+ const list = products.value
+ if (!list.length) return list
+ const marked = list.filter(isShowOnTv)
+ return marked.length ? marked : list
+ })
+
+ 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 tickerProducts = computed(() =>
+ catalogProducts.value
+ .filter((p) => !isParsianCoin(p) && !isBankCoin(p))
+ .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 hasBoardContent = computed(
+ () => hasCoinBoard.value || tickerProducts.value.length > 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 }> }
+ }
+ 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 toggleFullscreen(event?: Event) {
+ event?.stopPropagation()
+ hideCursorSoon()
+ try {
+ if (isFullscreen.value) {
+ await exitFullscreen()
+ } else {
+ await enterFullscreen()
+ }
+ } catch {
+ /* browser may block without 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 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
+ }
+
+ 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} | تابلو قیمت`
+ })
+
+ 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,
+ hasBoardContent,
+ coinRowCount,
+ tickerProducts,
+ tickerTitle,
+ statusBanner,
+ liveBuy,
+ liveSell,
+ isLive,
+ isFullscreen,
+ cursorHidden,
+ loading,
+ productWeightLabel,
+ tickerItemLabel,
+ productBuyLabel,
+ productSellLabel,
+ liveQuoteLabel,
+ toggleFullscreen,
+ }
+}
diff --git a/src/views/tv/composables/useTvTheme.ts b/src/views/tv/composables/useTvTheme.ts
new file mode 100644
index 0000000..8b1b376
--- /dev/null
+++ b/src/views/tv/composables/useTvTheme.ts
@@ -0,0 +1,211 @@
+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 = 'boursecu_tv_theme'
+
+type Rgb = { r: number; g: number; b: number }
+
+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])
+const appSecondary = String(colors.secondary)
+const appTertiary = String(colors.tertiary)
+
+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 readAppPrimary(): string {
+ try {
+ const css = getComputedStyle(document.documentElement)
+ .getPropertyValue('--App-primary-color')
+ .trim()
+ return normalizeHex(css) || normalizeHex(String(colors.secondary)) || '#135953'
+ } catch {
+ return normalizeHex(String(colors.secondary)) || '#135953'
+ }
+}
+
+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 appPrimary = readAppPrimary()
+
+ 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(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,
+ }
+}
diff --git a/src/views/tv/index.vue b/src/views/tv/index.vue
new file mode 100644
index 0000000..1c8126f
--- /dev/null
+++ b/src/views/tv/index.vue
@@ -0,0 +1,926 @@
+
+
+
+
+
{{ statusBanner }}
+
+
+
+
+
+
+ سکه پارسیان
+ تمام اوزان
+
+
+
+
+
+
+
+
+ {{ productBuyLabel(item) }}
+
+
+ {{ productSellLabel(item) }}
+
+
+
+
سکهای برای نمایش نیست
+
+
+
+
+
+ سکه بانکی
+ تمام · نیم · ربع
+
+
+
+
+
+
+
+
+ {{ productBuyLabel(item) }}
+
+
+ {{ productSellLabel(item) }}
+
+
+
+
سکهای برای نمایش نیست
+
+
+
+
+
+ محصولی برای نمایش روی تابلو انتخاب نشده است
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 5e52a37..9c6c5a5 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,9 +1,18 @@
///
+interface ImportMetaEnv {
+ readonly VITE_SUPERADMIN_URL?: string
+ readonly VITE_TENANT_DOMAIN?: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
+
declare global {
interface Window {
- __APP_CONFIG__?: Record;
+ __APP_CONFIG__?: Record
}
}
-export {};
+export {}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 11fd429..6f4fcff 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -7,7 +7,6 @@
"vite-plugin-pwa/client"
],
"composite": true,
- "baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
diff --git a/vite.config.ts b/vite.config.ts
index 9e84b7c..73de2ed 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -92,6 +92,16 @@ export default defineConfig({
},
server: {
port: 3100,
+ host: true,
+ // لوکال: بدون CORS به سوپرادمین واقعی وصل شو
+ proxy: {
+ '/__sa': {
+ target: 'https://adm.montzar.ir',
+ changeOrigin: true,
+ secure: true,
+ rewrite: (path) => path.replace(/^\/__sa/, ''),
+ },
+ },
},
build: {
rollupOptions: {