diff --git a/.env.example b/.env.example index ef3b176..2420ae5 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,15 @@ +# کپی به `.env.production` / `.env.development` + +# لوکال (پیشنهادی): پروکسی Vite → بدون CORS +# VITE_SUPERADMIN_URL=/__sa +# VITE_TENANT_DOMAIN=k.montzar.ir + +# لوکال با API سوپرادمین روی سیستم خودت: +# VITE_SUPERADMIN_URL=http://localhost:3001 + +# پروداکشن: +# VITE_SUPERADMIN_URL=https://adm.montzar.ir +# failover: adm.montzar.ir → adm.montech.ir → adm.montest.ir + +# URL لندینگ داخل اپ VITE_LANDING_APP_URL=/landing/ diff --git a/.gitignore b/.gitignore index 38adffa..3a944b6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ dist-ssr coverage *.local +# Local env +.env +.env.* +!.env.example + /cypress/videos/ /cypress/screenshots/ diff --git a/components.d.ts b/components.d.ts index 1c276b1..99e0757 100644 --- a/components.d.ts +++ b/components.d.ts @@ -51,13 +51,16 @@ declare module 'vue' { Disabled: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled.vue')['default'] DisabledDate: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled-date.vue')['default'] Empty: typeof import('./src/components/empty.vue')['default'] + EnamadBadge: typeof import('./src/components/EnamadBadge.vue')['default'] ExtraFooter: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/extra-footer.vue')['default'] Format: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/format.vue')['default'] + GoldDayCalculatorModal: typeof import('./src/components/GoldDayCalculatorModal.vue')['default'] GoldLoading: typeof import('./src/components/GoldLoading.vue')['default'] InstallButton: typeof import('./src/components/InstallButton.vue')['default'] ListAnim: typeof import('./src/components/ListAnim.vue')['default'] Mode: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/mode.vue')['default'] PresettedRanges: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/presetted-ranges.vue')['default'] + ProductImage: typeof import('./src/components/ProductImage.vue')['default'] ProfileAvatarUpload: typeof import('./src/components/ProfileAvatarUpload.vue')['default'] PwaInstallBanner: typeof import('./src/components/pwa/PwaInstallBanner.vue')['default'] PwaInstallGuideModal: typeof import('./src/components/pwa/PwaInstallGuideModal.vue')['default'] @@ -76,6 +79,7 @@ declare module 'vue' { Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default'] Text: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/text.vue')['default'] Time: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/time.vue')['default'] + TradeFeeConfirmSummary: typeof import('./src/components/TradeFeeConfirmSummary.vue')['default'] TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default'] VanActionSheet: typeof import('vant/es')['ActionSheet'] VanDialog: typeof import('vant/es')['Dialog'] diff --git a/index.html b/index.html index 3183b63..dab5f2a 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,65 @@ + @@ -343,30 +402,49 @@ return window.location.protocol === 'https:' ? 'https:' : 'http:' } - function getVersionUrls() { - var protocol = getProtocol() + function resolveTenantDomain() { + var host = String(window.location.hostname || '').trim() + if (!host || host === 'localhost' || host === '127.0.0.1') { + return 'k.montzar.ir' + } + return host + } + function getSuperAdminBases() { return [ - 'https://nerkh.gold/get_version', - 'https://tablo.talasevin.ir/get_version', - 'https://abshode-service.montgit.ir/server/get_version', - protocol + '//212.23.201.173:8381/get_version', - protocol + '//91.212.174.137:4040/server/get_version' + 'https://adm.montzar.ir', + 'https://adm.montech.ir', + 'https://adm.montest.ir' ] } + function getVersionUrls() { + var domain = encodeURIComponent(resolveTenantDomain()) + return getSuperAdminBases().map(function (base) { + return base + '/get_version?domain=' + domain + }) + } + function getConfigUrls() { - var protocol = getProtocol() - - return [ - protocol + '//nerkh.gold/get_user', - protocol + '//tablo.talasevin.ir/get_user', - protocol + '//abshode-service.montgit.ir/server/get_user', - protocol + '//212.23.201.173:8381/get_user', - protocol + '//91.212.174.137:4040/server/get_user' - ] + var domain = encodeURIComponent(resolveTenantDomain()) + return getSuperAdminBases().map(function (base) { + return base + '/get_user?domain=' + domain + }) } + // پاک‌کردن URL موفقیت قدیمی رجیستری (مثل nerkh.gold) + ;(function clearLegacyConfigSuccess() { + var last = localStorage.getItem(CONFIG_SUCCESS_URL_KEY) + if (!last) return + var ok = getSuperAdminBases().some(function (base) { + return last.indexOf(base) === 0 + }) + if (!ok) { + localStorage.removeItem(CONFIG_SUCCESS_URL_KEY) + document.cookie = CONFIG_COOKIE_NAME + '=; Path=/; Max-Age=0; SameSite=Lax' + } + })() + function sortVersionUrls(urls) { var lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY) @@ -382,6 +460,11 @@ function sortByLastSuccess(urls) { var lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY) + if (lastSuccessUrl && urls.indexOf(lastSuccessUrl) === -1) { + localStorage.removeItem(CONFIG_SUCCESS_URL_KEY) + lastSuccessUrl = null + } + return urls.slice().sort(function (a, b) { if (a === lastSuccessUrl) return -1 if (b === lastSuccessUrl) return 1 @@ -665,10 +748,10 @@ '
⚠️
' + '

خطا در بارگذاری برنامه

' + '

' + - 'ارتباط با سرور برقرار نشد یا فایل‌های برنامه به‌درستی دریافت نشدند.' + + 'برقراری ارتباط با سرور امکان‌پذیر نیست. لطفاً اتصال اینترنت خود را بررسی کرده و صفحه را تازه‌سازی نمایید.' + '

