Improve boursecu UX: banners, support FAB, wallet deposit guide, fund/physical layouts, nav, and sidebar credit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 14:10:40 +02:00
co-authored by Cursor
parent 21345318ba
commit 4fd12277e7
40 changed files with 1844 additions and 305 deletions
+2
View File
@@ -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']
+17 -4
View File
@@ -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;
}
</style>
+108 -119
View File
@@ -1,4 +1,19 @@
<template>
<!-- Splash outside .desktopConfig so the 426px mobile frame cannot clip it -->
<div
v-if="showSplash"
class="app-splash-screen flex justify-center"
:style="{ backgroundColor: getProjectSplashBackground() }"
>
<div class="self-center splash_appear_fade_in">
<img
:src="getProjectSplashLogo(config?.config?.avatar)"
class="w-[min(72vw,280px)] h-auto object-contain"
alt=""
>
</div>
</div>
<div class="desktopConfig w-full right-0 left-0">
<div v-if="popVisible" class="top-0 bottom-0 wrap-fix bg-primary z-100 opacity-60">
<div class="absolute right-0 left-0 bottom-10vh flex flex-col items-center">
@@ -10,17 +25,7 @@
</div>
</div>
<AConfigProvider :theme="antTheme ?? undefined" direction="rtl">
<div v-if="showSplash" class="app-splash-screen flex justify-center" :style="{ backgroundColor: getProjectSplashBackground() }">
<div class="self-center splash_appear_fade_in">
<img
:src="getProjectSplashLogo(config?.config?.avatar)"
class="w-[min(72vw,280px)] h-auto object-contain"
alt=""
>
</div>
</div>
<div v-else class="app-main">
<div v-if="!showSplash" class="app-main">
<router-view v-slot="{ Component, route }">
<component :is="Component" :key="route.fullPath" />
</router-view>
@@ -29,8 +34,12 @@
<div
v-if="showSupportButton && !showSplash"
id="support-button"
class="draggable-support-button"
:class="{ 'is-dragging': supportDragging }"
ref="supportButtonEl"
class="fixed z-[5100] cursor-grab select-none rounded-full"
:class="{
'cursor-grabbing': supportDragging,
'z-[var(--app-z-support-fab-open,5615)]': supportPickerOpen,
}"
:style="supportButtonStyle"
title="پشتیبانی"
@pointerdown="onSupportPointerDown"
@@ -38,9 +47,20 @@
@pointerup="handleSupportPointerUp"
@pointercancel="onSupportPointerCancel"
>
<div class="support-button-inner" :style="{ '--support-accent': supportAccentColor }">
<BasicIcon :icon="supportIcon" size="18" class="support-button-icon" />
<span class="support-button-label">پشتیبانی</span>
<div
class="inline-flex items-center gap-2 rounded-full bg-white border border-solid border-gray-200 shadow-[0_6px_20px_rgba(15,23,42,0.14)] px-3.5 py-[9px] text-slate-800 text-[13px] font-600 leading-none transition-all duration-200"
:class="{
'shadow-[0_10px_28px_rgba(15,23,42,0.22)]': supportDragging,
'scale-[0.94] shadow-[0_4px_14px_rgba(15,23,42,0.12)]': supportPickerOpen && !supportDragging,
}"
>
<BasicIcon
:icon="supportIcon"
size="18"
:color="supportAccentColor"
class="shrink-0"
/>
<span class="whitespace-nowrap">پشتیبانی</span>
</div>
</div>
</div>
@@ -57,6 +77,13 @@
<AppCreditLink variant="float" />
</div>
<SupportMessengerPicker
v-model:open="supportPickerOpen"
:entries="supportMessengerEntries"
:anchor-el="pickerAnchorEl"
@select="openMessengerEntry"
/>
<VersionUpdateModal />
</template>
@@ -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<HTMLElement | null>(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) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<circle cx="128" cy="128" r="124" fill="#12B76A"/>
<!-- Bootstrap Icons telephone-fill — گوشی کامل -->
<g transform="translate(48 48) scale(10)">
<path
fill="#fff"
d="M1.885.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.967.315 1.49l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.49.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.038-2.137.703-2.877L1.885.511z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
<circle cx="128" cy="128" r="128" fill="#3B82F6"/>
<path
fill="#fff"
d="M78 76h100c11 0 20 9 20 20v64c0 11-9 20-20 20h-52l-30 26v-26H78c-11 0-20-9-20-20V96c0-11 9-20 20-20zm16 40h68v14H94v-14zm0 28h48v14H94v-14z"
/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

