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,145 @@
import { inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
import { cloneDeep } from 'lodash'
import { type FormInstance, message } from 'ant-design-vue'
import { createAddress, getCity, updateAddress } from '@/api/address'
import type { Address } from '@/models/address'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { loading, loadingMore } from '@/utils'
import { isBazretalaProject } from '@/utils/config'
import {
addressItems,
addressProvinceList,
initAddressPageData,
refreshAddressItems,
} from './useAddressPageData'
export interface AddressPageContext {
provinceList: typeof addressProvinceList
cityList: Ref<any[] | undefined>
items: typeof addressItems
addAddressVisible: Ref<boolean>
editId: Ref<number | undefined>
address: Ref<Address>
formRef: Ref<FormInstance | undefined>
loading: typeof loading
loadingMore: typeof loadingMore
isBazretalaProject: typeof isBazretalaProject
resetFields: () => void
openAdd: () => void
openEdit: (val: Address) => void
handleOk: (e?: MouseEvent) => void
initItems: () => Promise<void>
changeProvince: (obj: { id: number }) => Promise<void>
}
export const ADDRESS_PAGE_CONTEXT_KEY: InjectionKey<AddressPageContext> =
Symbol('addressPageContext')
const emptyAddress = (): Address => ({
city: '',
province: '',
phone: '',
address: '',
zip: '',
default_address: false,
})
export function useAddressPage() {
const isDesktop = useDesktopBreakpoint()
const cityList = ref<any[]>()
const addAddressVisible = ref(false)
const editId = ref<number | undefined>(undefined)
const formRef = ref<FormInstance>()
const address = ref<Address>(emptyAddress())
function resetFields() {
editId.value = undefined
address.value = emptyAddress()
cityList.value = undefined
}
function openAdd() {
resetFields()
editId.value = undefined
addAddressVisible.value = !isDesktop.value
}
async function openEdit(val: Address) {
address.value = cloneDeep(val)
editId.value = address.value.id
if (address.value.province) {
const province = addressProvinceList.value.find((item) => item.title === address.value.province)
if (province?.id) {
cityList.value = await getCity(province.id)
}
}
addAddressVisible.value = true
}
const handleOk = (_e?: MouseEvent) => {
formRef.value?.validate().then(async () => {
if (editId.value) {
await updateAddress(editId.value, address.value)
} else {
await createAddress(address.value)
}
await initItems()
addAddressVisible.value = false
message.success('آدرس با موفقیت ثبت شد')
resetFields()
}).catch((error) => {
console.log('error', error)
})
}
async function initItems() {
await refreshAddressItems()
}
const changeProvince = async (obj: { id: number }) => {
try {
loading.value = true
cityList.value = undefined
address.value.city = undefined
cityList.value = await getCity(obj.id)
} catch (e) {
message.error('خطا در دریافت شهرها')
} finally {
loading.value = false
}
}
onMounted(async () => {
await initAddressPageData()
})
const context: AddressPageContext = {
provinceList: addressProvinceList,
cityList,
items: addressItems,
addAddressVisible,
editId,
address,
formRef,
loading,
loadingMore,
isBazretalaProject,
resetFields,
openAdd,
openEdit,
handleOk,
initItems,
changeProvince,
}
provide(ADDRESS_PAGE_CONTEXT_KEY, context)
return context
}
export function useAddressPageContext() {
const context = inject(ADDRESS_PAGE_CONTEXT_KEY)
if (!context) {
throw new Error('useAddressPageContext must be used within a page that calls useAddressPage()')
}
return context
}
@@ -0,0 +1,82 @@
import { ref } from 'vue'
import { getProvince, tableAddress } from '@/api/address'
import type { Address } from '@/models/address'
import { loading, loadingMore } from '@/utils'
import { runOnceAsync, resetOnceAsyncByPrefix } from '@/composables/useOnceAsync'
/**
* Shared address page datasets — mobile and desktop views read this module state only.
*/
let initPromise: Promise<void> | null = null
const initialized = ref(false)
export const addressItems = ref<Address[]>([])
export const addressProvinceList = ref<any[]>([])
export function isAddressPageDataInitialized() {
return initialized.value
}
export async function refreshAddressItems() {
try {
loading.value = true
const data = await tableAddress({ page: 0, limit: 150 })
addressItems.value = data.rows
} catch (error) {
console.error('Address list refresh error:', error)
} finally {
loading.value = false
loadingMore.value = false
}
}
async function loadAddressItems() {
const data = await tableAddress({ page: 0, limit: 150 })
addressItems.value = data.rows
}
async function loadProvinces() {
addressProvinceList.value = await getProvince()
}
export async function initAddressPageData(force = false) {
if (!force && initialized.value) {
return
}
if (!force && initPromise) {
return initPromise
}
initPromise = (async () => {
try {
if (force) {
resetAddressPageData()
}
loading.value = true
await Promise.all([
runOnceAsync('address:items', () => loadAddressItems(), force),
runOnceAsync('address:provinces', () => loadProvinces(), force),
])
initialized.value = true
} catch (error) {
console.error('Address page initialization error:', error)
} finally {
loading.value = false
initPromise = null
}
})()
return initPromise
}
export function resetAddressPageData() {
initialized.value = false
initPromise = null
resetOnceAsyncByPrefix('address:')
addressItems.value = []
addressProvinceList.value = []
}