Split desktop and mobile layouts with shared page data so APIs fetch once per context.

Add layout shells, desktop views, module-level page caches, and UI fixes for auth, home, wallet, transactions, address, and related pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 10:08:46 +03:30
co-authored by Cursor
parent 4404b4cb92
commit a5e458ed25
234 changed files with 18425 additions and 5503 deletions
@@ -0,0 +1,291 @@
import { computed, inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
import OtpInput from 'vue3-otp-input'
import { formatCashWalletBalanceText } from '@/utils/productDealMethod'
import { usePriceWithoutType } from '@/utils/usePrice'
import { loading, loadingMore } from '@/utils'
import router from '@/router'
import { useWalletStore } from '@/store/walletStore'
import type { Transaction } from '@/models/transaction'
import { createCard, withdraw, confirmCode } from '@/api/wallet'
import { useInfiniteScroll } from '@vueuse/core'
import { message } from 'ant-design-vue'
import type { Card } from '@/models/wallet'
import { useUserStore } from '@/store/userStore'
import { useConfigStore } from '@/store/configStore'
import { isBazretalaProject } from '@/utils/config'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import {
buildSupportMessengerUrl,
DEFAULT_SUPPORT_MESSENGER_TEXT,
supportMessengerSettings,
} from '@/utils/supportMessenger'
import {
initWalletPageData,
loadMoreWalletDeposits,
loadMoreWalletWithdrawals,
refreshWalletCards,
walletCardItems,
walletDepositItems,
walletWithdrawItems,
} from './useWalletPageData'
export interface WalletPageContext {
isDeposit: Ref<boolean>
walletStore: ReturnType<typeof useWalletStore>
config: ReturnType<typeof useConfigStore>
showAddCard: Ref<boolean>
chargeAccountModal: Ref<boolean>
showConfirmCode: Ref<boolean>
withdrawInfModal: Ref<boolean>
infoVisible: Ref<boolean>
refOtpInput: Ref<InstanceType<typeof OtpInput> | null>
form: Ref<Transaction>
cardForm: Ref<Card>
disableButton: ReturnType<typeof computed<boolean>>
cardItems: typeof walletCardItems
depositItems: typeof walletDepositItems
withdrawItems: typeof walletWithdrawItems
loading: typeof loading
loadingMore: typeof loadingMore
mobileDepositScrollEl: Ref<HTMLElement | null>
desktopDepositScrollEl: Ref<HTMLElement | null>
supportMessengerText: ReturnType<typeof computed<string>>
supportMessengerLink: ReturnType<typeof computed<string | undefined>>
isBazretalaProject: typeof isBazretalaProject
usePriceWithoutType: typeof usePriceWithoutType
formatCashWalletBalanceText: typeof formatCashWalletBalanceText
setIsDeposit: (val: boolean) => void
useStyle: () => string
formatter: (value: number) => string
percentChange: (val: number) => void
ok: () => Promise<void>
addCard: () => Promise<void>
getCards: () => Promise<void>
handleOnComplete: (value: string) => Promise<void>
}
export const WALLET_PAGE_CONTEXT_KEY: InjectionKey<WalletPageContext> = Symbol('walletPageContext')
export function useWalletPage() {
const isDeposit = ref(true)
const walletStore = useWalletStore()
const userStore = useUserStore()
const config = useConfigStore()
const isDesktop = useDesktopBreakpoint()
const showAddCard = ref(false)
const chargeAccountModal = ref(false)
const showConfirmCode = ref(false)
const withdrawInfModal = ref(false)
const infoVisible = ref(false)
const refOtpInput = ref<InstanceType<typeof OtpInput> | null>(null)
const mobileDepositScrollEl = ref<HTMLElement | null>(null)
const desktopDepositScrollEl = ref<HTMLElement | null>(null)
const activeDepositScrollEl = computed(() =>
isDesktop.value ? desktopDepositScrollEl.value : mobileDepositScrollEl.value,
)
const supportMessengerText = computed(
() => supportMessengerSettings.value.messengerText || DEFAULT_SUPPORT_MESSENGER_TEXT,
)
const supportMessengerLink = computed(() =>
buildSupportMessengerUrl(
supportMessengerSettings.value.messenger,
supportMessengerSettings.value.messengerId,
),
)
const form = ref<Transaction>({
value: undefined,
percentWallet: 0,
card_id: undefined,
})
const cardForm = ref<Card>({
birth_date: 0,
account_number: '',
bank_name: '',
cart_number: '',
shaba_number: '',
})
const disableButton = computed(() => {
if (!loading.value) {
if (walletStore?.walletInfo?.cash && !isDeposit.value) {
if (walletCardItems.value.length > 0) {
return (
walletStore?.walletInfo?.cash <= 0
|| walletStore?.walletInfo?.cash < parseInt(form.value?.value)
|| isNaN(parseInt(form.value?.value))
|| parseInt(form.value?.value) < 1000000
)
}
return true
}
if (isDeposit.value) {
return isNaN(parseInt(form.value?.value)) || parseInt(form.value?.value) < 1000000
}
return false
}
return true
})
const unitCalculator = computed(() => (walletStore?.walletInfo?.cash ?? 0) / 100)
function resetData() {
form.value.value = undefined
form.value.percentWallet = 0
}
async function addCard() {
try {
loading.value = true
await createCard(cardForm.value)
await getCards()
showAddCard.value = false
cardForm.value = {
birth_date: 0,
account_number: '',
bank_name: '',
cart_number: '',
shaba_number: '',
}
message.success('کارت وارد شده با موفقیت افزوده شد و پس از تایید نمایش داده خواهد شد.')
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function getCards() {
await refreshWalletCards()
form.value.card_id = walletCardItems.value.length ? walletCardItems.value[0].id : undefined
}
function setIsDeposit(val: boolean) {
isDeposit.value = val
form.value.value = undefined
form.value.percentWallet = 0
}
function useStyle() {
if (isDeposit.value) return 'right: 2%;'
return ' right: 98%; transform: translate(100%, 0);'
}
const formatter = (value: number) => `${value}%`
function percentChange(val: number) {
if (val !== 0) form.value.value = parseInt((unitCalculator.value * val).toFixed())
else form.value.value = undefined
}
async function ok() {
try {
if (form.value.value !== undefined) {
loading.value = true
if (!isDeposit.value) {
await confirmCode({ value: form.value.value })
showConfirmCode.value = true
}
if (isDeposit.value) {
chargeAccountModal.value = true
}
} else {
message.error('ابتدا مبلغ را وارد کنید')
}
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function handleOnComplete(value: string) {
try {
infoVisible.value = true
showConfirmCode.value = false
await withdraw({ ...form.value, otp: value })
await walletStore.getMyWallet()
await message.success('عملیات با موفقیت انجام شد')
withdrawInfModal.value = true
resetData()
} catch (e) {
console.log(e)
} finally {
infoVisible.value = false
refOtpInput.value?.clearInput()
}
}
onMounted(async () => {
if (router.currentRoute.value.query.state === 'deposit') {
isDeposit.value = true
}
cardForm.value.birth_date = userStore.userInfo?.birth_date
await initWalletPageData()
form.value.card_id = walletCardItems.value.length ? walletCardItems.value[0].id : undefined
})
if (!userStore.isSingle) {
useInfiniteScroll(
activeDepositScrollEl,
async () => {
if (isDeposit.value) {
await loadMoreWalletDeposits()
} else {
await loadMoreWalletWithdrawals()
}
},
{ distance: 12 },
)
}
const context: WalletPageContext = {
isDeposit,
walletStore,
config,
showAddCard,
chargeAccountModal,
showConfirmCode,
withdrawInfModal,
infoVisible,
refOtpInput,
form,
cardForm,
disableButton,
cardItems: walletCardItems,
depositItems: walletDepositItems,
withdrawItems: walletWithdrawItems,
loading,
loadingMore,
mobileDepositScrollEl,
desktopDepositScrollEl,
supportMessengerText,
supportMessengerLink,
isBazretalaProject,
usePriceWithoutType,
formatCashWalletBalanceText,
setIsDeposit,
useStyle,
formatter,
percentChange,
ok,
addCard,
getCards,
handleOnComplete,
}
provide(WALLET_PAGE_CONTEXT_KEY, context)
return context
}
export function useWalletPageContext() {
const context = inject(WALLET_PAGE_CONTEXT_KEY)
if (!context) {
throw new Error('useWalletPageContext must be used within a page that calls useWalletPage()')
}
return context
}
@@ -0,0 +1,187 @@
import { ref } from 'vue'
import { loading, loadingMore } from '@/utils'
import type { Transaction } from '@/models/transaction'
import { getActivatedCards } from '@/api/wallet'
import { getTransactionTable, getTransactionTable2 } from '@/api/transaction'
import type { TableEngineInput, TableOutput } from '@/models/table'
import type { Card } from '@/models/wallet'
import { useWalletStore } from '@/store/walletStore'
import { runOnceAsync, resetOnceAsyncByPrefix } from '@/composables/useOnceAsync'
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
/**
* Shared wallet page datasets — mobile and desktop views read this module state only.
* Resize / breakpoint changes must not trigger new requests.
*/
let initPromise: Promise<void> | null = null
const initialized = ref(false)
export const walletCardItems = ref<Card[]>([])
export const walletDepositItems = ref<Transaction[]>([])
export const walletWithdrawItems = ref<Transaction[]>([])
export const walletDepositTableOutput = ref<TableOutput<Transaction> | null>(null)
export const walletWithdrawTableOutput = ref<TableOutput<Transaction> | null>(null)
export const walletDepositPagination = ref<TableEngineInput>({
filters: [],
limit: 6,
page: 0,
})
export const walletWithdrawPagination = ref<TableEngineInput>({
filters: [
{
field: 'type',
cr: 'and',
type: 'number',
op: 'eq',
value: 4,
},
],
limit: 6,
page: 0,
})
export function isWalletPageDataInitialized() {
return initialized.value
}
async function fetchDepositPage(append: boolean) {
const isFirstPage = walletDepositPagination.value.page === 0 && !append
if (isFirstPage) {
loading.value = true
} else {
loadingMore.value = true
}
try {
walletDepositTableOutput.value = await getTransactionTable2(walletDepositPagination.value)
const rows = walletDepositTableOutput.value.rows
if (isFirstPage) {
walletDepositItems.value = [...rows]
} else {
walletDepositItems.value.push(...rows)
}
} catch (error) {
console.error('Wallet deposit table error:', error)
} finally {
loading.value = false
loadingMore.value = false
}
}
async function fetchWithdrawPage(append: boolean) {
const isFirstPage = walletWithdrawPagination.value.page === 0 && !append
if (isFirstPage) {
loading.value = true
} else {
loadingMore.value = true
}
try {
walletWithdrawTableOutput.value = await getTransactionTable(walletWithdrawPagination.value)
const rows = walletWithdrawTableOutput.value.rows
if (isFirstPage) {
walletWithdrawItems.value = [...rows]
} else {
walletWithdrawItems.value.push(...rows)
}
} catch (error) {
console.error('Wallet withdraw table error:', error)
} finally {
loading.value = false
loadingMore.value = false
}
}
export async function refreshWalletCards() {
try {
walletCardItems.value = (await getActivatedCards()) ?? []
} catch (error) {
console.error('Wallet cards refresh error:', error)
}
}
export async function loadMoreWalletDeposits() {
if (
!walletDepositTableOutput.value
|| (walletDepositPagination.value.page + 1) * walletDepositPagination.value.limit
>= walletDepositTableOutput.value.query.total
) {
return
}
walletDepositPagination.value.page++
await fetchDepositPage(true)
}
export async function loadMoreWalletWithdrawals() {
if (
!walletWithdrawTableOutput.value
|| (walletWithdrawPagination.value.page + 1) * walletWithdrawPagination.value.limit
>= walletWithdrawTableOutput.value.query.total
) {
return
}
walletWithdrawPagination.value.page++
await fetchWithdrawPage(true)
}
export async function initWalletPageData(force = false) {
if (!force && initialized.value) {
return
}
if (!force && initPromise) {
return initPromise
}
initPromise = (async () => {
try {
if (force) {
resetWalletPageData()
}
loading.value = true
const walletStore = useWalletStore()
walletDepositPagination.value.page = 0
walletWithdrawPagination.value.page = 0
await Promise.all([
runOnceAsync('wallet:getMyWallet', () => walletStore.getMyWallet(), force),
runOnceAsync('wallet:cards', () => refreshWalletCards(), force),
runOnceAsync('wallet:deposit:init', () => fetchDepositPage(false), force),
runOnceAsync('wallet:withdraw:init', () => fetchWithdrawPage(false), force),
])
loadSupportMessengerFromSettings()
initialized.value = true
} catch (error) {
console.error('Wallet page initialization error:', error)
} finally {
loading.value = false
initPromise = null
}
})()
return initPromise
}
export function resetWalletPageData() {
initialized.value = false
initPromise = null
resetOnceAsyncByPrefix('wallet:')
walletCardItems.value = []
walletDepositItems.value = []
walletWithdrawItems.value = []
walletDepositTableOutput.value = null
walletWithdrawTableOutput.value = null
walletDepositPagination.value.page = 0
walletWithdrawPagination.value.page = 0
}