diff --git a/components.d.ts b/components.d.ts index 4ea8046..da14038 100644 --- a/components.d.ts +++ b/components.d.ts @@ -65,6 +65,8 @@ declare module 'vue' { StartEnd: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/start-end.vue')['default'] Suffix: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/suffix.vue')['default'] SupportContactBar: typeof import('./src/components/SupportContactBar.vue')['default'] + SupportMessengerBrandIcon: typeof import('./src/components/SupportMessengerBrandIcon.vue')['default'] + SupportMessengerPicker: typeof import('./src/components/SupportMessengerPicker.vue')['default'] 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'] diff --git a/index.html b/index.html index 3348930..f9c5aa7 100644 --- a/index.html +++ b/index.html @@ -33,15 +33,19 @@ body { margin: 0; min-height: 100%; + width: 100%; background: var(--boot-bg); } #app { min-height: 100vh; + width: 100%; + max-width: none; } .boot-loader { min-height: 100vh; + width: 100%; display: flex; align-items: center; justify-content: center; @@ -85,13 +89,22 @@ display: block !important; } } + #app, + .boot-loader { + width: 100% !important; + max-width: none !important; + } .app-splash-screen { - position: fixed; - inset: 0; - width: 100vw; - min-height: 100vh; + position: fixed !important; + inset: 0 !important; + width: 100vw !important; + min-width: 100vw !important; + max-width: none !important; + min-height: 100vh !important; left: 0 !important; right: 0 !important; + margin: 0 !important; + transform: none !important; z-index: 2147483646; } diff --git a/src/App.vue b/src/App.vue index 9dd716e..edb6fb0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,4 +1,19 @@ @@ -68,13 +95,11 @@ import {getProjectFontFamily, getProjectSplashBackground, getProjectSplashLogo, import {ConfigProvider as AConfigProvider} from 'ant-design-vue' import BasicIcon from "@/components/BasicPacks/BasicIcon.vue"; import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout' -import { - buildSupportMessengerUrl, - loadSupportMessengerFromSettings, - supportMessengerSettings, -} from '@/utils/supportMessenger' +import { useSupportMessenger } from '@/composables/useSupportMessenger' +import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger' import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton' import AppCreditLink from '@/components/AppCreditLink.vue' +import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue' import { useRoute } from 'vue-router' import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset' @@ -95,47 +120,25 @@ const config = useConfigStore() const showSplash = ref(true) const popVisible = ref(false) -const supportLink = computed(() => { - return buildSupportMessengerUrl( - supportMessengerSettings.value.messenger, - supportMessengerSettings.value.messengerId - ) -}) +const { + supportPickerOpen, + supportPickerAnchor, + supportMessengerEntries, + messengerMeta, + hasMessengerLink: showSupportButton, + openMessengerEntry, + openSupportMessenger, +} = useSupportMessenger() -const showSupportButton = computed(() => !!supportLink.value) +const supportButtonEl = ref(null) +const pickerAnchorEl = computed(() => supportPickerAnchor.value ?? supportButtonEl.value) -const supportIcon = computed(() => { - const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase() - switch (messenger) { - case 'whatsapp': - return 'vuesax-linear:whatsapp' - case 'telegram': - return 'vuesax-linear:send-2' - case 'bale': - return 'vuesax-linear:messages-2' - default: - return 'vuesax-linear:messages-2' - } -}) +const supportIcon = computed(() => messengerMeta.value.icon) const supportAccentColor = computed(() => { - const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase() - switch (messenger) { - case 'whatsapp': - return '#22c55e' - case 'telegram': - return '#0ea5e9' - case 'bale': - return '#16a34a' - case 'rubika': - return '#7c3aed' - case 'eitaa': - return '#f59e0b' - case 'soroush': - return '#2563eb' - default: - return config.config?.color || '#334155' - } + const accent = messengerMeta.value.accent + if (accent !== '#334155') return accent + return config.config?.color || accent }) const { @@ -153,14 +156,14 @@ const { }) function openSupportLink() { - if (!supportLink.value) return - window.open(supportLink.value, '_blank', 'noopener,noreferrer') + void openSupportMessenger(supportButtonEl.value) } function handleSupportPointerUp(event: PointerEvent) { - if (supportPointerUp(event)) { - openSupportLink() - } + const clicked = supportPointerUp(event) + if (!clicked) return + event.stopPropagation() + openSupportLink() } const antTheme = computed(() => { @@ -196,11 +199,19 @@ onMounted(async () => { }, 2000) }) -watch(showSplash, (visible) => { - if (!visible) { +watch( + showSplash, + (visible) => { + if (visible) { + const splashBg = getProjectSplashBackground() + document.documentElement.style.backgroundColor = splashBg + document.body.style.backgroundColor = splashBg + return + } resetAppSurfaceBackground() - } -}) + }, + { immediate: true }, +) watch( () => route.name, @@ -236,6 +247,8 @@ watch( --app-z-dialog: 5300; /* زیر لایه دراپ‌داون اینت (99999) تا تقویم/سلکت داخل مدال روی آن باز شود */ --app-z-modal: 5500; + --app-z-support-picker: 5600; + --app-z-support-fab-open: 5615; } .app-main { @@ -310,15 +323,30 @@ watch( } &--sidebar { - flex-direction: column; - gap: 6px; - padding: 4px; + flex-direction: row; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + gap: 8px; + width: 100%; + max-width: 100%; + padding: 4px 4px; color: #9ca3af; - font-size: 10px; + font-size: 11px; + line-height: 1.35; + text-align: center; + box-sizing: border-box; + + .app-credit-link__text { + white-space: nowrap; + flex-shrink: 0; + } .app-credit-link__logo { height: 15px; - opacity: 0.85; + max-width: 68px; + flex-shrink: 0; + opacity: 0.9; } } @@ -340,11 +368,15 @@ watch( html.project-boursecu { .app-credit-link--sidebar { - color: rgba(255, 255, 255, 0.5); + color: rgba(255, 255, 255, 0.55); .app-credit-link__logo { - filter: brightness(1.15); - opacity: 0.75; + filter: brightness(1.2); + opacity: 0.85; + } + + &:hover { + color: rgba(255, 255, 255, 0.85); } } @@ -362,50 +394,7 @@ html.project-boursecu { } #support-button { - position: fixed; - z-index: 5100; - cursor: grab; - user-select: none; - border-radius: 9999px; - - &.is-dragging { - cursor: grabbing; - } -} - -.support-button-inner { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 14px 9px 12px; - background: #ffffff; - border: 1px solid rgba(15, 23, 42, 0.08); - border-radius: 9999px; - box-shadow: 0 6px 20px rgba(15, 23, 42, 0.14); - color: #1e293b; - font-size: 13px; - font-weight: 600; - line-height: 1; - direction: rtl; - transition: box-shadow 0.2s ease, transform 0.2s ease; - - .support-button-icon { - color: var(--support-accent, #334155); - flex-shrink: 0; - } - - .support-button-label { - white-space: nowrap; - } -} - -#support-button:not(.is-dragging):hover .support-button-inner { - transform: translateY(-1px); - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18); -} - -#support-button.is-dragging .support-button-inner { - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22); + /* جای draggable از fabStyle می‌آید */ } @media screen and (min-width: 426px) and (max-width: 767.98px) { diff --git a/src/assets/messengers/bale.png b/src/assets/messengers/bale.png new file mode 100644 index 0000000..7b41254 Binary files /dev/null and b/src/assets/messengers/bale.png differ diff --git a/src/assets/messengers/call.svg b/src/assets/messengers/call.svg new file mode 100644 index 0000000..0ebc7ab --- /dev/null +++ b/src/assets/messengers/call.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/assets/messengers/eitaa.png b/src/assets/messengers/eitaa.png new file mode 100644 index 0000000..87d0cef Binary files /dev/null and b/src/assets/messengers/eitaa.png differ diff --git a/src/assets/messengers/rubika.png b/src/assets/messengers/rubika.png new file mode 100644 index 0000000..d764418 Binary files /dev/null and b/src/assets/messengers/rubika.png differ diff --git a/src/assets/messengers/sms.svg b/src/assets/messengers/sms.svg new file mode 100644 index 0000000..9902c71 --- /dev/null +++ b/src/assets/messengers/sms.svg @@ -0,0 +1,7 @@ + + + + diff --git a/src/assets/messengers/soroush.svg b/src/assets/messengers/soroush.svg new file mode 100644 index 0000000..f3dee3e --- /dev/null +++ b/src/assets/messengers/soroush.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/messengers/telegram.svg b/src/assets/messengers/telegram.svg new file mode 100644 index 0000000..53458c5 --- /dev/null +++ b/src/assets/messengers/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/messengers/whatsapp.svg b/src/assets/messengers/whatsapp.svg new file mode 100644 index 0000000..b2444e2 --- /dev/null +++ b/src/assets/messengers/whatsapp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/SupportContactBar.vue b/src/components/SupportContactBar.vue index 8822b10..c6838d4 100644 --- a/src/components/SupportContactBar.vue +++ b/src/components/SupportContactBar.vue @@ -1,32 +1,100 @@ + + + + + + + + + + + + + diff --git a/src/components/SupportMessengerBrandIcon.vue b/src/components/SupportMessengerBrandIcon.vue new file mode 100644 index 0000000..f1de77b --- /dev/null +++ b/src/components/SupportMessengerBrandIcon.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/SupportMessengerPicker.vue b/src/components/SupportMessengerPicker.vue new file mode 100644 index 0000000..7214970 --- /dev/null +++ b/src/components/SupportMessengerPicker.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/src/composables/useSupportMessenger.ts b/src/composables/useSupportMessenger.ts new file mode 100644 index 0000000..1506d5d --- /dev/null +++ b/src/composables/useSupportMessenger.ts @@ -0,0 +1,266 @@ +import { computed, ref } from 'vue' +import { message } from 'ant-design-vue' +import { useConfigStore } from '@/store/configStore' +import { + getProjectBaleContactLink, + getProjectSupportContactLink, + getProjectSupportPhoneLink, + isBazretalaProject, +} from '@/utils/config' +import { + buildDepositSmsUrl, + buildSupportPhoneUrl, + loadSupportMessengerFromSettings, + messengerNeedsClipboardCopy, + openSupportMessengerEntry, + openSupportMessengerWithMessage, + resolveDepositGuideText, + resolveDepositSupportMessage, + resolveSupportMessengerEntries, + resolveSupportMessengerLink, + resolveSupportMessengerMeta, + supportMessengerPickerAnchor, + supportMessengerPickerOpen, + supportMessengerSettings, + type ResolvedSupportMessengerEntry, +} from '@/utils/supportMessenger' +import { + copyDepositFieldValue, + displayDepositCardNumber, + displayDepositSheba, + getDepositGuideVarsFromSettings, + getDepositSettingLabel, + hasDepositFieldValue, +} from '@/utils/depositGuideText' +import { useSettingsStore } from '@/store/settingsStore' + +/** plain: منوی شناور / درباره ما — بدون پیش‌متن | deposit: مودال واریز — با deposit_support_message_text */ +type SupportPickerIntent = 'plain' | 'deposit' + +export function useSupportMessenger() { + const config = useConfigStore() + const settingsStore = useSettingsStore() + const supportPickerOpen = supportMessengerPickerOpen + const supportPickerIntent = ref('plain') + + const legacySupportLink = computed(() => { + if (config.config?.name === 'garoosi' || isBazretalaProject()) { + return getProjectBaleContactLink() + } + return getProjectSupportContactLink() + }) + + const depositSupportMessage = computed(() => resolveDepositSupportMessage()) + + /** لینک‌های تمیز بدون پیش‌متن — متن فقط موقع باز کردن (قصد deposit) اضافه می‌شود */ + const supportMessengerEntries = computed(() => + resolveSupportMessengerEntries(supportMessengerSettings.value), + ) + + const primaryMessengerEntry = computed(() => supportMessengerEntries.value[0] ?? null) + + const messengerType = computed( + () => (primaryMessengerEntry.value?.messenger ?? supportMessengerSettings.value.messenger ?? '').trim().toLowerCase(), + ) + + const messengerMeta = computed(() => + primaryMessengerEntry.value + ? { + label: primaryMessengerEntry.value.label, + icon: primaryMessengerEntry.value.icon, + accent: primaryMessengerEntry.value.accent, + } + : resolveSupportMessengerMeta(messengerType.value), + ) + + const messengerLink = computed(() => + primaryMessengerEntry.value?.url + || resolveSupportMessengerLink( + supportMessengerSettings.value.messenger, + supportMessengerSettings.value.messengerId, + legacySupportLink.value, + ), + ) + + async function openMessengerEntry(entry: ResolvedSupportMessengerEntry, text?: string) { + // فقط وقتی از مودال واریز آمده‌ایم پیش‌نویس می‌گذاریم؛ منوی شناور همیشه خالی + const messageText = + text !== undefined + ? text.trim() + : supportPickerIntent.value === 'deposit' + ? depositSupportMessage.value.trim() + : '' + const opened = await openSupportMessengerEntry(entry, messageText, legacySupportLink.value) + if (!opened) return false + + if (messageText && messengerNeedsClipboardCopy(entry.messenger)) { + message.success('متن پیام کپی شد — در پیام‌رسان paste کنید') + } + return true + } + + let supportPickerOpenedAt = 0 + + async function openSupportPicker( + intent: SupportPickerIntent, + anchorEl?: HTMLElement | null, + ) { + supportPickerIntent.value = intent + if (anchorEl !== undefined) { + supportMessengerPickerAnchor.value = anchorEl + } + + const draft = + intent === 'deposit' ? depositSupportMessage.value.trim() : '' + const entries = supportMessengerEntries.value + + if (entries.length === 0) { + return openSupportMessengerWithMessage( + supportMessengerSettings.value.messenger, + supportMessengerSettings.value.messengerId, + draft, + legacySupportLink.value, + ) + } + + if (entries.length === 1) { + return openMessengerEntry(entries[0], draft) + } + + const now = Date.now() + if (supportPickerOpen.value) { + if (now - supportPickerOpenedAt < 450) return true + supportPickerOpen.value = false + return true + } + + supportPickerOpen.value = true + supportPickerOpenedAt = now + return true + } + + /** منوی شناور / صفحات درباره ما — بدون پیش‌نویس پیام */ + async function openSupportMessenger(anchorEl?: HTMLElement | null) { + return openSupportPicker('plain', anchorEl) + } + + /** مودال راهنمای واریز — با متن deposit_support_message_text */ + async function openDepositSupportMessenger(anchorEl?: HTMLElement | null) { + return openSupportPicker('deposit', anchorEl) + } + + function handleSupportMessengerClick(event?: Event) { + const target = event?.currentTarget + const anchor = target instanceof HTMLElement ? target : null + void openSupportMessenger(anchor) + } + + const depositSmsLink = computed(() => + buildDepositSmsUrl( + supportMessengerSettings.value.depositSmsSupportNumber, + depositSupportMessage.value, + ), + ) + + const supportPhoneLink = computed(() => { + const fromSettings = buildSupportPhoneUrl( + supportMessengerSettings.value.depositSmsSupportNumber, + ) + if (fromSettings) return fromSettings + + const legacyPhone = getProjectSupportPhoneLink() + if (legacyPhone && legacyPhone !== 'tel:') return legacyPhone + + const legacy = legacySupportLink.value + if (legacy.startsWith('tel:') && legacy.length > 4) return legacy + + return '' + }) + + const supportMessengerText = computed(() => resolveDepositGuideText()) + + const depositCardNumberRaw = computed(() => { + void settingsStore.settings + const fromMessenger = supportMessengerSettings.value.depositCardNumber?.trim() + if (fromMessenger) return fromMessenger + return String(getDepositGuideVarsFromSettings().cardNumber ?? '').trim() + }) + + const depositShebaRaw = computed(() => { + void settingsStore.settings + const fromMessenger = supportMessengerSettings.value.depositSheba?.trim() + if (fromMessenger) return fromMessenger + return String(getDepositGuideVarsFromSettings().sheba ?? '').trim() + }) + + const depositCardNumber = computed(() => displayDepositCardNumber(depositCardNumberRaw.value)) + const depositSheba = computed(() => displayDepositSheba(depositShebaRaw.value)) + + const depositCardLabel = computed(() => { + void settingsStore.settings + return getDepositSettingLabel('deposit_card_number', 'شماره کارت واریز') + }) + + const depositShebaLabel = computed(() => { + void settingsStore.settings + return getDepositSettingLabel('deposit_sheba', 'شبا واریز') + }) + + const hasDepositCardValue = computed(() => hasDepositFieldValue(depositCardNumberRaw.value)) + const hasDepositShebaValue = computed(() => hasDepositFieldValue(depositShebaRaw.value)) + + async function copyDepositCardNumber() { + await copyDepositFieldValue( + depositCardNumberRaw.value, + 'card', + 'شماره کارت ثبت نشده است', + ) + } + + async function copyDepositShebaNumber() { + await copyDepositFieldValue( + depositShebaRaw.value, + 'sheba', + 'شماره شبا ثبت نشده است', + ) + } + + const hasMessengerLink = computed(() => supportMessengerEntries.value.length > 0 || !!messengerLink.value) + const hasMultipleMessengers = computed(() => supportMessengerEntries.value.length > 1) + const hasSupportPhoneLink = computed(() => !!supportPhoneLink.value) + const hasSupportActions = computed(() => hasSupportPhoneLink.value || hasMessengerLink.value) + const opensInNewTab = computed(() => /^https?:\/\//i.test(messengerLink.value)) + + return { + supportPickerOpen, + supportPickerAnchor: supportMessengerPickerAnchor, + supportMessengerEntries, + messengerType, + messengerMeta, + messengerLink, + supportPhoneLink, + depositSmsLink, + supportMessengerText, + depositSupportMessage, + depositCardNumber, + depositSheba, + depositCardNumberRaw, + depositShebaRaw, + depositCardLabel, + depositShebaLabel, + hasDepositCardValue, + hasDepositShebaValue, + copyDepositCardNumber, + copyDepositShebaNumber, + openSupportMessenger, + openDepositSupportMessenger, + openMessengerEntry, + handleSupportMessengerClick, + hasMessengerLink, + hasMultipleMessengers, + hasSupportPhoneLink, + hasSupportActions, + opensInNewTab, + loadSupportMessengerFromSettings, + } +} diff --git a/src/layouts/composables/useAppNavigation.ts b/src/layouts/composables/useAppNavigation.ts index 7a65a9d..cc8a6cb 100644 --- a/src/layouts/composables/useAppNavigation.ts +++ b/src/layouts/composables/useAppNavigation.ts @@ -10,7 +10,17 @@ export function useAppNavigation() { const hideMenu = computed(() => !!route.meta.hideMenu) function isNavActive(routeName: string) { - return currentRoute.value === routeName + if (currentRoute.value === routeName) return true + + // parent route (e.g. physicalReceive → list/new) + if (route.matched.some((record) => record.name === routeName)) return true + + // fund detail/add are sibling routes of fund + if (routeName === 'fund') { + return currentRoute.value === 'fund_info' || currentRoute.value === 'fund_add' + } + + return false } function navItemClass(routeName: string) { diff --git a/src/layouts/config/navigation.ts b/src/layouts/config/navigation.ts index dffc6f3..2560f09 100644 --- a/src/layouts/config/navigation.ts +++ b/src/layouts/config/navigation.ts @@ -8,7 +8,7 @@ const BOURSECU_PRIMARY_NAV: AppNavItem[] = [ { id: 'buyProduct', routeName: 'buyProduct', - label: 'سفارش‌ها', + label: 'خرید و فروش', icon: 'vuesax-linear:box', iconSrc: getBoursecuActionIconSrc('buySell'), }, @@ -33,6 +33,20 @@ const BOURSECU_PRIMARY_NAV: AppNavItem[] = [ icon: 'vuesax-linear:wallet-1', iconSrc: getBoursecuActionIconSrc('wallet'), }, + { + id: 'fund', + routeName: 'fund', + label: 'دارایی ها', + icon: 'vuesax-linear:cup', + iconSrc: getBoursecuActionIconSrc('assets'), + }, + { + id: 'physicalReceive', + routeName: 'physicalReceive', + label: 'تحویل فیزیکی', + icon: 'vuesax-linear:direct-inbox', + iconSrc: getBoursecuActionIconSrc('physicalReceive'), + }, { id: 'user', routeName: 'user', diff --git a/src/layouts/desktop/DesktopSidebar.vue b/src/layouts/desktop/DesktopSidebar.vue index ac167eb..b438e78 100644 --- a/src/layouts/desktop/DesktopSidebar.vue +++ b/src/layouts/desktop/DesktopSidebar.vue @@ -8,31 +8,33 @@ /> - +
+ -
+
- + +
diff --git a/src/layouts/desktop/desktop-layout.less b/src/layouts/desktop/desktop-layout.less index e49d6cb..beb700e 100644 --- a/src/layouts/desktop/desktop-layout.less +++ b/src/layouts/desktop/desktop-layout.less @@ -95,6 +95,7 @@ justify-content: center; padding: 8px 12px 20px; margin-bottom: 4px; + flex-shrink: 0; } .desktop-sidebar__logo { @@ -105,27 +106,50 @@ object-fit: contain; } +.desktop-sidebar__nav-scroll { + flex: 1 1 auto; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0; + padding-bottom: 8px; + -webkit-overflow-scrolling: touch; +} + .desktop-sidebar__section { display: flex; flex-direction: column; gap: 4px; + flex-shrink: 0; } .desktop-sidebar__section--secondary { - margin-top: auto; + margin-top: 0; } .desktop-sidebar__credit { - margin-top: 12px; - padding: 12px 8px 4px; + flex-shrink: 0; + margin-top: 8px; + padding: 14px 10px 6px; display: flex; justify-content: center; + align-items: center; + width: 100%; + box-sizing: border-box; + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + +.desktop-sidebar--boursecu .desktop-sidebar__credit { + border-top-color: rgba(255, 255, 255, 0.12); } .desktop-sidebar__divider { height: 1px; background: rgba(0, 0, 0, 0.08); margin: 12px 8px; + flex-shrink: 0; } .desktop-nav-item { diff --git a/src/layouts/styles/desktop-shell.less b/src/layouts/styles/desktop-shell.less index d883511..8e0a2cf 100644 --- a/src/layouts/styles/desktop-shell.less +++ b/src/layouts/styles/desktop-shell.less @@ -77,15 +77,19 @@ html.app-layout--desktop { } } -/* Splash — always full viewport; never sidebar-offset */ +/* Splash — always full viewport; never sidebar-offset or 426px mobile frame */ .app-splash-screen { - position: fixed; - inset: 0; - width: 100vw; - min-height: 100vh; - max-width: none; + position: fixed !important; + inset: 0 !important; + width: 100vw !important; + min-width: 100vw !important; + max-width: none !important; + min-height: 100vh !important; + min-height: 100dvh !important; left: 0 !important; right: 0 !important; + margin: 0 !important; + transform: none !important; z-index: 1000000000000; &__gradient { diff --git a/src/layouts/styles/mobile-frame.less b/src/layouts/styles/mobile-frame.less index ee3aba8..2fd1d66 100644 --- a/src/layouts/styles/mobile-frame.less +++ b/src/layouts/styles/mobile-frame.less @@ -83,6 +83,15 @@ html:not(.app-layout--desktop) { justify-content: center; } + /* Prevent flex shrink-to-content: otherwise #app collapses and white + gutters appear beside splash/boot during refresh. */ + #app { + width: 100% !important; + max-width: 100% !important; + display: flex; + justify-content: center; + } + .desktopConfig { width: 426px !important; max-width: 426px !important; diff --git a/src/utils/config.ts b/src/utils/config.ts index 7881199..2272a6a 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -24,6 +24,11 @@ export function isBoursecuProject() { return true } +/** فقط برای سازگاری با منطق پشتیبانی modular — در boursecu همیشه false */ +export function isBazretalaProject() { + return false +} + const WALLET_DEPOSIT_QUICK_AMOUNTS_RIAL = [10_000_000, 50_000_000, 100_000_000] as const export function getWalletDepositQuickAmountsRial(): readonly number[] { diff --git a/src/utils/depositGuideText.ts b/src/utils/depositGuideText.ts new file mode 100644 index 0000000..93f288f --- /dev/null +++ b/src/utils/depositGuideText.ts @@ -0,0 +1,200 @@ +import { message } from 'ant-design-vue' +import { useSettingsStore } from '@/store/settingsStore' +import { useUserStore } from '@/store/userStore' + +export interface DepositGuideVars { + accountHolder?: string | null + cardNumber?: string | null + sheba?: string | null + /** نام مشتری واردشده — برای پیام پشتیبانی واریز */ + customerName?: string | null +} + +export const DEFAULT_DEPOSIT_GUIDE_TEXT = + 'جهت شارژ حساب کاربری خود، کلمه «شماره حساب» را برای پشتیبانی ارسال نمایید.' + +export const DEFAULT_DEPOSIT_SUPPORT_MESSAGE_TEXT = + 'باسلام، %نام هستم. کدپیگیری و مبلغ واریزی من:' + +/** الگوی متغیر نام — ٪نام / %نام / % نام / %نام% */ +const NAME_PLACEHOLDER_RE = /[%٪]\s*نام(?:\s*[%٪])?/gu +const CARD_PLACEHOLDER_RE = /[%٪]\s*کارت(?:\s*[%٪])?/gu +const SHEBA_PLACEHOLDER_RE = /[%٪]\s*شبا(?:\s*[%٪])?/gu + +export function getCustomerNameFromUser(): string { + try { + return String(useUserStore().userInfo?.name ?? '').trim() + } catch { + return '' + } +} + +/** نام نمایشی در پیام پشتیبانی: اول مشتری، بعد نام صاحب حساب تنظیمات */ +function resolveSupportMessageName(vars: DepositGuideVars): string { + const customerName = String(vars.customerName ?? '').trim() + if (customerName) return customerName + return String(vars.accountHolder ?? '').trim() +} + +/** فقط %نام — برای پیام SMS/پیام‌رسان پشتیبانی (نه کارت/شبا) */ +export function renderDepositSupportMessageText( + template: string, + vars: DepositGuideVars, +): string { + const name = resolveSupportMessageName(vars) + return template.replace(NAME_PLACEHOLDER_RE, name) +} + +/** جایگزینی متغیرها در متن راهنمای واریز — ٪نام / %نام، ٪کارت / %کارت، ٪شبا / %شبا */ +export function renderDepositGuideText( + template: string, + vars: DepositGuideVars, +): string { + const accountHolder = String(vars.accountHolder ?? '').trim() + const cardNumber = String(vars.cardNumber ?? '').trim() + const sheba = String(vars.sheba ?? '').trim() + + return template + .replace(SHEBA_PLACEHOLDER_RE, sheba) + .replace(CARD_PLACEHOLDER_RE, cardNumber) + .replace(NAME_PLACEHOLDER_RE, accountHolder) +} + +export function getDepositGuideVarsFromSettings(): DepositGuideVars { + const customerName = getCustomerNameFromUser() + try { + const store = useSettingsStore() + return { + accountHolder: store.getSettingByKey('deposit_account_holder'), + cardNumber: store.getSettingByKey('deposit_card_number'), + sheba: store.getSettingByKey('deposit_sheba'), + customerName, + } + } catch { + return { customerName } + } +} + +export function getDepositGuideTemplate(): string { + try { + const fromSettings = String(useSettingsStore().getSettingByKey('support_messenger_text') ?? '').trim() + if (fromSettings) return fromSettings + } catch { + // settings store may not be ready yet + } + return DEFAULT_DEPOSIT_GUIDE_TEXT +} + +export function getDepositSupportMessageTemplate(): string { + try { + const fromSettings = String( + useSettingsStore().getSettingByKey('deposit_support_message_text') ?? '', + ).trim() + if (fromSettings) return fromSettings + } catch { + // settings store may not be ready yet + } + return DEFAULT_DEPOSIT_SUPPORT_MESSAGE_TEXT +} + +export function getRenderedDepositGuideText(vars?: DepositGuideVars): string { + return renderDepositGuideText(getDepositGuideTemplate(), vars ?? getDepositGuideVarsFromSettings()) +} + +export function getRenderedDepositSupportMessage(vars?: DepositGuideVars): string { + return renderDepositSupportMessageText( + getDepositSupportMessageTemplate(), + vars ?? getDepositGuideVarsFromSettings(), + ) +} + +export function formatDepositCardNumber(value?: string | null) { + const digits = String(value ?? '').replace(/\D/g, '') + if (!digits) return '' + + return digits.replace(/(\d{4})(?=\d)/g, '$1 ').trim() +} + +export function formatDepositSheba(value?: string | null) { + const normalized = String(value ?? '').replace(/\s/g, '').toUpperCase() + if (!normalized) return '' + + const digits = normalized.replace(/^IR/i, '').replace(/\D/g, '') + if (!digits) return normalized + + const full = `IR${digits}` + return full.replace(/(.{4})/g, '$1 ').trim() +} + +export function hasDepositFieldValue(value?: string | null) { + return Boolean(String(value ?? '').trim()) +} + +export function displayDepositCardNumber(value?: string | null) { + return formatDepositCardNumber(value) || '**** **** **** ****' +} + +export function displayDepositSheba(value?: string | null) { + return formatDepositSheba(value) || 'IR** **** **** **** **** **** **' +} + +export function getDepositSettingLabel(key: string, fallback: string) { + try { + const item = useSettingsStore().settings.find( + (setting) => String(setting?.key ?? '') === key, + ) + return item?.title_fa?.trim() || fallback + } catch { + return fallback + } +} + +function normalizeDepositCopyValue(value: string, kind: 'card' | 'sheba') { + const normalized = value.replace(/\s/g, '') + if (!normalized) return '' + + if (kind === 'sheba') { + const upper = normalized.toUpperCase() + if (upper.startsWith('IR')) return upper + const digits = upper.replace(/\D/g, '') + return digits ? `IR${digits}` : upper + } + + return normalized.replace(/\D/g, '') +} + +export async function copyDepositFieldValue( + value: string | null | undefined, + kind: 'card' | 'sheba', + emptyMessage: string, +) { + const raw = String(value ?? '').trim() + const normalized = normalizeDepositCopyValue(raw, kind) + + if (!normalized) { + message.warning(emptyMessage) + return + } + + try { + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(normalized) + } else if (typeof document !== 'undefined') { + const textarea = document.createElement('textarea') + textarea.value = normalized + textarea.setAttribute('readonly', 'true') + textarea.style.position = 'fixed' + textarea.style.left = '-9999px' + document.body.appendChild(textarea) + textarea.select() + document.execCommand('copy') + document.body.removeChild(textarea) + } else { + throw new Error('clipboard unavailable') + } + + message.info('کپی شد') + } catch { + message.error('کپی انجام نشد') + } +} diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts index 75d1ad1..22fbe78 100644 --- a/src/utils/supportMessenger.ts +++ b/src/utils/supportMessenger.ts @@ -1,5 +1,18 @@ import { ref } from 'vue' import { getSettings } from '@/api/configuration' +import type { DepositGuideVars } from '@/utils/depositGuideText' +import { + DEFAULT_DEPOSIT_GUIDE_TEXT, + DEFAULT_DEPOSIT_SUPPORT_MESSAGE_TEXT, + getCustomerNameFromUser, + getDepositGuideVarsFromSettings, + getDepositSupportMessageTemplate, + getRenderedDepositGuideText, + getRenderedDepositSupportMessage, + renderDepositGuideText, + renderDepositSupportMessageText, +} from '@/utils/depositGuideText' +import { useSettingsStore } from '@/store/settingsStore' export type SupportMessengerType = | 'bale' @@ -8,18 +21,277 @@ export type SupportMessengerType = | 'rubika' | 'eitaa' | 'soroush' + | 'call' + | 'sms' export interface SupportMessengerSettings { + messengers?: SupportMessengerEntry[] messenger?: SupportMessengerType | string messengerId?: string messengerText?: string + depositSupportMessageText?: string + depositAccountHolder?: string + depositCardNumber?: string + depositSheba?: string depositSmsSupportNumber?: string } -export const supportMessengerSettings = ref({}) +export interface SupportMessengerEntry { + messenger: string + id: string +} -export const DEFAULT_SUPPORT_MESSENGER_TEXT = - 'جهت شارژ حساب کاربری خود، کلمه «شماره حساب» را برای پشتیبانی ارسال نمایید.' +export interface ResolvedSupportMessengerEntry extends SupportMessengerEntry { + label: string + icon: string + accent: string + url: string +} + +const VALID_SUPPORT_MESSENGERS = new Set([ + 'bale', + 'telegram', + 'whatsapp', + 'rubika', + 'eitaa', + 'soroush', + 'call', + 'sms', +]) + +const PHONE_NATIVE_MESSENGERS = new Set(['call', 'sms']) + +export const supportMessengerSettings = ref({}) +export const supportMessengerPickerOpen = ref(false) +/** لنگر منوی انتخاب پیام‌رسان — FAB یا دکمه داخل مودال شارژ */ +export const supportMessengerPickerAnchor = ref(null) + +export function parseSupportMessengersJson(raw?: string | null): SupportMessengerEntry[] { + const value = (raw ?? '').trim() + if (!value) return [] + + try { + const decoded = JSON.parse(value) + if (!Array.isArray(decoded)) return [] + + return decoded + .map((row) => ({ + messenger: String(row?.messenger ?? '').trim().toLowerCase(), + id: String(row?.id ?? '').trim(), + })) + .filter((row) => VALID_SUPPORT_MESSENGERS.has(row.messenger) && row.id !== '') + } catch { + return [] + } +} + +export function resolveSupportMessengersFromSettings( + settings: Array<{ key?: string; value?: unknown }>, +): SupportMessengerEntry[] { + const fromJson = parseSupportMessengersJson(getSupportSettingValue(settings, 'support_messengers')) + if (fromJson.length > 0) return fromJson + + const legacyMessenger = getSupportSettingValue(settings, 'support_messenger').toLowerCase() + const legacyId = getSupportSettingValue(settings, 'support_messenger_id') + if (!legacyId) return [] + + const messenger = legacyMessenger || 'bale' + if (!VALID_SUPPORT_MESSENGERS.has(messenger)) return [] + + return [{ messenger, id: legacyId }] +} + +/** فقط موارد موجود در `support_messengers` — تماس/پیامک خودکار اضافه نمی‌شوند */ +export function resolveSupportMessengerEntries( + settings?: SupportMessengerSettings, + messageText?: string, +): ResolvedSupportMessengerEntry[] { + const resolved = settings ?? supportMessengerSettings.value + const text = (messageText ?? '').trim() + const source = resolved.messengers?.length + ? resolved.messengers + : resolveLegacySingleMessenger(resolved) + + return source + .map((entry) => { + const meta = resolveSupportMessengerMeta(entry.messenger) + const url = buildSupportMessengerUrl(entry.messenger, entry.id, text) + if (!url) return null + return { + ...entry, + ...meta, + url, + } + }) + .filter((entry): entry is ResolvedSupportMessengerEntry => entry !== null) +} + +function resolveLegacySingleMessenger(settings: SupportMessengerSettings): SupportMessengerEntry[] { + const messenger = (settings.messenger ?? '').trim().toLowerCase() + const id = (settings.messengerId ?? '').trim() + if (!messenger || !id) return [] + if (!VALID_SUPPORT_MESSENGERS.has(messenger)) return [] + return [{ messenger, id }] +} + +/** @deprecated از depositGuideText استفاده کنید */ +export const DEFAULT_SUPPORT_MESSENGER_TEXT = DEFAULT_DEPOSIT_GUIDE_TEXT + +export interface SupportMessengerMeta { + label: string + icon: string + accent: string +} + +export const SUPPORT_MESSENGER_META: Record = { + bale: { label: 'بله', icon: 'vuesax-linear:messages-2', accent: '#16a34a' }, + telegram: { label: 'تلگرام', icon: 'vuesax-linear:send-2', accent: '#0ea5e9' }, + whatsapp: { label: 'واتساپ', icon: 'vuesax-linear:whatsapp', accent: '#22c55e' }, + rubika: { label: 'روبیکا', icon: 'vuesax-linear:messages-2', accent: '#7c3aed' }, + eitaa: { label: 'ایتا', icon: 'vuesax-linear:messages-2', accent: '#f59e0b' }, + soroush: { label: 'سروش', icon: 'vuesax-linear:messages-2', accent: '#2563eb' }, + call: { label: 'تماس', icon: 'vuesax-linear:call-calling', accent: '#10b981' }, + sms: { label: 'پیامک', icon: 'vuesax-linear:sms', accent: '#3b82f6' }, +} + +export const DEFAULT_SUPPORT_MESSENGER_META: SupportMessengerMeta = { + label: 'پشتیبانی', + icon: 'vuesax-linear:headphone', + accent: '#334155', +} + +export function resolveSupportMessengerMeta(messenger?: string): SupportMessengerMeta { + const type = (messenger ?? '').trim().toLowerCase() + return SUPPORT_MESSENGER_META[type] ?? DEFAULT_SUPPORT_MESSENGER_META +} + +export function resolveSupportMessengerLink( + messenger?: string, + messengerId?: string, + fallbackLink = '', + messageText?: string, +) { + const fromSettings = buildSupportMessengerUrl(messenger, messengerId, messageText) + return fromSettings || fallbackLink +} + +export function getDepositGuideVarsFromSupportSettings( + settings: SupportMessengerSettings, +): DepositGuideVars { + return { + accountHolder: settings.depositAccountHolder, + cardNumber: settings.depositCardNumber, + sheba: settings.depositSheba, + customerName: getCustomerNameFromUser(), + } +} + +export function resolveDepositGuideText(settings?: SupportMessengerSettings): string { + try { + const fromStore = getRenderedDepositGuideText() + if (fromStore.trim()) return fromStore + } catch { + // settings store may not be ready + } + + const resolved = settings ?? supportMessengerSettings.value + const template = resolved.messengerText?.trim() || DEFAULT_DEPOSIT_GUIDE_TEXT + const vars = resolved.depositAccountHolder + ? getDepositGuideVarsFromSupportSettings(resolved) + : getDepositGuideVarsFromSettings() + + return renderDepositGuideText(template, vars) +} + +export function resolveDepositSupportMessage(settings?: SupportMessengerSettings): string { + try { + const fromStore = getRenderedDepositSupportMessage() + if (fromStore.trim()) return fromStore + } catch { + // settings store may not be ready + } + + const resolved = settings ?? supportMessengerSettings.value + const rawTemplate = resolved.depositSupportMessageText?.trim() || '' + const template = rawTemplate || getDepositSupportMessageTemplate() + const vars = resolved.depositAccountHolder + ? getDepositGuideVarsFromSupportSettings(resolved) + : getDepositGuideVarsFromSettings() + + return renderDepositSupportMessageText(template, vars) +} + +/** پیام‌رسان‌هایی که پارامتر text در URL دارند */ +export function messengerSupportsUrlText(messenger?: string): boolean { + const type = (messenger ?? '').trim().toLowerCase() + return type === 'telegram' || type === 'whatsapp' || type === 'sms' +} + +/** تماس/پیام بومی — نیازی به کپی متن در کلیپ‌بورد نیست */ +export function isPhoneNativeSupportMessenger(messenger?: string): boolean { + const type = (messenger ?? '').trim().toLowerCase() + return PHONE_NATIVE_MESSENGERS.has(type) +} + +export function messengerNeedsClipboardCopy(messenger?: string): boolean { + if (isPhoneNativeSupportMessenger(messenger) || messengerSupportsUrlText(messenger)) { + return false + } + return Boolean((messenger ?? '').trim()) +} + +export async function openSupportMessengerWithMessage( + messenger?: string, + messengerId?: string, + messageText?: string, + fallbackLink = '', +): Promise { + const text = (messageText ?? '').trim() + const url = + buildSupportMessengerUrl(messenger, messengerId, text) || + (fallbackLink.trim() || '') + + return openSupportMessengerUrl(url, messenger, text) +} + +export async function openSupportMessengerEntry( + entry: SupportMessengerEntry | ResolvedSupportMessengerEntry, + messageText?: string, + fallbackLink = '', +): Promise { + const text = (messageText ?? '').trim() + // همیشه URL را با متن فعلی بساز — url کش‌شدهٔ entry ممکن است text قدیمی داشته باشد + const url = + buildSupportMessengerUrl(entry.messenger, entry.id, text) || + ('url' in entry && !text ? entry.url : '') || + fallbackLink.trim() + + return openSupportMessengerUrl(url, entry.messenger, text) +} + +async function openSupportMessengerUrl( + url: string, + messenger?: string, + messageText?: string, +): Promise { + if (!url) return false + + if (messageText && messengerNeedsClipboardCopy(messenger) && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(messageText) + } catch { + // clipboard optional + } + } + + if (url.startsWith('tel:') || url.startsWith('sms:')) { + window.location.href = url + return true + } + + window.open(url, '_blank', 'noopener,noreferrer') + return true +} function getSupportSettingValue( settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>, @@ -34,7 +306,11 @@ function getSupportSettingValue( return String(value).trim() } -export function buildSupportMessengerUrl(messenger?: string, messengerId?: string) { +export function buildSupportMessengerUrl( + messenger?: string, + messengerId?: string, + messageText?: string, +) { const id = (messengerId ?? '').trim() if (!id) return '' @@ -42,6 +318,8 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin const type = (messenger ?? '').trim().toLowerCase() const username = id.replace(/^@/, '') + const text = (messageText ?? '').trim() + const textQuery = text ? `?text=${encodeURIComponent(text)}` : '' if (!type) return '' @@ -52,37 +330,63 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin } return `https://ble.ir/${username}` case 'telegram': - return `https://t.me/${username}` + return `https://t.me/${username}${textQuery}` case 'whatsapp': - return `https://wa.me/${username.replace(/\D/g, '')}` + return `https://wa.me/${username.replace(/\D/g, '')}${textQuery}` case 'rubika': return `https://rubika.ir/${username}` case 'eitaa': return `https://eitaa.com/${username}` case 'soroush': return `https://splus.ir/splus/${username}` + case 'call': + return buildSupportPhoneUrl(id) + case 'sms': + return buildDepositSmsUrl(id, text) default: return '' } } -export function buildDepositSmsUrl(phoneNumber?: string) { +export function buildDepositSmsUrl(phoneNumber?: string, messageText?: string) { const digits = (phoneNumber ?? '').replace(/\D/g, '') if (!digits) return '' - const body = encodeURIComponent('شماره حساب') - return `sms:${digits}?body=${body}` + const body = (messageText ?? '').trim() + // بدون پیش‌نویس مگر صریحاً متن داده شود (مودال واریز) + if (!body) return `sms:${digits}` + return `sms:${digits}?body=${encodeURIComponent(body)}` } -export async function loadSupportMessengerFromSettings() { +export function buildSupportPhoneUrl(phoneNumber?: string) { + const digits = (phoneNumber ?? '').replace(/\D/g, '') + if (!digits) return '' + return `tel:${digits}` +} + +export async function loadSupportMessengerFromSettings(forceSettingsRefresh = false) { try { + const settingsStore = useSettingsStore() + if (forceSettingsRefresh) { + await settingsStore.fetchSettings(true) + } else if (!settingsStore.loaded) { + await settingsStore.fetchSettings() + } + const settings = await getSettings() if (!settings || !Array.isArray(settings)) return + settingsStore.mergeSettings(settings) + supportMessengerSettings.value = { + messengers: resolveSupportMessengersFromSettings(settings), messenger: getSupportSettingValue(settings, 'support_messenger'), messengerId: getSupportSettingValue(settings, 'support_messenger_id'), messengerText: getSupportSettingValue(settings, 'support_messenger_text'), + depositSupportMessageText: getSupportSettingValue(settings, 'deposit_support_message_text'), + depositAccountHolder: getSupportSettingValue(settings, 'deposit_account_holder'), + depositCardNumber: getSupportSettingValue(settings, 'deposit_card_number'), + depositSheba: getSupportSettingValue(settings, 'deposit_sheba'), depositSmsSupportNumber: getSupportSettingValue(settings, 'deposit_sms_support_number'), } } catch (e) { diff --git a/src/utils/zIndex.ts b/src/utils/zIndex.ts index 1673ad0..ba4396a 100644 --- a/src/utils/zIndex.ts +++ b/src/utils/zIndex.ts @@ -7,6 +7,9 @@ */ export const APP_Z_MODAL = 5_500 +/** منوی انتخاب پیام‌رسان پشتیبانی (بالای FAB و مدال) */ +export const APP_Z_SUPPORT_PICKER = 5_600 + export function getModalContainer(): HTMLElement { return document.body } diff --git a/src/views/base/fund/index.vue b/src/views/base/fund/index.vue index ce9b24f..c83bfe9 100644 --- a/src/views/base/fund/index.vue +++ b/src/views/base/fund/index.vue @@ -17,7 +17,20 @@ useFundPage(isDesktop) diff --git a/src/views/base/fund/mobile/MobileFundView.vue b/src/views/base/fund/mobile/MobileFundView.vue index bab9f3a..f701bdc 100644 --- a/src/views/base/fund/mobile/MobileFundView.vue +++ b/src/views/base/fund/mobile/MobileFundView.vue @@ -1,81 +1,70 @@ + + diff --git a/src/views/base/home/components/BannerSlider.vue b/src/views/base/home/components/BannerSlider.vue index a1f71d1..5c8b791 100644 --- a/src/views/base/home/components/BannerSlider.vue +++ b/src/views/base/home/components/BannerSlider.vue @@ -56,4 +56,4 @@ defineEmits<{ .banner-swiper .swiper-pagination-bullet-active { background: #fff; } - \ No newline at end of file + diff --git a/src/views/base/home/composables/useHomePage.ts b/src/views/base/home/composables/useHomePage.ts index d153e72..8728fb5 100644 --- a/src/views/base/home/composables/useHomePage.ts +++ b/src/views/base/home/composables/useHomePage.ts @@ -10,10 +10,10 @@ import { formatRialAssetsInfoText, resolveAssetsRialValue, } from '@/utils/productAssets' -import { useSettingsStore } from '@/store/settingsStore' import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price' import { isStoreClosed } from '@/utils/storeHours' import { message, Modal } from 'ant-design-vue' +import { useSettingsStore } from '@/store/settingsStore' import { useBanners } from './useBanners' import { usePriceChart, type ChartMetalType } from './usePriceChart' import type { HomeActionItem } from '../layouts/types' diff --git a/src/views/base/home/desktop/layouts/BoursecuDesktopHome.vue b/src/views/base/home/desktop/layouts/BoursecuDesktopHome.vue index 51de001..dc070ea 100644 --- a/src/views/base/home/desktop/layouts/BoursecuDesktopHome.vue +++ b/src/views/base/home/desktop/layouts/BoursecuDesktopHome.vue @@ -12,6 +12,7 @@ import DesktopPriceRange from '../components/DesktopPriceRange.vue' import DesktopAssetsMetrics from '../components/DesktopAssetsMetrics.vue' import DesktopChartSection from '../components/DesktopChartSection.vue' import DesktopQuickActionsPanel from '../components/DesktopQuickActionsPanel.vue' +import DesktopBannersPanel from '../components/DesktopBannersPanel.vue' defineEmits<{ logout: [] @@ -42,6 +43,8 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value) + +
diff --git a/src/views/base/home/layouts/configs/boursecu.ts b/src/views/base/home/layouts/configs/boursecu.ts index afdaa3c..d7b25f2 100644 --- a/src/views/base/home/layouts/configs/boursecu.ts +++ b/src/views/base/home/layouts/configs/boursecu.ts @@ -7,6 +7,7 @@ export const boursecuHomeLayoutConfig: HomeLayoutConfig = { { id: 'assets' }, { id: 'currentPrice' }, { id: 'boursecuQuickActions' }, + { id: 'banners' }, ], sections: [ { id: 'priceChart' }, diff --git a/src/views/base/physicalReceive/components/PhysicalReceiveCartRow.vue b/src/views/base/physicalReceive/components/PhysicalReceiveCartRow.vue index 547370d..9fe6f44 100644 --- a/src/views/base/physicalReceive/components/PhysicalReceiveCartRow.vue +++ b/src/views/base/physicalReceive/components/PhysicalReceiveCartRow.vue @@ -17,6 +17,14 @@ مقدار موجودی {{ displayGold }} {{ quantityUnit }} +
+ حداقل مقدار تحویل: + {{ displayMinDelivery }} + {{ quantityUnit }} +
{ return isCountBased.value ? usePriceWithoutType(gold, false) : formatGoldAmount(gold) }) +const minDeliveryQuantity = computed(() => { + const min = Number(props.item.product.min_physical_delivery_quantity ?? 0) + return Number.isFinite(min) && min > 0 ? min : 0 +}) + +const displayMinDelivery = computed(() => { + if (minDeliveryQuantity.value <= 0) return '' + return isCountBased.value + ? usePriceWithoutType(minDeliveryQuantity.value, false) + : formatGoldAmount(minDeliveryQuantity.value) +}) + const config = useConfigStore() function selectCountInput(e: FocusEvent) { diff --git a/src/views/base/physicalReceive/components/RequestList.vue b/src/views/base/physicalReceive/components/RequestList.vue index b3ffa8f..b1a3209 100644 --- a/src/views/base/physicalReceive/components/RequestList.vue +++ b/src/views/base/physicalReceive/components/RequestList.vue @@ -1,5 +1,8 @@