From 9356a2ab5e8bf57e21bf30e1fcb785cba22bd35c Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Wed, 2 Sep 2026 08:26:52 -0400 Subject: [PATCH] Port modular price, product, and PWA fixes into boursecu Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor --- .env.example | 14 + .gitignore | 5 + components.d.ts | 4 + index.html | 123 ++- public/lottie/emptyList.json | 1 + public/lottie/goldBars.json | 1 + public/lottie/logoutConfirm.json | 1 + src/App.vue | 45 +- src/api/config.ts | 9 +- src/api/products.ts | 4 + src/api/superadminSettings.ts | 41 + src/app/bootstrap.ts | 13 +- src/app/setupRouter.ts | 40 +- src/assets/css/main.less | 1 + src/assets/css/mobile-keyboard-scroll.less | 51 + src/components/AppCreditLink.vue | 2 +- .../BasicPacks/BasicActionSheet.vue | 9 +- src/components/BasicPacks/BasicInput.vue | 26 +- .../BasicPriceInput/src/BasicPriceInput.vue | 4 +- src/components/EnamadBadge.vue | 70 ++ src/components/GoldDayCalculatorModal.vue | 303 ++++++ src/components/ProductImage.vue | 64 ++ src/components/TradeFeeConfirmSummary.vue | 227 +++++ src/composables/useGoldDayCalculator.ts | 12 + src/composables/useSupportMessenger.ts | 41 +- src/composables/useTrackingSystemsFeature.ts | 21 + src/config/apply-config.ts | 19 +- src/config/config-cache.ts | 61 ++ src/config/load-config.ts | 254 ++++- src/config/load-version.ts | 94 +- src/config/superadmin.ts | 130 +++ src/layouts/desktop/DesktopSidebar.vue | 16 +- src/main.ts | 5 +- src/models/PriceTrigger.ts | 2 + src/models/home.ts | 4 + src/models/products.ts | 3 + src/models/transaction.ts | 2 +- src/modules/pwa/manifest-cache.ts | 37 + src/modules/pwa/manifest.ts | 147 ++- src/modules/version/VersionUpdateModal.vue | 11 + src/router/index.ts | 21 + src/store/settingsStore.ts | 49 +- src/store/userStore.ts | 6 +- src/utils/chat.ts | 5 +- src/utils/depositGuideText.ts | 23 +- src/utils/file.ts | 75 +- src/utils/lottieAssets.ts | 37 + src/utils/mobileInputKeyboardScroll.ts | 176 ++++ src/utils/persianDigits.ts | 64 +- src/utils/price.ts | 103 +- src/utils/priceSocket.ts | 230 ++++- src/utils/productAssets.ts | 16 +- src/utils/productCashFee.ts | 8 +- src/utils/productDealMethod.ts | 6 + src/utils/safeAreaInsets.ts | 31 +- src/utils/serverUnavailable.ts | 76 ++ src/utils/serviceFee.ts | 194 +++- src/utils/specialPricing.ts | 80 ++ src/utils/storeHours.ts | 228 ++++- src/utils/superAdminMenuGates.ts | 37 + src/utils/supportMessenger.ts | 10 +- src/utils/trackingSystemsSettings.ts | 145 +++ src/utils/useAxios.ts | 53 +- .../aboutSoftware/aboutSoftwareContent.ts | 35 + src/views/base/aboutSoftware/index.vue | 181 ++++ src/views/base/copper&silver/index.vue | 48 +- .../educate/composables/useEducatePage.ts | 6 +- .../gold/desktop/styles/desktop-gold.less | 2 +- src/views/base/gold/index.vue | 165 ++-- .../base/home/components/CurrentPrice.vue | 22 +- .../home/components/LogoutConfirmDialog.vue | 28 +- src/views/base/home/composables/useBanners.ts | 10 +- .../desktop/components/DesktopLivePrice.vue | 23 +- .../components/DesktopQuickActionsGrid.vue | 8 +- .../components/DesktopQuickActionsPanel.vue | 8 +- src/views/base/home/indexgolak.vue | 4 +- .../layouts/configs/boursecuQuickActions.ts | 7 + .../sections/BoursecuQuickActionsSection.vue | 8 +- src/views/base/home/layouts/types.ts | 3 +- .../components/PhysicalReceiveCartRow.vue | 6 +- .../components/RequestList.vue | 11 +- .../styles/desktop-physical-receive.less | 4 +- src/views/base/physicalReceive/index.vue | 33 +- .../base/priceTrigger/components/Item.vue | 29 +- .../components/PriceTriggerForm.vue | 19 +- .../composables/usePriceTriggerPage.ts | 44 +- .../base/products/components/DealProduct.vue | 61 +- .../composables/useProductsPageData.ts | 54 +- .../components/RecoverySitesList.vue | 126 +++ .../composables/useRecoverySitesPage.ts | 89 ++ .../desktop/DesktopRecoverySitesView.vue | 115 +++ src/views/base/recoverySites/index.vue | 33 + .../mobile/MobileRecoverySitesView.vue | 35 + .../components/TransactionDetailPanel.vue | 4 + .../composables/useTransactionsPageData.ts | 4 +- .../base/transactions/config/boursecuTabs.ts | 2 +- src/views/base/tv/index.vue | 188 ++++ .../base/user/composables/useUserPage.ts | 37 +- .../components/WalletDepositGuideModal.vue | 30 +- .../base/wallet/composables/useWalletPage.ts | 6 +- src/views/login/components/Login.vue | 15 +- src/views/login/components/Register.vue | 22 +- src/views/login/composables/useLoginForm.ts | 6 +- .../login/composables/useRegisterForm.ts | 12 +- .../desktop/components/DesktopLoginForm.vue | 9 +- .../components/DesktopRegisterForm.vue | 9 + src/views/tv/composables/useTvMonitorPage.ts | 315 ++++++ src/views/tv/composables/useTvTheme.ts | 211 ++++ src/views/tv/index.vue | 926 ++++++++++++++++++ src/vite-env.d.ts | 13 +- tsconfig.app.json | 1 - vite.config.ts | 10 + 112 files changed, 6044 insertions(+), 583 deletions(-) create mode 100644 public/lottie/emptyList.json create mode 100644 public/lottie/goldBars.json create mode 100644 public/lottie/logoutConfirm.json create mode 100644 src/api/superadminSettings.ts create mode 100644 src/assets/css/mobile-keyboard-scroll.less create mode 100644 src/components/EnamadBadge.vue create mode 100644 src/components/GoldDayCalculatorModal.vue create mode 100644 src/components/ProductImage.vue create mode 100644 src/components/TradeFeeConfirmSummary.vue create mode 100644 src/composables/useGoldDayCalculator.ts create mode 100644 src/composables/useTrackingSystemsFeature.ts create mode 100644 src/config/superadmin.ts create mode 100644 src/utils/lottieAssets.ts create mode 100644 src/utils/mobileInputKeyboardScroll.ts create mode 100644 src/utils/serverUnavailable.ts create mode 100644 src/utils/specialPricing.ts create mode 100644 src/utils/superAdminMenuGates.ts create mode 100644 src/utils/trackingSystemsSettings.ts create mode 100644 src/views/base/aboutSoftware/aboutSoftwareContent.ts create mode 100644 src/views/base/aboutSoftware/index.vue create mode 100644 src/views/base/recoverySites/components/RecoverySitesList.vue create mode 100644 src/views/base/recoverySites/composables/useRecoverySitesPage.ts create mode 100644 src/views/base/recoverySites/desktop/DesktopRecoverySitesView.vue create mode 100644 src/views/base/recoverySites/index.vue create mode 100644 src/views/base/recoverySites/mobile/MobileRecoverySitesView.vue create mode 100644 src/views/base/tv/index.vue create mode 100644 src/views/tv/composables/useTvMonitorPage.ts create mode 100644 src/views/tv/composables/useTvTheme.ts create mode 100644 src/views/tv/index.vue 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" > -