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
+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;
}