+7
View File
@@ -0,0 +1,7 @@
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="12" fill="#1CA0E2"/>
<g transform="translate(12 12)">
<path d="M14.4501 0.922337C14.4249 0.922337 14.3996 0.922337 14.3749 0.922337C13.9177 0.865759 13.4564 0.848106 12.9962 0.869568C12.3937 0.891042 11.7968 0.992415 11.2211 1.17103C10.5799 1.36599 9.98324 1.6851 9.46511 2.11021C9.27918 2.26684 9.10383 2.43563 8.94023 2.61545C8.62473 2.95789 8.39401 3.35478 8.19696 3.77245C8.09086 3.997 7.99543 4.22773 7.8955 4.45565C7.88203 4.48764 7.8708 4.5202 7.86014 4.55333C7.82589 4.66167 7.79558 4.77114 7.75853 4.87836C7.65492 5.2025 7.5737 5.53337 7.51545 5.86863C7.45552 6.17826 7.41539 6.4914 7.39532 6.80613C7.37904 7.03574 7.36669 7.2659 7.36613 7.49607C7.36593 7.89339 7.3888 8.29039 7.43461 8.68506C7.49075 9.19592 7.58787 9.70059 7.69341 10.2036C7.81298 10.7756 7.99094 11.3303 8.1644 11.8877C8.33787 12.4452 8.5394 12.9847 8.73757 13.5297C8.89756 13.9716 9.04576 14.4167 9.16702 14.8709C9.24027 15.1385 9.29245 15.4114 9.32309 15.6871C9.37053 16.0792 9.35422 16.4763 9.27481 16.8632C9.26358 16.9126 9.25179 16.9614 9.23719 17.0097C9.17854 17.206 9.10843 17.3987 9.02724 17.5868C8.97171 17.7142 8.90383 17.8359 8.82458 17.95C8.54294 18.3624 8.19617 18.7262 7.79782 19.0273C7.33066 19.3746 6.78803 19.6066 6.21418 19.7043C5.97198 19.7479 5.72653 19.7708 5.48046 19.7728C4.65255 19.785 3.84735 19.5021 3.20913 18.9745C3.06738 18.8636 2.93586 18.7402 2.81616 18.6057C2.79988 18.5866 2.78024 18.5704 2.76003 18.553C2.73982 18.5355 2.73701 18.5479 2.72578 18.553C2.70445 18.5423 2.69715 18.5198 2.68536 18.5019C2.53435 18.2655 2.37492 18.0348 2.23289 17.7923C2.02466 17.4393 1.83484 17.0758 1.66422 16.7032C1.4634 16.2612 1.29071 15.8068 1.14719 15.343C1.028 14.9577 0.929988 14.5662 0.853591 14.1703C0.802506 13.9081 0.760402 13.6437 0.730649 13.3776C0.707633 13.171 0.686862 12.9644 0.674512 12.7567C0.663846 12.6024 0.663846 12.448 0.655425 12.2942C0.647566 12.1645 0.644197 12.0354 0.647004 11.9062C0.650934 11.7204 0.654302 11.5341 0.666091 11.3488C0.679564 11.1349 0.696405 10.921 0.722229 10.7088C0.800805 10.0199 0.942301 9.33971 1.14495 8.67664C1.27499 8.25508 1.42887 7.84124 1.60584 7.43712C1.89207 6.78413 2.23982 6.15987 2.64438 5.57279C3.12563 4.87288 3.68354 4.22892 4.30774 3.65288C5.11591 2.9082 6.02747 2.28423 7.01414 1.80033C7.73201 1.44668 8.48602 1.17169 9.26302 0.980159C9.59984 0.898198 9.93667 0.829149 10.2814 0.776941C10.4554 0.750369 10.6303 0.728288 10.8062 0.710698C10.9943 0.691611 11.1829 0.681506 11.371 0.663542C11.4367 0.657367 11.5035 0.65849 11.5697 0.657367C11.9065 0.649508 12.2383 0.649508 12.5723 0.661297C12.7015 0.665788 12.83 0.681506 12.9586 0.69105C13.0338 0.696664 13.109 0.703962 13.1831 0.711259C13.4533 0.737083 13.7213 0.774508 13.987 0.823535C14.1229 0.848797 14.2587 0.875743 14.3951 0.901566C14.4154 0.901603 14.4349 0.908983 14.4501 0.922337Z" fill="#ffffff"/>
<path d="M5.25874 21.1173C5.27981 21.122 5.29921 21.1323 5.31488 21.1471C5.63283 21.3639 5.96476 21.5595 6.30852 21.7326C6.92863 22.0651 7.58142 22.3326 8.2565 22.5309C8.68006 22.6535 9.11143 22.7472 9.54766 22.8116C10.1444 22.8977 10.748 22.9264 11.3502 22.8974C11.7252 22.8804 12.0987 22.8397 12.4685 22.7756C12.6768 22.7403 12.8834 22.6976 13.0894 22.6516C13.6547 22.5202 14.2077 22.3411 14.7426 22.116C15.2004 21.927 15.6441 21.7057 16.0703 21.4536C16.5961 21.1443 17.0675 20.7507 17.4659 20.2887C17.8669 19.8343 18.2251 19.3438 18.5359 18.8235C19.0119 18.0108 19.35 17.1247 19.5362 16.2013C19.5979 15.8984 19.6419 15.5921 19.6682 15.2841C19.6987 14.9384 19.7084 14.5912 19.6974 14.2444C19.6623 13.1958 19.4489 12.1608 19.0664 11.1838C18.9917 10.9929 18.9131 10.8037 18.8351 10.614C18.8194 10.5763 18.8008 10.5399 18.7818 10.5017C18.4002 9.77858 17.9286 9.1067 17.3783 8.50205C17.0213 8.10516 16.6334 7.7397 16.2449 7.37481C15.9642 7.1132 15.6661 6.87687 15.3658 6.64109C15.1553 6.47267 14.938 6.31829 14.7365 6.1409C14.6708 6.07719 14.6006 6.01828 14.5265 5.96463C14.5033 5.94836 14.482 5.92953 14.4631 5.90849C14.3716 5.81306 14.2778 5.71931 14.1897 5.62106C13.9048 5.30938 13.7133 4.9238 13.6373 4.50841C13.5599 4.04993 13.605 3.57914 13.7681 3.14371C14.0347 2.46051 14.5209 2.00355 15.2063 1.7571C15.6623 1.58876 16.1576 1.55753 16.6311 1.66728C16.6586 1.67346 16.6873 1.67683 16.7148 1.68132C16.716 1.68322 16.7176 1.68484 16.7195 1.68607C16.7214 1.68729 16.7235 1.68809 16.7258 1.68841C16.728 1.68873 16.7303 1.68856 16.7325 1.68792C16.7346 1.68728 16.7366 1.68617 16.7383 1.68469C16.8039 1.70602 16.8668 1.73484 16.9258 1.77058C17.6094 2.10085 18.2582 2.4986 18.8626 2.95789C19.1186 3.1525 19.366 3.35834 19.6047 3.57541C19.8577 3.8037 20.1001 4.04303 20.3317 4.29341C21.5814 5.64007 22.4838 7.27123 22.9606 9.04547C23.0482 9.37163 23.1201 9.70059 23.1801 10.0324C23.2149 10.2277 23.2434 10.424 23.2655 10.6213C23.2885 10.8312 23.3087 11.0412 23.3216 11.2517C23.3312 11.3836 23.3345 11.5161 23.339 11.6486C23.3424 11.7608 23.3508 11.8686 23.3491 11.9792C23.3463 12.1998 23.3441 12.421 23.33 12.6411C23.3147 12.8858 23.2926 13.1304 23.2638 13.3748C23.0982 14.7647 22.6752 16.1116 22.0164 17.3466C21.8345 17.6892 21.6334 18.0213 21.4141 18.3413C21.3957 18.3665 21.3825 18.3952 21.3754 18.4255C21.3682 18.4559 21.3673 18.4874 21.3725 18.5181C21.3966 18.677 21.4185 18.8359 21.4404 18.9953C21.4623 19.1547 21.4837 19.3198 21.505 19.482C21.528 19.6566 21.551 19.8318 21.5735 20.0064C21.5886 20.1287 21.6016 20.2506 21.6167 20.3724C21.633 20.5071 21.6509 20.6413 21.6672 20.7754C21.6835 20.9096 21.6975 21.032 21.7127 21.16C21.7279 21.288 21.7413 21.4042 21.7565 21.526C21.7722 21.6574 21.789 21.7887 21.8053 21.919C21.8233 22.0627 21.8407 22.207 21.8581 22.3507C21.8733 22.4759 21.8873 22.6005 21.9019 22.7257C21.9097 22.7913 21.9182 22.857 21.926 22.9227C21.9339 22.9884 21.9182 23.0064 21.8463 22.9884C21.7851 22.9732 21.7245 22.9558 21.6639 22.9396L20.2486 22.5612C19.7995 22.4416 19.3538 22.3232 18.9069 22.2025C18.5662 22.111 18.226 22.0166 17.8858 21.9218C17.8224 21.9044 17.7578 21.8881 17.6949 21.8656C17.6773 21.8594 17.6585 21.8573 17.6399 21.8594C17.6213 21.8616 17.6035 21.8679 17.5877 21.878C17.3474 22.0172 17.0993 22.1424 16.8489 22.262C16.493 22.4309 16.1283 22.5808 15.7565 22.7111C15.361 22.8498 14.958 22.9664 14.5495 23.0602C14.3008 23.1164 14.051 23.1686 13.7984 23.204C13.6395 23.2264 13.4812 23.255 13.3218 23.2724C13.1467 23.291 12.9709 23.3073 12.7947 23.319C12.4674 23.3404 12.1395 23.3544 11.8117 23.3482C11.6444 23.3449 11.4775 23.3385 11.3109 23.3291C11.1599 23.3202 11.0089 23.3089 10.8585 23.2921C10.6429 23.2674 10.4273 23.241 10.2129 23.2068C9.96701 23.1675 9.72281 23.122 9.4831 23.067C8.84985 22.9227 8.23008 22.7246 7.63056 22.4747C7.28999 22.3321 6.9571 22.1735 6.63187 21.9987C6.17367 21.752 5.73289 21.4742 5.31264 21.1673C5.28962 21.1555 5.26492 21.1448 5.25874 21.1173Z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><defs><linearGradient id="SVG6DaOZcwt" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2aabee"/><stop offset="100%" stop-color="#229ed9"/></linearGradient></defs><path fill="url(#SVG6DaOZcwt)" d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"/><path fill="#fff" d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 258"><defs><linearGradient id="SVGK3KZq49U" x1="50%" x2="50%" y1="100%" y2="0%"><stop offset="0%" stop-color="#1faf38"/><stop offset="100%" stop-color="#60d669"/></linearGradient><linearGradient id="SVGefMkoEOd" x1="50%" x2="50%" y1="100%" y2="0%"><stop offset="0%" stop-color="#f9f9f9"/><stop offset="100%" stop-color="#fff"/></linearGradient></defs><path fill="url(#SVGK3KZq49U)" d="M5.463 127.456c-.006 21.677 5.658 42.843 16.428 61.499L4.433 252.697l65.232-17.104a123 123 0 0 0 58.8 14.97h.054c67.815 0 123.018-55.183 123.047-123.01c.013-32.867-12.775-63.773-36.009-87.025c-23.23-23.25-54.125-36.061-87.043-36.076c-67.823 0-123.022 55.18-123.05 123.004"/><path fill="url(#SVGefMkoEOd)" d="M1.07 127.416c-.007 22.457 5.86 44.38 17.014 63.704L0 257.147l67.571-17.717c18.618 10.151 39.58 15.503 60.91 15.511h.055c70.248 0 127.434-57.168 127.464-127.423c.012-34.048-13.236-66.065-37.3-90.15C194.633 13.286 162.633.014 128.536 0C58.276 0 1.099 57.16 1.071 127.416m40.24 60.376l-2.523-4.005c-10.606-16.864-16.204-36.352-16.196-56.363C22.614 69.029 70.138 21.52 128.576 21.52c28.3.012 54.896 11.044 74.9 31.06c20.003 20.018 31.01 46.628 31.003 74.93c-.026 58.395-47.551 105.91-105.943 105.91h-.042c-19.013-.01-37.66-5.116-53.922-14.765l-3.87-2.295l-40.098 10.513z"/><path fill="#fff" d="M96.678 74.148c-2.386-5.303-4.897-5.41-7.166-5.503c-1.858-.08-3.982-.074-6.104-.074c-2.124 0-5.575.799-8.492 3.984c-2.92 3.188-11.148 10.892-11.148 26.561s11.413 30.813 13.004 32.94c1.593 2.123 22.033 35.307 54.405 48.073c26.904 10.609 32.379 8.499 38.218 7.967c5.84-.53 18.844-7.702 21.497-15.139c2.655-7.436 2.655-13.81 1.859-15.142c-.796-1.327-2.92-2.124-6.105-3.716s-18.844-9.298-21.763-10.361c-2.92-1.062-5.043-1.592-7.167 1.597c-2.124 3.184-8.223 10.356-10.082 12.48c-1.857 2.129-3.716 2.394-6.9.801c-3.187-1.598-13.444-4.957-25.613-15.806c-9.468-8.442-15.86-18.867-17.718-22.056c-1.858-3.184-.199-4.91 1.398-6.497c1.431-1.427 3.186-3.719 4.78-5.578c1.588-1.86 2.118-3.187 3.18-5.311c1.063-2.126.531-3.986-.264-5.579c-.798-1.593-6.987-17.343-9.819-23.64"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+86 -18
View File
@@ -1,32 +1,100 @@
<template>
<div :class="variant === 'desktop' ? 'dpage-support' : 'wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white'">
<a
class="w-full"
:href="supportHref"
:target="opensSupportInNewTab ? '_blank' : undefined"
:rel="opensSupportInNewTab ? 'noopener noreferrer' : undefined"
<div :class="variant === 'desktop' ? desktopRootClass : mobileRootClass">
<button
ref="triggerEl"
type="button"
class="support-contact-bar__btn"
:disabled="!canContactSupport"
@click="onSupportClick"
>
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a :href="baleHref" target="_blank" rel="noopener noreferrer">
<img src="@/assets/img/bale.png" alt="بله" :class="variant === 'desktop' ? 'dpage-support__bale' : 'h-10 w-10'" />
</a>
<BasicIcon icon="vuesax-linear:messages-2" size="20" color="#fff" />
<span>ارتباط با پشتیبانی</span>
</button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { getProjectBaleContactLink, getProjectSupportContactLink } from '@/utils/config'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { computed, ref } from 'vue'
withDefaults(
defineProps<{
variant?: 'mobile' | 'desktop'
mobileRootClass?: string
desktopRootClass?: string
}>(),
{ variant: 'mobile' },
{
variant: 'mobile',
mobileRootClass: 'wrap-fix bottom-0 px-10 pb-3rem flex bg-white',
desktopRootClass: 'dpage-support',
},
)
const supportHref = computed(() => getProjectSupportContactLink())
const baleHref = computed(() => getProjectBaleContactLink())
const opensSupportInNewTab = computed(() => supportHref.value.startsWith('http'))
const {
supportPhoneLink,
hasMessengerLink,
hasSupportPhoneLink,
openSupportMessenger,
} = useSupportMessenger()
const triggerEl = ref<HTMLElement | null>(null)
const canContactSupport = computed(() => hasMessengerLink.value || hasSupportPhoneLink.value)
async function onSupportClick() {
// همان رفتار دکمه شناور پشتیبانی — بدون پیش‌نویس واریز
if (hasMessengerLink.value) {
await openSupportMessenger(triggerEl.value)
return
}
if (hasSupportPhoneLink.value && supportPhoneLink.value) {
window.location.href = supportPhoneLink.value
}
}
</script>
<style scoped lang="less">
.support-contact-bar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
min-height: 2.75rem;
padding: 0.65rem 1rem;
border: 0;
border-radius: 0.75rem;
background: var(--App-primary, var(--ant-color-primary, #7f1d1d));
color: #fff;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s ease, transform 0.15s ease;
&:active {
transform: scale(0.98);
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}
.dpage-support {
width: 100%;
}
</style>
@@ -0,0 +1,43 @@
<template>
<span
class="block w-full h-full rounded-full overflow-hidden shrink-0 bg-slate-500"
:class="{ 'bg-[#e37600]': normalized === 'eitaa' }"
aria-hidden="true"
>
<img
v-if="logoSrc"
:src="logoSrc"
:alt="normalized"
class="block w-full h-full object-cover object-center bg-white"
draggable="false"
/>
<span v-else class="block w-full h-full bg-slate-500" />
</span>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import baleLogo from '@/assets/messengers/bale.png'
import callLogo from '@/assets/messengers/call.svg'
import eitaaLogo from '@/assets/messengers/eitaa.png'
import rubikaLogo from '@/assets/messengers/rubika.png'
import smsLogo from '@/assets/messengers/sms.svg'
import soroushLogo from '@/assets/messengers/soroush.svg'
import telegramLogo from '@/assets/messengers/telegram.svg'
import whatsappLogo from '@/assets/messengers/whatsapp.svg'
const LOGOS: Record<string, string> = {
bale: baleLogo,
telegram: telegramLogo,
whatsapp: whatsappLogo,
rubika: rubikaLogo,
eitaa: eitaaLogo,
soroush: soroushLogo,
call: callLogo,
sms: smsLogo,
}
const props = defineProps<{ messenger: string }>()
const normalized = computed(() => (props.messenger ?? '').trim().toLowerCase())
const logoSrc = computed(() => LOGOS[normalized.value])
</script>
+259
View File
@@ -0,0 +1,259 @@
<template>
<Teleport to="body">
<Transition name="support-picker-fade">
<div
v-if="open"
class="fixed inset-0 bg-slate-900/20"
:class="backdropActive ? 'pointer-events-auto' : 'pointer-events-none'"
:style="{ zIndex: pickerZIndex }"
@click="onBackdropClick"
/>
</Transition>
<div
v-show="open || closing"
class="fixed flex pointer-events-none"
:class="expandUp ? 'flex-col-reverse' : 'flex-col'"
:style="{ ...stackStyle, zIndex: stackZIndex }"
role="menu"
aria-label="انتخاب پیام‌رسان"
>
<div
class="support-picker-panel pointer-events-auto flex flex-col gap-2 rounded-2xl p-1 shadow-lg"
:class="{ 'is-open': panelOpen }"
>
<button
v-for="(entry, index) in entries"
:key="`${entry.messenger}-${entry.id}`"
type="button"
class="support-picker-item m-0 border-0 bg-transparent p-0 cursor-pointer"
:style="{ '--i': index, '--total': entries.length }"
:title="entry.label"
:aria-label="entry.label"
@click.stop="select(entry)"
>
<span class="inline-flex items-center gap-2 h-11 rounded-full bg-white shadow-md py-1 pl-3 pr-1 w-full">
<span class="block w-[30px] h-[30px] shrink-0">
<SupportMessengerBrandIcon :messenger="entry.messenger" />
</span>
<span class="text-sm font-600 text-slate-800 whitespace-nowrap leading-none">
{{ entry.label }}
</span>
</span>
</button>
</div>
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import SupportMessengerBrandIcon from '@/components/SupportMessengerBrandIcon.vue'
import type { ResolvedSupportMessengerEntry } from '@/utils/supportMessenger'
import { APP_Z_SUPPORT_PICKER } from '@/utils/zIndex'
const props = withDefaults(
defineProps<{
open: boolean
entries: ResolvedSupportMessengerEntry[]
anchorEl?: HTMLElement | null
}>(),
{
anchorEl: null,
},
)
const emit = defineEmits<{
'update:open': [value: boolean]
select: [entry: ResolvedSupportMessengerEntry]
}>()
const ITEM_H = 46
const GAP = 8
const PANEL_PAD = 24
const STACK_GAP = 10
const EDGE_PAD = 12
const CLOSE_MS = 220
const BACKDROP_GUARD_MS = 380
const pickerZIndex = APP_Z_SUPPORT_PICKER
const stackZIndex = APP_Z_SUPPORT_PICKER + 10
const stackStyle = ref<Record<string, string>>({})
const closing = ref(false)
const expandUp = ref(true)
const backdropActive = ref(false)
/** یک فریم بعد از open تا transition باز شدن دیده شود */
const panelOpen = ref(false)
let closeTimer: ReturnType<typeof setTimeout> | null = null
let backdropTimer: ReturnType<typeof setTimeout> | null = null
let openRaf = 0
function close() {
emit('update:open', false)
}
function select(entry: ResolvedSupportMessengerEntry) {
emit('select', entry)
close()
}
function clearTimers() {
if (closeTimer) {
clearTimeout(closeTimer)
closeTimer = null
}
if (backdropTimer) {
clearTimeout(backdropTimer)
backdropTimer = null
}
if (openRaf) {
cancelAnimationFrame(openRaf)
openRaf = 0
}
}
function onBackdropClick() {
if (!backdropActive.value) return
close()
}
function armBackdropGuard() {
backdropActive.value = false
if (backdropTimer) clearTimeout(backdropTimer)
backdropTimer = setTimeout(() => {
backdropActive.value = true
backdropTimer = null
}, BACKDROP_GUARD_MS)
}
function schedulePanelOpen() {
panelOpen.value = false
openRaf = requestAnimationFrame(() => {
openRaf = requestAnimationFrame(() => {
panelOpen.value = true
openRaf = 0
})
})
}
async function reposition() {
await nextTick()
const anchor = props.anchorEl
const count = props.entries.length
if (!anchor || count === 0) {
expandUp.value = true
stackStyle.value = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
right: 'auto',
bottom: 'auto',
}
return
}
const rect = anchor.getBoundingClientRect()
const stackHeight = count * ITEM_H + Math.max(0, count - 1) * GAP + PANEL_PAD
const right = Math.max(EDGE_PAD, window.innerWidth - rect.right)
const spaceAbove = rect.top
const spaceBelow = window.innerHeight - rect.bottom
expandUp.value = spaceAbove >= stackHeight + 24 || spaceAbove >= spaceBelow
if (expandUp.value) {
stackStyle.value = {
right: `${right}px`,
bottom: `${Math.max(EDGE_PAD, window.innerHeight - rect.top + STACK_GAP)}px`,
left: 'auto',
top: 'auto',
transform: 'none',
}
} else {
stackStyle.value = {
right: `${right}px`,
top: `${Math.max(EDGE_PAD, rect.bottom + STACK_GAP)}px`,
left: 'auto',
bottom: 'auto',
transform: 'none',
}
}
}
watch(
() => props.open,
async (isOpen) => {
clearTimers()
if (isOpen) {
closing.value = false
panelOpen.value = false
armBackdropGuard()
await reposition()
await nextTick()
schedulePanelOpen()
return
}
panelOpen.value = false
backdropActive.value = false
closing.value = true
const delay = CLOSE_MS + Math.max(0, props.entries.length - 1) * 28
closeTimer = setTimeout(() => {
closing.value = false
closeTimer = null
}, delay)
},
{ flush: 'post' },
)
watch(
() => [props.entries.length, props.anchorEl] as const,
() => {
if (props.open) void reposition()
},
)
function onViewportChange() {
if (props.open || closing.value) void reposition()
}
if (typeof window !== 'undefined') {
window.addEventListener('resize', onViewportChange, { passive: true })
window.addEventListener('scroll', onViewportChange, { passive: true, capture: true })
}
onBeforeUnmount(() => {
clearTimers()
if (typeof window === 'undefined') return
window.removeEventListener('resize', onViewportChange, { capture: true })
window.removeEventListener('scroll', onViewportChange, { capture: true })
})
</script>
<style scoped>
/* انیمیشن باز/بسته — فقط جایی که utility کافی نیست */
.support-picker-item {
--i: 0;
opacity: 0;
transform: translateY(12px) scale(0.92);
transform-origin: right center;
transition:
transform 0.22s cubic-bezier(0.2, 1, 0.3, 1),
opacity 0.16s ease;
transition-delay: calc(var(--i) * 38ms);
}
.support-picker-panel.is-open .support-picker-item {
opacity: 1;
transform: translateY(0) scale(1);
}
.support-picker-fade-enter-active,
.support-picker-fade-leave-active {
transition: opacity 0.16s ease;
}
.support-picker-fade-enter-from,
.support-picker-fade-leave-to {
opacity: 0;
}
</style>
+266
View File
@@ -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<SupportPickerIntent>('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,
}
}
+11 -1
View File
@@ -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) {
+15 -1
View File
@@ -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',
+25 -23
View File
@@ -8,31 +8,33 @@
/>
</div>
<nav class="desktop-sidebar__section" aria-label="منوی اصلی">
<DesktopNavItem
v-for="item in primaryItems"
:key="item.id"
:icon="item.icon"
:icon-src="item.iconSrc"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
<div class="desktop-sidebar__nav-scroll">
<nav class="desktop-sidebar__section" aria-label="منوی اصلی">
<DesktopNavItem
v-for="item in primaryItems"
:key="item.id"
:icon="item.icon"
:icon-src="item.iconSrc"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
<div class="desktop-sidebar__divider" />
<div class="desktop-sidebar__divider" />
<nav class="desktop-sidebar__section desktop-sidebar__section--secondary" aria-label="منوی فرعی">
<DesktopNavItem
v-for="item in secondaryItems"
:key="item.id"
:icon="item.icon"
:icon-src="item.iconSrc"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
<nav class="desktop-sidebar__section desktop-sidebar__section--secondary" aria-label="منوی فرعی">
<DesktopNavItem
v-for="item in secondaryItems"
:key="item.id"
:icon="item.icon"
:icon-src="item.iconSrc"
:label="item.label"
:active="isNavActive(item.routeName)"
@click="navigateTo(item.routeName)"
/>
</nav>
</div>
<div class="desktop-sidebar__credit">
<AppCreditLink variant="sidebar" />
+27 -3
View File
@@ -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 {
+10 -6
View File
@@ -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 {
+9
View File
@@ -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;
+5
View File
@@ -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[] {
+200
View File
@@ -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('کپی انجام نشد')
}
}
+314 -10
View File
@@ -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<SupportMessengerSettings>({})
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<SupportMessengerSettings>({})
export const supportMessengerPickerOpen = ref(false)
/** لنگر منوی انتخاب پیام‌رسان — FAB یا دکمه داخل مودال شارژ */
export const supportMessengerPickerAnchor = ref<HTMLElement | null>(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<string, SupportMessengerMeta> = {
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<boolean> {
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<boolean> {
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<boolean> {
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) {
+3
View File
@@ -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
}
+14 -1
View File
@@ -17,7 +17,20 @@ useFundPage(isDesktop)
<style scoped>
.fund-page-root {
min-height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
width: 100%;
max-width: 100%;
overflow: hidden;
}
.fund-page-root > :deep(.mobile-fund),
.fund-page-root > :deep(.dfund) {
flex: 1 1 0;
min-height: 0;
width: 100%;
max-width: 100%;
}
</style>
+165 -73
View File
@@ -1,81 +1,70 @@
<template>
<div>
<div
class="bg-white w-full mobile-top-chrome px-3 pb-3 text-primary font-bold drop-shadow fixed md:absolute top-0 flex min-h-12 z-20"
>
<div @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" />
</div>
<div class="mr-2">دارایی ها</div>
</div>
<div
class="w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden text-tertiary bg-primary"
>
<img
:src="fundHeaderBackground"
alt=""
class="pointer-events-none z-0 w-full absolute bottom-0 left-0 opacity-5"
/>
<div class="mobile-page-body--pt15 flex flex-col relative z-10 text-tertiary">
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
<div
v-if="meta?.has_count_based_products"
class="flex mt-4 justify-center mt-2 w-full z-20"
>
محصولات:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<template v-if="meta?.single_metal && primaryMetalTotal">
<div class="flex justify-center mt-2 w-full z-20">
{{ primaryMetalTotal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
</div>
</template>
<template v-else>
<div class="mobile-fund">
<MobilePageHeader title="دارایی ها" show-back class="mobile-fund__header" />
<div class="mobile-fund__scroll">
<section class="mobile-fund__hero text-tertiary bg-primary">
<img
:src="fundHeaderBackground"
alt=""
class="mobile-fund__hero-bg"
/>
<div class="mobile-fund__hero-body">
<div class="font-bold">موجودی دارایی ها</div>
<div
v-for="metal in metalTotals"
:key="`metal-${metal.metal_id}`"
class="flex justify-center mt-2 w-full z-20"
v-if="meta?.has_count_based_products"
class="mobile-fund__stat"
>
{{ metal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(metal) }}</div>
<span>محصولات:</span>
<strong>{{ countBasedInventory }} عدد</strong>
</div>
<template v-if="meta?.single_metal && primaryMetalTotal">
<div class="mobile-fund__stat">
<span>{{ primaryMetalTotal.label }}:</span>
<strong>{{ formatMetalTotal(primaryMetalTotal) }}</strong>
</div>
</template>
<template v-else>
<div
v-for="metal in metalTotals"
:key="`metal-${metal.metal_id}`"
class="mobile-fund__stat"
>
<span>{{ metal.label }}:</span>
<strong>{{ formatMetalTotal(metal) }}</strong>
</div>
</template>
<div
v-if="meta?.has_count_based_products"
class="mobile-fund__stat"
>
<span>مجموع موجودی:</span>
<strong>{{ countBasedInventory }} عدد</strong>
</div>
<div class="mobile-fund__stat">
<span>دارایی ریالی:</span>
<strong>{{ usePriceWithoutType(cashWallet) }}ریال</strong>
</div>
<div class="mobile-fund__stat">
<span>معادل ریالی مجموع دارایی ها:</span>
<strong>{{ usePriceWithoutType(totalRialValue) }}ریال</strong>
</div>
</template>
<div
v-if="meta?.has_count_based_products"
class="flex justify-center mt-2 w-full z-20"
>
مجموع موجودی:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
دارایی ریالی:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet) }}ریال</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
معادل ریالی مجموع دارایی ها:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(totalRialValue) }}ریال</div>
</div>
</div>
</div>
<div class="pt-[20rem]">
<div
class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
style="box-shadow: 0px -2px 4px -2px #0000001a"
>
</section>
<section class="mobile-fund__sheet">
<div
v-if="inventoryCards.length === 0 && !loading"
class="w-full h-full flex justify-center items-center"
class="mobile-fund__empty"
>
<div>
<img
:src="fundEmptyImage"
alt=""
class="w-70"
/>
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">
{{ fundEmptyText }}
</div>
<img :src="fundEmptyImage" alt="" class="w-70" />
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">
{{ fundEmptyText }}
</div>
</div>
@@ -89,13 +78,13 @@
/>
</div>
</a-spin>
</div>
</section>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import { loading } from '@/utils'
import { usePriceWithoutType } from '@/utils/usePrice'
import Item from '../components/Item.vue'
@@ -115,6 +104,109 @@ const {
fundEmptyText,
formatMetalTotal,
openFundInfo,
goBack,
} = useFundPageContext()
</script>
<style scoped lang="less">
.mobile-fund {
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
overflow: hidden;
background: @App-primary-color;
}
.mobile-fund__header {
z-index: 30;
}
.mobile-fund__scroll {
flex: 1 1 0;
min-height: 0;
width: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
}
.mobile-fund__hero {
position: relative;
flex: 0 0 auto;
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
/* بک‌گراند از لبه بالا؛ متن زیر هدر ثابت */
padding: calc(var(--mpage-header-offset) + 0.5rem) 1rem 3.5rem;
box-sizing: border-box;
}
.mobile-fund__hero-bg {
pointer-events: none;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.05;
z-index: 0;
}
.mobile-fund__hero-body {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.65rem;
width: 100%;
max-width: 100%;
}
.mobile-fund__stat {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: 0.35rem 0.75rem;
width: 100%;
text-align: center;
font-size: 0.95rem;
strong {
font-weight: 700;
}
}
.mobile-fund__sheet {
position: relative;
z-index: 2;
flex: 1 1 auto;
width: 100%;
max-width: 100%;
min-width: 0;
min-height: calc(100dvh - var(--app-bottom-chrome-height, 108px) - 14rem);
margin-top: -1.75rem;
border-radius: 1.5rem 1.5rem 0 0;
background: #f3f4f6;
box-shadow: 0 -2px 4px -2px #0000001a;
box-sizing: border-box;
padding-bottom: 1rem;
}
.mobile-fund__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 16rem;
padding: 2rem 1rem;
}
</style>
@@ -56,4 +56,4 @@ defineEmits<{
.banner-swiper .swiper-pagination-bullet-active {
background: #fff;
}
</style>
</style>
@@ -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'
@@ -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)
<DesktopPriceRange />
</section>
<DesktopBannersPanel />
<section class="dhome-main-grid">
<DesktopChartSection />
<DesktopQuickActionsPanel />
@@ -7,6 +7,7 @@ export const boursecuHomeLayoutConfig: HomeLayoutConfig = {
{ id: 'assets' },
{ id: 'currentPrice' },
{ id: 'boursecuQuickActions' },
{ id: 'banners' },
],
sections: [
{ id: 'priceChart' },
@@ -17,6 +17,14 @@
<span>مقدار موجودی</span>
<span class="font-bold">{{ displayGold }}</span>
<span>{{ quantityUnit }}</span>
<div
v-if="minDeliveryQuantity > 0"
class="mt-0.5 w-full text-xs text-gray-500"
>
حداقل مقدار تحویل:
<span class="font-bold text-gray-700">{{ displayMinDelivery }}</span>
{{ quantityUnit }}
</div>
<div
v-if="getProductUnitWeightLabel(item.product)"
class="mt-0.5 text-xs text-gray-500"
@@ -90,6 +98,18 @@ const displayGold = computed(() => {
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) {
@@ -1,5 +1,8 @@
<template>
<div class="physical-receive-list-root">
<div
class="physical-receive-list-root"
:class="{ 'physical-receive-list-root--desktop': isDesktop }"
>
<MobileRequestListView v-show="!isDesktop" />
<DesktopRequestListView v-show="isDesktop" />
</div>
@@ -30,4 +33,20 @@ usePhysicalReceiveList()
min-height: 0;
height: 100%;
}
/* Desktop: let list grow with content; page scroll handles overflow */
.physical-receive-list-root--desktop {
flex: none;
display: block;
height: auto;
min-height: 0;
overflow: visible;
}
.physical-receive-list-root--desktop > * {
flex: none;
height: auto;
min-height: 0;
overflow: visible;
}
</style>
@@ -2,14 +2,17 @@
.dpr {
min-height: 100%;
height: auto;
background: #f6f7f9;
color: #111827;
overflow: visible;
}
.dpr__container {
max-width: 1180px;
margin: 0 auto;
padding: 28px 36px 40px;
overflow: visible;
}
.dpr-topbar {
@@ -81,6 +84,8 @@
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
height: auto;
overflow: visible;
&__toolbar {
display: flex;
@@ -88,6 +93,7 @@
justify-content: space-between;
gap: 12px;
margin-bottom: 20px;
flex-shrink: 0;
}
&__count {
+8
View File
@@ -51,6 +51,14 @@ const { isDesktop } = usePhysicalReceiveShell()
}
.physical-receive-page-root--desktop {
display: block;
height: auto;
min-height: 100%;
overflow: visible;
}
.physical-receive-page-root--desktop :deep(.physical-receive-list-root),
.physical-receive-page-root--desktop :deep(.dpr-list) {
height: auto;
min-height: 0;
overflow: visible;
@@ -47,8 +47,8 @@
]"
>
<div class="flex items-center gap-1">
<span>{{ item.unit }}</span>
<div class="text-base">{{ formatTransactionGoldAmount(item, item.gold) }}</div>
{{ item.unit }}
</div>
<div
v-if="shouldShowTransactionTotalWeightLine(tradeProduct)"
@@ -4,7 +4,7 @@
<div class="boursecu-bank-picker__header">
<span class="boursecu-bank-picker__heading">واریز به</span>
<button type="button" class="boursecu-bank-picker__add" @click="$emit('add-account')">
+ افزودن شبا
افزودن کارت
</button>
</div>
@@ -17,30 +17,74 @@
<slot />
</div>
<a
v-if="supportLink"
class="deposit-guide__support-link"
:href="supportLink"
target="_blank"
rel="noopener noreferrer"
>
<div class="deposit-guide__accounts">
<div class="deposit-guide__account-row">
<span class="deposit-guide__account-label">{{ depositCardLabel }}</span>
<button
type="button"
class="deposit-guide__account-value"
:class="{ 'deposit-guide__account-value--empty': !hasDepositCardValue }"
:aria-label="`کپی ${depositCardLabel}`"
@click="copyDepositCardNumber"
>
<span class="deposit-guide__account-text" dir="ltr">{{ depositCardNumber }}</span>
<BasicIcon
icon="vuesax-linear:copy"
size="18"
color="#15803d"
class="deposit-guide__copy-icon"
:class="{ 'deposit-guide__copy-icon--disabled': !hasDepositCardValue }"
/>
</button>
</div>
<div class="deposit-guide__account-row">
<span class="deposit-guide__account-label">{{ depositShebaLabel }}</span>
<button
type="button"
class="deposit-guide__account-value"
:class="{ 'deposit-guide__account-value--empty': !hasDepositShebaValue }"
:aria-label="`کپی ${depositShebaLabel}`"
@click="copyDepositShebaNumber"
>
<span class="deposit-guide__account-text" dir="ltr">{{ depositSheba }}</span>
<BasicIcon
icon="vuesax-linear:copy"
size="18"
color="#15803d"
class="deposit-guide__copy-icon"
:class="{ 'deposit-guide__copy-icon--disabled': !hasDepositShebaValue }"
/>
</button>
</div>
<a
v-if="hasMessengerLink"
ref="supportMessengerTriggerEl"
class="deposit-guide__support-link"
href="#"
@click.prevent="onDepositSupportClick"
>
<span class="deposit-guide__support-btn">
<BasicIcon icon="vuesax-linear:headphone" size="18" color="#16a34a" />
<span>پیام به پشتیبانی</span>
</span>
</a>
</a>
<a
v-if="smsLink"
class="deposit-guide__support-link"
:class="{ 'deposit-guide__support-link--spaced': supportLink }"
:href="smsLink"
>
<a
v-if="smsLink"
class="deposit-guide__support-link"
:class="{ 'deposit-guide__support-link--spaced': hasMessengerLink }"
:href="smsLink"
>
<span class="deposit-guide__support-btn">
<BasicIcon icon="vuesax-linear:direct-inbox" size="18" color="#16a34a" />
<span>پیام به پشتیبانی (sms)</span>
</span>
</a>
</a>
</div>
</div>
<template #footer>
@@ -52,10 +96,12 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { computed, ref, watch } from 'vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
const props = withDefaults(
defineProps<{
@@ -71,6 +117,25 @@ const props = withDefaults(
},
)
const {
depositCardNumber,
depositSheba,
depositCardLabel,
depositShebaLabel,
hasDepositCardValue,
hasDepositShebaValue,
copyDepositCardNumber,
copyDepositShebaNumber,
hasMessengerLink,
openDepositSupportMessenger,
} = useSupportMessenger()
const supportMessengerTriggerEl = ref<HTMLElement | null>(null)
function onDepositSupportClick() {
void openDepositSupportMessenger(supportMessengerTriggerEl.value)
}
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
@@ -79,6 +144,15 @@ const localVisible = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value),
})
watch(
() => props.visible,
(visible) => {
if (visible) {
void loadSupportMessengerFromSettings(true)
}
},
)
</script>
<style scoped lang="less">
@@ -115,6 +189,71 @@ const localVisible = computed({
color: #4b5563;
}
.deposit-guide__accounts {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
margin-top: 0.85rem;
text-align: right;
}
.deposit-guide__account-row {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.deposit-guide__account-label {
font-size: 0.78rem;
font-weight: 700;
color: #6b7280;
}
.deposit-guide__account-value {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
width: 100%;
min-height: 2.75rem;
padding: 0.65rem 0.85rem;
border: 1px solid #bbf7d0;
border-radius: 0.75rem;
background: #f0fdf4;
color: #111827;
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
text-align: left;
&:hover:not(.deposit-guide__account-value--empty) {
background: #dcfce7;
border-color: #86efac;
}
&--empty {
color: #9ca3af;
cursor: not-allowed;
}
}
.deposit-guide__account-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.deposit-guide__copy-icon {
flex-shrink: 0;
&--disabled {
opacity: 0.45;
}
}
.deposit-guide__support-link {
display: block;
width: 100%;
@@ -1,4 +1,4 @@
import { computed, inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
import { computed, inject, onMounted, provide, ref, watch, type InjectionKey, type Ref } from 'vue'
import OtpInput from 'vue3-otp-input'
import { formatCashWalletBalanceText } from '@/utils/productDealMethod'
import { usePriceWithoutType } from '@/utils/usePrice'
@@ -16,12 +16,8 @@ import {
getWalletDepositQuickAmountsRial,
} from '@/utils/config'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import {
buildDepositSmsUrl,
buildSupportMessengerUrl,
DEFAULT_SUPPORT_MESSENGER_TEXT,
supportMessengerSettings,
} from '@/utils/supportMessenger'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
import {
initWalletPageData,
loadMoreWalletDeposits,
@@ -56,6 +52,7 @@ export interface WalletPageContext {
supportMessengerText: ReturnType<typeof computed<string>>
depositGuideSupportLink: ReturnType<typeof computed<string>>
depositSmsSupportLink: ReturnType<typeof computed<string>>
openDepositSupportMessenger: () => Promise<boolean>
depositQuickAmounts: ReturnType<typeof computed<readonly number[]>>
usePriceWithoutType: typeof usePriceWithoutType
formatCashWalletBalanceText: typeof formatCashWalletBalanceText
@@ -77,6 +74,12 @@ export function useWalletPage() {
const userStore = useUserStore()
const config = useConfigStore()
const isDesktop = useDesktopBreakpoint()
const {
messengerLink: depositGuideSupportLink,
depositSmsLink: depositSmsSupportLink,
supportMessengerText,
openDepositSupportMessenger,
} = useSupportMessenger()
const showAddCard = ref(false)
const chargeAccountModal = ref(false)
const showConfirmCode = ref(false)
@@ -90,23 +93,8 @@ export function useWalletPage() {
isDesktop.value ? desktopDepositScrollEl.value : mobileDepositScrollEl.value,
)
const supportMessengerText = computed(
() => supportMessengerSettings.value.messengerText || DEFAULT_SUPPORT_MESSENGER_TEXT,
)
const depositQuickAmounts = computed(() => getWalletDepositQuickAmountsRial())
const depositGuideSupportLink = computed(() =>
buildSupportMessengerUrl(
supportMessengerSettings.value.messenger,
supportMessengerSettings.value.messengerId,
),
)
const depositSmsSupportLink = computed(() =>
buildDepositSmsUrl(supportMessengerSettings.value.depositSmsSupportNumber),
)
const extraNote = ref('')
const form = ref<Transaction>({
@@ -250,6 +238,11 @@ export function useWalletPage() {
form.value.card_id = walletCardItems.value.length ? walletCardItems.value[0].id : undefined
})
watch(chargeAccountModal, async (open) => {
if (!open) return
await loadSupportMessengerFromSettings(true)
})
if (!userStore.isSingle) {
useInfiniteScroll(
activeDepositScrollEl,
@@ -288,6 +281,7 @@ export function useWalletPage() {
supportMessengerText,
depositGuideSupportLink,
depositSmsSupportLink,
openDepositSupportMessenger,
depositQuickAmounts,
usePriceWithoutType,
formatCashWalletBalanceText,
+6 -3
View File
@@ -83,9 +83,7 @@
:support-link="depositGuideSupportLink"
:sms-link="depositSmsSupportLink"
>
<template>
<p>{{ supportMessengerText }}</p>
</template>
<p class="deposit-guide-text">{{ supportMessengerText }}</p>
</WalletDepositGuideModal>
<BasicModal v-model:visible="withdrawInfModal" custom-footer :closable="false">
@@ -145,6 +143,11 @@ const {
height: 100%;
}
.deposit-guide-text {
margin: 0;
white-space: pre-wrap;
}
.deposit-guide-note {
margin: 0.35rem 0 0;
}