fixed price
This commit is contained in:
+2
-1
@@ -268,7 +268,8 @@ watch(showSplash, (visible) => {
|
|||||||
--app-z-wallet-add-bar: 4510;
|
--app-z-wallet-add-bar: 4510;
|
||||||
--app-z-action-sheet: 5200;
|
--app-z-action-sheet: 5200;
|
||||||
--app-z-dialog: 5300;
|
--app-z-dialog: 5300;
|
||||||
--app-z-modal: 1010000;
|
/* زیر لایه دراپداون اینت (99999) تا تقویم/سلکت داخل مدال روی آن باز شود */
|
||||||
|
--app-z-modal: 5500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
|
|||||||
@@ -65,6 +65,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* =================== Input =================== */
|
/* =================== 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,
|
||||||
//.ant-input:hover,
|
//.ant-input:hover,
|
||||||
//.ant-input-focused,
|
//.ant-input-focused,
|
||||||
|
|||||||
@@ -152,10 +152,36 @@ html.app-layout--desktop {
|
|||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ماسک مدال روی کل صفحه — هدر، ناوبری و فوتر اعتبار */
|
/* ماسک مدال — زیر لایه دراپداون اینت (۹۹۹۹۹) تا تقویم/سلکت داخل مدال روی آن باز شود */
|
||||||
.ant-modal-mask,
|
.ant-modal-mask,
|
||||||
.ant-modal-wrap {
|
.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,
|
.slide-left-enter-active,
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div ref="rootRef" @click="onWrapperClick">
|
||||||
<DatePicker :locale="locale" v-model:value="_date" :size="props.size" v-bind="_attrs" inputReadOnly/>
|
<DatePicker
|
||||||
|
:locale="locale"
|
||||||
|
v-model:value="_date"
|
||||||
|
:size="props.size"
|
||||||
|
v-bind="_attrs"
|
||||||
|
:open="open"
|
||||||
|
inputReadOnly
|
||||||
|
@openChange="onOpenChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -9,7 +17,7 @@ import DatePicker from './datePick'
|
|||||||
import locale from 'ant-design-vue/es/date-picker/locale/fa_IR'
|
import locale from 'ant-design-vue/es/date-picker/locale/fa_IR'
|
||||||
import dayjs, { Dayjs } from 'dayjs'
|
import dayjs, { Dayjs } from 'dayjs'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { computed, useAttrs } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from 'vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: string | Dayjs
|
value?: string | Dayjs
|
||||||
@@ -34,12 +42,71 @@ const emit = defineEmits<{
|
|||||||
(e: 'onchange', val: string | null): void,
|
(e: 'onchange', val: string | null): void,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// روی iOS باز کردن تقویم از mousedown/mouseup داخلی پیکر قابل اتکا نیست.
|
||||||
|
// وضعیت باز/بسته کنترلشده است؛ بعد از بستن، iOS اغلب یک click/mouseup
|
||||||
|
// دیررس ارسال میکند که دوباره open=true میزند — با suppressOpen موقتاً
|
||||||
|
// نادیده گرفته میشود.
|
||||||
|
const open = ref(false)
|
||||||
|
const rootRef = ref<HTMLElement | null>(null)
|
||||||
|
const suppressOpen = ref(false)
|
||||||
|
let suppressOpenTimer: ReturnType<typeof setTimeout> | 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({
|
const _date = computed({
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
let temp: string | null = null
|
let temp: string | null = null
|
||||||
if (val) {
|
if (val) {
|
||||||
temp = val.calendar('gregory').locale('en').format(props.format)
|
temp = val.calendar('gregory').locale('en').format(props.format)
|
||||||
}
|
}
|
||||||
|
open.value = false
|
||||||
|
armSuppressOpen()
|
||||||
emit('update:value', temp)
|
emit('update:value', temp)
|
||||||
emit('onchange', 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)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less"></style>
|
<style lang="less"></style>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface PriceUp {
|
|||||||
sell: number
|
sell: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PriceStatus = 'open' | 'closed' | 'no_quote'
|
||||||
|
|
||||||
export interface Price {
|
export interface Price {
|
||||||
sell: number
|
sell: number
|
||||||
buy: number
|
buy: number
|
||||||
@@ -25,6 +27,7 @@ export interface Price {
|
|||||||
goldir?: number
|
goldir?: number
|
||||||
up?: PriceUp
|
up?: PriceUp
|
||||||
closed?: boolean
|
closed?: boolean
|
||||||
|
status?: PriceStatus
|
||||||
time?: string
|
time?: string
|
||||||
main_product?: number
|
main_product?: number
|
||||||
montzar?: number
|
montzar?: number
|
||||||
|
|||||||
+46
-1
@@ -1,6 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useSettingsStore } from '@/store/settingsStore'
|
import { useSettingsStore } from '@/store/settingsStore'
|
||||||
import { lastPrice } from '@/utils/price'
|
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||||
|
|
||||||
export interface StoreTradingHours {
|
export interface StoreTradingHours {
|
||||||
start: string
|
start: string
|
||||||
@@ -162,3 +162,48 @@ export function getStoreClosedMessage(): string {
|
|||||||
|
|
||||||
return `فروشگاه در حال حاضر بسته است. ساعت کاری از ${open} تا ${close} میباشد.`
|
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 ''
|
||||||
|
}
|
||||||
|
|||||||
+8
-2
@@ -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 {
|
export function getModalContainer(): HTMLElement {
|
||||||
return document.body
|
return document.body
|
||||||
|
|||||||
@@ -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="w-full relative mt-2 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in transition-colors duration-300"
|
||||||
:class="containerClass"
|
:class="containerClass"
|
||||||
>
|
>
|
||||||
<template v-if="isBoursecu && showStoreClosed">
|
<template v-if="displayState !== 'open'">
|
||||||
<div class="boursecu-closed-box">
|
<div class="status-box" :class="isBoursecu ? 'status-box--boursecu' : 'status-box--default'">
|
||||||
<div class="boursecu-closed-box__icon-wrap">
|
<div class="status-box__icon-wrap">
|
||||||
<Icon icon="vuesax-linear:lock" size="22" color="#ffffff" />
|
<Icon
|
||||||
|
:icon="displayState === 'closed' ? 'vuesax-linear:lock' : 'vuesax-linear:info-circle'"
|
||||||
|
size="22"
|
||||||
|
:color="isBoursecu ? '#ffffff' : '#64748b'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="boursecu-closed-box__title">فروشگاه بسته است</div>
|
<div class="status-box__title">{{ statusTitle }}</div>
|
||||||
<div class="boursecu-closed-box__hint">{{ storeClosedHint }}</div>
|
<div class="status-box__hint">{{ statusHint }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -52,7 +56,11 @@ import { useConfigStore } from '@/store/configStore'
|
|||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { isBoursecuProject } from '@/utils/config'
|
import { isBoursecuProject } from '@/utils/config'
|
||||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged, price } from '@/utils/price'
|
import { getTradeBuyPrice, lastPrice, percentPriceChanged, price } from '@/utils/price'
|
||||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
import {
|
||||||
|
NO_QUOTE_MESSAGE,
|
||||||
|
getPriceDisplayState,
|
||||||
|
getStoreClosedMessage,
|
||||||
|
} from '@/utils/storeHours'
|
||||||
import Vue3Odometer from 'vue3-odometer'
|
import Vue3Odometer from 'vue3-odometer'
|
||||||
import 'odometer/themes/odometer-theme-default.css'
|
import 'odometer/themes/odometer-theme-default.css'
|
||||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||||
@@ -65,26 +73,28 @@ const priceLabel = computed(() =>
|
|||||||
isBoursecu ? 'قیمت لحظهای کاتد مس' : `قیمت خرید از ${config.config.name_fa}`
|
isBoursecu ? 'قیمت لحظهای کاتد مس' : `قیمت خرید از ${config.config.name_fa}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const showStoreClosed = computed(() => {
|
const displayState = computed(() => getPriceDisplayState())
|
||||||
if (isBoursecu) {
|
|
||||||
return isStoreClosed()
|
|
||||||
}
|
|
||||||
return lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const storeClosedHint = computed(() => {
|
const statusTitle = computed(() =>
|
||||||
const custom = userStore.getValueByKey('closed_message')
|
displayState.value === 'closed' ? 'فروشگاه بسته است' : NO_QUOTE_MESSAGE
|
||||||
if (typeof custom === 'string' && custom.trim()) {
|
)
|
||||||
return custom.replace(/<[^>]*>/g, '').trim()
|
|
||||||
|
const statusHint = computed(() => {
|
||||||
|
if (displayState.value === 'closed') {
|
||||||
|
const custom = userStore.getValueByKey('closed_message')
|
||||||
|
if (typeof custom === 'string' && custom.trim()) {
|
||||||
|
return custom.replace(/<[^>]*>/g, '').trim()
|
||||||
|
}
|
||||||
|
return getStoreClosedMessage()
|
||||||
}
|
}
|
||||||
return getStoreClosedMessage()
|
return 'در حال حاضر مظنهای برای نمایش وجود ندارد.'
|
||||||
})
|
})
|
||||||
|
|
||||||
const containerClass = computed(() => {
|
const containerClass = computed(() => {
|
||||||
if (!isBoursecu) {
|
if (!isBoursecu) {
|
||||||
return 'border-gray-200 border-1'
|
return 'border-gray-200 border-1'
|
||||||
}
|
}
|
||||||
if (showStoreClosed.value) {
|
if (displayState.value !== 'open') {
|
||||||
return 'boursecu-price-box boursecu-price-box--closed'
|
return 'boursecu-price-box boursecu-price-box--closed'
|
||||||
}
|
}
|
||||||
return 'boursecu-price-box boursecu-price-box--open'
|
return 'boursecu-price-box boursecu-price-box--open'
|
||||||
@@ -96,7 +106,7 @@ const percentChange = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const displayGoldVal = computed(() => {
|
const displayGoldVal = computed(() => {
|
||||||
if (showStoreClosed.value) return 0
|
if (displayState.value !== 'open') return 0
|
||||||
return getTradeBuyPrice(lastPrice.value)
|
return getTradeBuyPrice(lastPrice.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -137,7 +147,7 @@ void price
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boursecu-closed-box {
|
.status-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -147,30 +157,51 @@ void price
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boursecu-closed-box__icon-wrap {
|
.status-box__icon-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boursecu-closed-box__title {
|
.status-box__title {
|
||||||
color: #ffffff;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boursecu-closed-box__hint {
|
.status-box__hint {
|
||||||
color: rgba(255, 255, 255, 0.78);
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-box--boursecu .status-box__icon-wrap {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box--boursecu .status-box__title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box--boursecu .status-box__hint {
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box--default .status-box__icon-wrap {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box--default .status-box__title {
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box--default .status-box__hint {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
.price-change-badge {
|
.price-change-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ export function useHomePage() {
|
|||||||
const closedMessage = computed(() => userStore.getValueByKey('closed_message'))
|
const closedMessage = computed(() => userStore.getValueByKey('closed_message'))
|
||||||
const marketClosedText = computed(() => {
|
const marketClosedText = computed(() => {
|
||||||
if (!isStoreClosed()) return ''
|
if (!isStoreClosed()) return ''
|
||||||
return closedMessage.value || getStoreClosedMessage()
|
// فقط پیام سفارشی ادمین رو نشون بده — پیام ساعت کاری در CurrentPrice نمایش داده میشه
|
||||||
|
const custom = typeof closedMessage.value === 'string' ? closedMessage.value.trim() : ''
|
||||||
|
return custom || ''
|
||||||
})
|
})
|
||||||
const showClosedMessage = computed(() => !!marketClosedText.value)
|
const showClosedMessage = computed(() => !!marketClosedText.value)
|
||||||
const limitedPrice = computed(() => walletStore.limitedPrice?.[0])
|
const limitedPrice = computed(() => walletStore.limitedPrice?.[0])
|
||||||
|
|||||||
@@ -2,17 +2,21 @@
|
|||||||
<div
|
<div
|
||||||
class="dhome-surface dhome-hero-price"
|
class="dhome-surface dhome-hero-price"
|
||||||
:class="{
|
:class="{
|
||||||
'dhome-hero-price--boursecu-open': isBoursecu && !showStoreClosed,
|
'dhome-hero-price--boursecu-open': isBoursecu && displayState === 'open',
|
||||||
'dhome-hero-price--boursecu-closed': isBoursecu && showStoreClosed,
|
'dhome-hero-price--boursecu-closed': isBoursecu && displayState !== 'open',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="isBoursecu && showStoreClosed">
|
<template v-if="displayState !== 'open'">
|
||||||
<div class="dhome-hero-price__closed">
|
<div class="dhome-hero-price__closed" :class="{ 'dhome-hero-price__closed--default': !isBoursecu }">
|
||||||
<div class="dhome-hero-price__closed-icon">
|
<div class="dhome-hero-price__closed-icon">
|
||||||
<BasicIcon icon="vuesax-linear:lock" size="22" color="#ffffff" />
|
<BasicIcon
|
||||||
|
:icon="displayState === 'closed' ? 'vuesax-linear:lock' : 'vuesax-linear:info-circle'"
|
||||||
|
size="22"
|
||||||
|
:color="isBoursecu ? '#ffffff' : '#64748b'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="dhome-hero-price__closed-title">فروشگاه بسته است</p>
|
<p class="dhome-hero-price__closed-title">{{ statusTitle }}</p>
|
||||||
<p class="dhome-hero-price__closed-hint">{{ storeClosedHint }}</p>
|
<p class="dhome-hero-price__closed-hint">{{ statusHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,30 +58,36 @@ import { useConfigStore } from '@/store/configStore'
|
|||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { isBoursecuProject } from '@/utils/config'
|
import { isBoursecuProject } from '@/utils/config'
|
||||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
|
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
|
||||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
import {
|
||||||
|
NO_QUOTE_MESSAGE,
|
||||||
|
getPriceDisplayState,
|
||||||
|
getStoreClosedMessage,
|
||||||
|
} from '@/utils/storeHours'
|
||||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const isBoursecu = isBoursecuProject()
|
const isBoursecu = isBoursecuProject()
|
||||||
|
|
||||||
const showStoreClosed = computed(() => {
|
const displayState = computed(() => getPriceDisplayState())
|
||||||
if (isBoursecu) {
|
|
||||||
return isStoreClosed()
|
|
||||||
}
|
|
||||||
return lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const storeClosedHint = computed(() => {
|
const statusTitle = computed(() =>
|
||||||
const custom = userStore.getValueByKey('closed_message')
|
displayState.value === 'closed' ? 'فروشگاه بسته است' : NO_QUOTE_MESSAGE
|
||||||
if (typeof custom === 'string' && custom.trim()) {
|
)
|
||||||
return custom.replace(/<[^>]*>/g, '').trim()
|
|
||||||
|
const statusHint = computed(() => {
|
||||||
|
if (displayState.value === 'closed') {
|
||||||
|
const custom = userStore.getValueByKey('closed_message')
|
||||||
|
if (typeof custom === 'string' && custom.trim()) {
|
||||||
|
return custom.replace(/<[^>]*>/g, '').trim()
|
||||||
|
}
|
||||||
|
return getStoreClosedMessage()
|
||||||
}
|
}
|
||||||
return getStoreClosedMessage()
|
return 'در حال حاضر مظنهای برای نمایش وجود ندارد.'
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayGoldVal = computed(() => {
|
const displayGoldVal = computed(() => {
|
||||||
if (showStoreClosed.value) return 0
|
if (displayState.value !== 'open') return 0
|
||||||
return getTradeBuyPrice(lastPrice.value)
|
return getTradeBuyPrice(lastPrice.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -143,4 +153,18 @@ const percentChange = computed(() => {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: rgba(255, 255, 255, 0.78);
|
color: rgba(255, 255, 255, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dhome-hero-price__closed--default {
|
||||||
|
.dhome-hero-price__closed-icon {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dhome-hero-price__closed-title {
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dhome-hero-price__closed-hint {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
subtitle="محصول مورد نظر را انتخاب کنید و سفارش خود را ثبت کنید"
|
subtitle="محصول مورد نظر را انتخاب کنید و سفارش خود را ثبت کنید"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- بنر بسته بودن فروشگاه -->
|
||||||
|
<div v-if="storeClosed" class="dproducts-closed-banner">
|
||||||
|
<span>🔒</span>
|
||||||
|
<span class="dproducts-closed-banner__title">فروشگاه بسته است</span>
|
||||||
|
<span v-if="storeClosedMessage" class="dproducts-closed-banner__hint">— {{ storeClosedMessage }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dproducts-tabs">
|
<div class="dproducts-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -50,6 +57,8 @@
|
|||||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||||
import DesktopProductCard from './components/DesktopProductCard.vue'
|
import DesktopProductCard from './components/DesktopProductCard.vue'
|
||||||
import { useProductsPageContext } from '../composables/useProductsPage'
|
import { useProductsPageContext } from '../composables/useProductsPage'
|
||||||
|
import { getPriceDisplayState, isStoreClosedBySchedule, getStoreClosedMessage } from '@/utils/storeHours'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
activekey,
|
activekey,
|
||||||
@@ -60,6 +69,34 @@ const {
|
|||||||
clickProduct,
|
clickProduct,
|
||||||
isProductPriceInactive,
|
isProductPriceInactive,
|
||||||
} = useProductsPageContext()
|
} = useProductsPageContext()
|
||||||
|
|
||||||
|
const storeClosed = computed(() => getPriceDisplayState() === 'closed' || isStoreClosedBySchedule())
|
||||||
|
const storeClosedMessage = computed(() => getStoreClosedMessage())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" src="./styles/desktop-products.less"></style>
|
<style lang="less" src="./styles/desktop-products.less"></style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dproducts-closed-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(239, 68, 68, 0.07);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.22);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
direction: rtl;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
color: #9b1c1c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
<MobilePageHeader title="خرید و فروش" />
|
<MobilePageHeader title="خرید و فروش" />
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<div class="!h-full overflow-y-scroll mpage-content mpage-scroll">
|
<div class="!h-full overflow-y-scroll mpage-content mpage-scroll">
|
||||||
|
|
||||||
|
<!-- بنر بسته بودن فروشگاه -->
|
||||||
|
<div v-if="storeClosed" class="mx-4 mt-4 closed-banner">
|
||||||
|
<div class="closed-banner__icon">🔒</div>
|
||||||
|
<div class="closed-banner__body">
|
||||||
|
<div class="closed-banner__title">فروشگاه بسته است</div>
|
||||||
|
<div v-if="storeClosedMessage" class="closed-banner__hint">{{ storeClosedMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-center top-0 py-5">
|
<div class="w-full flex justify-center top-0 py-5">
|
||||||
<ARadioGroup
|
<ARadioGroup
|
||||||
v-model:value="activekey"
|
v-model:value="activekey"
|
||||||
@@ -44,6 +54,8 @@ import img from '@/assets/img/user_ly1.svg'
|
|||||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||||
import ProductItem from '@/views/base/products/components/ProductItem.vue'
|
import ProductItem from '@/views/base/products/components/ProductItem.vue'
|
||||||
import { useProductsPageContext } from '../composables/useProductsPage'
|
import { useProductsPageContext } from '../composables/useProductsPage'
|
||||||
|
import { getPriceDisplayState, isStoreClosedBySchedule, getStoreClosedMessage } from '@/utils/storeHours'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
activekey,
|
activekey,
|
||||||
@@ -54,6 +66,9 @@ const {
|
|||||||
clickProduct,
|
clickProduct,
|
||||||
isProductPriceInactive,
|
isProductPriceInactive,
|
||||||
} = useProductsPageContext()
|
} = useProductsPageContext()
|
||||||
|
|
||||||
|
const storeClosed = computed(() => getPriceDisplayState() === 'closed' || isStoreClosedBySchedule())
|
||||||
|
const storeClosedMessage = computed(() => getStoreClosedMessage())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
@@ -61,4 +76,38 @@ const {
|
|||||||
--un-text-opacity: 1;
|
--un-text-opacity: 1;
|
||||||
color: rgba(206, 163, 76, var(--un-text-opacity)) !important;
|
color: rgba(206, 163, 76, var(--un-text-opacity)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closed-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
direction: rtl;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9b1c1c;
|
||||||
|
margin-top: 3px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user