' + '' + ' + + + ` +} + +export function hideServerUnavailableOverlay() { + if (typeof document === 'undefined') return + const root = document.getElementById(OVERLAY_ID) + if (root) root.remove() +} + +export function assertServerReachableConfig(config: unknown) { + if (isServerUnreachableConfig(config)) { + showServerUnavailableOverlay() + throw new ServerUnavailableError() + } +} diff --git a/src/utils/serviceFee.ts b/src/utils/serviceFee.ts index 3b5df08..05ea7b9 100644 --- a/src/utils/serviceFee.ts +++ b/src/utils/serviceFee.ts @@ -1,7 +1,7 @@ import { ref } from 'vue' -import { getSettings } from '@/api/configuration' +import { useSettingsStore } from '@/store/settingsStore' -export const serviceFeePercentage = ref(0.01) +export const serviceFeePercentage = ref(0) /** true = مبالغ رند: فروش → ریال دقیق (کارمزد از طلا)؛ خرید → وزن دقیق (کارمزد از ریال) */ export const feeDeductFromGold = ref(false) @@ -9,6 +9,11 @@ export const feeDeductFromGold = ref(false) /** آخرین فیلدی که کاربر مستقیم ویرایش کرده — مبنای به‌روزرسانی هنگام تغییر قیمت */ export type TradeInputLock = 'gram' | 'rial' | null +/** ریال قفل است؟ (کاربر مبلغ ریالی را مستقیم وارد کرده) → کارمزد از طلا */ +export function isRialLocked(inputLock?: TradeInputLock): boolean { + return inputLock === 'rial' +} + /** حداکثر اعشار ذخیره/ارسال به سرور (مطابق orders.amount) */ export const TRADE_GRAM_STORAGE_DECIMALS = 10 @@ -44,13 +49,18 @@ export function normalizeServiceFeeRatio(value: number): number { export async function loadServiceFeeFromSettings() { try { - const settings = await getSettings() - if (settings && Array.isArray(settings)) { + const settingsStore = useSettingsStore() + if (!settingsStore.loaded) { + await settingsStore.fetchSettings() + } + const settings = settingsStore.settings + if (settings?.length) { const feeSetting = settings.find( (s) => s?.key === 'fee_service_charge_percentage' && s?.is_active === true ) if (feeSetting?.value !== undefined && feeSetting?.value !== null) { - serviceFeePercentage.value = normalizeServiceFeeRatio(Number(feeSetting.value)) + const raw = String(feeSetting.value).trim() + serviceFeePercentage.value = raw === '' ? 0 : normalizeServiceFeeRatio(Number(raw)) } const feeModeSetting = settings.find( @@ -215,3 +225,177 @@ export function calcSellGoldDebit(goldValue?: number | null, feeInGrams?: number } return gold } + +/** مبلغ پایه معامله قبل از اعمال کارمزد ریالی (خرید با افزودن کارمزد به ریال) */ +export function calcTradeBasePriceRial(orderGoldAmount: number, unitPriceRial: number) { + return Math.round(Number(orderGoldAmount) * Number(unitPriceRial)) +} + +export interface TradeConfirmRow { + label: string + gram?: number | null + rial?: number | null + bold?: boolean + minus?: boolean + plus?: boolean + dividerBefore?: boolean +} + +export interface TradeConfirmSection { + heading: string + rows: TradeConfirmRow[] + footnote?: string +} + +export interface TradeConfirmBreakdown { + sections: TradeConfirmSection[] + formHint: string +} + +function hasTradeFee(feeInGrams: number, feeInRial: number) { + return feeInGrams > 0 || feeInRial > 0 +} + +function grossGoldFromRial(rial: number, unitPriceRial: number) { + if (!rial || !unitPriceRial) return 0 + return normalizeOrderGoldAmount(rial / unitPriceRial) +} + +/** + * جزئیات مدال تأیید — دو بخش «کسر از کیف» و «واریز به کیف». + * فقط بر پایهٔ قفل ورودی تصمیم می‌گیرد: + * • ریال قفل → مبلغ دقیق، کارمزد از طلا. + * • گرم قفل → وزن دقیق، کارمزد از ریال. + */ +export function buildTradeConfirmBreakdown(params: { + isBuy: boolean + inputLock: TradeInputLock + orderGoldAmount: number + feeInGrams: number + feeInRial: number + payablePrice: number + unitPriceRial: number + sellGoldDebit?: number +}): TradeConfirmBreakdown { + const { + isBuy, + inputLock, + orderGoldAmount, + feeInGrams, + feeInRial, + payablePrice, + unitPriceRial, + sellGoldDebit, + } = params + const hasFee = hasTradeFee(feeInGrams, feeInRial) + const rialLocked = isRialLocked(inputLock) + const weight = orderGoldAmount + + if (!isBuy) { + if (rialLocked) { + const debit = sellGoldDebit ?? parseFloat((weight + feeInGrams).toFixed(4)) + const goldRows: TradeConfirmRow[] = hasFee + ? [ + { label: 'وزن معادل مبلغ', gram: weight }, + { label: 'کارمزد', gram: feeInGrams, minus: true }, + { label: 'جمع کسر از کیف طلا', gram: debit, bold: true, dividerBefore: true }, + ] + : [{ label: 'وزن فروش', gram: weight, bold: true }] + + return { + sections: [ + { heading: 'از کیف طلا کسر می‌شود', rows: goldRows }, + { + heading: 'به کیف پول واریز می‌شود', + rows: [{ label: 'مبلغ دریافتی', rial: payablePrice, bold: true }], + footnote: hasFee + ? 'دقیقاً همان مبلغی که وارد کردید واریز می‌شود؛ کارمزد فقط از طلای کیف کسر می‌شود.' + : undefined, + }, + ], + formHint: hasFee + ? `${payablePrice.toLocaleString('fa-IR')} ریال دریافت — ${formatTradeGramDisplay(debit)} گرم از کیف کسر می‌شود (شامل ${formatTradeGramDisplay(feeInGrams)} گرم کارمزد)` + : `${payablePrice.toLocaleString('fa-IR')} ریال دریافت`, + } + } + + const grossSellRial = Math.round(weight * unitPriceRial) + const cashRows: TradeConfirmRow[] = hasFee + ? [ + { label: 'ارزش فروش', rial: grossSellRial }, + { label: 'کارمزد', rial: feeInRial, minus: true }, + { label: 'خالص دریافتی', rial: payablePrice, bold: true, dividerBefore: true }, + ] + : [{ label: 'مبلغ دریافتی', rial: payablePrice, bold: true }] + + return { + sections: [ + { heading: 'از کیف طلا کسر می‌شود', rows: [{ label: 'وزن فروش', gram: weight, bold: true }] }, + { + heading: 'به کیف پول واریز می‌شود', + rows: cashRows, + footnote: hasFee ? 'کارمزد از مبلغ ریالی دریافتی کم شده است.' : undefined, + }, + ], + formHint: hasFee + ? `${formatTradeGramDisplay(weight)} گرم فروش — ${feeInRial.toLocaleString('fa-IR')} ریال کارمزد از مبلغ دریافتی کسر می‌شود` + : `${formatTradeGramDisplay(weight)} گرم فروش — ${payablePrice.toLocaleString('fa-IR')} ریال به کیف پول`, + } + } + + if (rialLocked) { + const grossGram = grossGoldFromRial(payablePrice, unitPriceRial) + const feeGram = parseFloat(Math.max(0, grossGram - weight).toFixed(4)) + const goldRows: TradeConfirmRow[] = hasFee + ? [ + { label: 'معادل طلای خام', gram: grossGram }, + { label: 'کارمزد', gram: feeGram, minus: true }, + { label: 'خالص واریزی به کیف طلا', gram: weight, bold: true, dividerBefore: true }, + ] + : [{ label: 'واریزی به کیف طلا', gram: weight, bold: true }] + + return { + sections: [ + { + heading: 'از کیف پول کسر می‌شود', + rows: [{ label: 'مبلغ پرداختی', rial: payablePrice, bold: true }], + footnote: 'دقیقاً همان مبلغی که وارد کردید؛ کارمزد جداگانه اضافه نمی‌شود.', + }, + { + heading: 'به کیف طلا واریز می‌شود', + rows: goldRows, + footnote: hasFee ? 'کارمزد از وزن طلای دریافتی کسر شده است.' : undefined, + }, + ], + formHint: hasFee + ? `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت — ${formatTradeGramDisplay(weight)} گرم واریز (کارمزد از طلا کسر شد)` + : `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت`, + } + } + + const basePriceRial = calcTradeBasePriceRial(weight, unitPriceRial) + const cashRows: TradeConfirmRow[] = hasFee + ? [ + { label: `ارزش ${formatTradeGramDisplay(weight)} گرم`, rial: basePriceRial }, + { label: 'کارمزد', rial: feeInRial, plus: true }, + { label: 'جمع پرداختی', rial: payablePrice, bold: true, dividerBefore: true }, + ] + : [{ label: 'مبلغ پرداختی', rial: payablePrice, bold: true }] + + return { + sections: [ + { + heading: 'از کیف پول کسر می‌شود', + rows: cashRows, + footnote: hasFee ? 'کارمزد به مبلغ پرداختی اضافه شده است.' : undefined, + }, + { + heading: 'به کیف طلا واریز می‌شود', + rows: [{ label: 'دقیقاً', gram: weight, bold: true }], + }, + ], + formHint: hasFee + ? `${payablePrice.toLocaleString('fa-IR')} ریال (شامل ${feeInRial.toLocaleString('fa-IR')} ریال کارمزد) — ${formatTradeGramDisplay(weight)} گرم واریز` + : `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت`, + } +} diff --git a/src/utils/specialPricing.ts b/src/utils/specialPricing.ts new file mode 100644 index 0000000..3f6b57a --- /dev/null +++ b/src/utils/specialPricing.ts @@ -0,0 +1,80 @@ +import { ref } from 'vue' + +export type SpecialPricingRule = { + product_id?: number + package_id: number + title: string + profit_buy: number + profit_sell: number + show_product: boolean +} + +export type SpecialPricingPayload = { + enabled?: boolean + products?: SpecialPricingRule[] + board?: SpecialPricingRule | null +} + +const state = ref<{ + enabled: boolean + byProduct: Record + board: SpecialPricingRule | null +}>({ + enabled: false, + byProduct: {}, + board: null, +}) + +export function setSpecialPricingPayload(payload?: SpecialPricingPayload | null) { + if (!payload || !payload.enabled) { + state.value = { enabled: false, byProduct: {}, board: null } + return + } + + const byProduct: Record = {} + for (const row of payload.products ?? []) { + const pid = Number(row.product_id ?? 0) + if (pid > 0) byProduct[pid] = row + } + + state.value = { + enabled: true, + byProduct, + board: payload.board ?? null, + } +} + +export function getSpecialPricingState() { + return state +} + +/** اگر سرور قبلاً special_pricing نزده، اختلاف تابلو را روی last اعمال کن */ +export function applyBoardSpecialToLastPayload>(payload: T): T { + if (!payload || typeof payload !== 'object') return payload + if (payload.special_pricing) return payload + + const board = state.value.board + if (!state.value.enabled || !board) return payload + + const buyExtra = Number(board.profit_buy ?? 0) + const sellExtra = Number(board.profit_sell ?? 0) + if (!buyExtra && !sellExtra) return payload + + const next = { ...payload } as T & Record + if (Number(next.buy ?? 0) > 0 && buyExtra) { + next.buy = Math.max(0, Number(next.buy) + buyExtra) + } + if (Number(next.sell ?? 0) > 0 && sellExtra) { + next.sell = Math.max(0, Number(next.sell) - sellExtra) + } + if (Number(next.price_last ?? 0) > 0 && buyExtra) { + next.price_last = Math.max(0, Number(next.price_last) + buyExtra) + } + next.special_pricing = { + package_id: board.package_id, + title: board.title, + profit_buy: buyExtra, + profit_sell: sellExtra, + } + return next +} diff --git a/src/utils/storeHours.ts b/src/utils/storeHours.ts index 34f152d..2ab58c9 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 { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price' +import { isLastPriceAbsenceSettled, lastPrice, lastPriceReady } from '@/utils/price' export interface StoreTradingHours { start: string @@ -12,8 +12,24 @@ export interface EffectiveTradingHours { start: string end: string enforced: boolean + dayEnabled: boolean } +type WeekdayKey = 'sat' | 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' + +type DaySchedule = { + enabled: boolean + start: string + end: string +} + +type WeeklySchedule = { + mode: 'uniform' | 'per_day' + days: Record +} + +const WEEKDAY_BY_JS: WeekdayKey[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] + const productTradingHours = ref<{ start: string; end: string } | null>(null) const productTradingHoursVersion = ref(0) @@ -54,6 +70,11 @@ function hasProductTradingWindow(start?: string | null, end?: string | null): bo return open !== close } +/** بعد از merge زندهٔ تنظیمات ساعت کاری — computedهای صفحه اصلی دوباره حساب شوند */ +export function touchStoreHoursSettings() { + productTradingHoursVersion.value += 1 +} + export function setProductTradingHours(start?: string | null, end?: string | null) { if (!hasProductTradingWindow(start, end)) { productTradingHours.value = null @@ -81,29 +102,77 @@ export function syncProductTradingHoursFromProducts( setProductTradingHours(null, null) } -export function getStoreTradingHours(): StoreTradingHours { - const settings = useSettingsStore() - const closeRaw = settings.getSettingByKey('close_price') - const closePriceEnabled = - closeRaw === true || - closeRaw === 1 || - String(closeRaw ?? 'false').trim().toLowerCase() === 'true' +function weekdayKey(now: Date): WeekdayKey { + return WEEKDAY_BY_JS[now.getDay()] ?? 'sat' +} - return { - start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'), - end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'), - closePriceEnabled, +function parseWeeklySchedule(raw: unknown): WeeklySchedule | null { + const text = String(raw ?? '').trim() + if (!text) return null + try { + const parsed = JSON.parse(text) as Partial + if (!parsed?.days || typeof parsed.days !== 'object') return null + const mode = parsed.mode === 'per_day' ? 'per_day' : 'uniform' + const defaults = getStoreTradingHours() + const keys: WeekdayKey[] = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'] + const days = {} as Record + for (const key of keys) { + const row = parsed.days[key] + days[key] = { + enabled: row?.enabled !== false, + start: normalizeTradingTimeSetting(row?.start, defaults.start), + end: normalizeTradingTimeSetting(row?.end, defaults.end), + } + } + return { mode, days } + } catch { + return null } } -export function getEffectiveTradingHours(): EffectiveTradingHours { +function isOutsideTimeWindow(now: Date, start: string, end: string): boolean { + const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + const openSeconds = parseTimeToSeconds(start) + const closeSeconds = parseTimeToSeconds(end) + if (closeSeconds <= openSeconds) { + return currentSeconds < openSeconds && currentSeconds > closeSeconds + } + return currentSeconds < openSeconds || currentSeconds > closeSeconds +} + +export function getStoreTradingHours(): StoreTradingHours { + const settings = useSettingsStore() + void settings.settingsRevision + return { + start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'), + end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'), + closePriceEnabled: settings.isSettingTruthy('close_price'), + } +} + +export function getEffectiveTradingHours(now = new Date()): EffectiveTradingHours { void productTradingHoursVersion.value const settings = getStoreTradingHours() + const weekly = parseWeeklySchedule(useSettingsStore().getSettingByKey('trading_hours_by_day')) + + if (settings.closePriceEnabled && weekly?.mode === 'per_day') { + const day = weekly.days[weekdayKey(now)] + if (day) { + return { + start: day.start, + end: day.end, + enforced: true, + dayEnabled: day.enabled, + } + } + } + if (settings.closePriceEnabled) { return { start: settings.start, end: settings.end, enforced: true, + dayEnabled: true, } } @@ -112,6 +181,7 @@ export function getEffectiveTradingHours(): EffectiveTradingHours { start: productTradingHours.value.start, end: productTradingHours.value.end, enforced: true, + dayEnabled: true, } } @@ -119,16 +189,40 @@ export function getEffectiveTradingHours(): EffectiveTradingHours { start: settings.start, end: settings.end, enforced: false, + dayEnabled: true, } } -export function isStoreClosedBySchedule(now = new Date()): boolean { - const { start, end, enforced } = getEffectiveTradingHours() - if (!enforced) { +/** خارج از ساعت کاری سراسری — فقط وقتی close_price روشن باشد (صفحه اصلی) */ +export function isOutsideGlobalStoreHours(now = new Date()): boolean { + const settings = getStoreTradingHours() + if (!settings.closePriceEnabled) { return false } - return isOutsideStoreTradingHours(now, start, end) + const weekly = parseWeeklySchedule(useSettingsStore().getSettingByKey('trading_hours_by_day')) + if (weekly?.mode === 'per_day') { + const day = weekly.days[weekdayKey(now)] + if (day && !day.enabled) { + return true + } + if (day) { + return isOutsideTimeWindow(now, day.start, day.end) + } + } + + return isOutsideTimeWindow(now, settings.start, settings.end) +} + +export function isStoreClosedBySchedule(now = new Date()): boolean { + const { start, end, enforced, dayEnabled } = getEffectiveTradingHours(now) + if (!enforced) { + return false + } + if (!dayEnabled) { + return true + } + return isOutsideTimeWindow(now, start, end) } export function isOutsideStoreTradingHours( @@ -136,20 +230,14 @@ export function isOutsideStoreTradingHours( start?: string, end?: string, ): boolean { - const hours = start != null && end != null - ? { start, end } - : getEffectiveTradingHours() - - const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() - const openSeconds = parseTimeToSeconds(hours.start) - const closeSeconds = parseTimeToSeconds(hours.end) - - return currentSeconds < openSeconds || currentSeconds > closeSeconds + if (start != null && end != null) { + return isOutsideTimeWindow(now, start, end) + } + return isStoreClosedBySchedule(now) } export function hasLivePriceQuote(): boolean { - const p = lastPrice.value - return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0 + return hasShowableHomePrice(lastPrice.value) } export function isStoreClosed(): boolean { @@ -199,41 +287,81 @@ export const NO_QUOTE_MESSAGE = 'مظنه در دسترس نیست' /** سوییچ: خارج از ساعت کاری هم قیمت صفحه اصلی نشان داده شود */ export function shouldShowHomePriceWhenClosed(): boolean { - const settings = useSettingsStore() - return settings.isSettingTruthy('show_home_price_when_closed') + return useSettingsStore().isSettingTruthy('show_home_price_when_closed') +} + +export function shouldShowLastSavedPrice(): boolean { + return useSettingsStore().isSettingTruthy('show_last_price') +} + +/** فقط این منابع «آخرین قیمت ذخیره‌شده» هستند؛ باقی price_source مظنهٔ زنده است */ +const STORED_PRICE_SOURCES = ['database_last'] + +function hasShowableHomePrice(p = lastPrice.value): boolean { + if (!p) return false + return Number(p.buy) > 0 || Number(p.sell) > 0 || Number(p.price_last) > 0 || Number(p.price) > 0 +} + +/** قیمت از مرجع زنده آمده یا از آخرین ردیف ذخیره‌شده */ +function hasLiveQuote(p = lastPrice.value): boolean { + if (!hasShowableHomePrice(p)) return false + + const source = String(p?.price_source ?? '').trim().toLowerCase() + if (source) return !STORED_PRICE_SOURCES.includes(source) + + return ( + Number(p?.montzar ?? 0) > 0 + || Number(p?.zaarbox ?? 0) > 0 + || Number(p?.main_product ?? 0) > 0 + ) } /** - * وضعیت نمایش قیمت: - * - closed → فروشگاه بسته است (ساعت کاری، سیگنال سرور، یا نبود مظنه) - * - open → نمایش عادی قیمت - * - * اگر show_home_price_when_closed روشن باشد و قیمت موجود باشد، به‌جای closed قیمت نشان داده می‌شود. + * وضعیت نمایش قیمت در صفحه اصلی — همان منطق بک‌اند: + * خارج از ساعت کاری (close_price روشن) → show_home_price_when_closed روشن: آخرین قیمت؛ خاموش: closed + * داخل ساعت کاری → مظنه زنده: open ؛ وگرنه فقط با show_last_price آخرین قیمت؛ وگرنه no_quote */ export function getPriceDisplayState(): PriceDisplayState { void productTradingHoursVersion.value + void useSettingsStore().settingsRevision const p = lastPrice.value - const showWhenClosed = shouldShowHomePriceWhenClosed() - if (p?.status === 'closed' || p?.closed === true) { - if (showWhenClosed && hasLivePriceQuote()) { - return 'open' - } - return 'closed' + const serverStatus = p?.status + // اگر عدد قیمت هست، no_quote لحظه‌ای سرور را جدی نگیر (race با تیک بعدی) + if (serverStatus === 'no_quote' && hasShowableHomePrice(p)) { + // fall through به منطق کلاینت + } else if (serverStatus === 'open' || serverStatus === 'closed' || serverStatus === 'no_quote') { + return serverStatus } - if (isStoreClosed()) { - if (showWhenClosed && hasLivePriceQuote()) { - return 'open' - } - return 'closed' + if (isOutsideGlobalStoreHours()) { + if (!shouldShowHomePriceWhenClosed()) return 'closed' + return hasShowableHomePrice(p) ? 'open' : 'no_quote' } - if (!hasLivePriceQuote()) { - return 'closed' - } + if (hasLiveQuote(p)) return 'open' + if (shouldShowLastSavedPrice() && hasShowableHomePrice(p)) return 'open' + if (hasShowableHomePrice(p)) return 'open' - return 'open' + return 'no_quote' +} + +/** + * اسکلتون تا وقتی قیمت قابل‌نمایش برسد، یا وضعیت بسته قطعی شود، + * یا بعد از اولین پاسخ خالی پنجرهٔ settle تمام شود (مظنه واقعاً نیست). + */ +export function isHomeQuotePending(): boolean { + const settings = useSettingsStore() + void lastPriceReady.value + void lastPrice.value + void isLastPriceAbsenceSettled() + void settings.loaded + void settings.settingsRevision + + if (!settings.loaded || !lastPriceReady.value) return true + if (hasShowableHomePrice(lastPrice.value)) return false + if (lastPrice.value?.status === 'closed') return false + return !isLastPriceAbsenceSettled() } export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string { diff --git a/src/utils/superAdminMenuGates.ts b/src/utils/superAdminMenuGates.ts new file mode 100644 index 0000000..5f2defc --- /dev/null +++ b/src/utils/superAdminMenuGates.ts @@ -0,0 +1,37 @@ +/** + * نگاشت منو/مسیر اپ کاربر → کلید دسترسی سوپرادمین. + * مقدار false/غایب = مخفی (پیش‌فرض امن). + */ +export const ROUTE_FEATURE_SETTING: Record = { + fund: 'enable_product_funds', + fund_info: 'enable_product_funds', + fund_add: 'enable_product_funds', + piggy: 'enable_product_funds', + earnMoney: 'has_page_earn_money', + giftCard: 'has_page_gift_card', + notifyTrigger: 'has_page_price_trigger', + appNotifications: 'has_page_notification', + conditionalOrder: 'enable_conditional_order', + permissions: 'permision__enabled', +} + +export const MENU_ID_FEATURE_SETTING: Record = { + fund: 'enable_product_funds', + piggy: 'enable_product_funds', + earnMoney: 'has_page_earn_money', + giftCard: 'has_page_gift_card', + notify: 'has_page_price_trigger', + notifyTrigger: 'has_page_price_trigger', + conditionalOrder: 'enable_conditional_order', + permissions: 'permision__enabled', +} + +export function featureSettingForRoute(routeName: string | null | undefined): string | null { + if (!routeName) return null + return ROUTE_FEATURE_SETTING[String(routeName)] ?? null +} + +export function featureSettingForMenuId(id: string | null | undefined): string | null { + if (!id) return null + return MENU_ID_FEATURE_SETTING[String(id)] ?? null +} diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index f52c61f..286f31f 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -1,5 +1,4 @@ import { ref } from 'vue' -import { getSettings } from '@/api/configuration' import type { DepositGuideVars } from '@/utils/depositGuideText' import { DEFAULT_DEPOSIT_GUIDE_TEXT, @@ -32,6 +31,7 @@ export interface SupportMessengerSettings { depositSupportMessageText?: string depositAccountHolder?: string depositCardNumber?: string + depositAccountNumber?: string depositSheba?: string depositSmsSupportNumber?: string } @@ -175,6 +175,7 @@ export function getDepositGuideVarsFromSupportSettings( return { accountHolder: settings.depositAccountHolder, cardNumber: settings.depositCardNumber, + accountNumber: settings.depositAccountNumber, sheba: settings.depositSheba, customerName: getCustomerNameFromUser(), } @@ -367,10 +368,8 @@ export async function loadSupportMessengerFromSettings(forceSettingsRefresh = fa await settingsStore.fetchSettings() } - const settings = await getSettings() - if (!settings || !Array.isArray(settings)) return - - settingsStore.mergeSettings(settings) + const settings = settingsStore.settings + if (!settings?.length) return supportMessengerSettings.value = { messengers: resolveSupportMessengersFromSettings(settings), @@ -380,6 +379,7 @@ export async function loadSupportMessengerFromSettings(forceSettingsRefresh = fa depositSupportMessageText: getSupportSettingValue(settings, 'deposit_support_message_text'), depositAccountHolder: getSupportSettingValue(settings, 'deposit_account_holder'), depositCardNumber: getSupportSettingValue(settings, 'deposit_card_number'), + depositAccountNumber: getSupportSettingValue(settings, 'deposit_account_number'), depositSheba: getSupportSettingValue(settings, 'deposit_sheba'), depositSmsSupportNumber: getSupportSettingValue(settings, 'deposit_sms_support_number'), } diff --git a/src/utils/trackingSystemsSettings.ts b/src/utils/trackingSystemsSettings.ts new file mode 100644 index 0000000..50f176f --- /dev/null +++ b/src/utils/trackingSystemsSettings.ts @@ -0,0 +1,145 @@ +import type { AppSetting } from '@/store/settingsStore' +import { useSettingsStore } from '@/store/settingsStore' + +export const TRACKING_SYSTEMS_KEY = 'tracking_systems' +export const TRACKING_SYSTEMS_ENABLED_KEY = 'tracking_systems_enabled' + +export interface TrackingSystemSite { + id: string + name: string + url: string +} + +let trackingSettingsCache: AppSetting[] | null = null +let trackingSettingsPromise: Promise | null = null + +export function normalizeSettingsResponse(data: unknown): AppSetting[] { + if (Array.isArray(data)) { + return data as AppSetting[] + } + + if (data && typeof data === 'object') { + const record = data as Record + const nested = record.data ?? record.settings ?? record.rows + if (Array.isArray(nested)) { + return nested as AppSetting[] + } + } + + return [] +} + +export function findSettingValue(settings: AppSetting[], key: string): unknown { + const setting = settings.find((item) => String(item?.key ?? '').trim() === key) + if (!setting) { + return null + } + + return setting.value ?? null +} + +export function isSettingTruthy(value: unknown): boolean { + if (value === true) { + return true + } + + const normalized = String(value ?? '').trim().toLowerCase() + return normalized === 'true' || normalized === '1' || normalized === 'yes' +} + +export function parseTrackingSystems(raw: unknown): TrackingSystemSite[] { + let value: unknown = raw + + for (let depth = 0; depth < 2; depth += 1) { + if (typeof value !== 'string') { + break + } + + const trimmed = value.trim() + if (!trimmed) { + return [] + } + + try { + value = JSON.parse(trimmed) + } catch { + return [] + } + } + + if (!Array.isArray(value)) { + return [] + } + + return value + .map((item) => { + if (!item || typeof item !== 'object') { + return null + } + + const record = item as Record + const id = String(record.id ?? '').trim() + const name = String(record.name ?? record.title ?? '').trim() + const url = String(record.url ?? record.link ?? '').trim() + + if (!name || !url) { + return null + } + + return { + id: id || `${name}-${url}`, + name, + url, + } + }) + .filter((item): item is TrackingSystemSite => item != null) +} + +export function resolveTrackingSystems(settings: AppSetting[]): TrackingSystemSite[] { + const raw = findSettingValue(settings, TRACKING_SYSTEMS_KEY) + return parseTrackingSystems(raw) +} + +export function isTrackingSystemsEnabled(settings: AppSetting[]): boolean { + const raw = findSettingValue(settings, TRACKING_SYSTEMS_ENABLED_KEY) + return isSettingTruthy(raw) +} + +export async function fetchTrackingSettings(force = false): Promise { + if (!force && trackingSettingsCache) { + return trackingSettingsCache + } + + if (!force && trackingSettingsPromise) { + return trackingSettingsPromise + } + + trackingSettingsPromise = (async () => { + const settingsStore = useSettingsStore() + + try { + await settingsStore.fetchSettings(force) + } catch (error) { + console.log(error) + } + + const settings = [...settingsStore.settings] + trackingSettingsCache = settings + return settings + })() + .catch((error) => { + console.log(error) + trackingSettingsCache = [] + return [] + }) + .finally(() => { + trackingSettingsPromise = null + }) + + return trackingSettingsPromise +} + +export function resetTrackingSettingsCache() { + trackingSettingsCache = null + trackingSettingsPromise = null +} diff --git a/src/utils/useAxios.ts b/src/utils/useAxios.ts index 75912a3..cbc5d1a 100644 --- a/src/utils/useAxios.ts +++ b/src/utils/useAxios.ts @@ -7,6 +7,10 @@ import { findStoredAuthToken } from '@/utils/authToken'; import router from "@/router"; import {get} from 'lodash' import {message} from "ant-design-vue"; +import { + hideServerUnavailableOverlay, + showServerUnavailableOverlay, +} from '@/utils/serverUnavailable' interface Meta { errorMessage?: 'message' | 'notification' | 'toast' | 'none' @@ -71,6 +75,7 @@ function scheduleSessionVerify() { meta: { errorMessage: 'none' }, }) } catch { + showPleaseLoginAgain() await logoutToLogin() } finally { sessionVerifyInFlight = false @@ -100,10 +105,7 @@ _axios.interceptors.request.use( ) _axios.interceptors.response.use( (response) => { - //console.log('this is response') - // Any status code that lie within the range of 2xx cause this function to trigger - // Do something with response data - //console.log('this is fulfilled on interceptors.response', response.data) + hideServerUnavailableOverlay() return response.data }, async function (error) { @@ -115,6 +117,7 @@ _axios.interceptors.response.use( if (hadAuth) { const path = requestPath(config) if (path.includes('/customer/me')) { + showPleaseLoginAgain() await logoutToLogin() } else { scheduleSessionVerify() @@ -123,34 +126,68 @@ _axios.interceptors.response.use( return Promise.reject(error) } + if (response?.status === 503) { + showServerUnavailableOverlay(SERVER_ERROR) + return Promise.reject(error) + } + if (!shouldShowApiError(config)) { return Promise.reject(error) } const apiMessage = extractApiErrorMessage(response?.data) + const status = Number(response?.status ?? 0) if (response?.status === 403) { - showApiError(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', config) + showApiError( + withHttpStatus(apiMessage || 'شما دسترسی لازم برای انجام این عملیات را ندارید', 403), + config, + ) return Promise.reject(error) } if (response?.status === 422) { if (apiMessage) { - showApiError(apiMessage, config) + showApiError(withHttpStatus(apiMessage, 422), config) + } else { + showApiError(withHttpStatus('خطا در اعتبارسنجی', 422), config) } return Promise.reject(error) } if (response) { - showApiError(apiMessage || DEFAULT_LOAD_ERROR, config) + if (status >= 500) { + showApiError(withHttpStatus(SERVER_ERROR, status), config) + } else { + showApiError(withHttpStatus(apiMessage || DEFAULT_LOAD_ERROR, status), config) + } return Promise.reject(error) } - showApiError(DEFAULT_LOAD_ERROR, config) + showApiError(NETWORK_ERROR, config) return Promise.reject(error) }) const DEFAULT_LOAD_ERROR = 'خطا در بارگذاری اطلاعات' +const SERVER_ERROR = 'خطا در برقراری ارتباط با سرور' +const NETWORK_ERROR = 'لطفا اینترنت خود را بررسی کنید و مجددا تلاش کنید' +const LOGIN_AGAIN = 'لطفا مجددا وارد شوید' +let lastAuthToastAt = 0 + +function showPleaseLoginAgain() { + const now = Date.now() + if (now - lastAuthToastAt < 2500) return + lastAuthToastAt = now + message.error({ + content: LOGIN_AGAIN, + key: 'please-login-again', + duration: 3, + }) +} + +function withHttpStatus(message: string, status: number): string { + return `${message} (${status})` +} function extractApiErrorMessage(data: unknown): string { if (!data || typeof data !== 'object') { diff --git a/src/views/base/aboutSoftware/aboutSoftwareContent.ts b/src/views/base/aboutSoftware/aboutSoftwareContent.ts new file mode 100644 index 0000000..31b6168 --- /dev/null +++ b/src/views/base/aboutSoftware/aboutSoftwareContent.ts @@ -0,0 +1,35 @@ +export function getDefaultAboutSoftwareText(projectName: string) { + const name = projectName.trim() || 'این نرم‌افزار' + return `${name} بستری امن و آنلاین برای خرید و فروش طلا فراهم کرده تا کاربران بتوانند سرمایه‌های خود را به‌سادگی مدیریت کنند.\n\nهدف ما ارائه تجربه‌ای سریع، شفاف و کاربرپسند در معاملات طلا است.` +} + +function looksLikeHtml(value: string) { + return /<\/?[a-z][\s\S]*>/i.test(value) +} + +export type AboutSoftwareBody = + | { kind: 'html'; html: string } + | { kind: 'text'; text: string } + +/** اولویت با تنظیم about_software؛ در صورت خالی بودن، متن پیش‌فرض */ +export function resolveAboutSoftwareBody( + raw: unknown, + projectName: string, +): AboutSoftwareBody { + const value = String(raw ?? '').trim() + if (!value) { + return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) } + } + if (looksLikeHtml(value)) { + const stripped = value + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/\s+/g, ' ') + .trim() + if (!stripped) { + return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) } + } + return { kind: 'html', html: value } + } + return { kind: 'text', text: value } +} diff --git a/src/views/base/aboutSoftware/index.vue b/src/views/base/aboutSoftware/index.vue new file mode 100644 index 0000000..3078e65 --- /dev/null +++ b/src/views/base/aboutSoftware/index.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/views/base/copper&silver/index.vue b/src/views/base/copper&silver/index.vue index 89f9bba..293c8a6 100644 --- a/src/views/base/copper&silver/index.vue +++ b/src/views/base/copper&silver/index.vue @@ -155,39 +155,20 @@
-
-
-
وزن:
-
-
{{ form.goldValue }}
-
گرم
-
-
-
-
مظنه:
-
-
{{ usePriceWithoutType(Math.round(basePriceValue ?? 0)) }}
-
ریال
-
-
-
-
کارمزد:
-
-
{{ feeInGrams }}
-
گرم
-
-
-
(معادل {{ equivalentPrice }} ریال)
-
-
{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}
-
-
{{ usePriceWithoutType(payablePrice.toFixed(0)) }}
-
ریال
-
-
- -
- ثبت نهایی + + ثبت نهایی
@@ -222,6 +203,7 @@ import { getLastPrice } from '@/api/home' import Icon from '@/components/BasicPacks/BasicIcon.vue' import { useUserStore } from '@/store/userStore' import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue' +import TradeFeeConfirmSummary from '@/components/TradeFeeConfirmSummary.vue' import { justNumber } from '@/utils/inputRules' import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue' import GoldLoading from '@/components/GoldLoading.vue' diff --git a/src/views/base/educate/composables/useEducatePage.ts b/src/views/base/educate/composables/useEducatePage.ts index 679e55a..3cc9349 100644 --- a/src/views/base/educate/composables/useEducatePage.ts +++ b/src/views/base/educate/composables/useEducatePage.ts @@ -65,10 +65,12 @@ export function useEducatePage() { formRef.value?.validateFields().then(async () => { try { loading.value = true - await updateMe(userFormState.value) + const res: any = await updateMe(userFormState.value) await userStore.getMyInfo() editPassVisible.value = false - message.success('اطلاعات شما با موفقیت به روز رسانی شد') + message.success( + res?.message || 'اطلاعات شما با موفقیت به روز رسانی شد', + ) } catch (e) { console.log(e) } finally { diff --git a/src/views/base/gold/desktop/styles/desktop-gold.less b/src/views/base/gold/desktop/styles/desktop-gold.less index c6fb445..2c64809 100644 --- a/src/views/base/gold/desktop/styles/desktop-gold.less +++ b/src/views/base/gold/desktop/styles/desktop-gold.less @@ -10,7 +10,7 @@ } .gold-embed { - max-height: min(70vh, 35rem); + max-height: min(78vh, 40rem); overflow-x: hidden; overflow-y: auto; width: 100%; diff --git a/src/views/base/gold/index.vue b/src/views/base/gold/index.vue index 0134c65..e01adc2 100644 --- a/src/views/base/gold/index.vue +++ b/src/views/base/gold/index.vue @@ -18,33 +18,45 @@
- -
-
- خرید {{ tradeProductLabel }} +
+
+ خرید
-
- فروش {{ tradeProductLabel }} +
+ فروش
- -
+ class="w-full relative rounded-2xl h-[88px] flex flex-col justify-center background-color-transition">
-
{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}
+
+ {{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }} + + {{ tradeProductLabel }} + +
{{ usePriceWithoutType(goldPriceValue) }} @@ -58,7 +70,7 @@
-
+
@@ -137,7 +149,7 @@ موجودی کیف پول ناکافی می باشد.
-
+
کارمزد:
@@ -162,51 +174,19 @@
-
-
-
وزن:
-
-
{{ displayTradeGoldAmount }}
-
{{ weightUnit }}
-
-
-
-
مظنه:
-
-
{{ usePriceWithoutType(Math.round(goldPriceValue)) }}
-
ریال
-
-
-
-
کارمزد:
-
-
{{ feeInGrams }}
-
{{ weightUnit }}
-
-
-
(معادل {{ equivalentPrice }} ریال)
-
-
طلا دریافتی:
-
-
{{ displayTradeGoldAmount }}
-
{{ weightUnit }} (دقیق — کارمزد از ریال)
-
-
-
-
کسر از کیف طلا:
-
-
{{ sellGoldDebit }}
-
{{ weightUnit }}
-
-
-
-
{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}
-
-
{{ usePriceWithoutType(payablePrice.toFixed(0)) }}
-
ریال
-
-
-
+ -
-
-
وزن:
-
-
{{ displayTradeGoldAmount }}
-
{{ weightUnit }}
-
-
-
-
مظنه:
-
-
{{ usePriceWithoutType(Math.round(goldPriceValue)) }}
-
ریال
-
-
-
-
کارمزد:
-
-
{{ feeInGrams }}
-
{{ weightUnit }}
-
-
-
(معادل {{ equivalentPrice }} ریال)
-
-
طلا دریافتی:
-
-
{{ displayTradeGoldAmount }}
-
{{ weightUnit }} (دقیق — کارمزد از ریال)
-
-
-
-
کسر از کیف طلا:
-
-
{{ sellGoldDebit }}
-
{{ weightUnit }}
-
-
-
-
{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}
-
-
{{ usePriceWithoutType(payablePrice.toFixed(0)) }}
-
ریال
-
-
-
+ ثبت نهایی @@ -311,6 +259,7 @@ import Icon from '@/components/BasicPacks/BasicIcon.vue' import { useUserStore } from '@/store/userStore' import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue' import BasicModal from '@/components/BasicPacks/BasicModal.vue' +import TradeFeeConfirmSummary from '@/components/TradeFeeConfirmSummary.vue' import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout' import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue' import { justNumber } from '@/utils/inputRules' @@ -398,7 +347,7 @@ const contentClass = computed(() => { if (isStandaloneMobile.value) { return 'px-3 mpage-content mpage-content--with-subheader pb-[calc(var(--app-bottom-chrome-height)+1rem)]' } - return 'mt-5 px-3 pb-4' + return 'mt-5 px-3 pb-10' }) const isBuy = ref(true) const inputLock = ref(null) diff --git a/src/views/base/home/components/CurrentPrice.vue b/src/views/base/home/components/CurrentPrice.vue index 69309ba..cf51e41 100644 --- a/src/views/base/home/components/CurrentPrice.vue +++ b/src/views/base/home/components/CurrentPrice.vue @@ -3,7 +3,18 @@ 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" > -