feat(user): gift card FAB, menu order, wallet deposit status

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-09 11:25:42 +03:30
co-authored by Cursor
parent 036184c68b
commit 2977b479dd
6 changed files with 189 additions and 60 deletions
@@ -3,6 +3,10 @@
فرمت هر خط:
- YYYY-MM-DD | موضوع | مسیر فایل(ها) | نکتهٔ ۱ خطی
- 2026-11-09 | FAB ثبت کارت هدیه | `App.vue` `#gift-card-button` | مثل پشتیبانی؛ `has_page_gift_card` + لاگین؛ کلیک → `giftCard`؛ موقعیت پیش‌فرض بالای دکمه پشتیبانی
- 2026-11-09 | ترتیب منوی کاربر | `useUserPage.ts` `menuActions` | موبایل+دسکتاپ یکسان؛ اول giftCard… بعد password؛ خروج جدا؛ هشدار قیمت دیگر فقط موبایل نیست
- 2026-11-09 | وضعیت تاریخچه واریز کیف پول | `wallet/components/Item.vue` + `resolveTransactionStatusLabel` | قبلاً فقط `transaction[0]` → ناموفق؛ واریز pending باید «در انتظار تایید» مثل تب تراکنش‌ها
- 2026-10-25 | انتخاب روش واریز | `WalletDepositMethodModal.vue` بعد از دکمهٔ واحد واریز | آنی = `INSTANT_DEPOSIT_PAYMENT_URL`؛ حساب = مودال کارت/شبا
- 2026-10-22 | addon اینپوت | `assets/css/main.less` | پس‌زمینه addon = secondary تم، نه primary (سبز tenant)
- 2026-10-21 | نمودار خانه = لندینگ | `LandingLiveChartMain.vue` + `useLandingLiveChart` + `zaarboxPriceHistory`؛ جایگزین `PriceChart` در PriceChartSection/DesktopChart* | همان `dlanding-live__main` SVG؛ داده از zaarbox + `/api/price/last`
+64 -3
View File
@@ -32,6 +32,31 @@
</div>
<RequiredKycModal v-if="!showSplash && !isTvBoardRoute && !isLoginRoute" />
</AConfigProvider>
<div
v-if="showGiftCardButton"
id="gift-card-button"
class="fixed z-[5095] cursor-grab select-none rounded-full"
:class="{ 'cursor-grabbing': giftCardDragging }"
:style="giftCardButtonStyle"
title="ثبت کارت هدیه"
@pointerdown="onGiftCardPointerDown"
@pointermove="onGiftCardPointerMove"
@pointerup="handleGiftCardPointerUp"
@pointercancel="onGiftCardPointerCancel"
>
<div
class="relative 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)]': giftCardDragging }"
>
<BasicIcon
icon="vuesax-linear:gift"
size="18"
:color="giftCardFabAccent"
class="shrink-0"
/>
<span class="whitespace-nowrap">ثبت کارت هدیه</span>
</div>
</div>
<div
v-if="showSupportButton && !showSplash && route.name !== 'support' && route.name !== 'login' && !isTvBoardRoute && !isKycRequired"
id="support-button"
@@ -127,7 +152,7 @@ import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import AppCreditLink from '@/components/AppCreditLink.vue'
import EnamadBadge from '@/components/EnamadBadge.vue'
import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
import { usePwaInstall } from '@/composables/usePwaInstall'
import PwaInstallBanner from '@/components/pwa/PwaInstallBanner.vue'
@@ -155,6 +180,7 @@ useApplyDesktopLayoutClass()
const isDesktop = useDesktopBreakpoint()
const route = useRoute()
const router = useRouter()
const showDesktopFloatingCredit = computed(() => {
if (!isDesktop.value) return false
// پشتیبانی خودش سایدبار دارد؛ float روی دکمه ارسال می‌افتد
@@ -301,10 +327,44 @@ watch(
)
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const isKycRequired = computed(
() => Boolean(getToken()) && (userStore.userInfo?.is_legal === 0 || !userStore.userInfo?.is_legal),
)
const settingsStore = useSettingsStore()
const giftCardFabAccent = computed(
() => config.config?.color || getProjectPrimaryColor() || '#334155',
)
const showGiftCardButton = computed(() => {
if (showSplash.value) return false
if (isTvBoardRoute.value || isLoginRoute.value || isKycRequired.value) return false
if (route.name === 'giftCard') return false
if (!getToken()) return false
return settingsStore.isSettingTruthy('has_page_gift_card')
})
const {
dragging: giftCardDragging,
fabStyle: giftCardButtonStyle,
handlePointerDown: onGiftCardPointerDown,
handlePointerMove: onGiftCardPointerMove,
handlePointerUp: giftCardPointerUp,
handlePointerCancel: onGiftCardPointerCancel,
} = useDraggableFloatingButton('boursecu_gift_card_button_position_v1', {
width: 148,
height: 42,
defaultBottomGap: 62,
defaultRightOffset: 16,
})
function handleGiftCardPointerUp(event: PointerEvent) {
const clicked = giftCardPointerUp(event)
if (!clicked) return
event.stopPropagation()
void router.push({ name: 'giftCard' })
}
const showLoginCreditExtras = computed(() => {
if (!isLoginRoute.value) return false
const href = String(settingsStore.getSettingByKey('enamad_url') ?? '').trim()
@@ -606,7 +666,8 @@ html.project-boursecu {
}
html.app-layout--desktop {
#support-button {
#support-button,
#gift-card-button {
bottom: 12px;
right: calc(var(--app-desktop-sidebar-width, 260px) + 20px);
}
+3 -1
View File
@@ -24,7 +24,9 @@ export interface Transaction {
value?: number
sts_str?: string
order?: Order
withdraw?: Order
withdraw?: Withdraw
sts?: number
transaction?: unknown[]
card_id?: number|string
eaj?: string
end_at?: string
+45 -47
View File
@@ -17,7 +17,6 @@ import { useWalletAssets } from '@/composables/useWalletAssets'
import { getUserAvatarUrl } from '@/utils/file'
import { useConfigStore } from '@/store/configStore'
import { getBoursecuActionIconSrc } from '@/utils/config'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { getReferralShareText, getReferralShareTitle, shareReferralText } from '@/utils/referralShare'
import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { performUserLogout } from '@/utils/authSession'
@@ -85,7 +84,6 @@ export function useUserPage() {
const walletStore = useWalletStore()
const { clearWalletAssets } = useWalletAssets()
const settingsStore = useSettingsStore()
const isDesktop = useDesktopBreakpoint()
const nameProject = useConfigStore().config.name_fa
const editPassVisible = ref(false)
const editPasswordVisible = ref(false)
@@ -123,12 +121,36 @@ export function useUserPage() {
const menuActions = computed<UserMenuAction[]>(() => [
{
id: 'password',
label: 'تغییر رمز عبور',
icon: 'vuesax-linear:lock',
onClick: () => { editPasswordVisible.value = true },
id: 'giftCard',
label: 'ثبت کارت هدیه',
icon: 'vuesax-linear:gift',
onClick: () => router.push({ name: 'giftCard' }),
visible: settingsStore.isSettingTruthy('has_page_gift_card'),
},
{
id: 'installment',
label: 'خرید اقساطی',
icon: 'vuesax-linear:calendar',
onClick: () => router.push({ name: 'installment' }),
visible: settingsStore.isSettingTruthy('enable_digipay_installment'),
badgeLabel: 'جدید',
},
{
id: 'physicalReceive',
label: 'تحویل فیزیکی',
icon: 'vuesax-linear:direct-inbox',
iconSrc: getBoursecuActionIconSrc('physicalReceive'),
onClick: () => router.push({ name: 'physicalReceive' }),
visible: true,
},
{
id: 'notifyTrigger',
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
iconSrc: getBoursecuActionIconSrc('priceTrigger'),
onClick: () => router.push({ name: 'notifyTrigger' }),
visible: settingsStore.isSettingTruthy('has_page_price_trigger'),
},
{
id: 'goldDayCalculator',
label: 'ماشین حساب',
@@ -151,12 +173,12 @@ export function useUserPage() {
visible: settingsStore.isSettingTruthy('enable_product_funds'),
},
{
id: 'notifyTrigger',
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
iconSrc: getBoursecuActionIconSrc('priceTrigger'),
onClick: () => router.push({ name: 'notifyTrigger' }),
visible: !isDesktop.value && settingsStore.isSettingTruthy('has_page_price_trigger'),
id: 'rules',
label: 'قوانین و مقررات',
icon: 'vuesax-linear:task',
iconSrc: getBoursecuActionIconSrc('rules'),
onClick: () => router.push({ name: 'rules' }),
visible: true,
},
{
id: 'about',
@@ -166,6 +188,13 @@ export function useUserPage() {
onClick: () => router.push({ name: 'about' }),
visible: true,
},
{
id: 'recoverySites',
label: 'سایت‌های ری‌گیری',
icon: 'vuesax-linear:global',
onClick: () => router.push({ name: 'recoverySites' }),
visible: trackingSystemsEnabled.value,
},
{
id: 'support',
label: 'پشتیبانی',
@@ -176,41 +205,10 @@ export function useUserPage() {
badge: unreadSupportCount.value > 0 ? unreadSupportCount.value : undefined,
},
{
id: 'physicalReceive',
label: 'تحویل فیزیکی',
icon: 'vuesax-linear:direct-inbox',
iconSrc: getBoursecuActionIconSrc('physicalReceive'),
onClick: () => router.push({ name: 'physicalReceive' }),
visible: true,
},
{
id: 'installment',
label: 'خرید اقساطی',
icon: 'vuesax-linear:calendar',
onClick: () => router.push({ name: 'installment' }),
visible: settingsStore.isSettingTruthy('enable_digipay_installment'),
badgeLabel: 'جدید',
},
{
id: 'giftCard',
label: 'ثبت کارت هدیه',
icon: 'vuesax-linear:gift',
onClick: () => router.push({ name: 'giftCard' }),
visible: settingsStore.isSettingTruthy('has_page_gift_card'),
},
{
id: 'recoverySites',
label: 'سایت‌های ری‌گیری',
icon: 'vuesax-linear:global',
onClick: () => router.push({ name: 'recoverySites' }),
visible: trackingSystemsEnabled.value,
},
{
id: 'rules',
label: 'قوانین و مقررات',
icon: 'vuesax-linear:task',
iconSrc: getBoursecuActionIconSrc('rules'),
onClick: () => router.push({ name: 'rules' }),
id: 'password',
label: 'تغییر رمز عبور',
icon: 'vuesax-linear:lock',
onClick: () => { editPasswordVisible.value = true },
visible: true,
},
].filter((action) => action.visible)
+72 -9
View File
@@ -1,16 +1,17 @@
<template>
<div >
<div class="flex">
<div :class="isUp? 'bg-green-50':'bg-red-50'" class="rounded-lg p-4 h-fit w-fit flex justify-center items-center">
<Icon :class="isUp? 'text-green-600':'text-red-600'"
:icon="`vuesax-linear:${isUp? 'tick-circle':'close-circle'}`"/>
<div :class="statusIconWrapClass" class="rounded-lg p-4 h-fit w-fit flex justify-center items-center">
<Icon :class="statusIconClass" :icon="`vuesax-linear:${statusIconName}`"/>
</div>
<div class=" w-full mr-2 ">
<div class="flex w-full text-xs ">{{item.note ?? '-'}}</div>
<div class="flex mt-1 text-xs">
<div class="flex mt-1 text-xs items-center gap-1 flex-wrap">
<div>وضعیت:</div>
<div :class="'text-green-600'" class="mr-1" v-if="item.transaction[0]">موفق</div>
<div :class="'text-red-600'" class="mr-1" v-else>ناموفق</div>
<ATag v-if="statusLabel" :color="statusTagColor" class="!py-0 !px-2 !ml-0 !text-xs !leading-5">
{{ statusLabel }}
</ATag>
<span v-else class="text-gray-500 mr-1">—</span>
</div>
<div class="flex mt-1 text-xs ">
<div class="w-4rem">کد پیگیری: </div>
@@ -134,9 +135,10 @@
<script lang="ts" setup>
import Icon from "@/components/BasicPacks/BasicIcon.vue";
import {formatGoldAmount, getTransactionTradePrice, usePriceWithoutType} from "../../../../utils/usePrice";
import {formatGoldAmount, getTransactionTradePrice, resolveTransactionStatusLabel, usePriceWithoutType} from "../../../../utils/usePrice";
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
import {computed, ref} from "vue";
import { Tag as ATag } from 'ant-design-vue'
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Transaction} from "@/models/transaction";
import {getCashWalletLabel, getGoldWalletLabel, getMetalTypeTradeLabel, getTradePriceUnitLabel, getWeightUnit} from '@/utils/productDealMethod'
@@ -149,8 +151,69 @@ interface Prop {
}
const props = withDefaults(defineProps<Prop>(), {})
// const isUp = computed(() => props.item.type === 3 || props.item.type === 1)
const isUp = computed(() => props.item.transaction[0])
type WalletStatusTone = 'success' | 'pending' | 'failed' | 'unknown'
function walletItemSts(item: Transaction): number | null {
const raw = item.withdraw?.sts ?? item.order?.sts ?? (item as Transaction & { sts?: number }).sts
if (raw == null) return null
const sts = Number(raw)
return Number.isFinite(sts) ? sts : null
}
const statusLabel = computed(() => {
const label = resolveTransactionStatusLabel(props.item)
if (label) return label
const legacy = (props.item as Transaction & { transaction?: unknown[] }).transaction
return legacy?.[0] ? 'موفق' : 'ناموفق'
})
const legacySucceeded = computed(
() => Boolean((props.item as Transaction & { transaction?: unknown[] }).transaction?.[0]),
)
const statusTone = computed<WalletStatusTone>(() => {
const sts = walletItemSts(props.item)
if (sts === 1 || sts === 50) return 'pending'
if (sts === 2 || sts === 51 || sts === 4 || sts === 5 || sts === 9) return 'success'
if (sts === 3 || sts === 52) return 'failed'
const label = statusLabel.value
if (label === 'در انتظار تایید' || label === 'در انتظار ارسال') return 'pending'
if (label === 'موفق' || label === 'انجام شد' || label === 'ارسال شده') return 'success'
if (label === 'ناموفق' || label === 'لغو شده') return 'failed'
if (legacySucceeded.value) return 'success'
return 'unknown'
})
const statusTagColor = computed(() => {
if (statusTone.value === 'pending') return 'processing'
if (statusTone.value === 'failed') return 'error'
if (statusTone.value === 'success') return 'success'
return 'default'
})
const statusIconWrapClass = computed(() => {
if (statusTone.value === 'pending') return 'bg-sky-50'
if (statusTone.value === 'failed') return 'bg-red-50'
if (statusTone.value === 'success') return 'bg-green-50'
return legacySucceeded.value ? 'bg-green-50' : 'bg-red-50'
})
const statusIconClass = computed(() => {
if (statusTone.value === 'pending') return 'text-sky-600'
if (statusTone.value === 'failed') return 'text-red-600'
if (statusTone.value === 'success') return 'text-green-600'
return legacySucceeded.value ? 'text-green-600' : 'text-red-600'
})
const statusIconName = computed(() => {
if (statusTone.value === 'pending') return 'clock'
if (statusTone.value === 'failed') return 'close-circle'
if (statusTone.value === 'success') return 'tick-circle'
return legacySucceeded.value ? 'tick-circle' : 'close-circle'
})
const isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10)
/*const title = computed(() => {
if (isUp.value) {
+1
View File
@@ -920,6 +920,7 @@ html.app-layout--tv body {
html.app-layout--tv .app-splash-screen,
html.app-layout--tv #support-button,
html.app-layout--tv #gift-card-button,
html.app-layout--tv #goftino_w {
display: none !important;
}