From 809bd69068aca6e241f1b0a10560a6f52e158011 Mon Sep 17 00:00:00 2001 From: adonis Date: Sun, 28 Jun 2026 21:48:48 +0330 Subject: [PATCH] fixed price --- src/App.vue | 3 +- src/assets/css/ant.less | 17 ++++ src/assets/css/main.less | 30 ++++++- .../BaseDatePicker/src/BaseDatePicker.vue | 85 ++++++++++++++++++- src/models/home.ts | 3 + src/utils/storeHours.ts | 47 +++++++++- src/utils/zIndex.ts | 10 ++- .../base/home/components/CurrentPrice.vue | 85 +++++++++++++------ .../base/home/composables/useHomePage.ts | 4 +- .../desktop/components/DesktopLivePrice.vue | 64 +++++++++----- .../products/desktop/DesktopProductsView.vue | 37 ++++++++ .../products/mobile/MobileProductsView.vue | 49 +++++++++++ 12 files changed, 377 insertions(+), 57 deletions(-) diff --git a/src/App.vue b/src/App.vue index 5a595b8..ebdde98 100644 --- a/src/App.vue +++ b/src/App.vue @@ -268,7 +268,8 @@ watch(showSplash, (visible) => { --app-z-wallet-add-bar: 4510; --app-z-action-sheet: 5200; --app-z-dialog: 5300; - --app-z-modal: 1010000; + /* زیر لایه دراپ‌داون اینت (99999) تا تقویم/سلکت داخل مدال روی آن باز شود */ + --app-z-modal: 5500; } .app-main { diff --git a/src/assets/css/ant.less b/src/assets/css/ant.less index ecc5e85..93e9ba5 100644 --- a/src/assets/css/ant.less +++ b/src/assets/css/ant.less @@ -65,6 +65,23 @@ } /* =================== Input =================== */ +/* + * iOS Safari فقط وقتی روی اینپوتی فوکوس می‌شود که فونتش کمتر از ۱۶ پیکسل باشد زوم می‌کند. + * روی دستگاه‌های لمسی فونت اینپوت‌ها را حداقل ۱۶ پیکسل می‌کنیم تا زوم ناخواسته رخ ندهد. + * (اینپوت‌های OTP که کلاس مخصوص خودشان را دارند دست‌نخورده می‌مانند.) + */ +@media (hover: none) and (pointer: coarse) { + .ant-input, + .ant-input-number-input, + .ant-input-affix-wrapper > input.ant-input, + .ant-input-affix-wrapper > textarea.ant-input, + .ant-select-selection-search-input, + .ant-picker-input > input, + .van-field__control, + textarea.ant-input { + font-size: 16px !important; + } +} //.ant-input, //.ant-input:hover, //.ant-input-focused, diff --git a/src/assets/css/main.less b/src/assets/css/main.less index 4704d57..5031c4f 100644 --- a/src/assets/css/main.less +++ b/src/assets/css/main.less @@ -152,10 +152,36 @@ html.app-layout--desktop { transition: background-color 0.3s ease; } -/* ماسک مدال روی کل صفحه — هدر، ناوبری و فوتر اعتبار */ +/* ماسک مدال — زیر لایه دراپ‌داون اینت (۹۹۹۹۹) تا تقویم/سلکت داخل مدال روی آن باز شود */ .ant-modal-mask, .ant-modal-wrap { - z-index: var(--app-z-modal, 1010000) !important; + z-index: var(--app-z-modal, 5500) !important; +} + +/* + * وقتی مدال/اکشن‌شیت/دیالوگ باز است، کروم ثابت اپ (ناوبار، هدر، نوار اعتبار، + * دکمه پشتیبانی) نباید روی پوشش تیره دیده شود. + * Vant موقع باز شدن popup کلاس `van-overflow-hidden` را به body اضافه می‌کند؛ + * برای مدال‌های اینت هم با :has وجود wrap قابل‌مشاهده را تشخیص می‌دهیم. + */ +body.van-overflow-hidden { + .bottom-appbar, + .app-credit-footer, + #support-button, + .mpage-header, + .mpage-subheader { + display: none !important; + } +} + +body:has(.ant-modal-root .ant-modal-wrap:not([style*='display: none'])) { + .bottom-appbar, + .app-credit-footer, + #support-button, + .mpage-header, + .mpage-subheader { + display: none !important; + } } .slide-left-enter-active, diff --git a/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue b/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue index ff8db80..6d0cd67 100644 --- a/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue +++ b/src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue @@ -1,6 +1,14 @@ @@ -9,7 +17,7 @@ import DatePicker from './datePick' import locale from 'ant-design-vue/es/date-picker/locale/fa_IR' import dayjs, { Dayjs } from 'dayjs' import { omit } from 'lodash' -import { computed, useAttrs } from 'vue' +import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from 'vue' interface Props { value?: string | Dayjs @@ -34,12 +42,71 @@ const emit = defineEmits<{ (e: 'onchange', val: string | null): void, }>() +// روی iOS باز کردن تقویم از mousedown/mouseup داخلی پیکر قابل اتکا نیست. +// وضعیت باز/بسته کنترل‌شده است؛ بعد از بستن، iOS اغلب یک click/mouseup +// دیررس ارسال می‌کند که دوباره open=true می‌زند — با suppressOpen موقتاً +// نادیده گرفته می‌شود. +const open = ref(false) +const rootRef = ref(null) +const suppressOpen = ref(false) +let suppressOpenTimer: ReturnType | undefined + +function isInsidePicker(target: EventTarget | null): boolean { + if (!(target instanceof Element)) return false + if (rootRef.value?.contains(target)) return true + return !!target.closest('.ant-picker-dropdown') +} + +function closePicker() { + if (!open.value) return + open.value = false + armSuppressOpen() +} + +function handleOutsidePointer(event: Event) { + if (!open.value) return + const target = + event instanceof TouchEvent + ? event.touches[0]?.target ?? event.target + : event.target + if (isInsidePicker(target)) return + closePicker() +} + +function armSuppressOpen(ms = 400) { + suppressOpen.value = true + if (suppressOpenTimer) clearTimeout(suppressOpenTimer) + suppressOpenTimer = setTimeout(() => { + suppressOpen.value = false + }, ms) +} + +const onOpenChange = (val: boolean) => { + if (val && suppressOpen.value) return + open.value = val + if (!val) armSuppressOpen() +} + +const onWrapperClick = (e: MouseEvent) => { + if (suppressOpen.value) return + const target = e.target as HTMLElement + if (target.closest('.ant-picker-clear')) return + if (target.closest('.ant-picker-dropdown')) return + if (open.value) { + closePicker() + return + } + open.value = true +} + const _date = computed({ set: (val) => { let temp: string | null = null if (val) { temp = val.calendar('gregory').locale('en').format(props.format) } + open.value = false + armSuppressOpen() emit('update:value', temp) emit('onchange', temp) }, @@ -49,6 +116,18 @@ const _date = computed({ } } }) + +onMounted(() => { + // mousedown داخلی پیکر روی iOS/touch قابل اتکا نیست — pointerdown/touchstart بیرون را می‌گیریم + document.addEventListener('pointerdown', handleOutsidePointer, true) + document.addEventListener('touchstart', handleOutsidePointer, true) +}) + +onBeforeUnmount(() => { + document.removeEventListener('pointerdown', handleOutsidePointer, true) + document.removeEventListener('touchstart', handleOutsidePointer, true) + if (suppressOpenTimer) clearTimeout(suppressOpenTimer) +}) diff --git a/src/models/home.ts b/src/models/home.ts index 82fd025..f68f6ac 100644 --- a/src/models/home.ts +++ b/src/models/home.ts @@ -17,6 +17,8 @@ export interface PriceUp { sell: number } +export type PriceStatus = 'open' | 'closed' | 'no_quote' + export interface Price { sell: number buy: number @@ -25,6 +27,7 @@ export interface Price { goldir?: number up?: PriceUp closed?: boolean + status?: PriceStatus time?: string main_product?: number montzar?: number diff --git a/src/utils/storeHours.ts b/src/utils/storeHours.ts index 5b74c8f..5232cc3 100644 --- a/src/utils/storeHours.ts +++ b/src/utils/storeHours.ts @@ -1,6 +1,6 @@ import { ref } from 'vue' import { useSettingsStore } from '@/store/settingsStore' -import { lastPrice } from '@/utils/price' +import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price' export interface StoreTradingHours { start: string @@ -162,3 +162,48 @@ export function getStoreClosedMessage(): string { return `فروشگاه در حال حاضر بسته است. ساعت کاری از ${open} تا ${close} می‌باشد.` } + +export type PriceDisplayState = 'open' | 'closed' | 'no_quote' + +export const NO_QUOTE_MESSAGE = 'مظنه در دسترس نیست' + +/** + * وضعیت نمایش قیمت در صفحه اصلی: + * - closed → فروشگاه بسته است (خارج از ساعت کاری close_price یا بازه محصول) + * - no_quote → مظنه در دسترس نیست (قیمت زنده نداریم و show_last_price خاموش است) + * - open → نمایش عادی قیمت + * + * اولویت با status ارسالی از سرور است؛ اگر نبود به منطق سمت کلاینت برمی‌گردیم. + */ +export function getPriceDisplayState(): PriceDisplayState { + void productTradingHoursVersion.value + const p = lastPrice.value + const status = p?.status + + if (status === 'closed') { + return 'closed' + } + if (status === 'no_quote') { + return getTradeBuyPrice(p) > 0 ? 'open' : 'no_quote' + } + if (status === 'open') { + return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0 ? 'open' : 'no_quote' + } + + // سرور status نفرستاده (سازگاری با نسخه قدیمی) → استنتاج سمت کلاینت + if (isStoreClosed()) { + return 'closed' + } + + return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0 ? 'open' : 'no_quote' +} + +export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string { + if (state === 'closed') { + return getStoreClosedMessage() + } + if (state === 'no_quote') { + return NO_QUOTE_MESSAGE + } + return '' +} diff --git a/src/utils/zIndex.ts b/src/utils/zIndex.ts index d83fda9..1673ad0 100644 --- a/src/utils/zIndex.ts +++ b/src/utils/zIndex.ts @@ -1,5 +1,11 @@ -/** بالای ناوبری (5000)، هدر (1999) و فوتر اعتبار (1000000) */ -export const APP_Z_MODAL = 1_010_000 +/** + * z-index مدال/اکشن‌شیت/دیالوگ. + * بالای ناوبری (5000)، هدر (1999) و دکمه پشتیبانی (5100) می‌نشیند تا این کروم‌ها + * پشت پوشش تیره بمانند، اما زیر لایه دراپ‌داون اینت (99999) است تا تقویم/سلکت + * داخل مدال روی آن باز شود. کروم ثابتی که بالاتر از این مقدار است (مثل نوار + * اعتبار) هنگام باز بودن مدال با CSS مخفی می‌شود. + */ +export const APP_Z_MODAL = 5_500 export function getModalContainer(): HTMLElement { return document.body diff --git a/src/views/base/home/components/CurrentPrice.vue b/src/views/base/home/components/CurrentPrice.vue index a05947d..c9209e2 100644 --- a/src/views/base/home/components/CurrentPrice.vue +++ b/src/views/base/home/components/CurrentPrice.vue @@ -3,13 +3,17 @@ class="w-full relative mt-2 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in transition-colors duration-300" :class="containerClass" > -