feat(user): gift card FAB, menu order, wallet deposit status
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user